// Primus IQ β Tagging Modal
const SAMPLE_DROPPED = [
{ id: 'd1', name: 'GCC_Market_Sizing_v4.pptx', size: '12.0 MB', ext: 'pptx', suggest: 'strict', pages: 28, tags: ['GCC', 'Sizing'] },
{ id: 'd2', name: 'Acme_Financial_Model.xlsx', size: '4.2 MB', ext: 'xlsx', suggest: 'strict', pages: 12, tags: ['Acme', 'Finance'] },
{ id: 'd3', name: 'IndustryReport_Q4.pdf', size: '8.6 MB', ext: 'pdf', suggest: 'public', pages: 64, tags: ['Research'] },
{ id: 'd4', name: 'Interview_Notes_CFO.docx', size: '120 KB', ext: 'docx', suggest: 'client', pages: 4, tags: ['Acme', 'Interview'] },
{ id: 'd5', name: 'ESG_Compliance_Brief.pdf', size: '2.1 MB', ext: 'pdf', suggest: 'internal', pages: 18, tags: ['ESG'] },
{ id: 'd6', name: 'Pitch_Storyline_draft.pptx', size: '6.8 MB', ext: 'pptx', suggest: 'internal', pages: 22, tags: ['Pitch'] },
{ id: 'd7', name: 'Supply_chain_diagram.png', size: '880 KB', ext: 'png', suggest: 'internal', pages: 1, tags: ['Diagram'] },
{ id: 'd8', name: 'Bid_Cost_Calculator.xlsx', size: '1.4 MB', ext: 'xlsx', suggest: 'strict', pages: 8, tags: ['RFP'] },
{ id: 'd9', name: 'Client_brief_v2.docx', size: '340 KB', ext: 'docx', suggest: 'client', pages: 6, tags: ['Acme', 'Brief'] },
{ id: 'd10', name: 'Site_visit_video.mp4', size: '48.2 MB', ext: 'mp4', suggest: 'internal', pages: 0, tags: ['Field'] },
];
const CONF_OPTIONS = [
{ id: 'public', label: 'Public', desc: 'Anyone' },
{ id: 'internal', label: 'Internal', desc: 'All Employees' },
{ id: 'client', label: 'Confidential', desc: 'Restricted Projects' },
{ id: 'strict', label: 'Restricted Visibility', desc: 'Only MDs' },
];
const CONF_LABELS = {
public: 'Public',
internal: 'Internal',
client: 'Confidential',
strict: 'Restricted Visibility',
};
const SUGGESTED_TAGS = ['GCC', 'KSA', 'UAE', 'Fintech', 'Climate', 'Policy', 'Healthcare', 'TCFD', 'CSRD', 'Acme', 'Globex'];
const TaggingModal = ({ open, onClose, onCommit, pendingFiles = [], editMode = false }) => {
const [files, setFiles] = React.useState([]);
const [active, setActive] = React.useState(0);
const [tagInput, setTagInput] = React.useState('');
React.useEffect(() => {
if (open) {
setFiles(pendingFiles.length > 0
? pendingFiles
: SAMPLE_DROPPED.map(f => ({ ...f, conf: f.suggest })));
setActive(0);
setTagInput('');
}
}, [open, pendingFiles]);
// Must be before any early return β hooks must always run in the same order.
React.useEffect(() => {
if (!open) return;
const onKey = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowDown') setActive(a => Math.min(a + 1, files.length - 1));
if (e.key === 'ArrowUp') setActive(a => Math.max(a - 1, 0));
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, files]);
if (!open) return null;
if (files.length === 0) return (
e.stopPropagation()} style={{
padding: '40px 48px', background: 'var(--bg-card)', borderRadius: 18,
boxShadow: '0 30px 80px rgba(26,22,20,0.30)', textAlign: 'center',
}}>
π
No files selected
Drop files on the zone or use the Upload button.
);
const f = files[active];
const updateFile = (idx, patch) => {
setFiles(prev => prev.map((file, i) => i === idx ? { ...file, ...patch } : file));
};
const addTag = (t) => {
const tag = t.trim();
if (!tag) return;
if (f.tags.includes(tag)) return;
updateFile(active, { tags: [...f.tags, tag] });
setTagInput('');
};
const removeTag = (t) => updateFile(active, { tags: f.tags.filter(x => x !== t) });
const applyAllConf = (confId) => setFiles(prev => prev.map(file => ({ ...file, conf: confId })));
const tagged = files.filter(x => x.conf).length;
return (
e.stopPropagation()} className="tagging-modal-card" style={{
width: 1040, maxWidth: '94vw', height: 640, maxHeight: '92vh',
background: 'var(--bg-card)', borderRadius: 18,
boxShadow: '0 30px 80px rgba(26,22,20,0.30), 0 0 0 1px var(--line)',
animation: 'rise 0.22s ease-out',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}>
{/* Header */}
{editMode ? 'Edit visibility & tags' : 'Tag your files'}
{editMode
? 'Update who can see this file and its tags'
: `${files.length} files Β· pick visibility and add tags Β· ${tagged}/${files.length} ready`}
{/* Body */}
{/* LEFT β file queue */}
Files
{active + 1} / {files.length}
{files.map((file, i) => {
const isActive = i === active;
return (
setActive(i)} className="tagging-file-item" style={{
background: isActive ? 'white' : 'transparent',
borderColor: isActive ? 'var(--maroon-20)' : 'transparent',
boxShadow: isActive ? '0 2px 8px rgba(60,30,20,0.06)' : 'none',
}}>
{file.name}
{file.size}
{file.conf && (
{CONF_LABELS[file.conf] || file.conf}
)}
);
})}
{/* RIGHT β tag panel */}
{/* File header */}
{f.name}
{f.size}
{f.pages > 0 &&
Β· {f.pages} pages}
Suggested: {CONF_LABELS[f.suggest] || f.suggest}
{/* Visibility */}
Visibility
{CONF_OPTIONS.map(opt => {
const sel = f.conf === opt.id;
return (
);
})}
{/* Tags */}
Tags
{f.tags.map(t => (
{t}
))}
setTagInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') { e.preventDefault(); addTag(tagInput); }
if (e.key === 'Backspace' && !tagInput && f.tags.length > 0) removeTag(f.tags[f.tags.length - 1]);
}}
placeholder={f.tags.length === 0 ? 'Add a tagβ¦' : ''}
style={{
flex: 1, minWidth: 100, border: 'none', outline: 'none',
fontSize: 13, fontFamily: 'inherit', color: 'var(--ink)', background: 'transparent',
padding: '4px 2px',
}}
/>
Suggested:
{SUGGESTED_TAGS.filter(t => !f.tags.includes(t)).slice(0, 7).map(t => (
))}
{/* Footer banner */}
Tip: {' '}
Use β β to move between files, Enter to add a tag.
{/* Footer */}
);
};
const sectionLabel = {
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
fontSize: 10, fontWeight: 700, color: 'var(--mute)',
letterSpacing: '0.08em', textTransform: 'uppercase',
marginBottom: 8,
};
const miniBtn = {
background: 'none', border: 'none', cursor: 'pointer',
fontSize: 11, color: 'var(--maroon)', fontFamily: 'inherit', fontWeight: 600,
textTransform: 'none', letterSpacing: 0,
};
const chipTagStyle = {
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '4px 4px 4px 10px', borderRadius: 6,
background: 'var(--maroon-06)', color: 'var(--maroon)',
fontSize: 12, fontWeight: 500, fontFamily: 'inherit',
};
const chipRemoveBtn = {
width: 16, height: 16, borderRadius: 4, border: 'none', cursor: 'pointer',
background: 'transparent', color: 'var(--maroon)',
display: 'grid', placeItems: 'center',
};
const suggestedTagBtn = {
padding: '4px 10px', borderRadius: 999,
background: 'white', border: '1px dashed var(--line-2)',
fontSize: 11, color: 'var(--mute)', fontFamily: 'inherit', cursor: 'pointer',
};
const navBtn = {
height: 32, padding: '0 12px', borderRadius: 7,
background: 'transparent', border: '1px solid var(--line-2)',
fontSize: 12, fontFamily: 'inherit', color: 'var(--ink-2)', cursor: 'pointer',
};
const kbdStyle = {
display: 'inline-block', padding: '1px 6px', borderRadius: 4,
background: 'white', border: '1px solid var(--line-2)',
fontSize: 10, fontFamily: 'ui-monospace, monospace', color: 'var(--ink)',
};
Object.assign(window, {
TaggingModal,
SAMPLE_DROPPED,
CONF_OPTIONS,
CONF_LABELS,
SUGGESTED_TAGS,
});