74 lines
12 KiB
JavaScript
74 lines
12 KiB
JavaScript
window.__ModuleLoader__.load({
|
||
id: '@local/dsh-queue-steer',
|
||
factory(require) {
|
||
const React = require('react');
|
||
const h = React.createElement;
|
||
const NS = 'dshQueueSteer';
|
||
const P = require('@deepseek-ai/dsh-client-ui-primitives') || {};
|
||
const css = `
|
||
.dqs-dock{width:calc(100% - 32px);max-width:760px;margin:0 auto -3px;padding:0 8px;font:13px var(--dsw-font-family,Inter,sans-serif)}
|
||
.dqs-panel{background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-l1);border-bottom:0;border-radius:12px 12px 0 0;padding:4px 8px;overflow:hidden}
|
||
.dqs-header{width:100%;display:flex;align-items:center;gap:8px;border:0;background:none;color:var(--dsw-alias-label-primary);padding:7px 5px;cursor:pointer;text-align:left}
|
||
.dqs-count{flex:1;font-weight:600}.dqs-tabs{display:flex;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:2px 0 5px}
|
||
.dqs-tab{border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);padding:5px 8px;cursor:pointer}.dqs-tab.active{background:var(--dsw-alias-bg-l2);color:var(--dsw-alias-label-primary);font-weight:600}
|
||
.dqs-list{list-style:none;padding:3px 0;margin:0;display:grid;gap:2px}.dqs-row{display:flex;align-items:center;gap:7px;padding:6px;border-radius:7px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-l1)}
|
||
.dqs-row.editing{outline:2px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-l2)}.dqs-row.over{border-top:2px solid var(--dsw-alias-state-business-primary)}.dqs-grip{cursor:grab;opacity:.6}.dqs-preview{white-space:pre-wrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0;max-height:3.2em}.dqs-editor{flex:1;min-width:0;resize:none;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-alias-bg-base);color:inherit;padding:5px;font:inherit}.dqs-actions{display:flex;gap:2px}.dqs-action{border:0;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:4px;border-radius:5px}.dqs-action:hover{background:var(--dsw-alias-bg-l2);color:var(--dsw-alias-label-primary)}.dqs-empty{padding:12px;color:var(--dsw-alias-label-tertiary);text-align:center}.dqs-draft{font-size:10px;color:var(--dsw-alias-state-business-primary)}
|
||
.dqs-settings{padding:4px 0}.dqs-settings h2{font-size:18px;margin:0 0 6px}.dqs-settings p{color:var(--dsw-alias-label-secondary);line-height:1.45}.dqs-field{display:grid;gap:5px;margin:14px 0}.dqs-field select,.dqs-field textarea{font:inherit;color:inherit;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:7px;padding:8px}.dqs-field textarea{min-height:80px}.dqs-save{border:0;border-radius:7px;background:var(--dsw-alias-state-business-primary);color:white;padding:8px 14px;cursor:pointer}.dqs-status{margin-left:10px;color:var(--dsw-alias-state-positive)}
|
||
`;
|
||
let style;
|
||
const installCss = () => { if (!style) { style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } };
|
||
const icon = (name, fallback) => P[name] ? h(P[name], { size: 14 }) : h('span', null, fallback);
|
||
const key = (id) => `dqs:${id}`;
|
||
const read = (id, fallback) => { try { const v = localStorage.getItem(key(id)); return v == null ? fallback : JSON.parse(v); } catch { return fallback; } };
|
||
const write = (id, v) => { try { localStorage.setItem(key(id), JSON.stringify(v)); } catch {} };
|
||
const rowText = (row) => (row.content || []).filter((b) => b.type === 'text').map((b) => b.text).join('');
|
||
|
||
function SettingsPanel() {
|
||
const [value, setValue] = React.useState({ defaultMode: 'queue', defaultQueueList: 'batch', batchHeaderTemplate: 'Queued messages batch ({count}):\\n\\n{messages}' });
|
||
const [status, setStatus] = React.useState('');
|
||
React.useEffect(() => { fetch('dsh-queue-steer/settings', { credentials: 'same-origin' }).then((r) => r.json()).then(setValue).catch(() => {}); }, []);
|
||
const save = () => { setStatus('Saving…'); fetch('dsh-queue-steer/settings', { method: 'POST', credentials: 'same-origin', headers: {'content-type':'application/json'}, body: JSON.stringify(value) }).then((r) => r.json()).then((v) => { setValue(v); setStatus('Saved'); }).catch(() => setStatus('Save failed')); };
|
||
return h('section', { className: 'dqs-settings', 'data-dsh-plugin': 'dsh-queue-steer' },
|
||
h('h2', null, 'Queue & Steering'), h('p', null, 'Choose how busy-composer messages are delivered and which queue list receives new entries.'),
|
||
h('label', { className: 'dqs-field' }, 'Default busy message action', h('select', { value: value.defaultMode, onChange: (e) => setValue({...value, defaultMode:e.target.value}) }, h('option', {value:'queue'}, 'Queue'), h('option', {value:'steer'}, 'Steer'))),
|
||
h('label', { className: 'dqs-field' }, 'Default queue list', h('select', { value: value.defaultQueueList, onChange: (e) => setValue({...value, defaultQueueList:e.target.value}) }, h('option', {value:'batch'}, 'List 1 — send all entries at once'), h('option', {value:'sequential'}, 'List 2 — send one entry at a time'))),
|
||
h('label', { className: 'dqs-field' }, 'Batch message template', h('textarea', { value: value.batchHeaderTemplate, onChange: (e) => setValue({...value, batchHeaderTemplate:e.target.value}) }), h('small', null, 'Placeholders: {count} and {messages}')),
|
||
h('button', { className: 'dqs-save', onClick: save }, 'Save settings'), status && h('span', {className:'dqs-status'}, status)
|
||
);
|
||
}
|
||
|
||
function QueueDock({ useSession, useProjection, updateQueue, notify }) {
|
||
const inbox = useProjection('inbox');
|
||
const rows = inbox?.['next-turn'] || [];
|
||
const sessionId = useSession((s) => s.sessionId || s.id || 'current');
|
||
const running = useSession((s) => !!s.running);
|
||
const [tab, setTab] = React.useState(() => read(`${sessionId}:tab`, 'batch'));
|
||
const [open, setOpen] = React.useState(true);
|
||
const [editing, setEditing] = React.useState(null);
|
||
const [busy, setBusy] = React.useState(null);
|
||
const [drag, setDrag] = React.useState(null);
|
||
const [assign, setAssign] = React.useState(() => read(`${sessionId}:assign`, {}));
|
||
const [order, setOrder] = React.useState(() => read(`${sessionId}:order`, []));
|
||
const [drafts, setDrafts] = React.useState(() => read(`${sessionId}:drafts`, {}));
|
||
React.useEffect(installCss, []);
|
||
React.useEffect(() => { write(`${sessionId}:assign`, assign); }, [sessionId, assign]);
|
||
React.useEffect(() => { write(`${sessionId}:order`, order); }, [sessionId, order]);
|
||
React.useEffect(() => { write(`${sessionId}:drafts`, drafts); }, [sessionId, drafts]);
|
||
const sorted = React.useMemo(() => { const rank = new Map(order.map((id, i) => [id, i])); return [...rows].sort((a,b) => (rank.get(a.id) ?? 1e9) - (rank.get(b.id) ?? 1e9)); }, [rows, order]);
|
||
const listOf = (row) => assign[row.id] || 'batch';
|
||
const visible = sorted.filter((r) => listOf(r) === tab);
|
||
const move = (id, target) => { setAssign((v) => ({...v, [id]: target})); setTab(target); };
|
||
const startEdit = (row) => setEditing({ id: row.id, value: drafts[row.id] ?? rowText(row) });
|
||
const cancelEdit = () => { if (editing) setDrafts((v) => ({...v, [editing.id]: editing.value})); setEditing(null); };
|
||
const saveEdit = async () => { if (!editing || !editing.value.trim()) return false; const id = editing.id; const value = editing.value; setBusy(id); try { await Promise.resolve(updateQueue(id, {kind:'edit', content:[{type:'text', text:value}]})); setDrafts((v) => { const n={...v}; delete n[id]; return n; }); setEditing(null); return true; } catch { notify('error', 'Queue edit failed'); return false; } finally { setBusy(null); } };
|
||
const action = async (id, value) => { setBusy(id); try { await Promise.resolve(updateQueue(id, value)); setDrafts((v) => { const n={...v}; delete n[id]; return n; }); } catch { notify('error', 'Queue action failed'); } finally { setBusy(null); } };
|
||
const drop = (targetId) => { if (!drag || drag.id === targetId) return; const ids = sorted.map((r) => r.id).filter((id) => id !== drag.id); const at = ids.indexOf(targetId); ids.splice(Math.max(0, at), 0, drag.id); setOrder(ids); if (drag.list !== tab) move(drag.id, tab); setDrag(null); };
|
||
if (!rows.length) return null;
|
||
const renderItem = (row) => { const isEdit = editing?.id === row.id; const draft = drafts[row.id]; return h('li', { className: `dqs-row${isEdit?' editing':''}${drag?.over===row.id?' over':''}`, draggable: !isEdit, onDragStart: () => setDrag({id:row.id,list:tab}), onDragOver: (e) => { e.preventDefault(); setDrag((v) => v && ({...v, over:row.id})); }, onDrop: () => drop(row.id), onDragEnd: () => setDrag(null) }, h('span',{className:'dqs-grip','aria-hidden':true},'⋮⋮'), isEdit ? h('textarea',{className:'dqs-editor',autoFocus:true,value:editing.value,onChange:(e)=>setEditing({...editing,value:e.target.value}),onKeyDown:(e)=>{if(e.key==='Escape')cancelEdit();if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();saveEdit();}}}) : h('span',{className:'dqs-preview'}, draft || rowText(row)), draft && !isEdit && h('small',{className:'dqs-draft'},'draft'), h('span',{className:'dqs-actions'}, isEdit ? [h('button',{key:'save',className:'dqs-action',disabled:!!busy,onClick:saveEdit},icon('IconCheckOutlineRegular','✓')),h('button',{key:'cancel',className:'dqs-action',disabled:!!busy,onClick:cancelEdit},icon('IconCloseOutlineRegular','×'))] : [h('button',{key:'edit',className:'dqs-action',disabled:!!busy,onClick:()=>startEdit(row)},icon('IconEditOutlineRegular','✎')),h('button',{key:'move',className:'dqs-action',disabled:!!busy,onClick:()=>move(row.id,tab==='batch'?'sequential':'batch')},tab==='batch'?'⇢':'⇠'),h('button',{key:'steer',className:'dqs-action',disabled:!!busy||!running,onClick:()=>action(row.id,{kind:'steer'})},icon('IconSendOutlineRegular','➤')),h('button',{key:'remove',className:'dqs-action',disabled:!!busy,onClick:()=>action(row.id,{kind:'remove'})},icon('IconTrashOutlineRegular','×'))])); }
|
||
return h('div',{className:'dqs-dock','data-queue-dock':'dsh-queue-steer'},h('div',{className:'dqs-panel'},h('button',{className:'dqs-header',onClick:()=>setOpen(!open)},icon('IconQueueOutlineRegular','☷'),h('span',{className:'dqs-count'},`Queue & Steering (${rows.length})`),h('span',null,open?'⌃':'⌄')),open && h(React.Fragment,null,h('div',{className:'dqs-tabs'},h('button',{className:`dqs-tab ${tab==='batch'?'active':''}`,onClick:()=>{setTab('batch');write(`${sessionId}:tab`,'batch')}},`List 1 · all at once (${sorted.filter((r)=>listOf(r)==='batch').length})`),h('button',{className:`dqs-tab ${tab==='sequential'?'active':''}`,onClick:()=>{setTab('sequential');write(`${sessionId}:tab`,'sequential')}},`List 2 · one at a time (${sorted.filter((r)=>listOf(r)==='sequential').length})`)),visible.length?h('ul',{className:'dqs-list'},visible.map((r)=>renderItem(r))):h('div',{className:'dqs-empty'},'No entries in this list'))));
|
||
}
|
||
|
||
return { inject: ['slots', 'conversation', 'sessions'], apply(ctx) { installCss(); ctx.slots.inject('settings.section', () => ctx.slots.register({name:'settings.section', id:'dsh-queue-steer', order:170, label:'Queue & Steering'}, SettingsPanel)); ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({name:'conversation.input.dock', id:'queue', priority:-1, order:20, inject:(sessionId)=>{ const actx = ctx.sessions.scope(sessionId); const conversation = actx.get('conversation'); return { updateQueue:(id,a)=>conversation.updateQueue(id,a), notify:(level,msg)=>conversation.input.for(actx).notify(level,msg) }; }}, QueueDock)); } };
|
||
}
|
||
});
|