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 6fb80c90..a6748770 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -221,9 +221,10 @@ const I18N = { 'app.title': 'GenericAgent 桌面版', 'brand.sub': '桌面终端', 'nav.chat': '聊天', 'nav.channels': '消息通道', 'nav.status': '状态面板', - 'nav.collab': '协作动态', 'nav.token': 'Token 统计', + 'nav.collab': 'Agent 指挥', 'nav.token': 'Token 统计', 'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版', 'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动', + 'preset.butler.t': 'Conductor', 'preset.butler.d': '复杂任务自动拆活,进度在 Conductor 查看', 'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成', 'preset.explore.t': '自主探索', 'preset.explore.d': '自动浏览并周期汇总', 'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚', @@ -258,7 +259,41 @@ const I18N = { 'confirm.modelDelete': '确定删除该模型配置?', 'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)', 'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停', - 'page.collab.title': '协作动态', 'page.collab.sub': 'subagent / Hive worker 的实时状态与产出', + 'page.collab.title': 'Conductor', 'page.collab.sub': '交代目标,自动拆活与跟进', + 'collab.progressTitle': '分工进度', + 'collab.progressEmpty': '还没有任务在执行。告诉 Conductor 你的目标后,这里会显示拆分后的处理进度。', + 'collab.chatTitle': '与 Conductor 对话', + 'collab.placeholder': '描述你想完成的目标,Enter 发送…', + 'collab.guideTitle': '把要完成的事告诉 Conductor', + 'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。', + 'collab.guideStep1t': '描述目标', + 'collab.guideStep1d': '在聊天框里写下你想做的事,发给 Conductor', + 'collab.guideStep2t': '自动拆解', + 'collab.guideStep2d': 'Conductor 自动拆解、分配任务,实时监督和调度', + 'collab.guideStep3t': '交付摘要', + 'collab.guideStep3d': 'Conductor 根据执行状态,呈上任务简报', + 'collab.guideStep4t': '随时调整', + 'collab.guideStep4d': '随时补充要求或细节,Conductor 都会处理', + 'collab.chipProgress': '现在进展如何?', + 'collab.chipPause': '先暂停当前任务', + 'collab.chipSummary': '总结一下目前的结果', + 'collab.showProgressTitle': '查看任务执行进度(窄屏)', + 'collab.progressStats': '{running} 进行中 · {done} 已完成 · {issue} 需关注', + 'collab.typing': 'Conductor 正在处理', + 'collab.offline': '无法连接 Conductor(8900)。请确认服务已启动且本地已穿透 8900 端口。', + 'collab.retry': '重试', + 'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。', + 'collab.reconnectIn': '{n} 秒后重试', + 'collab.sticky': '{n} 个任务进行中', + 'collab.stRunning': '执行中', 'collab.stReported': '已回报', 'collab.stPaused': '已暂停', + 'collab.stFailed': '遇到问题', 'collab.stTerminated': '已终止', + 'collab.summaryRunning': '正在处理中…', 'collab.summaryWait': '等待回报', + 'collab.taskFallback': '任务 {n}', + 'collab.timeJust': '刚刚', + 'collab.timeSec': '{n} 秒前', + 'collab.timeMin': '{n} 分钟前', + 'collab.timeHr': '{n} 小时前', + 'collab.timeDay': '{n} 天前', 'page.token.title': 'Token 统计', 'page.token.sub': '每会话与累计的 token 用量及估算成本', 'status.connecting': '连接中…', 'status.ready': '就绪', 'status.running': '运行中', 'status.disconnected': '未连接', 'status.stopped': '已停止', 'status.idle': '空闲', @@ -327,9 +362,10 @@ const I18N = { 'app.title': 'GenericAgent Desktop', 'brand.sub': 'Desktop terminal', 'nav.chat': 'Chat', 'nav.channels': 'Channels', 'nav.status': 'Status', - 'nav.collab': 'Collaboration', 'nav.token': 'Token usage', + 'nav.collab': 'Conductor', 'nav.token': 'Token usage', 'foot.settings': 'Settings', 'foot.ver': 'GenericAgent · Desktop', 'chat.startTitle': 'Start a conversation', 'chat.startSub': 'Type a message, or pick a preset', + 'preset.butler.t': 'Conductor', 'preset.butler.d': 'Delegate complex tasks; track progress in Conductor', 'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously', 'preset.explore.t': 'Auto explore', 'preset.explore.d': 'Browse & summarize periodically', 'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration', @@ -364,7 +400,41 @@ const I18N = { 'confirm.modelDelete': 'Delete this model profile?', 'page.channels.title': 'Channels', 'page.channels.sub': 'Background IM processes: list, start/stop, logs (hub.pyw style)', 'page.status.title': 'Status', 'page.status.sub': 'Background processes/services managed by hub.pyw', - 'page.collab.title': 'Collaboration', 'page.collab.sub': 'Live state & output of subagents / Hive workers', + 'page.collab.title': 'Conductor', 'page.collab.sub': 'Describe a goal — split, delegate, and follow up', + 'collab.progressTitle': 'Progress', + 'collab.progressEmpty': 'No tasks running yet. After you describe a goal to Conductor, split tasks will appear here.', + 'collab.chatTitle': 'Chat with Conductor', + 'collab.placeholder': 'Describe your goal, Enter to send…', + 'collab.guideTitle': 'Tell Conductor what you want done', + 'collab.guideWhen': 'Best for multi-step goals that take a while. For everyday chat and quick questions, use Chat in the sidebar.', + 'collab.guideStep1t': 'Describe your goal', + 'collab.guideStep1d': 'Write what you want done in the chat box and send it to Conductor', + 'collab.guideStep2t': 'Auto breakdown', + 'collab.guideStep2d': 'Conductor breaks down, assigns, monitors, and coordinates', + 'collab.guideStep3t': 'Summary', + 'collab.guideStep3d': 'Conductor delivers a briefing based on execution status', + 'collab.guideStep4t': 'Adjust anytime', + 'collab.guideStep4d': 'Add requirements or details anytime — Conductor handles them', + 'collab.chipProgress': 'How is it going?', + 'collab.chipPause': 'Pause current tasks', + 'collab.chipSummary': 'Summarize progress so far', + 'collab.showProgressTitle': 'View task progress (narrow screens)', + 'collab.progressStats': '{running} running · {done} done · {issue} need attention', + 'collab.typing': 'Conductor is working', + 'collab.offline': 'Cannot reach Conductor (8900). Start the service and forward port 8900.', + 'collab.retry': 'Retry', + 'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.', + 'collab.reconnectIn': 'Retry in {n}s', + 'collab.sticky': '{n} task(s) running', + 'collab.stRunning': 'Running', 'collab.stReported': 'Reported', 'collab.stPaused': 'Paused', + 'collab.stFailed': 'Issue', 'collab.stTerminated': 'Ended', + 'collab.summaryRunning': 'Working…', 'collab.summaryWait': 'Awaiting report', + 'collab.taskFallback': 'Task {n}', + 'collab.timeJust': 'just now', + 'collab.timeSec': '{n}s ago', + 'collab.timeMin': '{n}m ago', + 'collab.timeHr': '{n}h ago', + 'collab.timeDay': '{n}d ago', 'page.token.title': 'Token usage', 'page.token.sub': 'Per-session and total token usage & estimated cost', 'status.connecting': 'Connecting…', 'status.ready': 'Ready', 'status.running': 'Running', 'status.disconnected': 'Disconnected', 'status.stopped': 'Stopped', 'status.idle': 'Idle', @@ -499,6 +569,14 @@ async function bridgeFetch(path, opts = {}) { return data; } function t(key) { return (I18N[lang] && I18N[lang][key]) || (I18N.zh[key]) || key; } +window.gaT = t; +document.addEventListener('collab:running-count', e => { + const b = document.getElementById('collab-badge'); + if (!b) return; + const n = e.detail?.count || 0; + b.hidden = !n; + b.textContent = n ? (n > 9 ? '9+' : String(n)) : ''; +}); function optionalPh(key) { const sep = (lang === 'en') ? ', ' : ','; return `${t('common.optional')}${sep}${t(key)}`; @@ -513,6 +591,7 @@ function applyI18n() { }); document.querySelectorAll('[data-i18n-title]').forEach(el => { el.setAttribute('title', t(el.dataset.i18nTitle)); }); renderLangList(); + window.collabRetranslate?.(); } // 语言对应国旗 SVG(en 用美国旗,按要求) const FLAGS = { @@ -665,12 +744,22 @@ function applyAppearance(nextApp, nextPlain, { persist } = { persist: true }) { /* ═══════════════ 侧边栏导航 ═══════════════ */ const nav = document.getElementById('nav'); const pages = document.querySelectorAll('#pages .page'); +let currentPage = 'chat'; +function gaGoPage(key) { + const item = nav?.querySelector(`.nav-item[data-page="${key}"]`); + if (!item) return; + currentPage = key; + nav.querySelectorAll('.nav-item').forEach(n => n.classList.toggle('active', n === item)); + pages.forEach(p => p.classList.toggle('active', p.dataset.page === key)); + renderSessionList(); + window.gaSetActiveFileComposer?.(key === 'collab' ? 'collab' : 'chat'); + if (key === 'collab') window.collabInit?.(); +} +window.gaGoPage = gaGoPage; nav.addEventListener('click', (e) => { const item = e.target.closest('.nav-item'); if (!item) return; - const key = item.dataset.page; - nav.querySelectorAll('.nav-item').forEach(n => n.classList.toggle('active', n === item)); - pages.forEach(p => p.classList.toggle('active', p.dataset.page === key)); + gaGoPage(item.dataset.page); }); /* ═══════════════ 弹窗开关 ═══════════════ */ @@ -1313,7 +1402,7 @@ function renderSessionList() { const r = state.runtime.get(sess.id); const busy = !!(r && r.busy); const item = document.createElement('div'); - item.className = 'conv-item' + (sess.id === state.activeId ? ' active' : '') + (busy ? '' : ' idle'); + item.className = 'conv-item' + (currentPage === 'chat' && sess.id === state.activeId ? ' active' : '') + (busy ? '' : ' idle'); item.dataset.id = sess.id; const pinSvg = sess.pinned ? `` : ''; item.innerHTML = @@ -1485,8 +1574,9 @@ async function pollSession(sess) { function removeUsedPendingFiles(usedFiles) { if (!usedFiles.length) return; const usedSids = new Set(usedFiles.map(f => f.sid)); + const touched = new Set(usedFiles.map(f => fileCtx(f))); state.pendingFiles = state.pendingFiles.filter(f => !usedSids.has(f.sid)); - renderThumbStrip(); + touched.forEach(ctx => renderThumbStrip(ctx)); } function clearDraft(sess) { @@ -1702,6 +1792,8 @@ document.querySelectorAll('.feature-grid').forEach(grid => { return; } if (!key) { inputEl.focus(); closeModals(); return; } + const bp = BUILTIN_PRESETS.find(p => p.key === key); + if (bp?.navigate) { closeModals(); gaGoPage(bp.navigate); window.collabFocus?.(); return; } const prompt = I18N[lang]['presetPrompt.' + key] || I18N.zh['presetPrompt.' + key]; closeModals(); if (prompt) sendPrompt(prompt); @@ -2010,6 +2102,72 @@ const imgInput = document.getElementById('img-input'); const thumbStrip = document.getElementById('thumb-strip'); const uploadBtn = document.getElementById('upload-btn'); const chatPanel = document.querySelector('main.main'); +let activeFileComposer = 'chat'; + +function fileCtx(f) { return f.ctx || 'chat'; } +function filesForCtx(ctx) { return state.pendingFiles.filter(f => fileCtx(f) === ctx); } + +function composerCfg(ctx = activeFileComposer) { + if (ctx === 'collab') { + return { + input: document.getElementById('collab-input'), + strip: document.getElementById('collab-thumb-strip'), + uploadBtn: document.getElementById('collab-upload-btn'), + imgInput: document.getElementById('collab-img-input'), + dropZone: document.querySelector('.collab-chat'), + }; + } + return { input: inputEl, strip: thumbStrip, uploadBtn, imgInput, dropZone: chatPanel }; +} + +function renderThumbStrip(ctx = activeFileComposer) { + const cfg = composerCfg(ctx); + if (!cfg.strip) return; + const files = filesForCtx(ctx); + if (files.length === 0) { + cfg.strip.innerHTML = ''; + cfg.strip.hidden = true; + return; + } + cfg.strip.innerHTML = files.map(f => { + if (f.isImage && f.dataUrl) { + return `
`; + } + const name = f.name || 'file'; + const label = name.replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c])); + const sub = fileSubLabel(name).replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c])); + const path = (f.path || '').replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])); + const dataName = name.replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])); + return `
${label}${sub}
`; + }).join(''); + cfg.strip.hidden = false; + applyI18n(); +} + +function insertPlaceholderInComposer(marker, ctx = activeFileComposer) { + const input = composerCfg(ctx).input; + if (!input) return; + const start = input.selectionStart ?? input.value.length; + const end = input.selectionEnd ?? input.value.length; + const before = input.value.slice(0, start); + const after = input.value.slice(end); + const needSpace = before && !/\s$/.test(before); + const insertion = (needSpace ? ' ' : '') + marker + ' '; + input.value = before + insertion + after; + const caret = (before + insertion).length; + input.setSelectionRange(caret, caret); + input.dispatchEvent(new Event('input', { bubbles: true })); + input.focus(); +} + +function removePlaceholderFromComposer(file) { + const input = composerCfg(fileCtx(file)).input; + if (!input) return; + const marker = placeholderFor(file); + const re = new RegExp('\\s?' + marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s?', ''); + input.value = input.value.replace(re, ' ').replace(/ +/g, ' ').trim(); + input.dispatchEvent(new Event('input', { bubbles: true })); +} function isImageFile(f) { return (f && (f.type || '').startsWith('image/')) || IMG_EXT_RE.test(f?.name || ''); @@ -2019,51 +2177,6 @@ function placeholderFor(file) { return file.isImage ? `[Image #${file.sid}]` : `[File #${file.sid}]`; } -function renderThumbStrip() { - if (!thumbStrip) return; - if (state.pendingFiles.length === 0) { - thumbStrip.innerHTML = ''; - thumbStrip.hidden = true; - return; - } - thumbStrip.innerHTML = state.pendingFiles.map(f => { - if (f.isImage && f.dataUrl) { - return `
`; - } - const name = f.name || 'file'; - const label = name.replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c])); - const sub = fileSubLabel(name).replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c])); - const path = (f.path || '').replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c])); - const dataName = name.replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c])); - return `
${label}${sub}
`; - }).join(''); - thumbStrip.hidden = false; - applyI18n(); -} - -function insertPlaceholderInComposer(marker) { - if (!inputEl) return; - const start = inputEl.selectionStart ?? inputEl.value.length; - const end = inputEl.selectionEnd ?? inputEl.value.length; - const before = inputEl.value.slice(0, start); - const after = inputEl.value.slice(end); - const needSpace = before && !/\s$/.test(before); - const insertion = (needSpace ? ' ' : '') + marker + ' '; - inputEl.value = before + insertion + after; - const caret = (before + insertion).length; - inputEl.setSelectionRange(caret, caret); - inputEl.dispatchEvent(new Event('input', { bubbles: true })); - inputEl.focus(); -} - -function removePlaceholderFromComposer(file) { - if (!inputEl) return; - const marker = placeholderFor(file); - const re = new RegExp('\\s?' + marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s?', ''); - inputEl.value = inputEl.value.replace(re, ' ').replace(/ +/g, ' ').trim(); - inputEl.dispatchEvent(new Event('input', { bubbles: true })); -} - function expandFilePlaceholders(text) { return text.replace(/\[(Image|File) #(\d+)\]/g, (m, kind, n) => { const f = state.pendingFiles.find(x => x.sid === Number(n)); @@ -2109,11 +2222,16 @@ async function addFiles(fileList) { if (skipped) showChanToast(t('upload.tooLarge'), '', 'err'); return; } - // group uploads under the active session (create one if there's none yet) - let upSess = activeSess(); - if (!upSess) { await newSession(); upSess = activeSess(); } - if (upSess && !upSess.bridgeSessionId) { try { await ensureBridgeSession(upSess); } catch (_) {} } - const uploadSid = (upSess && upSess.bridgeSessionId) || ''; + const ctx = activeFileComposer; + let uploadSid = ''; + if (ctx === 'collab') { + uploadSid = 'collab'; + } else { + let upSess = activeSess(); + if (!upSess) { await newSession(); upSess = activeSess(); } + if (upSess && !upSess.bridgeSessionId) { try { await ensureBridgeSession(upSess); } catch (_) {} } + uploadSid = (upSess && upSess.bridgeSessionId) || ''; + } for (const f of accepted) { try { const dataUrl = await new Promise((resolve, reject) => { @@ -2129,10 +2247,11 @@ async function addFiles(fileList) { const entry = { sid, name: f.name || 'file', isImage, path, dataUrl: isImage ? dataUrl : '', + ctx, }; state.pendingFiles.push(entry); - insertPlaceholderInComposer(placeholderFor(entry)); - renderThumbStrip(); + insertPlaceholderInComposer(placeholderFor(entry), ctx); + renderThumbStrip(ctx); } catch (e) { showChanToast(t('upload.failed'), e.message || String(e), 'err'); } @@ -2140,26 +2259,16 @@ async function addFiles(fileList) { if (skipped) showChanToast(t('upload.tooLarge'), '', 'err'); } -if (uploadBtn && imgInput) uploadBtn.addEventListener('click', (e) => { - e.preventDefault(); - imgInput.click(); -}); - -if (imgInput) imgInput.addEventListener('change', () => { - addFiles(imgInput.files); - imgInput.value = ''; -}); - -if (thumbStrip) thumbStrip.addEventListener('click', (e) => { +function handleThumbStripClick(e, ctx) { const x = e.target.closest('.x'); if (x) { const sid = Number(x.dataset.sid); - const idx = state.pendingFiles.findIndex(f => f.sid === sid); + const idx = state.pendingFiles.findIndex(f => f.sid === sid && fileCtx(f) === ctx); if (idx >= 0) { const removed = state.pendingFiles[idx]; state.pendingFiles.splice(idx, 1); removePlaceholderFromComposer(removed); - renderThumbStrip(); + renderThumbStrip(ctx); if (removed.path) { fetch(`http://${location.hostname}:14168/upload`, { method: 'DELETE', @@ -2179,51 +2288,25 @@ if (thumbStrip) thumbStrip.addEventListener('click', (e) => { } const img = e.target.closest('img'); if (img && img.src) openLightbox(img.src); -}); - -/* ─── drag & drop on chat panel ─── */ -if (chatPanel) { - let dragDepth = 0; - const hasFiles = (e) => { - const types = e.dataTransfer && e.dataTransfer.types; - if (!types) return false; - for (let i = 0; i < types.length; i += 1) { - if (types[i] === 'Files') return true; - } - return false; - }; - // Prevent the browser from opening dropped files outside chatPanel. - window.addEventListener('dragover', (e) => { if (hasFiles(e)) e.preventDefault(); }); - window.addEventListener('drop', (e) => { if (hasFiles(e)) e.preventDefault(); }); - chatPanel.addEventListener('dragenter', (e) => { - if (!hasFiles(e)) return; - e.preventDefault(); - dragDepth += 1; - chatPanel.classList.add('dragover'); - chatPanel.dataset.dropHint = t('upload.dropHint'); - }); - chatPanel.addEventListener('dragover', (e) => { - if (!hasFiles(e)) return; - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - }); - chatPanel.addEventListener('dragleave', (e) => { - if (!hasFiles(e)) return; - dragDepth = Math.max(0, dragDepth - 1); - if (dragDepth === 0) chatPanel.classList.remove('dragover'); - }); - chatPanel.addEventListener('drop', (e) => { - if (!hasFiles(e)) return; - e.preventDefault(); - dragDepth = 0; - chatPanel.classList.remove('dragover'); - addFiles(e.dataTransfer.files); - }); } -/* ─── paste file/image into composer ─── */ -if (inputEl) { - inputEl.addEventListener('paste', (e) => { +function bindComposerUpload(ctx) { + const cfg = composerCfg(ctx); + if (!cfg.uploadBtn || cfg.uploadBtn.dataset.bound) return; + cfg.uploadBtn.dataset.bound = '1'; + cfg.uploadBtn.addEventListener('click', (e) => { + e.preventDefault(); + activeFileComposer = ctx; + cfg.imgInput?.click(); + }); + cfg.imgInput?.addEventListener('change', () => { + activeFileComposer = ctx; + addFiles(cfg.imgInput.files); + cfg.imgInput.value = ''; + }); + cfg.strip?.addEventListener('click', (e) => handleThumbStripClick(e, ctx)); + cfg.input?.addEventListener('paste', (e) => { + activeFileComposer = ctx; const items = e.clipboardData && e.clipboardData.items; if (!items) return; const files = []; @@ -2237,6 +2320,66 @@ if (inputEl) { e.preventDefault(); addFiles(files); }); + const zone = cfg.dropZone; + if (!zone || zone.dataset.dropBound) return; + zone.dataset.dropBound = '1'; + let dragDepth = 0; + const hasFiles = (e) => { + const types = e.dataTransfer && e.dataTransfer.types; + if (!types) return false; + for (let i = 0; i < types.length; i += 1) { + if (types[i] === 'Files') return true; + } + return false; + }; + zone.addEventListener('dragenter', (e) => { + if (!hasFiles(e)) return; + e.preventDefault(); + activeFileComposer = ctx; + dragDepth += 1; + zone.classList.add('dragover'); + zone.dataset.dropHint = t('upload.dropHint'); + }); + zone.addEventListener('dragover', (e) => { + if (!hasFiles(e)) return; + e.preventDefault(); + activeFileComposer = ctx; + e.dataTransfer.dropEffect = 'copy'; + }); + zone.addEventListener('dragleave', (e) => { + if (!hasFiles(e)) return; + dragDepth = Math.max(0, dragDepth - 1); + if (dragDepth === 0) zone.classList.remove('dragover'); + }); + zone.addEventListener('drop', (e) => { + if (!hasFiles(e)) return; + e.preventDefault(); + dragDepth = 0; + zone.classList.remove('dragover'); + activeFileComposer = ctx; + addFiles(e.dataTransfer.files); + }); +} + +bindComposerUpload('chat'); +bindComposerUpload('collab'); + +Object.assign(window, { + gaSetActiveFileComposer: ctx => { activeFileComposer = ctx === 'collab' ? 'collab' : 'chat'; }, + gaExpandFilePlaceholders: expandFilePlaceholders, + gaCollectUsedFiles: collectUsedFiles, + gaClearUsedPendingFiles: text => removeUsedPendingFiles(collectUsedFiles(text)), + gaFileSubLabel: fileSubLabel, +}); + +if (chatPanel) { + const blockFileDrop = e => { + const types = e.dataTransfer?.types; + if (!types) return; + for (let i = 0; i < types.length; i += 1) if (types[i] === 'Files') { e.preventDefault(); return; } + }; + window.addEventListener('dragover', blockFileDrop); + window.addEventListener('drop', blockFileDrop); } /* ═══════════════ bridge 事件 ═══════════════ */ @@ -2423,6 +2566,8 @@ const CP_KEY = 'ga_custom_presets'; const HB_KEY = 'ga_hidden_builtins'; const BUILTIN_PRESETS = [ + { key: 'butler', titleKey: 'preset.butler.t', descKey: 'preset.butler.d', navigate: 'collab', + iconSvg: '' }, { key: 'goal', titleKey: 'preset.goal.t', descKey: 'preset.goal.d', promptKey: 'presetPrompt.goal', iconSvg: '' }, { key: 'explore', titleKey: 'preset.explore.t', descKey: 'preset.explore.d', promptKey: 'presetPrompt.explore', @@ -3017,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/index.html b/frontends/desktop/static/index.html index 2ffc4c40..a319fbdc 100644 --- a/frontends/desktop/static/index.html +++ b/frontends/desktop/static/index.html @@ -3,7 +3,7 @@ GenericAgent - + @@ -73,6 +73,7 @@ + @@ -172,16 +173,72 @@ - +
-
-
-
-
-
hive-master
-
hive-worker-1
-
hive-worker-2
-
subagent-abc
+ + + +
+ +
+ +
+ + +
+
+
+ +
+
+ + + +
+
+ +
+
@@ -451,7 +508,7 @@ - + diff --git a/frontends/desktop/static/styles.css b/frontends/desktop/static/styles.css index 09469d74..fb359e5a 100644 --- a/frontends/desktop/static/styles.css +++ b/frontends/desktop/static/styles.css @@ -1349,3 +1349,186 @@ html[data-appearance="dark"] .bubble.md pre code{ color:var(--txt-2); } padding:0 6px; margin-bottom:2px; border-radius:var(--radius-sm); } + +/* ---- Conductor ---- */ +.page[data-page="collab"]{ + padding:0; min-height:0; overflow:hidden; + background: + radial-gradient(ellipse 120% 80% at 50% -20%, color-mix(in srgb, var(--blue) 8%, transparent), transparent 55%), + var(--panel); +} +.collab-offline,.collab-reconnect{ + flex:0 0 auto; padding:10px 16px; font-size:13px; text-align:center; + background:color-mix(in srgb, var(--amber) 12%, var(--panel)); + border-bottom:1px solid var(--line); align-items:center; justify-content:center; gap:10px; +} +.collab-offline[hidden], .collab-reconnect[hidden], .collab-sticky[hidden], #collab-welcome[hidden], #collab-chat-head[hidden], .collab-msgs .msgs[hidden], .collab-progress-stats[hidden]{ display:none !important; } +.collab-offline:not([hidden]), .collab-reconnect:not([hidden]){ display:flex; } +.collab-reconnect{ background:color-mix(in srgb, var(--blue) 10%, var(--panel)); } +.collab-sticky{ + flex:0 0 auto; padding:8px 16px; font-size:12px; color:var(--muted); + border-bottom:1px solid var(--line-soft); background:var(--field-bg); +} +.collab-sticky:not([hidden]){ display:block; } +.collab-body{ + flex:1 1 0; min-height:0; display:grid; grid-template-columns:1fr; gap:0; overflow:hidden; +} +.collab-body.collab-body--split{ grid-template-columns:minmax(240px,36%) 1fr; } +.collab-body:not(.collab-body--split) .collab-progress{ display:none; } +.collab-progress,.collab-chat{ min-height:0; min-width:0; display:flex; flex-direction:column; overflow:hidden; } +.collab-chat{ flex:1; } +.collab-progress{ + border-right:1px solid var(--line-soft); + background:linear-gradient(180deg, var(--field-bg), var(--panel)); + padding:16px; overflow:auto; +} +.collab-progress-stats{ + font-size:11px; font-weight:600; color:var(--muted); letter-spacing:.02em; + padding:8px 10px; margin-bottom:10px; border-radius:var(--radius-sm); + background:color-mix(in srgb, var(--panel) 70%, var(--field-bg)); + border:1px solid var(--line-soft); +} +.collab-progress-head,.collab-chat-head{ + font-size:13px; font-weight:700; color:var(--txt-strong); margin-bottom:12px; flex:0 0 auto; +} +.collab-chat-head{ display:flex; align-items:center; justify-content:space-between; gap:10px; padding:16px 20px 0; } +.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); + transition:border-color .3s, box-shadow .3s, transform .2s; +} +.collab-card:hover{ transform:translateY(-1px); box-shadow:0 4px 14px rgba(0,0,0,.07); } +.collab-card--running{ --collab-st:var(--ok); } +.collab-card--reported{ --collab-st:var(--muted); } +.collab-card--paused{ --collab-st:var(--amber); } +.collab-card--failed{ --collab-st:var(--danger); } +.collab-card--terminated{ --collab-st:var(--line-strong); } +.collab-card-st{ display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); margin-bottom:6px; flex-wrap:wrap; } +.collab-dot{ width:7px; height:7px; border-radius:50%; background:var(--collab-st); flex:0 0 auto; } +.collab-card--running .collab-dot{ animation:collab-pulse 1.6s ease-in-out infinite; } +@keyframes collab-pulse{ + 0%,100%{ opacity:1; transform:scale(1); box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 40%, transparent); } + 50%{ opacity:.85; transform:scale(1.15); box-shadow:0 0 0 4px color-mix(in srgb, var(--ok) 0%, transparent); } +} +.collab-st-ic{ display:inline-flex; align-items:center; justify-content:center; width:14px; height:14px; font-size:10px; font-weight:700; flex:0 0 auto; } +.collab-st-ic--spin{ + width:12px; height:12px; border:2px solid color-mix(in srgb, var(--ok) 25%, transparent); + border-top-color:var(--ok); border-radius:50%; animation:collab-spin .8s linear infinite; +} +@keyframes collab-spin{ to{ transform:rotate(360deg); } } +.collab-st-ic--ok{ color:var(--ok); } +.collab-st-ic--warn{ color:var(--danger); } +.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; 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{ + content:attr(data-drop-hint); position:absolute; inset:8px; z-index:20; pointer-events:none; + display:flex; align-items:center; justify-content:center; border-radius:var(--radius); + border:2px dashed var(--blue); background:color-mix(in srgb, var(--blue) 8%, var(--panel)); + font-size:13px; color:var(--blue); font-weight:600; +} +.collab-composer{ flex:0 0 auto; background:var(--panel); } +.collab-composer .composer-top{ align-items:center; flex-wrap:wrap; } +.collab-chips{ display:flex; flex-wrap:wrap; gap:6px; flex:1; min-width:0; padding:0; } +.collab-msgs{ flex:1 1 0; min-height:0; } +.collab-msgs:not(.has-msgs){ position:relative; } +.collab-msg-enter{ animation:collab-msg-in .35s ease-out both; } +.bubble.sys .collab-wait-dots{ + display:inline-flex; align-items:center; justify-content:center; gap:5px; + vertical-align:middle; padding:2px 0; +} +.collab-wait-dots i{ + display:block; width:5px; height:5px; border-radius:50%; background:var(--muted); + animation:collab-wait-dot 1.2s ease-in-out infinite; font-style:normal; +} +.collab-wait-dots i:nth-child(1){ animation-delay:0s; } +.collab-wait-dots i:nth-child(2){ animation-delay:.2s; } +.collab-wait-dots i:nth-child(3){ animation-delay:.4s; } +@keyframes collab-wait-dot{ + 0%,80%,100%{ opacity:.35; transform:translateY(0); } + 40%{ opacity:1; transform:translateY(-3px); } +} +@keyframes collab-msg-in{ + from{ opacity:0; transform:translateY(8px); } + to{ opacity:1; transform:translateY(0); } +} +.collab-guide{ width:100%; max-width:var(--chat-max-w,920px); margin:0 auto; padding:96px 0 0; } +.collab-guide .feature-head{ + position:absolute; top:32px; + left:max(28px, calc(50% - var(--chat-max-w, 920px) / 2 + 28px)); + right:28px; max-width:var(--chat-max-w,920px); + margin:0; text-align:left; +} +.collab-guide .page-title{ font-size:20px; line-height:1.35; margin-bottom:2px; } +.collab-guide .page-sub{ font-size:13px; line-height:1.6; margin-top:10px; margin-bottom:0; } +.collab-guide-steps{ + display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:48px 56px; + margin:0 0 8px; align-items:start; +} +.collab-guide-step{ + display:flex; flex-direction:column; align-items:center; gap:22px; + text-align:center; min-height:100%; +} +.collab-guide-ic{ + flex:0 0 auto; width:52px; height:52px; border-radius:14px; + display:flex; align-items:center; justify-content:center; + background:color-mix(in srgb, var(--blue) 10%, var(--panel)); + color:var(--blue); + border:1px solid color-mix(in srgb, var(--blue) 16%, var(--line)); +} +.collab-guide-ic svg{ width:24px; height:24px; } +.collab-guide-txt{ display:flex; flex-direction:column; gap:12px; width:100%; } +.collab-guide-label{ font-size:14px; font-weight:600; color:var(--txt-strong); line-height:1.45; letter-spacing:.01em; } +.collab-guide-desc{ font-size:12px; font-weight:400; font-style:italic; color:var(--muted); line-height:1.7; } +.collab-nav-badge{ + margin-left:auto; min-width:18px; height:18px; padding:0 5px; border-radius:999px; + background:var(--ok); color:#fff; font-size:10px; font-weight:700; line-height:18px; text-align:center; +} +@media (max-width:900px){ + .collab-body.collab-body--split{ grid-template-columns:1fr; } + .collab-body.collab-body--split .collab-progress{ + display:none; position:fixed; z-index:40; top:0; left:0; bottom:0; width:min(88vw,320px); + max-width:320px; box-shadow:8px 0 24px rgba(0,0,0,.12); + animation:collab-drawer-in .25s ease-out; + } + .collab-body.collab-body--split.collab-progress-open .collab-progress{ display:flex; } + .collab-body.collab-body--split.collab-progress-open::before{ + content:''; position:fixed; inset:0; z-index:35; + background:rgba(0,0,0,.25); animation:collab-fade-in .2s ease-out; + } + .collab-body.collab-body--split .collab-progress-toggle:not([hidden]){ display:inline-flex !important; } + @keyframes collab-drawer-in{ from{ transform:translateX(-12px); opacity:0; } to{ transform:translateX(0); opacity:1; } } + @keyframes collab-fade-in{ from{ opacity:0; } to{ opacity:1; } } + .collab-guide{ padding-top:88px; } + .collab-guide .feature-head{ top:24px; left:24px; right:24px; } + .collab-guide-steps{ grid-template-columns:repeat(2, minmax(0, 1fr)); gap:40px 32px; } + .collab-guide-step{ gap:20px; } + .collab-msgs:not(.has-msgs){ padding:20px 20px 12px; } +}