diff --git a/frontends/conductor.py b/frontends/conductor.py index 8a15f4e2..25ac3dea 100644 --- a/frontends/conductor.py +++ b/frontends/conductor.py @@ -13,6 +13,7 @@ builtins.print = _filtered_print from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel # allow: python frontends/conductor.py @@ -27,6 +28,7 @@ PORT = 8900 HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor.html") app = FastAPI(title="Conductor") +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) class ChatIn(BaseModel): msg: str diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index 3f46c55c..a6748770 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -3162,3 +3162,370 @@ else refreshEmptyState(null); runLabel.textContent = t('status.connecting'); window.ga.startBridge && window.ga.startBridge(); })(); + +/* Conductor 页 — 直连 Conductor WS,不走 bridge session */ +(function () { + 'use strict'; + const wsUrl = () => `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.hostname}:8900/ws`; + const FAIL_MAX = 5, RECON_BASE = 1200, RECON_MAX = 30000; + const $ = id => document.getElementById(id); + const t = k => (window.gaT && window.gaT(k)) || k; + const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + const md = s => { try { return typeof marked !== 'undefined' ? marked.parse(s || '') : esc(s); } catch { return esc(s); } }; + const stripAttach = text => String(text || '').replace(/\[(Image|File)\s+#\d+\]\s*/g, '').trim(); + const FC_SVG = ''; + const ST_ICONS = { + running: '', + reported: '', + paused: '', + failed: '', + terminated: '', + }; + const ST_KEYS = { running: 'collab.stRunning', reported: 'collab.stReported', paused: 'collab.stPaused', failed: 'collab.stFailed', terminated: 'collab.stTerminated' }; + const CHIP_KEYS = ['collab.chipProgress', 'collab.chipPause', 'collab.chipSummary']; + + const S = { + everConnected: false, reconnecting: false, serviceAvailable: false, + messages: [], workers: [], runningCount: 0, + conductorTyping: false, failCount: 0, + historyReady: false, reconnectAt: 0, progressOpen: false, + }; + let ws, connectTimer, reconnectTick, titleSeq = 0, wsGen = 0, localSeq = 0; + const titleSeen = new Map(); + + let draftEl = null; + + const scrollMsgs = () => { const a = $('collab-msgs'); if (a) a.scrollTop = a.scrollHeight; }; + const showDraft = () => S.conductorTyping && S.serviceAvailable && S.historyReady && S.messages.length > 0; + + function clearDraft() { + if (draftEl) { draftEl.remove(); draftEl = null; } + } + + function syncDraft() { + const list = $('collab-msg-list'); + if (!list || list.hidden || !showDraft()) return clearDraft(); + if (!draftEl) draftEl = document.createElement('div'); + draftEl.className = 'msg system collab-msg-enter'; + draftEl.setAttribute('aria-label', t('collab.typing')); + draftEl.innerHTML = '
'; + list.appendChild(draftEl); + requestAnimationFrame(scrollMsgs); + } + + function relTime(ts) { + if (!ts) return ''; + const ms = typeof ts === 'number' ? (ts > 1e12 ? ts : ts * 1000) : Date.parse(ts); + if (!ms || Number.isNaN(ms)) return ''; + const sec = Math.max(0, Math.floor((Date.now() - ms) / 1000)); + if (sec < 10) return t('collab.timeJust'); + if (sec < 60) return t('collab.timeSec').replace('{n}', sec); + const min = Math.floor(sec / 60); + if (min < 60) return t('collab.timeMin').replace('{n}', min); + const hr = Math.floor(min / 60); + return hr < 24 ? t('collab.timeHr').replace('{n}', hr) : t('collab.timeDay').replace('{n}', Math.floor(hr / 24)); + } + + function mapStatus(status, reply) { + const r = (reply || '').trim(); + if (status === 'running') return 'running'; + if (status === 'failed') return 'failed'; + if (status === 'aborted') return 'terminated'; + if (status === 'stopped') return r ? 'reported' : 'paused'; + return 'paused'; + } + + function normalizeWorker(raw) { + if (!titleSeen.has(raw.id)) titleSeen.set(raw.id, ++titleSeq); + const ui = mapStatus(raw.status, raw.reply); + let title = String(raw.prompt ?? '').replace(/^[\s请帮我麻烦]+/u, '').trim(); + if (!title) title = t('collab.taskFallback').replace('{n}', titleSeen.get(raw.id)); + else { + title = (title.split(/[\n。!?.!?]/)[0] || '').trim(); + if (title.length > 18) title = title.slice(0, 18) + '…'; + } + const reply = String(raw.reply || '').replace(/\s+/g, ' ').trim(); + let summary = reply ? (reply.length > 80 ? reply.slice(0, 80) + '…' : reply) : t(ui === 'running' ? 'collab.summaryRunning' : 'collab.summaryWait'); + return { id: raw.id, title, status: ui, summary, fullReply: raw.reply || '', updatedAt: raw.updated_at }; + } + + function syncProgressToggle() { + const btn = $('collab-progress-toggle'), body = $('collab-body'); + if (!btn || !body) return; + const split = body.classList.contains('collab-body--split'); + btn.hidden = !split; + body.classList.toggle('collab-progress-open', split && S.progressOpen); + } + + function setConnUi() { + const off = $('collab-offline'), recon = $('collab-reconnect'), inp = $('collab-input'), btn = $('collab-send'); + const avail = S.serviceAvailable; + const trying = !avail && !S.everConnected && S.failCount < FAIL_MAX; + if (off) off.hidden = avail || S.reconnecting || trying; + if (recon) { + recon.hidden = !S.reconnecting; + recon.textContent = S.reconnecting && S.reconnectAt > Date.now() + ? t('collab.reconnect') + ' ' + t('collab.reconnectIn').replace('{n}', Math.ceil((S.reconnectAt - Date.now()) / 1000)) + : t('collab.reconnect'); + } + if (inp) inp.disabled = !avail; + if (btn) btn.disabled = !avail; + syncDraft(); + syncProgressToggle(); + } + + let cardMenu = null; + function hideCardMenu() { if (cardMenu) { cardMenu.remove(); cardMenu = null; } } + function showCardMenu(x, y, sid) { + hideCardMenu(); + cardMenu = document.createElement('div'); + cardMenu.className = 'ctx-menu'; + cardMenu.style.left = x + 'px'; + cardMenu.style.top = y + 'px'; + cardMenu.innerHTML = '
删除
'; + cardMenu.querySelector('.ctx-item').onmousedown = (e) => { + e.stopPropagation(); + fetch(`http://${location.hostname}:8900/subagent/${sid}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'kill' }) }); + hideCardMenu(); + }; + document.body.appendChild(cardMenu); + setTimeout(() => document.addEventListener('mousedown', hideCardMenu, { once: true }), 0); + } + + let drawerEl = null; + function closeWorkerDrawer() { if (drawerEl) { drawerEl.remove(); drawerEl = null; } } + function openWorkerDrawer(w) { + closeWorkerDrawer(); + drawerEl = document.createElement('div'); + drawerEl.className = 'collab-drawer-wrap'; + drawerEl.innerHTML = `
`; + drawerEl.querySelector('.collab-drawer-backdrop').onclick = closeWorkerDrawer; + drawerEl.querySelector('.collab-drawer-close').onclick = closeWorkerDrawer; + document.body.appendChild(drawerEl); + } + + function renderWorkers() { + const box = $('collab-workers'), empty = $('collab-progress-empty'); + if (!box) return; + if (empty) empty.hidden = S.workers.length > 0; + box.innerHTML = S.workers.map(w => ` +
+
${ST_ICONS[w.status] || ''}${esc(t(ST_KEYS[w.status] || 'collab.stPaused'))}${w.updatedAt ? `${esc(relTime(w.updatedAt))}` : ''}
+
${esc(w.title)}
+
${esc(w.summary)}
+
`).join(''); + box.querySelectorAll('.collab-card').forEach(el => { + el.addEventListener('contextmenu', e => { + e.preventDefault(); + showCardMenu(e.clientX, e.clientY, el.dataset.sid); + }); + el.addEventListener('click', () => { + const w = S.workers.find(w => w.id === el.dataset.sid); + if (w) openWorkerDrawer(w); + }); + }); + const running = S.workers.filter(w => w.status === 'running').length; + S.runningCount = running; + document.dispatchEvent(new CustomEvent('collab:running-count', { detail: { count: running } })); + const sticky = $('collab-sticky'); + if (sticky) { sticky.hidden = !running; sticky.textContent = t('collab.sticky').replace('{n}', running); } + const stats = $('collab-progress-stats'); + if (stats) { + const has = S.workers.length > 0; + stats.hidden = !has; + if (has) stats.textContent = t('collab.progressStats') + .replace('{running}', running) + .replace('{done}', S.workers.filter(w => w.status === 'reported').length) + .replace('{issue}', S.workers.filter(w => w.status === 'failed' || w.status === 'terminated').length); + } + } + + function renderUserMsg(item) { + const sub = n => (window.gaFileSubLabel && window.gaFileSubLabel(n)) || n; + const imgs = (item.images || []).map(im => ``).join(''); + const files = (item.files || []).map(f => { + const name = f.name || 'file'; + return `
${FC_SVG}${esc(name)}${esc(sub(name))}
`; + }).join(''); + const clean = stripAttach(item.msg); + const text = clean ? `
${esc(clean).replace(/\n/g, '
')}
` : ''; + return `
${files ? `
${files}
` : ''}${imgs ? `
${imgs}
` : ''}${text}
`; + } + + function renderMsg(item) { + if (item.role === 'user') return renderUserMsg(item); + if (item.role === 'conductor') return `
${md(item.msg)}
`; + return `
${esc(item.msg)}
`; + } + + function syncLayout() { + const body = $('collab-body'); + if (!body) return; + body.classList.toggle('collab-body--split', S.historyReady && S.messages.length > 0); + syncProgressToggle(); + } + + function syncMessages() { + const area = $('collab-msgs'), welcome = $('collab-welcome'), list = $('collab-msg-list'), head = $('collab-chat-head'); + if (!area || !list) return; + if (!S.historyReady) { + area.classList.remove('has-msgs'); + if (welcome) welcome.hidden = true; + if (head) head.hidden = true; + list.hidden = true; + return syncLayout(); + } + const has = S.messages.length > 0; + area.classList.toggle('has-msgs', has); + if (welcome) welcome.hidden = has; + if (head) head.hidden = !has; + list.hidden = !has; + list.innerHTML = S.messages.map(renderMsg).join(''); + syncDraft(); + scrollMsgs(); + syncLayout(); + } + + function pushMsg(item) { + if (item.id && S.messages.some(m => m.id === item.id)) return; + if (item.role === 'user') { + const plain = stripAttach(item.msg); + for (let i = S.messages.length - 1; i >= 0; i--) { + const m = S.messages[i]; + if (m._local && m.role === 'user' && (stripAttach(m.msg) === plain || m.msg === item.msg)) { + S.messages.splice(i, 1); + break; + } + } + } + S.messages.push(item); + if (item.role === 'conductor') S.conductorTyping = false; + syncMessages(); + setConnUi(); + } + + function setWorkers(rawList) { + S.workers = (rawList || []).map(normalizeWorker); + renderWorkers(); + syncLayout(); + } + + function onWsData(data, gen) { + if (gen !== wsGen) return; + if (data.type === 'hello') { + S.historyReady = true; + S.messages = (data.chat || []).map(raw => ({ id: raw.id, role: raw.role || 'system', msg: raw.msg || '', ts: raw.ts, read: raw.read })); + setWorkers(data.subagents || []); + syncMessages(); + setConnUi(); + } else if (data.type === 'subagents') setWorkers(data.items || []); + else if (data.type === 'chat') pushMsg({ id: data.item.id, role: data.item.role || 'system', msg: data.item.msg || '', ts: data.item.ts, read: data.item.read }); + } + + function resetWs() { + wsGen++; + if (!ws) return; + const old = ws; + ws = null; + old.onopen = old.onclose = old.onerror = old.onmessage = null; + try { old.close(); } catch {} + } + + function scheduleReconnect() { + clearTimeout(connectTimer); + clearInterval(reconnectTick); + if (!S.everConnected && S.failCount >= FAIL_MAX) { + S.reconnecting = false; + return setConnUi(); + } + const delay = Math.min(RECON_MAX, RECON_BASE * Math.pow(2, Math.max(0, S.failCount - 1))); + S.reconnectAt = Date.now() + delay; + S.reconnecting = S.everConnected; + setConnUi(); + reconnectTick = setInterval(() => { if (!S.reconnecting) clearInterval(reconnectTick); else setConnUi(); }, 500); + connectTimer = setTimeout(connect, delay); + } + + function connect() { + if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return; + clearTimeout(connectTimer); + clearInterval(reconnectTick); + const gen = ++wsGen; + setConnUi(); + let sock; + try { sock = new WebSocket(wsUrl()); } catch (e) { + if (gen !== wsGen) return; + S.failCount++; + return scheduleReconnect(); + } + ws = sock; + sock.onopen = () => { + if (gen !== wsGen) return; + S.everConnected = true; + S.serviceAvailable = true; + S.reconnecting = false; + S.failCount = 0; + setConnUi(); + }; + sock.onclose = (ev) => { + if (gen !== wsGen) return; + S.serviceAvailable = false; + if (S.everConnected) S.reconnecting = true; + else S.failCount++; + setConnUi(); + scheduleReconnect(); + }; + sock.onerror = () => {}; + sock.onmessage = ev => { + if (gen !== wsGen) return; + try { onWsData(JSON.parse(ev.data), gen); } catch {} + }; + } + + function sendText(rawText) { + const text = (rawText || '').trim(); + if (!text || !ws || ws.readyState !== WebSocket.OPEN) return false; + const expand = window.gaExpandFilePlaceholders || (s => s); + const collect = window.gaCollectUsedFiles || (() => []); + const clearUsed = window.gaClearUsedPendingFiles || (() => {}); + const used = collect(text); + const images = [], files = []; + for (const f of used) (f.isImage ? images : files).push(f.isImage ? { path: f.path, dataUrl: f.dataUrl } : { path: f.path, name: f.name }); + S.messages.push({ id: `_local_${++localSeq}`, _local: true, role: 'user', msg: text, ts: Date.now() / 1000, images, files }); + S.conductorTyping = true; + syncMessages(); + ws.send(JSON.stringify({ msg: expand(text) })); + clearUsed(text); + const inp = $('collab-input'); + if (inp && inp.value.trim() === text) inp.value = ''; + setConnUi(); + return true; + } + + function buildChips() { + const box = $('collab-chips'); + if (box) box.innerHTML = CHIP_KEYS.map(k => ``).join(''); + } + + $('collab-send')?.addEventListener('click', () => sendText($('collab-input')?.value || '')); + $('collab-retry')?.addEventListener('click', () => { S.failCount = 0; S.reconnecting = false; resetWs(); connect(); }); + $('collab-input')?.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendText(e.target.value); } }); + $('collab-chips')?.addEventListener('click', e => { const k = e.target.closest('.collab-chip')?.dataset.textKey; if (k) sendText(t(k)); }); + $('collab-progress-toggle')?.addEventListener('click', () => { S.progressOpen = !S.progressOpen; syncProgressToggle(); }); + document.querySelector('[data-page="collab"]')?.addEventListener('click', e => { + if (!S.progressOpen || e.target.closest('.collab-progress') || e.target.closest('#collab-progress-toggle')) return; + S.progressOpen = false; + syncProgressToggle(); + }); + + window.collabInit = () => { + window.gaSetActiveFileComposer?.('collab'); + buildChips(); + syncMessages(); + setConnUi(); + renderWorkers(); + connect(); + }; + window.collabFocus = () => $('collab-input')?.focus(); + window.collabRetranslate = () => { buildChips(); renderWorkers(); syncMessages(); setConnUi(); }; +})(); \ No newline at end of file diff --git a/frontends/desktop/static/collab.js b/frontends/desktop/static/collab.js deleted file mode 100644 index 25ec0a8a..00000000 --- a/frontends/desktop/static/collab.js +++ /dev/null @@ -1,326 +0,0 @@ -/* Conductor 页 — 直连 Conductor WS,不走 bridge session */ -(function () { - 'use strict'; - const wsUrl = () => `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.hostname}:8900/ws`; - const FAIL_MAX = 5, RECON_BASE = 1200, RECON_MAX = 30000; - const $ = id => document.getElementById(id); - const t = k => (window.gaT && window.gaT(k)) || k; - const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); - const md = s => { try { return typeof marked !== 'undefined' ? marked.parse(s || '') : esc(s); } catch { return esc(s); } }; - const stripAttach = text => String(text || '').replace(/\[(Image|File)\s+#\d+\]\s*/g, '').trim(); - const FC_SVG = ''; - const ST_ICONS = { - running: '', - reported: '', - paused: '', - failed: '', - terminated: '', - }; - const ST_KEYS = { running: 'collab.stRunning', reported: 'collab.stReported', paused: 'collab.stPaused', failed: 'collab.stFailed', terminated: 'collab.stTerminated' }; - const CHIP_KEYS = ['collab.chipProgress', 'collab.chipPause', 'collab.chipSummary']; - - const S = { - everConnected: false, reconnecting: false, serviceAvailable: false, - messages: [], workers: [], runningCount: 0, - conductorTyping: false, failCount: 0, - historyReady: false, reconnectAt: 0, progressOpen: false, - }; - let ws, connectTimer, reconnectTick, titleSeq = 0, wsGen = 0, localSeq = 0; - const titleSeen = new Map(); - - let draftEl = null; - - const scrollMsgs = () => { const a = $('collab-msgs'); if (a) a.scrollTop = a.scrollHeight; }; - const showDraft = () => S.conductorTyping && S.serviceAvailable && S.historyReady && S.messages.length > 0; - - function clearDraft() { - if (draftEl) { draftEl.remove(); draftEl = null; } - } - - function syncDraft() { - const list = $('collab-msg-list'); - if (!list || list.hidden || !showDraft()) return clearDraft(); - if (!draftEl) draftEl = document.createElement('div'); - draftEl.className = 'msg system collab-msg-enter'; - draftEl.setAttribute('aria-label', t('collab.typing')); - draftEl.innerHTML = '
'; - list.appendChild(draftEl); - requestAnimationFrame(scrollMsgs); - } - - function relTime(ts) { - if (!ts) return ''; - const ms = typeof ts === 'number' ? (ts > 1e12 ? ts : ts * 1000) : Date.parse(ts); - if (!ms || Number.isNaN(ms)) return ''; - const sec = Math.max(0, Math.floor((Date.now() - ms) / 1000)); - if (sec < 10) return t('collab.timeJust'); - if (sec < 60) return t('collab.timeSec').replace('{n}', sec); - const min = Math.floor(sec / 60); - if (min < 60) return t('collab.timeMin').replace('{n}', min); - const hr = Math.floor(min / 60); - return hr < 24 ? t('collab.timeHr').replace('{n}', hr) : t('collab.timeDay').replace('{n}', Math.floor(hr / 24)); - } - - function mapStatus(status, reply) { - const r = (reply || '').trim(); - if (status === 'running') return 'running'; - if (status === 'failed') return 'failed'; - if (status === 'aborted') return 'terminated'; - if (status === 'stopped') return r ? 'reported' : 'paused'; - return 'paused'; - } - - function normalizeWorker(raw) { - if (!titleSeen.has(raw.id)) titleSeen.set(raw.id, ++titleSeq); - const ui = mapStatus(raw.status, raw.reply); - let title = String(raw.prompt ?? '').replace(/^[\s请帮我麻烦]+/u, '').trim(); - if (!title) title = t('collab.taskFallback').replace('{n}', titleSeen.get(raw.id)); - else { - title = (title.split(/[\n。!?.!?]/)[0] || '').trim(); - if (title.length > 18) title = title.slice(0, 18) + '…'; - } - const reply = String(raw.reply || '').replace(/\s+/g, ' ').trim(); - let summary = reply ? (reply.length > 80 ? reply.slice(0, 80) + '…' : reply) : t(ui === 'running' ? 'collab.summaryRunning' : 'collab.summaryWait'); - return { id: raw.id, title, status: ui, summary, updatedAt: raw.updated_at }; - } - - function syncProgressToggle() { - const btn = $('collab-progress-toggle'), body = $('collab-body'); - if (!btn || !body) return; - const split = body.classList.contains('collab-body--split'); - btn.hidden = !split; - body.classList.toggle('collab-progress-open', split && S.progressOpen); - } - - function setConnUi() { - const off = $('collab-offline'), recon = $('collab-reconnect'), inp = $('collab-input'), btn = $('collab-send'); - const avail = S.serviceAvailable; - const trying = !avail && !S.everConnected && S.failCount < FAIL_MAX; - if (off) off.hidden = avail || S.reconnecting || trying; - if (recon) { - recon.hidden = !S.reconnecting; - recon.textContent = S.reconnecting && S.reconnectAt > Date.now() - ? t('collab.reconnect') + ' ' + t('collab.reconnectIn').replace('{n}', Math.ceil((S.reconnectAt - Date.now()) / 1000)) - : t('collab.reconnect'); - } - if (inp) inp.disabled = !avail; - if (btn) btn.disabled = !avail; - syncDraft(); - syncProgressToggle(); - } - - function renderWorkers() { - const box = $('collab-workers'), empty = $('collab-progress-empty'); - if (!box) return; - if (empty) empty.hidden = S.workers.length > 0; - box.innerHTML = S.workers.map(w => ` -
-
${ST_ICONS[w.status] || ''}${esc(t(ST_KEYS[w.status] || 'collab.stPaused'))}${w.updatedAt ? `${esc(relTime(w.updatedAt))}` : ''}
-
${esc(w.title)}
-
${esc(w.summary)}
-
`).join(''); - const running = S.workers.filter(w => w.status === 'running').length; - S.runningCount = running; - document.dispatchEvent(new CustomEvent('collab:running-count', { detail: { count: running } })); - const sticky = $('collab-sticky'); - if (sticky) { sticky.hidden = !running; sticky.textContent = t('collab.sticky').replace('{n}', running); } - const stats = $('collab-progress-stats'); - if (stats) { - const has = S.workers.length > 0; - stats.hidden = !has; - if (has) stats.textContent = t('collab.progressStats') - .replace('{running}', running) - .replace('{done}', S.workers.filter(w => w.status === 'reported').length) - .replace('{issue}', S.workers.filter(w => w.status === 'failed' || w.status === 'terminated').length); - } - } - - function renderUserMsg(item) { - const sub = n => (window.gaFileSubLabel && window.gaFileSubLabel(n)) || n; - const imgs = (item.images || []).map(im => ``).join(''); - const files = (item.files || []).map(f => { - const name = f.name || 'file'; - return `
${FC_SVG}${esc(name)}${esc(sub(name))}
`; - }).join(''); - const clean = stripAttach(item.msg); - const text = clean ? `
${esc(clean).replace(/\n/g, '
')}
` : ''; - return `
${files ? `
${files}
` : ''}${imgs ? `
${imgs}
` : ''}${text}
`; - } - - function renderMsg(item) { - if (item.role === 'user') return renderUserMsg(item); - if (item.role === 'conductor') return `
${md(item.msg)}
`; - return `
${esc(item.msg)}
`; - } - - function syncLayout() { - const body = $('collab-body'); - if (!body) return; - body.classList.toggle('collab-body--split', S.historyReady && S.messages.length > 0); - syncProgressToggle(); - } - - function syncMessages() { - const area = $('collab-msgs'), welcome = $('collab-welcome'), list = $('collab-msg-list'), head = $('collab-chat-head'); - if (!area || !list) return; - if (!S.historyReady) { - area.classList.remove('has-msgs'); - if (welcome) welcome.hidden = true; - if (head) head.hidden = true; - list.hidden = true; - return syncLayout(); - } - const has = S.messages.length > 0; - area.classList.toggle('has-msgs', has); - if (welcome) welcome.hidden = has; - if (head) head.hidden = !has; - list.hidden = !has; - list.innerHTML = S.messages.map(renderMsg).join(''); - syncDraft(); - scrollMsgs(); - syncLayout(); - } - - function pushMsg(item) { - if (item.id && S.messages.some(m => m.id === item.id)) return; - if (item.role === 'user') { - const plain = stripAttach(item.msg); - for (let i = S.messages.length - 1; i >= 0; i--) { - const m = S.messages[i]; - if (m._local && m.role === 'user' && (stripAttach(m.msg) === plain || m.msg === item.msg)) { - S.messages.splice(i, 1); - break; - } - } - } - S.messages.push(item); - if (item.role === 'conductor') S.conductorTyping = false; - syncMessages(); - setConnUi(); - } - - function setWorkers(rawList) { - S.workers = (rawList || []).map(normalizeWorker); - renderWorkers(); - syncLayout(); - } - - function onWsData(data, gen) { - if (gen !== wsGen) return; - if (data.type === 'hello') { - S.historyReady = true; - S.messages = (data.chat || []).map(raw => ({ id: raw.id, role: raw.role || 'system', msg: raw.msg || '', ts: raw.ts, read: raw.read })); - setWorkers(data.subagents || []); - syncMessages(); - setConnUi(); - } else if (data.type === 'subagents') setWorkers(data.items || []); - else if (data.type === 'chat') pushMsg({ id: data.item.id, role: data.item.role || 'system', msg: data.item.msg || '', ts: data.item.ts, read: data.item.read }); - } - - function resetWs() { - wsGen++; - if (!ws) return; - const old = ws; - ws = null; - old.onopen = old.onclose = old.onerror = old.onmessage = null; - try { old.close(); } catch {} - } - - function scheduleReconnect() { - clearTimeout(connectTimer); - clearInterval(reconnectTick); - if (!S.everConnected && S.failCount >= FAIL_MAX) { - S.reconnecting = false; - return setConnUi(); - } - const delay = Math.min(RECON_MAX, RECON_BASE * Math.pow(2, Math.max(0, S.failCount - 1))); - S.reconnectAt = Date.now() + delay; - S.reconnecting = S.everConnected; - setConnUi(); - reconnectTick = setInterval(() => { if (!S.reconnecting) clearInterval(reconnectTick); else setConnUi(); }, 500); - connectTimer = setTimeout(connect, delay); - } - - function connect() { - if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return; - clearTimeout(connectTimer); - clearInterval(reconnectTick); - const gen = ++wsGen; - setConnUi(); - let sock; - try { sock = new WebSocket(wsUrl()); } catch (e) { - if (gen !== wsGen) return; - S.failCount++; - return scheduleReconnect(); - } - ws = sock; - sock.onopen = () => { - if (gen !== wsGen) return; - S.everConnected = true; - S.serviceAvailable = true; - S.reconnecting = false; - S.failCount = 0; - setConnUi(); - }; - sock.onclose = (ev) => { - if (gen !== wsGen) return; - S.serviceAvailable = false; - if (S.everConnected) S.reconnecting = true; - else S.failCount++; - setConnUi(); - scheduleReconnect(); - }; - sock.onerror = () => {}; - sock.onmessage = ev => { - if (gen !== wsGen) return; - try { onWsData(JSON.parse(ev.data), gen); } catch {} - }; - } - - function sendText(rawText) { - const text = (rawText || '').trim(); - if (!text || !ws || ws.readyState !== WebSocket.OPEN) return false; - const expand = window.gaExpandFilePlaceholders || (s => s); - const collect = window.gaCollectUsedFiles || (() => []); - const clearUsed = window.gaClearUsedPendingFiles || (() => {}); - const used = collect(text); - const images = [], files = []; - for (const f of used) (f.isImage ? images : files).push(f.isImage ? { path: f.path, dataUrl: f.dataUrl } : { path: f.path, name: f.name }); - S.messages.push({ id: `_local_${++localSeq}`, _local: true, role: 'user', msg: text, ts: Date.now() / 1000, images, files }); - S.conductorTyping = true; - syncMessages(); - ws.send(JSON.stringify({ msg: expand(text) })); - clearUsed(text); - const inp = $('collab-input'); - if (inp && inp.value.trim() === text) inp.value = ''; - setConnUi(); - return true; - } - - function buildChips() { - const box = $('collab-chips'); - if (box) box.innerHTML = CHIP_KEYS.map(k => ``).join(''); - } - - $('collab-send')?.addEventListener('click', () => sendText($('collab-input')?.value || '')); - $('collab-retry')?.addEventListener('click', () => { S.failCount = 0; S.reconnecting = false; resetWs(); connect(); }); - $('collab-input')?.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendText(e.target.value); } }); - $('collab-chips')?.addEventListener('click', e => { const k = e.target.closest('.collab-chip')?.dataset.textKey; if (k) sendText(t(k)); }); - $('collab-progress-toggle')?.addEventListener('click', () => { S.progressOpen = !S.progressOpen; syncProgressToggle(); }); - document.querySelector('[data-page="collab"]')?.addEventListener('click', e => { - if (!S.progressOpen || e.target.closest('.collab-progress') || e.target.closest('#collab-progress-toggle')) return; - S.progressOpen = false; - syncProgressToggle(); - }); - - window.collabInit = () => { - window.gaSetActiveFileComposer?.('collab'); - buildChips(); - syncMessages(); - setConnUi(); - renderWorkers(); - connect(); - }; - window.collabFocus = () => $('collab-input')?.focus(); - window.collabRetranslate = () => { buildChips(); renderWorkers(); syncMessages(); setConnUi(); }; -})(); diff --git a/frontends/desktop/static/index.html b/frontends/desktop/static/index.html index d89c3642..a319fbdc 100644 --- a/frontends/desktop/static/index.html +++ b/frontends/desktop/static/index.html @@ -3,7 +3,7 @@ GenericAgent - + @@ -508,8 +508,7 @@ - - + diff --git a/frontends/desktop/static/styles.css b/frontends/desktop/static/styles.css index 8d7a28cb..fb359e5a 100644 --- a/frontends/desktop/static/styles.css +++ b/frontends/desktop/static/styles.css @@ -1395,6 +1395,7 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); } .collab-progress-toggle{ display:none !important; flex:0 0 auto; } .collab-progress-empty{ font-size:12px; color:var(--muted); line-height:1.5; margin-bottom:8px; } .collab-card{ + position:relative; border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--card); padding:12px 12px 12px 14px; margin-bottom:10px; border-left:3px solid var(--collab-st, var(--line)); box-shadow:0 2px 8px rgba(0,0,0,.04); @@ -1424,7 +1425,27 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); } .collab-st-ic--pause,.collab-st-ic--off{ color:var(--muted); } .collab-card-time{ margin-left:auto; font-size:10px; color:var(--muted); opacity:.85; } .collab-card-title{ font-size:13px; font-weight:600; color:var(--txt-strong); margin-bottom:6px; } -.collab-card-sum{ font-size:12px; color:var(--muted); line-height:1.45; } +.collab-card-sum{ font-size:12px; color:var(--muted); line-height:1.45; cursor:pointer; } +.collab-drawer-wrap{ position:fixed; inset:0; z-index:550; display:flex; } +.collab-drawer-backdrop{ position:absolute; inset:0; background:var(--overlay); } +.collab-drawer{ + position:absolute; right:0; top:0; bottom:0; width:min(420px,85vw); + background:var(--panel); border-left:1px solid var(--line); + box-shadow:var(--shadow-modal); display:flex; flex-direction:column; + animation:collabDrawerIn .2s ease-out; +} +@keyframes collabDrawerIn{ from{ transform:translateX(100%); } to{ transform:translateX(0); } } +.collab-drawer-head{ + display:flex; align-items:center; justify-content:space-between; + padding:14px 16px; border-bottom:1px solid var(--line-soft); +} +.collab-drawer-title{ font-size:14px; font-weight:700; color:var(--txt-strong); } +.collab-drawer-body{ + flex:1; overflow-y:auto; padding:16px; font-size:13px; line-height:1.6; color:var(--txt); +} +.collab-drawer-body p,.collab-drawer-body ul,.collab-drawer-body ol,.collab-drawer-body pre{ margin:0 0 .5em; } +.collab-drawer-body code{ font-size:12px; background:var(--field-bg); padding:1px 4px; border-radius:4px; } +.collab-drawer-body pre code{ display:block; padding:8px; border-radius:6px; white-space:pre-wrap; } .collab-chat{ padding:0; background:var(--panel); position:relative; } .collab-chat.dragover{ position:relative; } .collab-chat.dragover::after{