/* Desk UI Kit — Channels subsystem: Slack-like rooms where agents talk. Exposes window.ChannelsView; loaded before Workspace.jsx. */ (() => { const { useState, useEffect, useRef, useCallback } = React; const CH = window.DESK_CHANNELS; function CIco({ name, size = 16 }) { return ; } const byId = (id) => CH.members.find((m) => m.id === id) || { id, name: id, ava: '??', tone: 'c1', tag: 'agent' }; /* @mentions and `code` spans rendered inline */ function renderText(text) { const parts = text.split(/(@[\w-]+|`[^`]+`)/g); return parts.map((p, i) => { if (/^@/.test(p)) return {p}; if (/^`/.test(p)) return {p.slice(1, -1)}; return p; }); } function nowStamp() { const d = new Date(); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); } function ChannelsView() { const [chans, setChans] = useState(() => JSON.parse(JSON.stringify(CH.channels))); const [active, setActive] = useState('deploy'); const [draft, setDraft] = useState(''); const [typing, setTyping] = useState(null); // { who, chan } const scrollRef = useRef(null); const timers = useRef([]); const activeRef = useRef('deploy'); const replyIdx = useRef({}); activeRef.current = active; const chan = chans.find((c) => c.id === active) || chans[0]; useEffect(() => { try { window.lucide && window.lucide.createIcons(); } catch (e) {} }); useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [chans, typing, active]); const post = useCallback((chanId, who, text) => { setChans((cs) => cs.map((c) => c.id === chanId ? { ...c, msgs: [...c.msgs, { who, t: nowStamp(), text }], unread: chanId === activeRef.current ? 0 : c.unread + 1 } : c)); }, []); /* ambient scripted traffic */ useEffect(() => { (CH.drip || []).forEach((d) => { timers.current.push(setTimeout(() => setTyping({ who: d.who, chan: d.chan }), Math.max(0, d.delay - 1000))); timers.current.push(setTimeout(() => { setTyping(null); post(d.chan, d.who, d.text); }, d.delay)); }); return () => timers.current.forEach(clearTimeout); }, [post]); const send = () => { const text = draft.trim(); if (!text) return; setDraft(''); post(active, 'human', text); const m = text.match(/@([\w-]+)/); let responder = m ? CH.members.find((x) => x.id === m[1] && x.id !== 'human') : null; if (!responder) responder = byId(CH.defaultResponder); const pool = CH.replies[responder.id] || CH.replies[CH.defaultResponder]; replyIdx.current[responder.id] = ((replyIdx.current[responder.id] || 0) + 1) % pool.length; const reply = pool[replyIdx.current[responder.id]]; const chanAt = active; timers.current.push(setTimeout(() => setTyping({ who: responder.id, chan: chanAt }), 650)); timers.current.push(setTimeout(() => { setTyping(null); post(chanAt, responder.id, reply); }, 1900 + Math.random() * 900)); }; const openChan = (id) => { setActive(id); setChans((cs) => cs.map((x) => x.id === id ? { ...x, unread: 0 } : x)); }; return (
Channels
Rooms · {chans.length}
{chans.map((c) => (
openChan(c.id)}> #{c.name} {c.unread ? {c.unread} : null}
))}
Members · {CH.members.length}
{CH.members.map((m) => (
{m.name} {m.tag}
))}
{chan.name} {chan.topic} 4 agents · 1 human
today
{chan.msgs.map((msg, i) => { const m = byId(msg.who); const prev = chan.msgs[i - 1]; const grouped = !!prev && prev.who === msg.who; return (
{grouped ? {msg.t} : {m.ava}}
{!grouped ? (
{m.name} {m.tag}
) : null}
{renderText(msg.text)}
); })} {typing && typing.chan === active ? (
{byId(typing.who).name} is typing…
) : null}
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') send(); }} placeholder={`message #${chan.name} — @mention an agent to dispatch`} />
); } window.ChannelsView = ChannelsView; })();