fix(desktop): refine chat ui and persistence
This commit is contained in:
+280
-435
@@ -306,355 +306,8 @@ let bridgeUiOffline = false;
|
||||
})();
|
||||
|
||||
/* ═══════════════ i18n ═══════════════ */
|
||||
const I18N = {
|
||||
zh: {
|
||||
'app.title': 'GenericAgent 桌面版',
|
||||
'brand.sub': '桌面终端',
|
||||
'nav.chat': '聊天', 'nav.services': '后台服务', 'nav.channels': '消息通道', 'nav.status': '状态面板',
|
||||
'nav.collab': '指挥家', 'nav.token': '用量',
|
||||
'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版',
|
||||
'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动',
|
||||
'preset.butler.t': '指挥家', 'preset.butler.d': '复杂任务自动拆解,只需查看进度和简报',
|
||||
'preset.plan.t': 'Plan 模式', 'preset.plan.d': '加载 Plan SOP,按探索→规划→执行→验证流程',
|
||||
'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成',
|
||||
'preset.autonomous.t': '自主行动', 'preset.autonomous.d': '按 SOP 规划/执行任务,产出报告(reflect/autonomous.py 同源)',
|
||||
'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚',
|
||||
'preset.review.t': '深度复核', 'preset.review.d': '挑刺式质量把关',
|
||||
'preset.findwork.t': '找点事做', 'preset.findwork.d': '分析当前情况,推荐一批让你感兴趣的 TODO',
|
||||
'preset.mine.t': '我的·周报', 'preset.mine.d': '自定义:抓本周提交并写周报',
|
||||
'preset.add.t': '自定义', 'preset.add.d': '任意一句话存为功能',
|
||||
'composer.placeholder': 'GA 能帮你做些什么?',
|
||||
'search.placeholder': '搜索会话…', 'conv.new': '新对话',
|
||||
'ctx.pin': '置顶', 'ctx.unpin': '取消置顶', 'ctx.rename': '重命名', 'ctx.del': '删除',
|
||||
'common.close': '关闭', 'common.more': '更多', 'common.optional': '选填', 'common.save': '保存',
|
||||
'modal.preset': '预设功能', 'modal.addModel': '添加模型', 'modal.editModel': '编辑模型', 'modal.settings': '配置',
|
||||
'modal.customPreset': '自定义预设',
|
||||
'modal.editCustomPreset': '编辑任务',
|
||||
'customPreset.titlePh': '标题,例如「写周报」',
|
||||
'customPreset.promptPh': 'Prompt 内容,发送时会作为消息提交',
|
||||
'customPreset.empty': '标题和 Prompt 不能为空',
|
||||
'customPreset.removeTitle': '删除',
|
||||
'customPreset.editTitle': '编辑',
|
||||
'builtinPreset.restoreBtn': '恢复默认预设',
|
||||
'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py)', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理',
|
||||
'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?',
|
||||
'appearance.light': '浅色', 'appearance.dark': '深色',
|
||||
'set.noModels': '暂无模型,点击下方添加',
|
||||
'lang.zh': '简体中文', 'lang.en': 'English',
|
||||
'model.name': '备注', 'model.namePh': '会显示在模型列表',
|
||||
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': '留空则保持原 Key 不变',
|
||||
'model.apibase': 'API 地址', 'model.apibasePh': 'https://.../v1/messages',
|
||||
'model.protocol': '协议', 'model.protocolPick': '请选择…', 'model.protocolOai': 'OpenAI 兼容 (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
|
||||
'model.stream': '响应方式', 'model.streamOn': '流式', 'model.streamOff': '非流式',
|
||||
'model.model': '模型', 'model.modelPh': 'model 参数名',
|
||||
'model.modelHint': '须与中转站/官方文档中的 model 字段完全一致',
|
||||
'model.retries': '重试 (次)', 'model.connTimeout': '连接超时 (s)', 'model.readTimeout': '读取超时 (s)',
|
||||
'model.save': '保存', 'common.cancel': '取消', 'common.confirm': '确认', 'common.edit': '编辑', 'common.delete': '删除',
|
||||
'pq.title': '快速接入官方模型', 'pq.sub': '填好 API Key 即可使用', 'pq.toggle': '展开 / 收起',
|
||||
'pq.deepseekDesc': '官方 API · OpenAI 兼容', 'pq.qwenDesc': '通义千问 · 阿里云百炼',
|
||||
'guide.step1': '点击下方链接,登录后创建并复制 API Key',
|
||||
'guide.step2': '把 Key 粘贴到下方「API Key」输入框',
|
||||
'guide.step3': '点击保存,即可在模型列表中选用',
|
||||
'guide.prefillTip': '已为你预填 API 地址、协议与模型,可按需修改',
|
||||
'guide.getKey': '获取 {name} 的 API Key', 'guide.copy': '复制链接', 'guide.copied': '链接已复制',
|
||||
'err.modelSave': '保存失败', 'err.modelSwitch': '切换模型失败', 'err.modelRequired': '请填写模型、API Key 和 API 地址',
|
||||
'err.modelDelete': '删除失败', 'err.modelDeleteLast': '至少保留一个模型',
|
||||
'confirm.modelDelete': '确定删除该模型配置?',
|
||||
'model.aggregation': '渠道组(自动故障转移)', 'model.aggregationShort': '渠道组', 'model.aggregationDesc': '按顺序尝试,失败自动切换到下一个',
|
||||
'model.emptyMixin': '尚未加入模型',
|
||||
'model.addToMixin': '加入渠道组', 'model.inMixin': '已在渠道组', 'model.removeFromMixin': '移出渠道组', 'model.alreadyInMixin': '已在渠道组中', 'model.dragReorder': '拖拽调整顺序',
|
||||
'err.mixinFailed': '操作失败',
|
||||
'page.services.title': '后台服务', 'page.services.sub': 'IM 消息通道与后台进程,集中查看、启停与日志',
|
||||
'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)',
|
||||
'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停',
|
||||
'page.collab.title': '指挥家', 'page.collab.sub': '交代目标,自动拆活与跟进',
|
||||
'collab.progressTitle': '分工进度',
|
||||
'collab.progressEmpty': '还没有任务在执行。告诉指挥家你的目标后,这里会显示拆分后的处理进度。',
|
||||
'collab.placeholder': '请对指挥家描述你想完成的目标',
|
||||
'collab.guideTitle': '把要完成的事告诉指挥家',
|
||||
'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。',
|
||||
'collab.guideStep1t': '描述目标',
|
||||
'collab.guideStep1d': '在聊天框里写下你想做的事,发给指挥家',
|
||||
'collab.guideStep2t': '自动拆解',
|
||||
'collab.guideStep2d': '指挥家自动拆解、分配任务,实时监督和调度',
|
||||
'collab.guideStep3t': '交付摘要',
|
||||
'collab.guideStep3d': '指挥家根据执行状态,呈上任务简报',
|
||||
'collab.guideStep4t': '随时调整',
|
||||
'collab.guideStep4d': '随时补充要求或细节,指挥家都会处理',
|
||||
'collab.chipProgress': '现在进展如何?',
|
||||
'collab.chipPause': '先暂停当前任务',
|
||||
'collab.chipSummary': '总结一下目前的结果',
|
||||
'collab.showProgressTitle': '查看分工进度',
|
||||
'collab.statRunning': '进行中',
|
||||
'collab.statDone': '已完成',
|
||||
'collab.plusMenu': '更多操作',
|
||||
'collab.switchMode': '切换模式',
|
||||
'collab.typing': '指挥家正在处理',
|
||||
'collab.offline': '无法连接指挥家服务,请确认后端已启动。',
|
||||
'collab.retry': '重试',
|
||||
'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。',
|
||||
'collab.reconnectIn': '{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': '用量', 'page.token.sub': '每会话与累计用量及缓存率',
|
||||
'status.connecting': '正在连接…', 'status.ready': '服务在线', 'status.running': '处理中',
|
||||
'status.disconnected': '服务离线', 'status.stopped': '已停止', 'status.idle': '待命',
|
||||
'conv.emptyList': '暂无会话,点「+ 新对话」开始', 'conv.defaultTitle': '新对话',
|
||||
'err.bridge': '服务未响应', 'err.newSession': '新建会话失败', 'err.poll': '轮询失败', 'err.stop': '停止失败',
|
||||
'err.interruptTimeout': '等待上一轮停止超时,请稍后再试',
|
||||
'sys.interruptPrev.hint': '已停止上一轮,正在处理新消息',
|
||||
'chat.interrupting': '正在停止上一轮…',
|
||||
'chat.sessionLoading': '正在加载会话…',
|
||||
'sys.stopRequested': '已请求停止',
|
||||
'slash.help': '可用命令:\n/new 新会话 /clear 清屏 /stop 停止 /settings 设置',
|
||||
'slash.unknown': '未知命令',
|
||||
'upload.hint': '上传文件:选择 / 拖拽 / 粘贴',
|
||||
'upload.button': '上传文件',
|
||||
'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件',
|
||||
'upload.failed': '上传失败',
|
||||
'err.charLimit': '已达字数上限({n}),发送时将自动截断', 'err.charLimitReached': '已达字数上限({n})', 'err.numMax': '不能超过 {n}',
|
||||
'file.openFailed': '无法打开文件',
|
||||
'file.kindGeneric': '文件',
|
||||
'file.kindDoc': '文档',
|
||||
'file.kindSheet': '表格',
|
||||
'file.kindSlide': '幻灯片',
|
||||
'file.kindCode': '代码',
|
||||
'file.kindArchive': '压缩包',
|
||||
'file.kindAudio': '音频',
|
||||
'file.kindVideo': '视频',
|
||||
'upload.removeTitle': '移除',
|
||||
'upload.dropHint': '松开以上传文件',
|
||||
'lightbox.closeTitle': '关闭',
|
||||
'fold.thinking': '思考', 'fold.tool': '工具调用', 'fold.toolResult': '工具结果', 'fold.llm': 'LLM Running', 'fold.turn': '第 {n} 轮',
|
||||
'plan.header': '计划 ({done}/{total})', 'plan.complete': '✓ 计划完成 ({n}/{n})',
|
||||
'plan.running': '计划执行中', 'plan.completeTitle': '计划完成',
|
||||
'plan.placeholder': '计划模式已激活', 'plan.waiting': '等待写入 {path} …', 'plan.overflow': '还有 {n} 项',
|
||||
'plan.current': '当前', 'plan.fold': '折叠', 'plan.collapse': '收起', 'plan.expand': '展开', 'plan.details': '详情',
|
||||
'plan.capsuleRunning': '运行中', 'plan.capsuleComplete': '已完成',
|
||||
'timing.elapsed': '已运行 {t}',
|
||||
'model.auto': '自动选择',
|
||||
'model.menuLabel': '选择模型',
|
||||
'chip.plan': 'Plan',
|
||||
'chip.auto': 'Auto',
|
||||
'ch.wechat': '微信', 'ch.wecom': '企业微信', 'ch.lark': '飞书', 'ch.dingtalk': '钉钉',
|
||||
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
|
||||
'ch.loading': '加载中…', 'ch.empty': '未发现 IM 进程脚本',
|
||||
'ch.logEmpty': '暂无日志',
|
||||
'err.channelLoad': '加载失败', 'err.channelStart': '启动失败', 'err.channelStop': '停止失败',
|
||||
'err.mykeyImport': '导入模型配置失败',
|
||||
'err.mykeyExport': '导出模型配置失败',
|
||||
'err.channelNotConfigured': '请先在 mykey.py 中配置该平台',
|
||||
'sys.channelStarted': '已启动', 'sys.channelStopped': '已停止',
|
||||
'modal.channelLogs': '进程日志',
|
||||
'modal.mykeyConfig': 'mykey.py 配置',
|
||||
'sys.configSaved': '配置已保存',
|
||||
'sys.mykeyImported': '模型配置已导入',
|
||||
'sys.mykeyExported': '模型配置已导出',
|
||||
'st.starting': '启动中…', 'st.stopping': '停止中…', 'st.online': '在线', 'st.offline': '离线', 'st.error': '错误', 'st.running': '运行', 'st.abnormal': '异常',
|
||||
'act.configure': '配置', 'act.logs': '日志', 'act.restart': '重启', 'act.stop': '停止', 'act.start': '启动', 'act.exit': '退出',
|
||||
'act.copy': '复制', 'act.copied': '已复制', 'act.copyTex': 'TeX', 'act.send': '发送',
|
||||
'proc.imbotWechat': 'imbot · 微信', 'proc.imbotDing': 'imbot · 钉钉', 'proc.scheduler': '定时任务调度', 'proc.conductor': '指挥家',
|
||||
'cm.scheduling': '调度中', 'cm.running': '执行中', 'cm.idleSt': '空闲',
|
||||
'cm.master': '已派 3 子任务', 'cm.w1': '子任务:抓取数据', 'cm.w2': '子任务:复核结果', 'cm.sub': '等待派单',
|
||||
'tok.total': '累计', 'tok.cost': '缓存率', 'tok.today': '今日', 'tok.tabAll': '聊天', 'tok.tabConductor': '指挥家', 'tok.condTotal': '指挥家累计', 'tok.condCurrent': '指挥家本次', 'tok.condTip': '指挥家消耗不计入聊天累计', 'tok.condOffline': '指挥家服务离线', 'tok.disclaimer': '不同 API 网站的计费价格可能会有差异,请以实际网站为准。',
|
||||
'tok.colSession': '会话', 'tok.colIn': '输入', 'tok.colOut': '输出', 'tok.colCacheW': '缓存写入', 'tok.colCache': '缓存读取', 'tok.colCost': '成本',
|
||||
'tok.from': '从', 'tok.to': '到', 'tok.reset': '重置', 'tok.noData': '暂无记录', 'tok.deleted': '此会话已删除',
|
||||
'tok.pricingUnknown': '⚠ 此模型计费规则尚未明确,按默认估算',
|
||||
'tok.priceInput': '输入: $', 'tok.priceOutput': '输出: $',
|
||||
'tok.priceCacheW': '缓存写入: $', 'tok.priceCacheR': '缓存读取: $',
|
||||
'presetPrompt.goal': '进入 Goal 模式:读 L3 goal mode SOP,自主达成我接下来描述的目标。',
|
||||
'presetPrompt.plan': '进入 Plan 模式:先读 memory/plan_sop.md,按其中「探索→规划→执行→验证」流程,等我接下来描述要做的任务。',
|
||||
'presetPrompt.autonomous': '🤖 进入自主行动模式:阅读 memory/autonomous_operation_sop.md,按 SOP 选取或规划任务,独立执行并产出报告。',
|
||||
'presetPrompt.hive': '启动 Goal Hive 模式:按 hive SOP 拉起多个 worker 协同完成我接下来的目标。',
|
||||
'presetPrompt.review': '进入监察者模式:对刚才的产出严格挑刺、逐项复核并报告问题。',
|
||||
'presetPrompt.findwork': '按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。',
|
||||
'presetPrompt.mine': '抓取本周的 git 提交并写一份周报。',
|
||||
'ask.banner': 'GA 等你回答',
|
||||
'ask.replyHint': '在下方输入框回复',
|
||||
'ask.placeholderOpen': '在此输入你的回答… (Enter 发送)',
|
||||
},
|
||||
en: {
|
||||
'app.title': 'GenericAgent Desktop',
|
||||
'brand.sub': 'Desktop terminal',
|
||||
'nav.chat': 'Chat', 'nav.services': 'Services', 'nav.channels': 'Channels', 'nav.status': 'Status',
|
||||
'nav.collab': 'Conductor', 'nav.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': 'Auto-decompose complex tasks; just check progress and briefings',
|
||||
'preset.plan.t': 'Plan mode', 'preset.plan.d': 'Load Plan SOP — explore→plan→execute→verify',
|
||||
'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously',
|
||||
'preset.autonomous.t': 'Autonomous mode', 'preset.autonomous.d': 'Plan/execute tasks per SOP and produce reports (same as reflect/autonomous.py)',
|
||||
'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration',
|
||||
'preset.review.t': 'Deep review', 'preset.review.d': 'Strict quality check',
|
||||
'preset.findwork.t': 'Find me work', 'preset.findwork.d': 'Analyze my context and suggest a batch of interesting TODOs',
|
||||
'preset.mine.t': 'My · Weekly', 'preset.mine.d': 'Custom: weekly report from commits',
|
||||
'preset.add.t': 'Custom', 'preset.add.d': 'Save any prompt as a function',
|
||||
'composer.placeholder': 'What can GA do for you?',
|
||||
'search.placeholder': 'Search chats…', 'conv.new': 'New chat',
|
||||
'ctx.pin': 'Pin', 'ctx.unpin': 'Unpin', 'ctx.rename': 'Rename', 'ctx.del': 'Delete',
|
||||
'common.close': 'Close', 'common.more': 'More', 'common.optional': 'Optional', 'common.save': 'Save',
|
||||
'modal.preset': 'Presets', 'modal.addModel': 'Add model', 'modal.editModel': 'Edit model', 'modal.settings': 'Settings',
|
||||
'modal.customPreset': 'Custom preset',
|
||||
'modal.editCustomPreset': 'Edit task',
|
||||
'customPreset.titlePh': 'Title, e.g. "Weekly report"',
|
||||
'customPreset.promptPh': 'Prompt body — sent as the message when clicked',
|
||||
'customPreset.empty': 'Title and Prompt cannot be empty',
|
||||
'customPreset.removeTitle': 'Delete',
|
||||
'customPreset.editTitle': 'Edit',
|
||||
'builtinPreset.restoreBtn': 'Restore defaults',
|
||||
'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager',
|
||||
'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?',
|
||||
'appearance.light': 'Light', 'appearance.dark': 'Dark',
|
||||
'set.noModels': 'No models yet — add one below',
|
||||
'lang.zh': '简体中文', 'lang.en': 'English',
|
||||
'model.name': 'Note', 'model.namePh': 'Shown in the model list',
|
||||
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': 'Leave blank to keep the current key',
|
||||
'model.apibase': 'API base URL', 'model.apibasePh': 'https://.../v1/messages',
|
||||
'model.protocol': 'Protocol', 'model.protocolPick': 'Select…', 'model.protocolOai': 'OpenAI-compatible (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
|
||||
'model.stream': 'Response', 'model.streamOn': 'Stream', 'model.streamOff': 'Non-stream',
|
||||
'model.model': 'Model', 'model.modelPh': 'model parameter name',
|
||||
'model.modelHint': 'Must match the model field in your provider docs exactly',
|
||||
'model.retries': 'Retries (×)', 'model.connTimeout': 'Connect (s)', 'model.readTimeout': 'Read (s)',
|
||||
'model.save': 'Save', 'common.cancel': 'Cancel', 'common.confirm': 'Confirm', 'common.edit': 'Edit', 'common.delete': 'Delete',
|
||||
'pq.title': 'Quick connect a model', 'pq.sub': 'Add your API key to get started', 'pq.toggle': 'Expand / collapse',
|
||||
'pq.deepseekDesc': 'Official API · OpenAI-compatible', 'pq.qwenDesc': 'Tongyi Qwen · Aliyun Bailian',
|
||||
'guide.step1': 'Open the link, sign in, then create & copy your API key',
|
||||
'guide.step2': 'Paste the key into the “API Key” field below',
|
||||
'guide.step3': 'Click Save — then pick it from the model list',
|
||||
'guide.prefillTip': 'API base, protocol and model are pre-filled — edit if needed',
|
||||
'guide.getKey': 'Get your {name} API key', 'guide.copy': 'Copy link', 'guide.copied': 'Link copied',
|
||||
'err.modelSave': 'Save failed', 'err.modelSwitch': 'Failed to switch model', 'err.modelRequired': 'Model, API Key and base URL are required',
|
||||
'err.modelDelete': 'Delete failed', 'err.modelDeleteLast': 'At least one model is required',
|
||||
'confirm.modelDelete': 'Delete this model profile?',
|
||||
'model.aggregation': 'Channel group (auto failover)', 'model.aggregationShort': 'Channel group', 'model.aggregationDesc': 'Tries in order, switches to the next on failure',
|
||||
'model.emptyMixin': 'No models added yet',
|
||||
'model.addToMixin': 'Add to channel', 'model.inMixin': 'In channel', 'model.removeFromMixin': 'Remove from channel', 'model.alreadyInMixin': 'Already in the channel', 'model.dragReorder': 'Drag to reorder',
|
||||
'err.mixinFailed': 'Operation failed',
|
||||
'page.services.title': 'Services', 'page.services.sub': 'IM channels and background processes — view, start/stop, logs',
|
||||
'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': '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.placeholder': 'Describe the goal you want to accomplish',
|
||||
'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',
|
||||
'collab.statRunning': 'Running',
|
||||
'collab.statDone': 'Done',
|
||||
'collab.plusMenu': 'More actions',
|
||||
'collab.switchMode': 'Switch mode',
|
||||
'collab.typing': 'Conductor is working',
|
||||
'collab.offline': 'Cannot reach the service. Make sure the backend is running.',
|
||||
'collab.retry': 'Retry',
|
||||
'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.',
|
||||
'collab.reconnectIn': 'Retry in {n}s',
|
||||
'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': 'Usage', 'page.token.sub': 'Per-session and total usage & cache rate',
|
||||
'status.connecting': 'Connecting…', 'status.ready': 'Service online', 'status.running': 'Working…',
|
||||
'status.disconnected': 'Service offline', 'status.stopped': 'Stopped', 'status.idle': 'Standby',
|
||||
'conv.emptyList': 'No chats yet — click “+ New chat”', 'conv.defaultTitle': 'New chat',
|
||||
'err.bridge': 'Service not responding', 'err.newSession': 'Failed to create session', 'err.poll': 'Polling failed', 'err.stop': 'Stop failed',
|
||||
'err.interruptTimeout': 'Timed out waiting for the previous reply to stop — try again',
|
||||
'sys.interruptPrev.hint': 'Previous reply stopped — processing new message',
|
||||
'chat.interrupting': 'Stopping previous reply…',
|
||||
'chat.sessionLoading': 'Loading conversation…',
|
||||
'sys.stopRequested': 'Stop requested',
|
||||
'slash.help': 'Commands:\n/new new chat /clear clear /stop stop /settings settings',
|
||||
'slash.unknown': 'Unknown command',
|
||||
'upload.hint': 'Upload file: pick / drag / paste',
|
||||
'upload.button': 'Upload file',
|
||||
'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file',
|
||||
'upload.failed': 'Upload failed',
|
||||
'err.charLimit': 'Character limit reached ({n}), text will be truncated on send', 'err.charLimitReached': 'Character limit reached ({n})', 'err.numMax': 'Cannot exceed {n}',
|
||||
'file.openFailed': 'Cannot open file',
|
||||
'file.kindGeneric': 'File',
|
||||
'file.kindDoc': 'Document',
|
||||
'file.kindSheet': 'Spreadsheet',
|
||||
'file.kindSlide': 'Slides',
|
||||
'file.kindCode': 'Code',
|
||||
'file.kindArchive': 'Archive',
|
||||
'file.kindAudio': 'Audio',
|
||||
'file.kindVideo': 'Video',
|
||||
'upload.removeTitle': 'Remove',
|
||||
'upload.dropHint': 'Drop to upload files',
|
||||
'lightbox.closeTitle': 'Close',
|
||||
'fold.thinking': 'Thinking', 'fold.tool': 'Tool call', 'fold.toolResult': 'Tool result', 'fold.llm': 'LLM Running', 'fold.turn': 'Turn {n}',
|
||||
'plan.header': 'Plan ({done}/{total})', 'plan.complete': '✓ Plan complete ({n}/{n})',
|
||||
'plan.running': 'Running plan', 'plan.completeTitle': 'Plan complete',
|
||||
'plan.placeholder': 'Plan mode activated', 'plan.waiting': 'waiting for {path} …', 'plan.overflow': '+{n} more',
|
||||
'plan.current': 'Now', 'plan.fold': 'Fold', 'plan.collapse': 'Collapse', 'plan.expand': 'Expand', 'plan.details': 'Details',
|
||||
'plan.capsuleRunning': 'Running', 'plan.capsuleComplete': 'Done',
|
||||
'timing.elapsed': 'Elapsed {t}',
|
||||
'model.auto': 'Auto',
|
||||
'model.menuLabel': 'Select model',
|
||||
'chip.plan': 'Plan',
|
||||
'chip.auto': 'Auto',
|
||||
'ch.wechat': 'WeChat', 'ch.wecom': 'WeCom', 'ch.lark': 'Lark', 'ch.dingtalk': 'DingTalk',
|
||||
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
|
||||
'ch.loading': 'Loading…', 'ch.empty': 'No IM process scripts found',
|
||||
'ch.logEmpty': 'No log output yet',
|
||||
'err.channelLoad': 'Failed to load', 'err.channelStart': 'Start failed', 'err.channelStop': 'Stop failed',
|
||||
'err.mykeyImport': 'Failed to import model config',
|
||||
'err.mykeyExport': 'Failed to export model config',
|
||||
'err.channelNotConfigured': 'Configure this platform in mykey.py first',
|
||||
'sys.channelStarted': 'Started', 'sys.channelStopped': 'Stopped',
|
||||
'modal.channelLogs': 'Process logs',
|
||||
'modal.mykeyConfig': 'mykey.py',
|
||||
'sys.configSaved': 'Configuration saved',
|
||||
'sys.mykeyImported': 'Model config imported',
|
||||
'sys.mykeyExported': 'Model config exported',
|
||||
'st.starting': 'Starting…', 'st.stopping': 'Stopping…', 'st.online': 'Online', 'st.offline': 'Offline', 'st.error': 'Error', 'st.running': 'Running', 'st.abnormal': 'Error',
|
||||
'act.configure': 'Configure', 'act.logs': 'Logs', 'act.restart': 'Restart', 'act.stop': 'Stop', 'act.start': 'Start', 'act.exit': 'Exit',
|
||||
'act.copy': 'Copy', 'act.copied': 'Copied', 'act.copyTex': 'TeX', 'act.send': 'Send',
|
||||
'proc.imbotWechat': 'imbot · WeChat', 'proc.imbotDing': 'imbot · DingTalk', 'proc.scheduler': 'Scheduler', 'proc.conductor': 'Conductor',
|
||||
'cm.scheduling': 'Scheduling', 'cm.running': 'Running', 'cm.idleSt': 'Idle',
|
||||
'cm.master': 'Dispatched 3 subtasks', 'cm.w1': 'Subtask: fetch data', 'cm.w2': 'Subtask: review results', 'cm.sub': 'Waiting for tasks',
|
||||
'tok.total': 'Total', 'tok.cost': 'Cache rate', 'tok.today': 'Today', 'tok.tabAll': 'Chat', 'tok.tabConductor': 'Conductor', 'tok.condTotal': 'Conductor Total', 'tok.condCurrent': 'Conductor Current', 'tok.condTip': 'Conductor usage is not included in chat totals', 'tok.condOffline': 'Service offline', 'tok.disclaimer': 'Pricing may vary by API provider. Please refer to the actual website.',
|
||||
'tok.colSession': 'Session', 'tok.colIn': 'Input', 'tok.colOut': 'Output', 'tok.colCacheW': 'Cache write', 'tok.colCache': 'Cache read', 'tok.colCost': 'Cost',
|
||||
'tok.from': 'From', 'tok.to': 'To', 'tok.reset': 'Reset', 'tok.noData': 'No records', 'tok.deleted': 'Session deleted',
|
||||
'tok.pricingUnknown': '⚠ Pricing not confirmed, using defaults',
|
||||
'tok.priceInput': 'Input: $', 'tok.priceOutput': 'Output: $',
|
||||
'tok.priceCacheW': 'Cache write: $', 'tok.priceCacheR': 'Cache read: $',
|
||||
'presetPrompt.goal': 'Enter Goal mode: read the L3 goal-mode SOP and autonomously achieve the goal I describe next.',
|
||||
'presetPrompt.plan': 'Enter Plan mode: first read memory/plan_sop.md, follow its explore→plan→execute→verify flow, and wait for the task I describe next.',
|
||||
'presetPrompt.autonomous': '🤖 Enter autonomous mode: read memory/autonomous_operation_sop.md, follow the SOP to pick or plan a task, execute independently, and produce a report.',
|
||||
'presetPrompt.hive': 'Start Goal Hive mode: per the hive SOP, spawn multiple workers to collaboratively achieve the goal I describe next.',
|
||||
'presetPrompt.review': 'Enter reviewer mode: strictly scrutinize the previous output, review item by item and report issues.',
|
||||
'presetPrompt.findwork': 'Following the autonomous planning section, analyze my situation thoroughly and generate a batch of TODOs that genuinely interest me.',
|
||||
'presetPrompt.mine': 'Collect this week\'s git commits and write a weekly report.',
|
||||
'ask.banner': 'GA is waiting for your answer',
|
||||
'ask.replyHint': 'Reply in the input below',
|
||||
'ask.placeholderOpen': 'Type your answer here… (Enter to send)',
|
||||
},
|
||||
};
|
||||
const LANGS = ['zh', 'en'];
|
||||
const I18N = window.GA_I18N?.dict || { zh: {}, en: {} };
|
||||
const LANGS = window.GA_I18N?.languages || ['zh', 'en'];
|
||||
const STORE = { lang: 'ga_lang', theme: 'ga_theme', appearance: 'ga_appearance', plain: 'ga_plain', fontSize: 'ga_font_size' };
|
||||
const APPEARANCE_IDS = ['light', 'dark'];
|
||||
const CHAT_FONT_MIN = 10;
|
||||
@@ -721,7 +374,14 @@ async function bridgeFetch(path, opts = {}) {
|
||||
if (!res.ok) throw new Error(data.error || data.message || res.statusText);
|
||||
return data;
|
||||
}
|
||||
function t(key) { return (I18N[lang] && I18N[lang][key]) || (I18N.zh[key]) || key; }
|
||||
function t(key, vars) {
|
||||
if (window.GA_I18N?.t) return window.GA_I18N.t(lang, key, vars);
|
||||
let text = (I18N[lang] && I18N[lang][key]) || (I18N.zh && I18N.zh[key]) || key;
|
||||
if (vars) {
|
||||
for (const [name, value] of Object.entries(vars)) text = text.replaceAll(`{${name}}`, String(value));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
window.gaT = t;
|
||||
document.addEventListener('collab:running-count', e => {
|
||||
const b = document.getElementById('collab-badge');
|
||||
@@ -734,6 +394,22 @@ function optionalPh(key) {
|
||||
const sep = (lang === 'en') ? ', ' : ',';
|
||||
return `${t('common.optional')}${sep}${t(key)}`;
|
||||
}
|
||||
function setTooltip(el, text) {
|
||||
if (!el) return;
|
||||
const value = String(text || '').trim();
|
||||
el.removeAttribute('title');
|
||||
if (el.hasAttribute('data-no-tooltip')) {
|
||||
delete el.dataset.tooltip;
|
||||
if (value) el.setAttribute('aria-label', value);
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
el.dataset.tooltip = value;
|
||||
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', value);
|
||||
} else {
|
||||
delete el.dataset.tooltip;
|
||||
}
|
||||
}
|
||||
function applyI18n() {
|
||||
document.documentElement.lang = (lang === 'en') ? 'en' : 'zh-CN';
|
||||
document.title = t('app.title');
|
||||
@@ -744,7 +420,8 @@ function applyI18n() {
|
||||
if (el.isContentEditable) el.setAttribute('data-ph', val); // contenteditable 用 :empty::before 显示
|
||||
else el.setAttribute('placeholder', val);
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => { el.setAttribute('title', t(el.dataset.i18nTitle)); });
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => { setTooltip(el, t(el.dataset.i18nTitle)); });
|
||||
migrateNativeTooltips();
|
||||
renderLangList();
|
||||
// 语言切换后重算激活模型 chip 文案;若当前会话已有渠道组运行态模型,保留运行态而非退回首选项
|
||||
const _ap = (state.modelProfiles || []).find(p => (p.id ?? 0) === state.llmNo);
|
||||
@@ -1738,7 +1415,7 @@ const state = {
|
||||
};
|
||||
function rt(sess) {
|
||||
let r = state.runtime.get(sess.id);
|
||||
if (!r) { r = { polling:false, busy:false, lastId:0, seen:new Set(), draftEl:null, draftSegs:null, draftTurn:0, taskStartedAt:null, taskEndedAt:null, taskTimerId:null, planCompleteAt:null, planLostAt:null, planHoldItems:[], planLastPayload:null, planLastComplete:false, planHideTimer:null, planDismissedComplete:false, planCollapsed:false, planShowAll:false }; state.runtime.set(sess.id, r); }
|
||||
if (!r) { r = { polling:false, busy:false, lastId:0, seen:new Set(), draftEl:null, draftSegs:null, draftTurn:0, taskStartedAt:null, taskEndedAt:null, taskTimerId:null, planCompleteAt:null, planLostAt:null, planHoldItems:[], planLastPayload:null, planVisiblePayload:null, planLastComplete:false, planHideTimer:null, planDismissedComplete:false, planCollapsed:false, planShowAll:false }; state.runtime.set(sess.id, r); }
|
||||
return r;
|
||||
}
|
||||
const activeSess = () => state.sessions.get(state.activeId) || null;
|
||||
@@ -1786,7 +1463,7 @@ const MIN_MSG_LOADING_MS = 450;
|
||||
const HYDRATE_LOADING_TIMEOUT_MS = 10000;
|
||||
const POLL_MSG_LIMIT = 200;
|
||||
const PLAN_LOST_GRACE_MS = 1500; // tuiapp_v2._PLAN_LOST_GRACE_SEC
|
||||
const PLAN_COMPLETE_GRACE_MS = 3000; // tuiapp_v2._PLAN_GRACE_SEC
|
||||
const PLAN_DISMISS_STORE = 'ga_plan_dismissed_v1';
|
||||
|
||||
let _submitInFlight = false;
|
||||
const runToggle = document.getElementById('run-toggle');
|
||||
@@ -1861,6 +1538,43 @@ function planTpl(tpl, v) {
|
||||
return String(tpl || '').replace(/\{(\w+)\}/g, (_, k) => (v[k] != null ? String(v[k]) : `{${k}}`));
|
||||
}
|
||||
|
||||
function planItemsSignature(items) {
|
||||
return (items || [])
|
||||
.map(it => `${String(it.status || '')}\u001e${String(it.content || '')}`)
|
||||
.join('\u001f');
|
||||
}
|
||||
|
||||
function planDismissId(sess, plan) {
|
||||
if (!sess || !plan) return '';
|
||||
const total = plan.total ?? (plan.items || []).length;
|
||||
const done = plan.done ?? (plan.items || []).filter(it => it.status === 'done').length;
|
||||
return `${sess.id || sess.bridgeSessionId || 'session'}\u001d${done}/${total}\u001d${planItemsSignature(plan.items || [])}`;
|
||||
}
|
||||
|
||||
function readPlanDismissed() {
|
||||
try {
|
||||
const raw = localStorage.getItem(PLAN_DISMISS_STORE);
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function isPlanDismissed(sess, plan) {
|
||||
if (!plan?.complete) return false;
|
||||
const id = planDismissId(sess, plan);
|
||||
return !!(id && readPlanDismissed()[id]);
|
||||
}
|
||||
|
||||
function markPlanDismissed(sess, plan) {
|
||||
const id = planDismissId(sess, plan);
|
||||
if (!id) return;
|
||||
const dismissed = readPlanDismissed();
|
||||
dismissed[id] = Date.now();
|
||||
try { localStorage.setItem(PLAN_DISMISS_STORE, JSON.stringify(dismissed)); } catch (_) {}
|
||||
}
|
||||
|
||||
let planPollTimer;
|
||||
function syncPlanPollTimer() {
|
||||
const on = !!(activeSess()?.bridgeSessionId && state.bridgeReady);
|
||||
@@ -1881,21 +1595,12 @@ function clearPlanGrace(r) {
|
||||
r.planCompleteAt = r.planLostAt = null;
|
||||
r.planHoldItems = [];
|
||||
r.planLastPayload = null;
|
||||
r.planVisiblePayload = null;
|
||||
r.planLastComplete = false;
|
||||
r.planDismissedComplete = false;
|
||||
if (r.planHideTimer) { clearTimeout(r.planHideTimer); r.planHideTimer = null; }
|
||||
}
|
||||
|
||||
function schedulePlanCompleteDismiss(sess) {
|
||||
const r = rt(sess);
|
||||
if (r.planHideTimer) clearTimeout(r.planHideTimer);
|
||||
r.planHideTimer = setTimeout(() => {
|
||||
r.planHideTimer = null;
|
||||
r.planDismissedComplete = true;
|
||||
if (isActive(sess)) refreshPlanBar(null);
|
||||
}, PLAN_COMPLETE_GRACE_MS);
|
||||
}
|
||||
|
||||
/** tuiapp_v2._refresh_planbar:用 runtime 里缓存的 items / placeholder 重绘 */
|
||||
function refreshPlanBarFromRuntime(sess) {
|
||||
const r = rt(sess);
|
||||
@@ -1910,11 +1615,6 @@ function refreshPlanBarFromRuntime(sess) {
|
||||
refreshPlanBar(null);
|
||||
return;
|
||||
}
|
||||
if (r.planCompleteAt != null && Date.now() - r.planCompleteAt >= PLAN_COMPLETE_GRACE_MS) {
|
||||
r.planDismissedComplete = true;
|
||||
refreshPlanBar(null);
|
||||
return;
|
||||
}
|
||||
if (!items.length) {
|
||||
if (lp?.active && lp.placeholder) {
|
||||
refreshPlanBar(lp);
|
||||
@@ -1975,7 +1675,9 @@ function applyPlanPayload(sess, raw) {
|
||||
const wasComplete = r.planLastComplete;
|
||||
if (nowComplete && !wasComplete) {
|
||||
r.planCompleteAt = now;
|
||||
schedulePlanCompleteDismiss(sess);
|
||||
r.planDismissedComplete = isPlanDismissed(sess, { ...raw, items: items.length ? items : r.planHoldItems });
|
||||
} else if (nowComplete) {
|
||||
r.planDismissedComplete = isPlanDismissed(sess, { ...raw, items: items.length ? items : r.planHoldItems });
|
||||
} else if (!nowComplete) {
|
||||
r.planCompleteAt = null;
|
||||
r.planDismissedComplete = false;
|
||||
@@ -2053,16 +1755,23 @@ function bindPlanCardUiOnce() {
|
||||
if (!sess) return;
|
||||
const r = rt(sess);
|
||||
const payload = r.planLastPayload;
|
||||
if (!payload?.active) return;
|
||||
const visiblePayload = r.planVisiblePayload || payload;
|
||||
if (e.target.closest('[data-plan-dismiss]')) {
|
||||
markPlanDismissed(sess, visiblePayload);
|
||||
r.planDismissedComplete = true;
|
||||
refreshPlanBar(null);
|
||||
return;
|
||||
}
|
||||
if (!payload?.active && !visiblePayload?.active) return;
|
||||
if (e.target.closest('[data-plan-expand]')) {
|
||||
r.planCollapsed = false;
|
||||
refreshPlanBar(payload);
|
||||
refreshPlanBar(visiblePayload);
|
||||
} else if (e.target.closest('[data-plan-collapse]')) {
|
||||
r.planCollapsed = true;
|
||||
refreshPlanBar(payload);
|
||||
refreshPlanBar(visiblePayload);
|
||||
} else if (e.target.closest('[data-plan-details]')) {
|
||||
r.planShowAll = !r.planShowAll;
|
||||
refreshPlanBar(payload);
|
||||
refreshPlanBar(visiblePayload);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2074,6 +1783,7 @@ function refreshPlanBar(plan) {
|
||||
planBarEl.hidden = true;
|
||||
planBarEl.replaceChildren();
|
||||
planBarEl.className = 'plan-card';
|
||||
planBarEl._planRenderSig = '';
|
||||
return;
|
||||
}
|
||||
const sess = activeSess();
|
||||
@@ -2088,6 +1798,21 @@ function refreshPlanBar(plan) {
|
||||
plan.complete ? 'plan-card--complete' : '',
|
||||
plan.placeholder ? 'plan-card--placeholder' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
const planItemsSig = planItemsSignature(plan.items || []);
|
||||
if (sess && plan.complete && isPlanDismissed(sess, plan)) {
|
||||
r.planDismissedComplete = true;
|
||||
planBarEl.hidden = true;
|
||||
planBarEl.replaceChildren();
|
||||
planBarEl.className = 'plan-card';
|
||||
planBarEl._planRenderSig = '';
|
||||
return;
|
||||
}
|
||||
r.planVisiblePayload = plan;
|
||||
const renderSig = JSON.stringify([
|
||||
lang, mod, stepText, done, total, !!r.planShowAll, plan.pathHint || '', planItemsSig
|
||||
]);
|
||||
if (!planBarEl.hidden && planBarEl.className === mod && planBarEl._planRenderSig === renderSig) return;
|
||||
planBarEl._planRenderSig = renderSig;
|
||||
planBarEl.hidden = false;
|
||||
planBarEl.className = mod;
|
||||
|
||||
@@ -2100,11 +1825,17 @@ function refreshPlanBar(plan) {
|
||||
btn.dataset.planExpand = '1';
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'plan-status-dot';
|
||||
const ico = document.createElement('span');
|
||||
ico.className = 'plan-capsule-ic';
|
||||
ico.innerHTML = GA_ICON('listChecks');
|
||||
const txt = document.createElement('span');
|
||||
txt.className = 'plan-capsule-text';
|
||||
if (cap.step) txt.innerHTML = `${escapeHtml(cap.tag)} · <em>${escapeHtml(cap.step)}</em>`;
|
||||
else txt.textContent = cap.tag;
|
||||
btn.append(dot, txt);
|
||||
const chev = document.createElement('span');
|
||||
chev.className = 'plan-capsule-chevron';
|
||||
chev.innerHTML = GA_ICON('caretRight');
|
||||
btn.append(dot, ico, txt, chev);
|
||||
planBarEl.append(btn);
|
||||
return;
|
||||
}
|
||||
@@ -2128,11 +1859,19 @@ function refreshPlanBar(plan) {
|
||||
}
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'plan-head-actions';
|
||||
if (plan.complete) {
|
||||
const dismissBtn = document.createElement('button');
|
||||
dismissBtn.type = 'button';
|
||||
dismissBtn.className = 'plan-btn';
|
||||
dismissBtn.dataset.planDismiss = '1';
|
||||
dismissBtn.innerHTML = `${GA_ICON('x', 'plan-btn-ic')}<span>${escapeHtml(t('plan.dismiss'))}</span>`;
|
||||
actions.append(dismissBtn);
|
||||
}
|
||||
const collapseBtn = document.createElement('button');
|
||||
collapseBtn.type = 'button';
|
||||
collapseBtn.className = 'plan-btn';
|
||||
collapseBtn.dataset.planCollapse = '1';
|
||||
collapseBtn.textContent = t('plan.fold');
|
||||
collapseBtn.innerHTML = `${GA_ICON('caretDown', 'plan-btn-ic')}<span>${escapeHtml(t('plan.fold'))}</span>`;
|
||||
actions.append(collapseBtn);
|
||||
head.append(actions);
|
||||
frag.append(head);
|
||||
@@ -2192,7 +1931,7 @@ function refreshPlanBar(plan) {
|
||||
det.type = 'button';
|
||||
det.className = 'plan-btn';
|
||||
det.dataset.planDetails = '1';
|
||||
det.textContent = r.planShowAll ? t('plan.collapse') : t('plan.details');
|
||||
det.innerHTML = `${GA_ICON(r.planShowAll ? 'caretDown' : 'caretRight', 'plan-btn-ic')}<span>${escapeHtml(r.planShowAll ? t('plan.collapse') : t('plan.details'))}</span>`;
|
||||
foot.append(det);
|
||||
}
|
||||
if (foot.childNodes.length) frag.append(foot);
|
||||
@@ -2781,7 +2520,7 @@ function renderSessionList() {
|
||||
`<span class="ci-dot"></span><div class="ci-main">` +
|
||||
`<div class="ci-title">${pinSvg}${escapeHtml(displayTitle(sess))}</div>` +
|
||||
`<div class="ci-meta">${busy ? t('status.running') : t('status.idle')}</div></div>` +
|
||||
`<button class="ci-more" title="${escapeHtml(t('common.more'))}">${GA_ICON('dotsThreeVertical')}</button>`;
|
||||
`<button class="ci-more" data-no-tooltip aria-label="${escapeHtml(t('common.more'))}">${GA_ICON('dotsThreeVertical')}</button>`;
|
||||
convListEl.appendChild(item);
|
||||
}
|
||||
}
|
||||
@@ -2905,6 +2644,26 @@ async function closeSession(id) {
|
||||
|
||||
const convMenu = document.getElementById('conv-menu');
|
||||
let menuTargetId = null;
|
||||
function hideConvMenu() {
|
||||
if (!convMenu) return;
|
||||
convMenu.hidden = true;
|
||||
menuTargetId = null;
|
||||
}
|
||||
function positionConvMenu(anchor) {
|
||||
if (!convMenu || !anchor) return;
|
||||
convMenu.hidden = false;
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const pad = 8;
|
||||
const gap = 4;
|
||||
const w = convMenu.offsetWidth;
|
||||
const h = convMenu.offsetHeight;
|
||||
const left = Math.max(pad, Math.min(window.innerWidth - w - pad, rect.right - w));
|
||||
const below = rect.bottom + gap;
|
||||
const above = rect.top - h - gap;
|
||||
const top = below + h <= window.innerHeight - pad ? below : Math.max(pad, above);
|
||||
convMenu.style.left = `${left}px`;
|
||||
convMenu.style.top = `${top}px`;
|
||||
}
|
||||
function isSessionRowInteractiveTarget(target) {
|
||||
const el = target instanceof Element ? target : null;
|
||||
return !!el?.closest?.(
|
||||
@@ -2915,7 +2674,18 @@ convListEl.addEventListener('click', (e) => {
|
||||
const more = e.target.closest('.ci-more');
|
||||
if (more) {
|
||||
e.stopPropagation();
|
||||
menuTargetId = more.closest('.conv-item').dataset.id;
|
||||
hideTooltip();
|
||||
const item = more.closest('.conv-item');
|
||||
const nextTargetId = item?.dataset.id || null;
|
||||
if (!nextTargetId) {
|
||||
hideConvMenu();
|
||||
return;
|
||||
}
|
||||
if (!convMenu.hidden && menuTargetId === nextTargetId) {
|
||||
hideConvMenu();
|
||||
return;
|
||||
}
|
||||
menuTargetId = nextTargetId;
|
||||
// 根据当前会话置顶状态切菜单文案:置顶 / 取消置顶
|
||||
const tgt = state.sessions.get(menuTargetId);
|
||||
const pinSpan = convMenu.querySelector('[data-act="pin"] [data-i18n]');
|
||||
@@ -2924,10 +2694,7 @@ convListEl.addEventListener('click', (e) => {
|
||||
pinSpan.setAttribute('data-i18n', k);
|
||||
pinSpan.textContent = t(k);
|
||||
}
|
||||
convMenu.hidden = false;
|
||||
const rect = more.getBoundingClientRect();
|
||||
convMenu.style.top = (rect.bottom + 4) + 'px';
|
||||
convMenu.style.left = (rect.right - convMenu.offsetWidth) + 'px';
|
||||
positionConvMenu(more);
|
||||
return;
|
||||
}
|
||||
if (isSessionRowInteractiveTarget(e.target)) return;
|
||||
@@ -2961,7 +2728,7 @@ convMenu.addEventListener('click', (e) => {
|
||||
patchSession(sess, { pinned: sess.pinned });
|
||||
renderSessionList();
|
||||
} else if (sess && act === 'rename') {
|
||||
convMenu.hidden = true;
|
||||
hideConvMenu();
|
||||
const item = convListEl.querySelector(`.conv-item[data-id="${sess.id}"]`);
|
||||
if (!item) return;
|
||||
const titleEl = item.querySelector('.ci-title');
|
||||
@@ -3003,9 +2770,9 @@ convMenu.addEventListener('click', (e) => {
|
||||
} else if (sess && act === 'del') {
|
||||
closeSession(sess.id);
|
||||
}
|
||||
convMenu.hidden = true;
|
||||
hideConvMenu();
|
||||
});
|
||||
document.addEventListener('click', () => { convMenu.hidden = true; });
|
||||
document.addEventListener('click', hideConvMenu);
|
||||
newConvBtn.addEventListener('click', (e) => { e.preventDefault(); newSession(); });
|
||||
|
||||
/* ═══════════════ 轮询 + 流式 ═══════════════ */
|
||||
@@ -3205,9 +2972,19 @@ async function pollSession(sess) {
|
||||
try {
|
||||
const result = await fetchSessionPoll(sess);
|
||||
consecutiveErrors = 0;
|
||||
const busy = applyPollResult(sess, result);
|
||||
let busy = applyPollResult(sess, result);
|
||||
if (busy) await new Promise(z => setTimeout(z, 500));
|
||||
else {
|
||||
const hasFinalAssistant = (result.messages || []).some(m => m && m.role === 'assistant');
|
||||
if (r.draftEl && !hasFinalAssistant) {
|
||||
await new Promise(z => setTimeout(z, 150));
|
||||
const finalResult = await fetchSessionPoll(sess);
|
||||
busy = applyPollResult(sess, finalResult);
|
||||
if (busy) {
|
||||
await new Promise(z => setTimeout(z, 500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (r.draftEl) { r.draftEl.remove(); r.draftEl = null; r.draftSegs = null; r.draftTurn = 0; }
|
||||
resetTypewriterState(r);
|
||||
break;
|
||||
@@ -3364,7 +3141,11 @@ async function sendPrompt(text) {
|
||||
if (res?.error) throw new Error(res.error.message || res.error);
|
||||
removeUsedPendingFiles(usedFiles);
|
||||
const uid = Number(res.userMessageId || res.result?.userMessageId || 0);
|
||||
if (uid) { r.seen.add(uid); r.lastId = Math.max(r.lastId, uid); }
|
||||
if (uid) {
|
||||
userMsg.id = uid;
|
||||
r.seen.add(uid);
|
||||
r.lastId = Math.max(r.lastId, uid);
|
||||
}
|
||||
planPoll(sess);
|
||||
pollSession(sess);
|
||||
return true;
|
||||
@@ -3392,9 +3173,10 @@ async function submitInput() {
|
||||
if (_submitInFlight) return;
|
||||
let text = composerText('chat');
|
||||
if (!text.trim()) return;
|
||||
if (text.trim().startsWith('/')) {
|
||||
const slashName = slashCommandName(text);
|
||||
if (slashName) {
|
||||
inputEl.innerHTML = '';
|
||||
handleSlash(text.trim());
|
||||
handleSlash(slashName);
|
||||
return;
|
||||
}
|
||||
if (text.length > 20000) {
|
||||
@@ -3443,8 +3225,103 @@ function showToast(text) {
|
||||
clearTimeout(_toastTimer);
|
||||
_toastTimer = setTimeout(() => el.classList.remove('show'), 1800);
|
||||
}
|
||||
async function handleSlash(cmd) {
|
||||
const name = cmd.slice(1).split(/\s+/)[0];
|
||||
|
||||
let tooltipEl = null;
|
||||
let tooltipTarget = null;
|
||||
let tooltipTimer = null;
|
||||
function ensureTooltipEl() {
|
||||
if (!tooltipEl) {
|
||||
tooltipEl = document.createElement('div');
|
||||
tooltipEl.className = 'ga-tooltip';
|
||||
tooltipEl.hidden = true;
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
return tooltipEl;
|
||||
}
|
||||
function migrateNativeTooltips(root = document) {
|
||||
const nodes = [];
|
||||
if (root?.nodeType === Node.ELEMENT_NODE && root.hasAttribute?.('title')) nodes.push(root);
|
||||
root?.querySelectorAll?.('[title]').forEach(el => nodes.push(el));
|
||||
nodes.forEach(el => {
|
||||
const value = el.getAttribute('title') || '';
|
||||
setTooltip(el, value);
|
||||
});
|
||||
}
|
||||
function tooltipNodeFromEventTarget(target) {
|
||||
const el = target?.closest?.('[data-tooltip],[title]');
|
||||
if (el?.hasAttribute('title')) {
|
||||
const value = el.getAttribute('title') || '';
|
||||
setTooltip(el, value);
|
||||
}
|
||||
return el?.dataset?.tooltip ? el : null;
|
||||
}
|
||||
function positionTooltip() {
|
||||
if (!tooltipEl || !tooltipTarget || tooltipEl.hidden) return;
|
||||
const r = tooltipTarget.getBoundingClientRect();
|
||||
const pad = 8;
|
||||
const tw = tooltipEl.offsetWidth;
|
||||
const th = tooltipEl.offsetHeight;
|
||||
const left = Math.max(pad, Math.min(window.innerWidth - tw - pad, r.left + r.width / 2 - tw / 2));
|
||||
const topAbove = r.top - th - 8;
|
||||
const top = topAbove >= pad ? topAbove : Math.min(window.innerHeight - th - pad, r.bottom + 8);
|
||||
tooltipEl.style.left = `${left}px`;
|
||||
tooltipEl.style.top = `${Math.max(pad, top)}px`;
|
||||
}
|
||||
function showTooltip(target) {
|
||||
const text = target?.dataset?.tooltip || '';
|
||||
if (!text) return;
|
||||
tooltipTarget = target;
|
||||
clearTimeout(tooltipTimer);
|
||||
tooltipTimer = setTimeout(() => {
|
||||
const el = ensureTooltipEl();
|
||||
el.textContent = text;
|
||||
el.hidden = false;
|
||||
el.classList.add('show');
|
||||
positionTooltip();
|
||||
}, 180);
|
||||
}
|
||||
function hideTooltip() {
|
||||
clearTimeout(tooltipTimer);
|
||||
tooltipTimer = null;
|
||||
tooltipTarget = null;
|
||||
if (tooltipEl) {
|
||||
tooltipEl.classList.remove('show');
|
||||
tooltipEl.hidden = true;
|
||||
}
|
||||
}
|
||||
function initCustomTooltips() {
|
||||
migrateNativeTooltips();
|
||||
document.addEventListener('pointerover', (e) => {
|
||||
const target = tooltipNodeFromEventTarget(e.target);
|
||||
if (target) showTooltip(target);
|
||||
}, true);
|
||||
document.addEventListener('pointerout', (e) => {
|
||||
if (!tooltipTarget) return;
|
||||
const from = e.target?.closest?.('[data-tooltip]');
|
||||
if (from === tooltipTarget && !tooltipTarget.contains(e.relatedTarget)) hideTooltip();
|
||||
}, true);
|
||||
document.addEventListener('focusin', (e) => {
|
||||
const target = tooltipNodeFromEventTarget(e.target);
|
||||
if (target) showTooltip(target);
|
||||
}, true);
|
||||
document.addEventListener('focusout', hideTooltip, true);
|
||||
window.addEventListener('scroll', hideTooltip, true);
|
||||
window.addEventListener('resize', hideTooltip);
|
||||
new MutationObserver((records) => {
|
||||
for (const rec of records) {
|
||||
if (rec.type === 'attributes' && rec.attributeName === 'title') migrateNativeTooltips(rec.target);
|
||||
rec.addedNodes?.forEach(node => migrateNativeTooltips(node));
|
||||
}
|
||||
}).observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['title'] });
|
||||
}
|
||||
const SLASH_COMMANDS = new Set(['help', 'new', 'clear', 'stop', 'settings']);
|
||||
function slashCommandName(text) {
|
||||
const trimmed = String(text || '').trim();
|
||||
if (!trimmed.startsWith('/')) return '';
|
||||
const token = trimmed.slice(1).split(/\s+/)[0];
|
||||
return SLASH_COMMANDS.has(token) ? token : '';
|
||||
}
|
||||
async function handleSlash(name) {
|
||||
switch (name) {
|
||||
case 'help': showSystem(t('slash.help')); break;
|
||||
case 'new': await newSession(); break;
|
||||
@@ -3919,7 +3796,7 @@ function renderModelMenu(menuEl) {
|
||||
}
|
||||
function openModelMenu(chipEl, menuEl) {
|
||||
if (!chipEl || !menuEl) return;
|
||||
if (typeof convMenu !== 'undefined' && convMenu) convMenu.hidden = true;
|
||||
if (typeof hideConvMenu === 'function') hideConvMenu();
|
||||
window.collabComposer?.closeMenu?.();
|
||||
closeAllModelMenus();
|
||||
renderModelMenu(menuEl);
|
||||
@@ -4462,7 +4339,10 @@ window.ga.onBridgeNotification((msg) => {
|
||||
if (msg && msg.type === 'session-state') {
|
||||
for (const sess of state.sessions.values()) {
|
||||
if (sess.bridgeSessionId === msg.sessionId) {
|
||||
if (msg.status === 'running' || msg.state === 'running') pollSession(sess);
|
||||
if (['running', 'idle', 'error', 'cancelled'].includes(msg.status) ||
|
||||
['running', 'idle', 'error', 'cancelled'].includes(msg.state)) {
|
||||
pollSession(sess);
|
||||
}
|
||||
if (msg.state === 'idle' || msg.status === 'idle') tokPollBridge();
|
||||
renderSessionList();
|
||||
break;
|
||||
@@ -4994,49 +4874,13 @@ if (msgArea) {
|
||||
function uploadRawUrl(path, download) {
|
||||
return `${BRIDGE_ORIGIN}/upload/raw?path=${encodeURIComponent(path || '')}${download ? '&download=1' : ''}`;
|
||||
}
|
||||
function bridgeIsLocal() {
|
||||
return location.hostname === '127.0.0.1' || location.hostname === 'localhost';
|
||||
}
|
||||
async function openUploadFile(path, name) {
|
||||
// 远程访问:浏览器无法调起 bridge 那台/本机的系统程序,降级为下载到本机
|
||||
if (!bridgeIsLocal()) {
|
||||
const a = document.createElement('a');
|
||||
a.href = uploadRawUrl(path, true);
|
||||
a.download = name || '';
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
return;
|
||||
}
|
||||
// 本地:bridge 与你同机,调系统默认程序打开 / 在文件夹显示
|
||||
const mode = isPreviewableByName(name || path) ? 'open' : 'reveal';
|
||||
try {
|
||||
const res = await fetch(`${BRIDGE_ORIGIN}/path/open`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ kind: 'upload', path, mode }),
|
||||
});
|
||||
const j = await res.json();
|
||||
if (!j.ok) throw new Error(j.error || 'open failed');
|
||||
} catch (e) {
|
||||
showChanToast(t('file.openFailed'), e.message || String(e), 'err');
|
||||
}
|
||||
}
|
||||
|
||||
const PREVIEWABLE_EXTS = new Set([
|
||||
'pdf',
|
||||
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'heic', 'tiff',
|
||||
'txt', 'md', 'log', 'json', 'yaml', 'yml', 'xml', 'csv', 'tsv', 'ini', 'toml', 'env', 'rtf',
|
||||
'py', 'js', 'ts', 'tsx', 'jsx', 'java', 'c', 'cpp', 'h', 'hpp', 'rs', 'go', 'rb', 'php', 'sh', 'bash', 'zsh', 'fish', 'lua', 'pl', 'r', 'scala', 'kt', 'swift',
|
||||
'html', 'htm', 'css', 'scss', 'sass', 'less', 'vue', 'svelte', 'sql',
|
||||
'doc', 'docx', 'pages', 'odt',
|
||||
'xls', 'xlsx', 'numbers', 'ods',
|
||||
'ppt', 'pptx', 'key', 'odp',
|
||||
'mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a',
|
||||
'mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv',
|
||||
]);
|
||||
function isPreviewableByName(name) {
|
||||
const m = String(name || '').match(/\.([^./\\]+)$/);
|
||||
if (!m) return false;
|
||||
return PREVIEWABLE_EXTS.has(m[1].toLowerCase());
|
||||
const a = document.createElement('a');
|
||||
a.href = uploadRawUrl(path, true);
|
||||
a.download = name || '';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
/* ═══════════════ 后台服务页 Tab(消息通道 / 状态面板) ═══════════════ */
|
||||
@@ -5690,6 +5534,7 @@ initChatFontStepper();
|
||||
applyChatFontSize(chatFontSize, { persist: false });
|
||||
syncHljsTheme();
|
||||
applyI18n();
|
||||
initCustomTooltips();
|
||||
updateModelChip();
|
||||
renderSessionList();
|
||||
loadCustomPresets();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GenericAgent — Setup</title>
|
||||
<title data-i18n="setup.title">GenericAgent</title>
|
||||
<style>
|
||||
:root{--bg:#fff;--text:#0a0a0a;--muted:#6b6b6b;--border:#e8e8e8;--accent:#0a0a0a;--accent-text:#fff;--radius:10px;--font:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",Helvetica,Arial,sans-serif}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#1a1a1a;--text:#ededed;--muted:#a0a0a0;--border:#2a2a2a;--accent:#ededed;--accent-text:#0a0a0a}}
|
||||
@@ -21,33 +21,31 @@ button:disabled{opacity:.4;cursor:not-allowed}
|
||||
#status{margin-top:1rem;font-size:.85rem;min-height:1.2em}
|
||||
.err{color:#dc2626}.ok{color:#16a34a}
|
||||
</style>
|
||||
<script src="i18n.js?v=1"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>⚙️ Setup Required</h1>
|
||||
<p>The backend could not start. Please configure the paths below.</p>
|
||||
<p>后端启动失败,请配置以下路径。</p>
|
||||
<h1><span aria-hidden="true">⚙️</span> <span data-i18n="setup.heading"></span></h1>
|
||||
<p data-i18n="setup.backend_failed"></p>
|
||||
|
||||
<label class="field-label">Python interpreter 解释器路径</label>
|
||||
<label class="field-label" data-i18n="setup.py_label"></label>
|
||||
<div class="input-row">
|
||||
<input id="pypath" type="text" placeholder="python / path to interpreter" spellcheck="false">
|
||||
<input id="pypath" type="text" data-i18n-placeholder="setup.py_placeholder" spellcheck="false">
|
||||
</div>
|
||||
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem">
|
||||
e.g. <code>C:/Python312/python.exe</code> / <code>~/miniconda3/envs/myenv/bin/python</code>
|
||||
</p>
|
||||
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem" data-i18n="setup.py_hint"></p>
|
||||
|
||||
<label class="field-label">Project directory 项目目录</label>
|
||||
<label class="field-label" data-i18n="setup.project_label"></label>
|
||||
<div class="input-row">
|
||||
<input id="projdir" type="text" placeholder="path to GenericAgent folder" spellcheck="false">
|
||||
<input id="projdir" type="text" data-i18n-placeholder="setup.project_placeholder" spellcheck="false">
|
||||
</div>
|
||||
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem">
|
||||
The folder containing <code>frontends/desktop_bridge.py</code>
|
||||
</p>
|
||||
<p style="color:var(--muted);font-size:.75rem;margin-top:.3rem" data-i18n="setup.project_hint"></p>
|
||||
|
||||
<button id="start-btn" onclick="doStart()" style="margin-top:1.2rem;width:100%">Start 启动</button>
|
||||
<button id="start-btn" onclick="doStart()" style="margin-top:1.2rem;width:100%" data-i18n="setup.start"></button>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
<script>
|
||||
const bootT = window.GA_BOOT_I18N?.t || ((key, vars) => vars?.error ? `${key}: ${vars.error}` : key);
|
||||
window.GA_BOOT_I18N?.apply(document);
|
||||
const {invoke} = window.__TAURI__.core;
|
||||
const {getCurrentWindow, Window} = window.__TAURI__.window;
|
||||
const statusEl = document.getElementById('status');
|
||||
@@ -64,13 +62,13 @@ invoke('get_config').then(([py, dir]) => {
|
||||
async function doStart() {
|
||||
const pythonPath = pyInput.value.trim();
|
||||
const projectDir = dirInput.value.trim();
|
||||
if (!pythonPath) { statusEl.className='err'; statusEl.textContent='Please enter Python path / 请输入 Python 路径'; return; }
|
||||
if (!projectDir) { statusEl.className='err'; statusEl.textContent='Please enter project directory / 请输入项目目录'; return; }
|
||||
if (!pythonPath) { statusEl.className='err'; statusEl.textContent=bootT('setup.err_py'); return; }
|
||||
if (!projectDir) { statusEl.className='err'; statusEl.textContent=bootT('setup.err_project'); return; }
|
||||
btn.disabled = true;
|
||||
statusEl.className=''; statusEl.textContent='Starting bridge… 正在启动…';
|
||||
statusEl.className=''; statusEl.textContent=bootT('setup.starting_bridge');
|
||||
try {
|
||||
await invoke('start_bridge_with_config', {pythonPath, projectDir});
|
||||
statusEl.className='ok'; statusEl.textContent='Connected! 已连接,正在打开主窗口…';
|
||||
statusEl.className='ok'; statusEl.textContent=bootT('setup.connected');
|
||||
// Show main window and hide setup
|
||||
const mainWin = await Window.getByLabel('main');
|
||||
if (mainWin) {
|
||||
@@ -80,7 +78,7 @@ async function doStart() {
|
||||
const setupWin = getCurrentWindow();
|
||||
await setupWin.hide();
|
||||
} catch(e) {
|
||||
statusEl.className='err'; statusEl.textContent='Failed: '+e;
|
||||
statusEl.className='err'; statusEl.textContent=bootT('setup.failed', { error: e });
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
@@ -104,7 +102,7 @@ const _dirInput = document.getElementById('projdir');
|
||||
el.value = el.value.slice(0, MAX_LEN);
|
||||
}
|
||||
if (el.value.length >= MAX_LEN) {
|
||||
hint.textContent = 'Character limit reached (' + MAX_LEN + ') / 已达字数上限(' + MAX_LEN + ')';
|
||||
hint.textContent = bootT('setup.limit', { n: MAX_LEN });
|
||||
hint.style.display = 'block';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
(() => {
|
||||
const I18N = {
|
||||
zh: {
|
||||
'app.title': 'GenericAgent 桌面版',
|
||||
'brand.sub': '桌面终端',
|
||||
'nav.chat': '聊天', 'nav.services': '后台服务', 'nav.channels': '消息通道', 'nav.status': '状态面板',
|
||||
'nav.collab': '指挥家', 'nav.token': '用量',
|
||||
'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版',
|
||||
'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动',
|
||||
'preset.butler.t': '指挥家', 'preset.butler.d': '复杂任务自动拆解,只需查看进度和简报',
|
||||
'preset.plan.t': 'Plan 模式', 'preset.plan.d': '加载 Plan SOP,按探索→规划→执行→验证流程',
|
||||
'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成',
|
||||
'preset.autonomous.t': '自主行动', 'preset.autonomous.d': '按 SOP 规划/执行任务,产出报告(reflect/autonomous.py 同源)',
|
||||
'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚',
|
||||
'preset.review.t': '深度复核', 'preset.review.d': '挑刺式质量把关',
|
||||
'preset.findwork.t': '找点事做', 'preset.findwork.d': '分析当前情况,推荐一批让你感兴趣的 TODO',
|
||||
'preset.mine.t': '我的·周报', 'preset.mine.d': '自定义:抓本周提交并写周报',
|
||||
'preset.add.t': '自定义', 'preset.add.d': '任意一句话存为功能',
|
||||
'composer.placeholder': 'GA 能帮你做些什么?',
|
||||
'search.placeholder': '搜索会话…', 'conv.new': '新对话',
|
||||
'ctx.pin': '置顶', 'ctx.unpin': '取消置顶', 'ctx.rename': '重命名', 'ctx.del': '删除',
|
||||
'common.close': '关闭', 'common.more': '更多', 'common.optional': '选填', 'common.save': '保存',
|
||||
'modal.preset': '预设功能', 'modal.addModel': '添加模型', 'modal.editModel': '编辑模型', 'modal.settings': '配置',
|
||||
'modal.customPreset': '自定义预设',
|
||||
'modal.editCustomPreset': '编辑任务',
|
||||
'customPreset.titlePh': '标题,例如「写周报」',
|
||||
'customPreset.promptPh': 'Prompt 内容,发送时会作为消息提交',
|
||||
'customPreset.empty': '标题和 Prompt 不能为空',
|
||||
'customPreset.removeTitle': '删除',
|
||||
'customPreset.editTitle': '编辑',
|
||||
'builtinPreset.restoreBtn': '恢复默认预设',
|
||||
'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py)', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理',
|
||||
'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?',
|
||||
'appearance.light': '浅色', 'appearance.dark': '深色',
|
||||
'set.noModels': '暂无模型,点击下方添加',
|
||||
'lang.zh': '简体中文', 'lang.en': 'English',
|
||||
'model.name': '备注', 'model.namePh': '会显示在模型列表',
|
||||
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': '留空则保持原 Key 不变',
|
||||
'model.apibase': 'API 地址', 'model.apibasePh': 'https://.../v1/messages',
|
||||
'model.protocol': '协议', 'model.protocolPick': '请选择…', 'model.protocolOai': 'OpenAI 兼容 (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
|
||||
'model.stream': '响应方式', 'model.streamOn': '流式', 'model.streamOff': '非流式',
|
||||
'model.model': '模型', 'model.modelPh': 'model 参数名',
|
||||
'model.modelHint': '须与中转站/官方文档中的 model 字段完全一致',
|
||||
'model.retries': '重试 (次)', 'model.connTimeout': '连接超时 (s)', 'model.readTimeout': '读取超时 (s)',
|
||||
'model.save': '保存', 'common.cancel': '取消', 'common.confirm': '确认', 'common.edit': '编辑', 'common.delete': '删除',
|
||||
'pq.title': '快速接入官方模型', 'pq.sub': '填好 API Key 即可使用', 'pq.toggle': '展开 / 收起',
|
||||
'pq.deepseekDesc': '官方 API · OpenAI 兼容', 'pq.qwenDesc': '通义千问 · 阿里云百炼',
|
||||
'guide.step1': '点击下方链接,登录后创建并复制 API Key',
|
||||
'guide.step2': '把 Key 粘贴到下方「API Key」输入框',
|
||||
'guide.step3': '点击保存,即可在模型列表中选用',
|
||||
'guide.prefillTip': '已为你预填 API 地址、协议与模型,可按需修改',
|
||||
'guide.getKey': '获取 {name} 的 API Key', 'guide.copy': '复制链接', 'guide.copied': '链接已复制',
|
||||
'err.modelSave': '保存失败', 'err.modelSwitch': '切换模型失败', 'err.modelRequired': '请填写模型、API Key 和 API 地址',
|
||||
'err.modelDelete': '删除失败', 'err.modelDeleteLast': '至少保留一个模型',
|
||||
'confirm.modelDelete': '确定删除该模型配置?',
|
||||
'model.aggregation': '渠道组(自动故障转移)', 'model.aggregationShort': '渠道组', 'model.aggregationDesc': '按顺序尝试,失败自动切换到下一个',
|
||||
'model.emptyMixin': '尚未加入模型',
|
||||
'model.addToMixin': '加入渠道组', 'model.inMixin': '已在渠道组', 'model.removeFromMixin': '移出渠道组', 'model.alreadyInMixin': '已在渠道组中', 'model.dragReorder': '拖拽调整顺序',
|
||||
'err.mixinFailed': '操作失败',
|
||||
'page.services.title': '后台服务', 'page.services.sub': 'IM 消息通道与后台进程,集中查看、启停与日志',
|
||||
'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)',
|
||||
'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停',
|
||||
'page.collab.title': '指挥家', 'page.collab.sub': '交代目标,自动拆活与跟进',
|
||||
'collab.progressTitle': '分工进度',
|
||||
'collab.progressEmpty': '还没有任务在执行。告诉指挥家你的目标后,这里会显示拆分后的处理进度。',
|
||||
'collab.placeholder': '请对指挥家描述你想完成的目标',
|
||||
'collab.guideTitle': '把要完成的事告诉指挥家',
|
||||
'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。',
|
||||
'collab.guideStep1t': '描述目标',
|
||||
'collab.guideStep1d': '在聊天框里写下你想做的事,发给指挥家',
|
||||
'collab.guideStep2t': '自动拆解',
|
||||
'collab.guideStep2d': '指挥家自动拆解、分配任务,实时监督和调度',
|
||||
'collab.guideStep3t': '交付摘要',
|
||||
'collab.guideStep3d': '指挥家根据执行状态,呈上任务简报',
|
||||
'collab.guideStep4t': '随时调整',
|
||||
'collab.guideStep4d': '随时补充要求或细节,指挥家都会处理',
|
||||
'collab.chipProgress': '现在进展如何?',
|
||||
'collab.chipPause': '先暂停当前任务',
|
||||
'collab.chipSummary': '总结一下目前的结果',
|
||||
'collab.showProgressTitle': '查看分工进度',
|
||||
'collab.statRunning': '进行中',
|
||||
'collab.statDone': '已完成',
|
||||
'collab.plusMenu': '更多操作',
|
||||
'collab.switchMode': '切换模式',
|
||||
'collab.typing': '指挥家正在处理',
|
||||
'collab.offline': '无法连接指挥家服务,请确认后端已启动。',
|
||||
'collab.retry': '重试',
|
||||
'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。',
|
||||
'collab.reconnectIn': '{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': '用量', 'page.token.sub': '每会话与累计用量及缓存率',
|
||||
'status.connecting': '正在连接…', 'status.ready': '服务在线', 'status.running': '处理中',
|
||||
'status.disconnected': '服务离线', 'status.stopped': '已停止', 'status.idle': '待命',
|
||||
'conv.emptyList': '暂无会话,点「+ 新对话」开始', 'conv.defaultTitle': '新对话',
|
||||
'err.bridge': '服务未响应', 'err.newSession': '新建会话失败', 'err.poll': '轮询失败', 'err.stop': '停止失败',
|
||||
'err.interruptTimeout': '等待上一轮停止超时,请稍后再试',
|
||||
'sys.interruptPrev.hint': '已停止上一轮,正在处理新消息',
|
||||
'chat.interrupting': '正在停止上一轮…',
|
||||
'chat.sessionLoading': '正在加载会话…',
|
||||
'sys.stopRequested': '已请求停止',
|
||||
'slash.help': '可用命令:\n/new 新会话 /clear 清屏 /stop 停止 /settings 设置',
|
||||
'slash.unknown': '未知命令',
|
||||
'upload.hint': '上传文件:选择 / 拖拽 / 粘贴',
|
||||
'upload.button': '上传文件',
|
||||
'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件',
|
||||
'upload.failed': '上传失败',
|
||||
'err.charLimit': '已达字数上限({n}),发送时将自动截断', 'err.charLimitReached': '已达字数上限({n})', 'err.numMax': '不能超过 {n}',
|
||||
'file.openFailed': '无法打开文件',
|
||||
'file.kindGeneric': '文件',
|
||||
'file.kindDoc': '文档',
|
||||
'file.kindSheet': '表格',
|
||||
'file.kindSlide': '幻灯片',
|
||||
'file.kindCode': '代码',
|
||||
'file.kindArchive': '压缩包',
|
||||
'file.kindAudio': '音频',
|
||||
'file.kindVideo': '视频',
|
||||
'upload.removeTitle': '移除',
|
||||
'upload.dropHint': '松开以上传文件',
|
||||
'lightbox.closeTitle': '关闭',
|
||||
'fold.thinking': '思考', 'fold.tool': '工具调用', 'fold.toolResult': '工具结果', 'fold.llm': 'LLM Running', 'fold.turn': '第 {n} 轮',
|
||||
'plan.header': '计划 ({done}/{total})', 'plan.complete': '✓ 计划完成 ({n}/{n})',
|
||||
'plan.running': '计划执行中', 'plan.completeTitle': '计划完成',
|
||||
'plan.placeholder': '计划模式已激活', 'plan.waiting': '等待写入 {path} …', 'plan.overflow': '还有 {n} 项',
|
||||
'plan.current': '当前', 'plan.fold': '折叠', 'plan.collapse': '收起', 'plan.expand': '展开', 'plan.details': '详情', 'plan.dismiss': '不再显示',
|
||||
'plan.capsuleRunning': '运行中', 'plan.capsuleComplete': '已完成',
|
||||
'timing.elapsed': '已运行 {t}',
|
||||
'model.auto': '自动选择',
|
||||
'model.menuLabel': '选择模型',
|
||||
'chip.plan': 'Plan',
|
||||
'chip.auto': 'Auto',
|
||||
'ch.wechat': '微信', 'ch.wecom': '企业微信', 'ch.lark': '飞书', 'ch.dingtalk': '钉钉',
|
||||
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
|
||||
'ch.loading': '加载中…', 'ch.empty': '未发现 IM 进程脚本',
|
||||
'ch.logEmpty': '暂无日志',
|
||||
'err.channelLoad': '加载失败', 'err.channelStart': '启动失败', 'err.channelStop': '停止失败',
|
||||
'err.mykeyImport': '导入模型配置失败',
|
||||
'err.mykeyExport': '导出模型配置失败',
|
||||
'err.channelNotConfigured': '请先在 mykey.py 中配置该平台',
|
||||
'sys.channelStarted': '已启动', 'sys.channelStopped': '已停止',
|
||||
'modal.channelLogs': '进程日志',
|
||||
'modal.mykeyConfig': 'mykey.py 配置',
|
||||
'sys.configSaved': '配置已保存',
|
||||
'sys.mykeyImported': '模型配置已导入',
|
||||
'sys.mykeyExported': '模型配置已导出',
|
||||
'st.starting': '启动中…', 'st.stopping': '停止中…', 'st.online': '在线', 'st.offline': '离线', 'st.error': '错误', 'st.running': '运行', 'st.abnormal': '异常',
|
||||
'act.configure': '配置', 'act.logs': '日志', 'act.restart': '重启', 'act.stop': '停止', 'act.start': '启动', 'act.exit': '退出',
|
||||
'act.copy': '复制', 'act.copied': '已复制', 'act.copyTex': 'TeX', 'act.send': '发送',
|
||||
'proc.imbotWechat': 'imbot · 微信', 'proc.imbotDing': 'imbot · 钉钉', 'proc.scheduler': '定时任务调度', 'proc.conductor': '指挥家',
|
||||
'cm.scheduling': '调度中', 'cm.running': '执行中', 'cm.idleSt': '空闲',
|
||||
'cm.master': '已派 3 子任务', 'cm.w1': '子任务:抓取数据', 'cm.w2': '子任务:复核结果', 'cm.sub': '等待派单',
|
||||
'tok.total': '累计', 'tok.cost': '缓存率', 'tok.today': '今日', 'tok.tabAll': '聊天', 'tok.tabConductor': '指挥家', 'tok.condTotal': '指挥家累计', 'tok.condCurrent': '指挥家本次', 'tok.condTip': '指挥家消耗不计入聊天累计', 'tok.condOffline': '指挥家服务离线', 'tok.disclaimer': '不同 API 网站的计费价格可能会有差异,请以实际网站为准。',
|
||||
'tok.colSession': '会话', 'tok.colIn': '输入', 'tok.colOut': '输出', 'tok.colCacheW': '缓存写入', 'tok.colCache': '缓存读取', 'tok.colCost': '成本',
|
||||
'tok.from': '从', 'tok.to': '到', 'tok.reset': '重置', 'tok.noData': '暂无记录', 'tok.deleted': '此会话已删除',
|
||||
'tok.pricingUnknown': '⚠ 此模型计费规则尚未明确,按默认估算',
|
||||
'tok.priceInput': '输入: $', 'tok.priceOutput': '输出: $',
|
||||
'tok.priceCacheW': '缓存写入: $', 'tok.priceCacheR': '缓存读取: $',
|
||||
'presetPrompt.goal': '进入 Goal 模式:读 L3 goal mode SOP,自主达成我接下来描述的目标。',
|
||||
'presetPrompt.plan': '进入 Plan 模式:先读 memory/plan_sop.md,按其中「探索→规划→执行→验证」流程,等我接下来描述要做的任务。',
|
||||
'presetPrompt.autonomous': '🤖 进入自主行动模式:阅读 memory/autonomous_operation_sop.md,按 SOP 选取或规划任务,独立执行并产出报告。',
|
||||
'presetPrompt.hive': '启动 Goal Hive 模式:按 hive SOP 拉起多个 worker 协同完成我接下来的目标。',
|
||||
'presetPrompt.review': '进入监察者模式:对刚才的产出严格挑刺、逐项复核并报告问题。',
|
||||
'presetPrompt.findwork': '按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。',
|
||||
'presetPrompt.mine': '抓取本周的 git 提交并写一份周报。',
|
||||
'ask.banner': 'GA 等你回答',
|
||||
'ask.replyHint': '在下方输入框回复',
|
||||
'ask.placeholderOpen': '在此输入你的回答… (Enter 发送)',
|
||||
},
|
||||
en: {
|
||||
'app.title': 'GenericAgent Desktop',
|
||||
'brand.sub': 'Desktop terminal',
|
||||
'nav.chat': 'Chat', 'nav.services': 'Services', 'nav.channels': 'Channels', 'nav.status': 'Status',
|
||||
'nav.collab': 'Conductor', 'nav.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': 'Auto-decompose complex tasks; just check progress and briefings',
|
||||
'preset.plan.t': 'Plan mode', 'preset.plan.d': 'Load Plan SOP — explore→plan→execute→verify',
|
||||
'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously',
|
||||
'preset.autonomous.t': 'Autonomous mode', 'preset.autonomous.d': 'Plan/execute tasks per SOP and produce reports (same as reflect/autonomous.py)',
|
||||
'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration',
|
||||
'preset.review.t': 'Deep review', 'preset.review.d': 'Strict quality check',
|
||||
'preset.findwork.t': 'Find me work', 'preset.findwork.d': 'Analyze my context and suggest a batch of interesting TODOs',
|
||||
'preset.mine.t': 'My · Weekly', 'preset.mine.d': 'Custom: weekly report from commits',
|
||||
'preset.add.t': 'Custom', 'preset.add.d': 'Save any prompt as a function',
|
||||
'composer.placeholder': 'What can GA do for you?',
|
||||
'search.placeholder': 'Search chats…', 'conv.new': 'New chat',
|
||||
'ctx.pin': 'Pin', 'ctx.unpin': 'Unpin', 'ctx.rename': 'Rename', 'ctx.del': 'Delete',
|
||||
'common.close': 'Close', 'common.more': 'More', 'common.optional': 'Optional', 'common.save': 'Save',
|
||||
'modal.preset': 'Presets', 'modal.addModel': 'Add model', 'modal.editModel': 'Edit model', 'modal.settings': 'Settings',
|
||||
'modal.customPreset': 'Custom preset',
|
||||
'modal.editCustomPreset': 'Edit task',
|
||||
'customPreset.titlePh': 'Title, e.g. "Weekly report"',
|
||||
'customPreset.promptPh': 'Prompt body — sent as the message when clicked',
|
||||
'customPreset.empty': 'Title and Prompt cannot be empty',
|
||||
'customPreset.removeTitle': 'Delete',
|
||||
'customPreset.editTitle': 'Edit',
|
||||
'builtinPreset.restoreBtn': 'Restore defaults',
|
||||
'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager',
|
||||
'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?',
|
||||
'appearance.light': 'Light', 'appearance.dark': 'Dark',
|
||||
'set.noModels': 'No models yet — add one below',
|
||||
'lang.zh': '简体中文', 'lang.en': 'English',
|
||||
'model.name': 'Note', 'model.namePh': 'Shown in the model list',
|
||||
'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': 'Leave blank to keep the current key',
|
||||
'model.apibase': 'API base URL', 'model.apibasePh': 'https://.../v1/messages',
|
||||
'model.protocol': 'Protocol', 'model.protocolPick': 'Select…', 'model.protocolOai': 'OpenAI-compatible (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)',
|
||||
'model.stream': 'Response', 'model.streamOn': 'Stream', 'model.streamOff': 'Non-stream',
|
||||
'model.model': 'Model', 'model.modelPh': 'model parameter name',
|
||||
'model.modelHint': 'Must match the model field in your provider docs exactly',
|
||||
'model.retries': 'Retries (×)', 'model.connTimeout': 'Connect (s)', 'model.readTimeout': 'Read (s)',
|
||||
'model.save': 'Save', 'common.cancel': 'Cancel', 'common.confirm': 'Confirm', 'common.edit': 'Edit', 'common.delete': 'Delete',
|
||||
'pq.title': 'Quick connect a model', 'pq.sub': 'Add your API key to get started', 'pq.toggle': 'Expand / collapse',
|
||||
'pq.deepseekDesc': 'Official API · OpenAI-compatible', 'pq.qwenDesc': 'Tongyi Qwen · Aliyun Bailian',
|
||||
'guide.step1': 'Open the link, sign in, then create & copy your API key',
|
||||
'guide.step2': 'Paste the key into the “API Key” field below',
|
||||
'guide.step3': 'Click Save — then pick it from the model list',
|
||||
'guide.prefillTip': 'API base, protocol and model are pre-filled — edit if needed',
|
||||
'guide.getKey': 'Get your {name} API key', 'guide.copy': 'Copy link', 'guide.copied': 'Link copied',
|
||||
'err.modelSave': 'Save failed', 'err.modelSwitch': 'Failed to switch model', 'err.modelRequired': 'Model, API Key and base URL are required',
|
||||
'err.modelDelete': 'Delete failed', 'err.modelDeleteLast': 'At least one model is required',
|
||||
'confirm.modelDelete': 'Delete this model profile?',
|
||||
'model.aggregation': 'Channel group (auto failover)', 'model.aggregationShort': 'Channel group', 'model.aggregationDesc': 'Tries in order, switches to the next on failure',
|
||||
'model.emptyMixin': 'No models added yet',
|
||||
'model.addToMixin': 'Add to channel', 'model.inMixin': 'In channel', 'model.removeFromMixin': 'Remove from channel', 'model.alreadyInMixin': 'Already in the channel', 'model.dragReorder': 'Drag to reorder',
|
||||
'err.mixinFailed': 'Operation failed',
|
||||
'page.services.title': 'Services', 'page.services.sub': 'IM channels and background processes — view, start/stop, logs',
|
||||
'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': '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.placeholder': 'Describe the goal you want to accomplish',
|
||||
'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',
|
||||
'collab.statRunning': 'Running',
|
||||
'collab.statDone': 'Done',
|
||||
'collab.plusMenu': 'More actions',
|
||||
'collab.switchMode': 'Switch mode',
|
||||
'collab.typing': 'Conductor is working',
|
||||
'collab.offline': 'Cannot reach the service. Make sure the backend is running.',
|
||||
'collab.retry': 'Retry',
|
||||
'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.',
|
||||
'collab.reconnectIn': 'Retry in {n}s',
|
||||
'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': 'Usage', 'page.token.sub': 'Per-session and total usage & cache rate',
|
||||
'status.connecting': 'Connecting…', 'status.ready': 'Service online', 'status.running': 'Working…',
|
||||
'status.disconnected': 'Service offline', 'status.stopped': 'Stopped', 'status.idle': 'Standby',
|
||||
'conv.emptyList': 'No chats yet — click “+ New chat”', 'conv.defaultTitle': 'New chat',
|
||||
'err.bridge': 'Service not responding', 'err.newSession': 'Failed to create session', 'err.poll': 'Polling failed', 'err.stop': 'Stop failed',
|
||||
'err.interruptTimeout': 'Timed out waiting for the previous reply to stop — try again',
|
||||
'sys.interruptPrev.hint': 'Previous reply stopped — processing new message',
|
||||
'chat.interrupting': 'Stopping previous reply…',
|
||||
'chat.sessionLoading': 'Loading conversation…',
|
||||
'sys.stopRequested': 'Stop requested',
|
||||
'slash.help': 'Commands:\n/new new chat /clear clear /stop stop /settings settings',
|
||||
'slash.unknown': 'Unknown command',
|
||||
'upload.hint': 'Upload file: pick / drag / paste',
|
||||
'upload.button': 'Upload file',
|
||||
'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file',
|
||||
'upload.failed': 'Upload failed',
|
||||
'err.charLimit': 'Character limit reached ({n}), text will be truncated on send', 'err.charLimitReached': 'Character limit reached ({n})', 'err.numMax': 'Cannot exceed {n}',
|
||||
'file.openFailed': 'Cannot open file',
|
||||
'file.kindGeneric': 'File',
|
||||
'file.kindDoc': 'Document',
|
||||
'file.kindSheet': 'Spreadsheet',
|
||||
'file.kindSlide': 'Slides',
|
||||
'file.kindCode': 'Code',
|
||||
'file.kindArchive': 'Archive',
|
||||
'file.kindAudio': 'Audio',
|
||||
'file.kindVideo': 'Video',
|
||||
'upload.removeTitle': 'Remove',
|
||||
'upload.dropHint': 'Drop to upload files',
|
||||
'lightbox.closeTitle': 'Close',
|
||||
'fold.thinking': 'Thinking', 'fold.tool': 'Tool call', 'fold.toolResult': 'Tool result', 'fold.llm': 'LLM Running', 'fold.turn': 'Turn {n}',
|
||||
'plan.header': 'Plan ({done}/{total})', 'plan.complete': '✓ Plan complete ({n}/{n})',
|
||||
'plan.running': 'Running plan', 'plan.completeTitle': 'Plan complete',
|
||||
'plan.placeholder': 'Plan mode activated', 'plan.waiting': 'waiting for {path} …', 'plan.overflow': '+{n} more',
|
||||
'plan.current': 'Now', 'plan.fold': 'Fold', 'plan.collapse': 'Collapse', 'plan.expand': 'Expand', 'plan.details': 'Details', 'plan.dismiss': 'Hide',
|
||||
'plan.capsuleRunning': 'Running', 'plan.capsuleComplete': 'Done',
|
||||
'timing.elapsed': 'Elapsed {t}',
|
||||
'model.auto': 'Auto',
|
||||
'model.menuLabel': 'Select model',
|
||||
'chip.plan': 'Plan',
|
||||
'chip.auto': 'Auto',
|
||||
'ch.wechat': 'WeChat', 'ch.wecom': 'WeCom', 'ch.lark': 'Lark', 'ch.dingtalk': 'DingTalk',
|
||||
'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord',
|
||||
'ch.loading': 'Loading…', 'ch.empty': 'No IM process scripts found',
|
||||
'ch.logEmpty': 'No log output yet',
|
||||
'err.channelLoad': 'Failed to load', 'err.channelStart': 'Start failed', 'err.channelStop': 'Stop failed',
|
||||
'err.mykeyImport': 'Failed to import model config',
|
||||
'err.mykeyExport': 'Failed to export model config',
|
||||
'err.channelNotConfigured': 'Configure this platform in mykey.py first',
|
||||
'sys.channelStarted': 'Started', 'sys.channelStopped': 'Stopped',
|
||||
'modal.channelLogs': 'Process logs',
|
||||
'modal.mykeyConfig': 'mykey.py',
|
||||
'sys.configSaved': 'Configuration saved',
|
||||
'sys.mykeyImported': 'Model config imported',
|
||||
'sys.mykeyExported': 'Model config exported',
|
||||
'st.starting': 'Starting…', 'st.stopping': 'Stopping…', 'st.online': 'Online', 'st.offline': 'Offline', 'st.error': 'Error', 'st.running': 'Running', 'st.abnormal': 'Error',
|
||||
'act.configure': 'Configure', 'act.logs': 'Logs', 'act.restart': 'Restart', 'act.stop': 'Stop', 'act.start': 'Start', 'act.exit': 'Exit',
|
||||
'act.copy': 'Copy', 'act.copied': 'Copied', 'act.copyTex': 'TeX', 'act.send': 'Send',
|
||||
'proc.imbotWechat': 'imbot · WeChat', 'proc.imbotDing': 'imbot · DingTalk', 'proc.scheduler': 'Scheduler', 'proc.conductor': 'Conductor',
|
||||
'cm.scheduling': 'Scheduling', 'cm.running': 'Running', 'cm.idleSt': 'Idle',
|
||||
'cm.master': 'Dispatched 3 subtasks', 'cm.w1': 'Subtask: fetch data', 'cm.w2': 'Subtask: review results', 'cm.sub': 'Waiting for tasks',
|
||||
'tok.total': 'Total', 'tok.cost': 'Cache rate', 'tok.today': 'Today', 'tok.tabAll': 'Chat', 'tok.tabConductor': 'Conductor', 'tok.condTotal': 'Conductor Total', 'tok.condCurrent': 'Conductor Current', 'tok.condTip': 'Conductor usage is not included in chat totals', 'tok.condOffline': 'Service offline', 'tok.disclaimer': 'Pricing may vary by API provider. Please refer to the actual website.',
|
||||
'tok.colSession': 'Session', 'tok.colIn': 'Input', 'tok.colOut': 'Output', 'tok.colCacheW': 'Cache write', 'tok.colCache': 'Cache read', 'tok.colCost': 'Cost',
|
||||
'tok.from': 'From', 'tok.to': 'To', 'tok.reset': 'Reset', 'tok.noData': 'No records', 'tok.deleted': 'Session deleted',
|
||||
'tok.pricingUnknown': '⚠ Pricing not confirmed, using defaults',
|
||||
'tok.priceInput': 'Input: $', 'tok.priceOutput': 'Output: $',
|
||||
'tok.priceCacheW': 'Cache write: $', 'tok.priceCacheR': 'Cache read: $',
|
||||
'presetPrompt.goal': 'Enter Goal mode: read the L3 goal-mode SOP and autonomously achieve the goal I describe next.',
|
||||
'presetPrompt.plan': 'Enter Plan mode: first read memory/plan_sop.md, follow its explore→plan→execute→verify flow, and wait for the task I describe next.',
|
||||
'presetPrompt.autonomous': '🤖 Enter autonomous mode: read memory/autonomous_operation_sop.md, follow the SOP to pick or plan a task, execute independently, and produce a report.',
|
||||
'presetPrompt.hive': 'Start Goal Hive mode: per the hive SOP, spawn multiple workers to collaboratively achieve the goal I describe next.',
|
||||
'presetPrompt.review': 'Enter reviewer mode: strictly scrutinize the previous output, review item by item and report issues.',
|
||||
'presetPrompt.findwork': 'Following the autonomous planning section, analyze my situation thoroughly and generate a batch of TODOs that genuinely interest me.',
|
||||
'presetPrompt.mine': 'Collect this week\'s git commits and write a weekly report.',
|
||||
'ask.banner': 'GA is waiting for your answer',
|
||||
'ask.replyHint': 'Reply in the input below',
|
||||
'ask.placeholderOpen': 'Type your answer here… (Enter to send)',
|
||||
},
|
||||
};
|
||||
|
||||
const BOOT_I18N = {
|
||||
zh: {
|
||||
'loading.starting_app': '???? GenericAgent...',
|
||||
'loading.start': '?????????????...',
|
||||
'loading.venv': '????????...',
|
||||
'loading.deps': '??????...',
|
||||
'loading.done': '??????',
|
||||
'loading.starting': '??????...',
|
||||
'setup.title': 'GenericAgent - ??',
|
||||
'setup.heading': '????',
|
||||
'setup.backend_failed': '???????????????',
|
||||
'setup.py_label': 'Python ?????',
|
||||
'setup.py_placeholder': 'python / ?????',
|
||||
'setup.py_hint': '?? C:/Python312/python.exe ? ~/miniconda3/envs/myenv/bin/python',
|
||||
'setup.project_label': '????',
|
||||
'setup.project_placeholder': 'GenericAgent ?????',
|
||||
'setup.project_hint': '?? frontends/desktop_bridge.py ????',
|
||||
'setup.start': '??',
|
||||
'setup.err_py': '??? Python ??',
|
||||
'setup.err_project': '???????',
|
||||
'setup.starting_bridge': '???? bridge...',
|
||||
'setup.connected': '???????????...',
|
||||
'setup.failed': '?????{error}',
|
||||
'setup.limit': '???????{n}?'
|
||||
},
|
||||
en: {
|
||||
'loading.starting_app': 'Starting GenericAgent...',
|
||||
'loading.start': 'First run: preparing the runtime...',
|
||||
'loading.venv': 'Creating the runtime environment...',
|
||||
'loading.deps': 'Installing dependencies...',
|
||||
'loading.done': 'Dependencies installed',
|
||||
'loading.starting': 'Starting services...',
|
||||
'setup.title': 'GenericAgent - Setup',
|
||||
'setup.heading': 'Setup Required',
|
||||
'setup.backend_failed': 'The backend could not start. Please configure the paths below.',
|
||||
'setup.py_label': 'Python interpreter path',
|
||||
'setup.py_placeholder': 'python / path to interpreter',
|
||||
'setup.py_hint': 'e.g. C:/Python312/python.exe or ~/miniconda3/envs/myenv/bin/python',
|
||||
'setup.project_label': 'Project directory',
|
||||
'setup.project_placeholder': 'path to GenericAgent folder',
|
||||
'setup.project_hint': 'The folder containing frontends/desktop_bridge.py',
|
||||
'setup.start': 'Start',
|
||||
'setup.err_py': 'Please enter Python path',
|
||||
'setup.err_project': 'Please enter project directory',
|
||||
'setup.starting_bridge': 'Starting bridge...',
|
||||
'setup.connected': 'Connected. Opening the main window...',
|
||||
'setup.failed': 'Failed: {error}',
|
||||
'setup.limit': 'Character limit reached ({n})'
|
||||
}
|
||||
};
|
||||
|
||||
for (const [code, messages] of Object.entries(BOOT_I18N)) {
|
||||
I18N[code] = Object.assign(I18N[code] || {}, messages);
|
||||
}
|
||||
|
||||
const LANGS = Object.freeze(Object.keys(I18N));
|
||||
|
||||
function normalizeLang(code) {
|
||||
return LANGS.includes(code) ? code : 'zh';
|
||||
}
|
||||
|
||||
function browserLang() {
|
||||
return (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
|
||||
}
|
||||
|
||||
function bootLang() {
|
||||
try {
|
||||
const saved = localStorage.getItem('ga_lang');
|
||||
if (LANGS.includes(saved)) return saved;
|
||||
} catch (_) {}
|
||||
return browserLang();
|
||||
}
|
||||
|
||||
function tFor(lang, key, vars) {
|
||||
const active = normalizeLang(lang);
|
||||
let text = (I18N[active] && I18N[active][key]) || (I18N.zh && I18N.zh[key]) || (I18N.en && I18N.en[key]) || key;
|
||||
if (vars) {
|
||||
for (const [name, value] of Object.entries(vars)) {
|
||||
text = text.replaceAll(`{${name}}`, String(value));
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function apply(root, activeLang) {
|
||||
const lang = normalizeLang(activeLang || bootLang());
|
||||
const scope = root || document;
|
||||
if (scope === document) document.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN';
|
||||
scope.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = tFor(lang, el.dataset.i18n);
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n-ph]').forEach(el => {
|
||||
el.setAttribute('data-ph', tFor(lang, el.dataset.i18nPh));
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
el.setAttribute('placeholder', tFor(lang, el.dataset.i18nPlaceholder));
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
el.setAttribute('title', tFor(lang, el.dataset.i18nTitle));
|
||||
el.setAttribute('aria-label', tFor(lang, el.dataset.i18nTitle));
|
||||
});
|
||||
}
|
||||
|
||||
window.GA_I18N = {
|
||||
dict: I18N,
|
||||
languages: LANGS,
|
||||
t: tFor,
|
||||
apply,
|
||||
bootLang,
|
||||
browserLang,
|
||||
};
|
||||
|
||||
window.GA_BOOT_I18N = {
|
||||
get lang() { return bootLang(); },
|
||||
t(key, vars) { return tFor(bootLang(), key, vars); },
|
||||
apply(root) { return apply(root || document, bootLang()); },
|
||||
};
|
||||
})();
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>GenericAgent</title>
|
||||
<link rel="stylesheet" href="assets/fonts/fonts.css?v=2">
|
||||
<link rel="stylesheet" href="styles.css?v=182">
|
||||
<link rel="stylesheet" href="styles.css?v=186">
|
||||
<!-- KaTeX: LaTeX 公式渲染 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous">
|
||||
<!-- highlight.js: 代码高亮 -->
|
||||
@@ -167,7 +167,7 @@
|
||||
<div class="input" id="chat-input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="composer.placeholder"></div>
|
||||
<div class="composer-bot">
|
||||
<div class="composer-plus-wrap">
|
||||
<button type="button" class="ic-btn composer-plus" id="chat-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
|
||||
<button type="button" class="ic-btn composer-plus" id="chat-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" data-no-tooltip title="">
|
||||
<span data-ga-icon="plus"></span>
|
||||
</button>
|
||||
<div class="ga-menu composer-menu" id="chat-menu" hidden role="menu">
|
||||
@@ -317,7 +317,7 @@
|
||||
<div class="input" id="cdb-input" contenteditable="true" role="textbox" aria-multiline="true" data-i18n-ph="collab.placeholder"></div>
|
||||
<div class="composer-bot">
|
||||
<div class="composer-plus-wrap">
|
||||
<button type="button" class="ic-btn composer-plus" id="cdb-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" title="">
|
||||
<button type="button" class="ic-btn composer-plus" id="cdb-plus-btn" aria-haspopup="menu" aria-expanded="false" data-i18n-title="collab.plusMenu" data-no-tooltip title="">
|
||||
<span data-ga-icon="plus"></span>
|
||||
</button>
|
||||
<div class="ga-menu composer-menu" id="cdb-menu" hidden role="menu">
|
||||
@@ -674,6 +674,7 @@
|
||||
<!-- highlight.js -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous"></script>
|
||||
<script src="phosphor-icons.js?v=2"></script>
|
||||
<script src="app.js?v=211"></script>
|
||||
<script src="i18n.js?v=1"></script>
|
||||
<script src="app.js?v=216"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,21 +17,16 @@ html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:c
|
||||
#ga-bar.indet{width:35%;animation:indet 1.1s ease-in-out infinite}
|
||||
@keyframes indet{0%{margin-left:-35%}100%{margin-left:100%}}
|
||||
</style>
|
||||
<script src="i18n.js?v=1"></script>
|
||||
<script>
|
||||
// Loading window shows before the main UI, on a different origin than the app, so it can't
|
||||
// read the app's saved language; fall back to the system/browser locale.
|
||||
var GA_ZH = (navigator.language || '').toLowerCase().indexOf('zh') === 0;
|
||||
var GA_MSG = {
|
||||
starting_app: GA_ZH ? '正在启动 GenericAgent…' : 'Starting GenericAgent…',
|
||||
start: GA_ZH ? '首次启动,正在准备运行环境…' : 'First run: preparing the runtime…',
|
||||
venv: GA_ZH ? '正在创建运行环境…' : 'Creating the runtime environment…',
|
||||
deps: GA_ZH ? '正在安装依赖…' : 'Installing dependencies…',
|
||||
done: GA_ZH ? '依赖安装完成' : 'Dependencies installed',
|
||||
starting: GA_ZH ? '正在启动服务…' : 'Starting services…'
|
||||
};
|
||||
const bootT = window.GA_BOOT_I18N?.t || ((key) => key);
|
||||
window.gaProgress = function(pct, key){
|
||||
var s = document.getElementById('ga-status');
|
||||
if (s) s.textContent = GA_MSG[key] || key;
|
||||
if (s) {
|
||||
var msgKey = 'loading.' + key;
|
||||
var msg = bootT(msgKey);
|
||||
s.textContent = msg === msgKey ? key : msg;
|
||||
}
|
||||
var bar = document.querySelector('.bar');
|
||||
var fill = document.getElementById('ga-bar');
|
||||
if (bar) bar.classList.add('show');
|
||||
@@ -41,15 +36,14 @@ window.gaProgress = function(pct, key){
|
||||
}
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var s = document.getElementById('ga-status');
|
||||
if (s) s.textContent = GA_MSG.starting_app;
|
||||
window.GA_BOOT_I18N?.apply(document);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="spinner"></div>
|
||||
<div id="ga-status">正在启动 GenericAgent…</div>
|
||||
<div id="ga-status" data-i18n="loading.starting_app"></div>
|
||||
<div class="bar"><div id="ga-bar"></div></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -92,8 +92,11 @@
|
||||
--shadow-pop:0 8px 24px #14192829;
|
||||
--shadow-modal:0 18px 50px #14192847;
|
||||
|
||||
--divider-base:rgba(15, 74, 133, 0.22);
|
||||
--divider-base:rgba(0,0,0,.12);
|
||||
--divider:var(--divider-base);
|
||||
--tooltip-bg:var(--card);
|
||||
--tooltip-fg:var(--txt);
|
||||
--tooltip-border:var(--line-strong);
|
||||
|
||||
--radius:4px; --radius-sm:2px;
|
||||
/* —— 折叠块 —— */
|
||||
@@ -124,7 +127,7 @@ html[data-appearance="light"]:not([data-plain="1"])[data-theme]{
|
||||
--bg:color-mix(in srgb, var(--accent) 7%, var(--bg-base));
|
||||
--line:color-mix(in srgb, var(--accent) 16%, var(--line-base));
|
||||
--line-soft:color-mix(in srgb, var(--accent) 10%, var(--line-soft-base));
|
||||
--divider:color-mix(in srgb, var(--accent) 22%, var(--divider-base));
|
||||
--divider:var(--divider-base);
|
||||
}
|
||||
|
||||
/* —— 外观:深色 —— */
|
||||
@@ -174,7 +177,7 @@ html[data-appearance="dark"]{
|
||||
--fold-tool-bg:#16202b;
|
||||
--fold-result-border:#3f6647;
|
||||
--fold-result-bg:#15241a;
|
||||
--divider-base:#569cd6;
|
||||
--divider-base:rgba(255,255,255,.10);
|
||||
--divider:var(--divider-base);
|
||||
/* 发送键:深色用鲜亮深蓝 */
|
||||
--ink-green:#2559d8;
|
||||
@@ -183,7 +186,7 @@ html[data-appearance="dark"]{
|
||||
html[data-appearance="dark"][data-theme]{
|
||||
--accent-bg:color-mix(in srgb, var(--accent) 22%, var(--panel));
|
||||
--accent-soft:color-mix(in srgb, var(--accent) 12%, var(--panel));
|
||||
--divider:color-mix(in srgb, var(--accent) 18%, var(--divider-base));
|
||||
--divider:var(--divider-base);
|
||||
}
|
||||
|
||||
html,body{ height:100%; }
|
||||
@@ -400,6 +403,17 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
|
||||
transition:opacity .18s, transform .18s; z-index:9999;
|
||||
}
|
||||
.ga-toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }
|
||||
.ga-tooltip{
|
||||
position:fixed; z-index:10000; max-width:min(320px, calc(100vw - 16px));
|
||||
padding:6px 8px; border:1px solid var(--tooltip-border); border-radius:4px;
|
||||
background:var(--tooltip-bg);
|
||||
color:var(--tooltip-fg); font:12px/1.35 var(--font-sans);
|
||||
box-shadow:0 8px 20px #00000026;
|
||||
pointer-events:none; opacity:0; transform:translateY(2px);
|
||||
transition:opacity .12s, transform .12s;
|
||||
white-space:normal; overflow-wrap:anywhere;
|
||||
}
|
||||
.ga-tooltip.show{ opacity:1; transform:translateY(0); }
|
||||
.set-btn{
|
||||
width:100%; display:flex; align-items:center; gap:8px;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--txt-2);
|
||||
@@ -584,7 +598,7 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
|
||||
background:var(--field-bg); cursor:pointer; text-align:left;
|
||||
transition:border-color .15s, background .15s;
|
||||
}
|
||||
.pq-btn:hover{ background:var(--card); border-color:var(--accent); }
|
||||
.pq-btn:hover{ background:var(--line-soft); border-color:var(--line-base); }
|
||||
.pq-logo{
|
||||
flex:0 0 auto; width:26px; height:26px; border-radius:7px;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
@@ -594,7 +608,7 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
|
||||
.pq-btn-name{ font-size:12px; font-weight:500; color:var(--txt); line-height:1.35; }
|
||||
.pq-btn-desc{ font-size:10px; color:var(--muted); line-height:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.pq-arrow{ flex:0 0 auto; width:13px; height:13px; color:var(--muted2); transition:transform .15s, color .15s; }
|
||||
.pq-btn:hover .pq-arrow{ color:var(--accent); transform:translateX(2px); }
|
||||
.pq-btn:hover .pq-arrow{ color:var(--txt-2); transform:none; }
|
||||
|
||||
/* ── 添加模型弹窗内的接入指引横幅 ── */
|
||||
.model-guide{
|
||||
@@ -721,9 +735,10 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
|
||||
.run-state.busy .dot::before{
|
||||
content:'';
|
||||
position:absolute;
|
||||
left:50%; top:50%;
|
||||
inset:0;
|
||||
margin:auto;
|
||||
width:var(--breathe-core); height:var(--breathe-core);
|
||||
transform:translate(-50%,-50%);
|
||||
transform:none;
|
||||
border-radius:50%;
|
||||
background:var(--breathe-color);
|
||||
z-index:2;
|
||||
@@ -732,9 +747,11 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
|
||||
.run-state.busy .dot::after{
|
||||
content:'';
|
||||
position:absolute;
|
||||
left:50%; top:50%;
|
||||
inset:0;
|
||||
margin:auto;
|
||||
width:var(--breathe-core); height:var(--breathe-core);
|
||||
transform:translate(-50%,-50%);
|
||||
transform:none;
|
||||
transform-origin:center;
|
||||
border-radius:50%;
|
||||
border:1px solid color-mix(in srgb, var(--breathe-color) 52%, transparent);
|
||||
z-index:1;
|
||||
@@ -742,8 +759,8 @@ body{ background:var(--bg); color:var(--txt); font-size:13px; overflow:hidden; f
|
||||
animation:ga-status-breathe 1.75s ease-out infinite;
|
||||
}
|
||||
@keyframes ga-status-breathe{
|
||||
0%{ transform:translate(-50%,-50%) scale(1); opacity:.55; }
|
||||
100%{ transform:translate(-50%,-50%) scale(2.35); opacity:0; }
|
||||
0%{ transform:scale(1); opacity:.55; }
|
||||
100%{ transform:scale(2.35); opacity:0; }
|
||||
}
|
||||
.link{ color:var(--accent); cursor:pointer; }
|
||||
.muted{ color:var(--muted); }
|
||||
@@ -1263,10 +1280,6 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
align-self:end;
|
||||
}
|
||||
.composer-inset.is-inline .composer-bot .spacer{ display:none; }
|
||||
.composer-inset:focus-within{
|
||||
border-color:var(--accent);
|
||||
box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
.composer-inset .input{
|
||||
width:100%; min-height:38px; max-height:160px; overflow-y:auto;
|
||||
border:none; border-radius:0; background:transparent;
|
||||
@@ -1368,7 +1381,7 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
margin:0 -3px;
|
||||
}
|
||||
.rp-resize:hover, .rp-resize.dragging,
|
||||
.sb-resize:hover, .sb-resize.dragging{ background:var(--accent); }
|
||||
.sb-resize:hover, .sb-resize.dragging{ background:var(--line-strong); }
|
||||
.body.rp-collapsed .rp-resize,
|
||||
.body.sb-collapsed .sb-resize{ display:none; }
|
||||
|
||||
@@ -1496,58 +1509,67 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
}
|
||||
.plan-card{
|
||||
position:absolute;
|
||||
left:10px;
|
||||
bottom:100%;
|
||||
margin-bottom:0;
|
||||
left:12px;
|
||||
bottom:calc(100% + 8px);
|
||||
z-index:13;
|
||||
width:min(600px, 100%);
|
||||
width:min(640px, calc(100% - 24px));
|
||||
min-width:280px;
|
||||
max-width:100%;
|
||||
box-sizing:border-box;
|
||||
font-size:12px;
|
||||
line-height:1.35;
|
||||
font-size:13px;
|
||||
line-height:1.45;
|
||||
color:var(--txt);
|
||||
pointer-events:auto;
|
||||
}
|
||||
.plan-card[hidden]{ display:none !important; }
|
||||
.plan-card--expanded{
|
||||
padding:10px 12px 8px;
|
||||
background:var(--card);
|
||||
padding:14px 16px 12px 18px;
|
||||
background:color-mix(in srgb, var(--card) 94%, var(--accent) 6%);
|
||||
border:1px solid var(--line);
|
||||
border-bottom:none;
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:var(--radius-sm) var(--radius-sm) 0 0;
|
||||
box-shadow:0 -4px 16px #14192812, 0 -1px 0 var(--line) inset;
|
||||
border-radius:8px;
|
||||
box-shadow:0 12px 32px #14192818, 0 1px 2px #14192814;
|
||||
overflow:hidden;
|
||||
}
|
||||
.plan-card--expanded::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
bottom:0;
|
||||
width:4px;
|
||||
background:var(--accent);
|
||||
}
|
||||
.plan-card--collapsed{
|
||||
width:auto;
|
||||
min-width:0;
|
||||
max-width:min(520px, 70%);
|
||||
margin-bottom:0;
|
||||
}
|
||||
/* 吸附:卡片底与输入框顶共边,视觉上连成一体 */
|
||||
/* Keep the composer rounded while the plan card floats above it. */
|
||||
.composer-slot:has(.plan-card:not([hidden])) .composer-inset{
|
||||
border-top-left-radius:0;
|
||||
border-top-right-radius:0;
|
||||
border-top-left-radius:var(--radius);
|
||||
border-top-right-radius:var(--radius);
|
||||
}
|
||||
.composer-slot:has(.plan-card--collapsed:not([hidden])) .plan-capsule{
|
||||
border-radius:var(--radius-sm) var(--radius-sm) 0 0;
|
||||
border-bottom:1px solid var(--line);
|
||||
box-shadow:0 -4px 12px #14192810;
|
||||
box-shadow:0 10px 24px #14192814, 0 1px 2px #14192810;
|
||||
}
|
||||
.plan-card-head{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
margin-bottom:6px;
|
||||
gap:10px;
|
||||
margin-bottom:10px;
|
||||
min-height:26px;
|
||||
}
|
||||
.plan-status-dot{
|
||||
flex:0 0 auto;
|
||||
width:7px; height:7px;
|
||||
width:8px; height:8px;
|
||||
border-radius:50%;
|
||||
background:var(--accent);
|
||||
box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
}
|
||||
.plan-card--complete .plan-status-dot{
|
||||
background:var(--success);
|
||||
box-shadow:0 0 0 4px var(--success-ring);
|
||||
}
|
||||
.plan-card--complete .plan-status-dot{ background:var(--success); }
|
||||
.plan-card--placeholder .plan-status-dot{
|
||||
animation:plan-dot-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
@@ -1557,8 +1579,8 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
}
|
||||
.plan-title{
|
||||
flex:1 1 auto;
|
||||
font-weight:600;
|
||||
font-size:12px;
|
||||
font-weight:700;
|
||||
font-size:14px;
|
||||
color:var(--txt-strong);
|
||||
min-width:0;
|
||||
white-space:nowrap;
|
||||
@@ -1567,104 +1589,198 @@ span.flatpickr-weekday{ color:var(--muted) !important; font-size:11px; }
|
||||
}
|
||||
.plan-progress{
|
||||
flex:0 0 auto;
|
||||
font-weight:600;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
min-height:24px;
|
||||
padding:0 8px;
|
||||
border:1px solid color-mix(in srgb, var(--accent) 26%, var(--line));
|
||||
border-radius:999px;
|
||||
background:color-mix(in srgb, var(--accent) 9%, var(--card));
|
||||
font-weight:700;
|
||||
font-size:12px;
|
||||
color:var(--accent);
|
||||
color:var(--txt-strong);
|
||||
font-family:var(--font-num);
|
||||
font-variant-numeric:var(--font-num-variant);
|
||||
}
|
||||
.plan-card--complete .plan-progress{ color:var(--success); }
|
||||
.plan-card--complete .plan-progress{
|
||||
color:var(--success);
|
||||
border-color:color-mix(in srgb, var(--success) 28%, var(--line));
|
||||
background:var(--success-bg);
|
||||
}
|
||||
.plan-head-actions{
|
||||
flex:0 0 auto;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:4px;
|
||||
gap:6px;
|
||||
}
|
||||
.plan-btn{
|
||||
border:0;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:5px;
|
||||
min-height:26px;
|
||||
border:1px solid transparent;
|
||||
background:transparent;
|
||||
padding:2px 6px;
|
||||
font-size:11px;
|
||||
font-weight:500;
|
||||
color:var(--accent);
|
||||
padding:0 8px;
|
||||
font-size:12px;
|
||||
font-weight:600;
|
||||
color:var(--muted);
|
||||
cursor:pointer;
|
||||
border-radius:var(--radius-sm);
|
||||
border-radius:6px;
|
||||
font-family:var(--font-sans);
|
||||
transition:background .15s, border-color .15s, color .15s;
|
||||
}
|
||||
.plan-btn:hover{
|
||||
background:var(--field-bg);
|
||||
border-color:var(--line);
|
||||
color:var(--txt-strong);
|
||||
}
|
||||
.plan-btn-ic{
|
||||
width:12px;
|
||||
height:12px;
|
||||
flex:0 0 auto;
|
||||
}
|
||||
.plan-btn:hover{ background:var(--accent-soft); }
|
||||
.plan-current{
|
||||
display:flex;
|
||||
gap:4px;
|
||||
gap:6px;
|
||||
align-items:flex-start;
|
||||
margin-bottom:8px;
|
||||
padding-left:15px;
|
||||
margin:0 0 10px 0;
|
||||
padding:8px 10px;
|
||||
border:1px solid var(--line-soft);
|
||||
border-radius:6px;
|
||||
background:color-mix(in srgb, var(--field-bg) 82%, var(--accent) 18%);
|
||||
}
|
||||
.plan-current-label{
|
||||
flex:0 0 auto;
|
||||
color:var(--muted);
|
||||
font-size:11px;
|
||||
font-size:12px;
|
||||
}
|
||||
.plan-current-text{
|
||||
flex:1 1 auto;
|
||||
color:var(--accent);
|
||||
font-weight:500;
|
||||
color:var(--txt-strong);
|
||||
font-weight:600;
|
||||
word-break:break-word;
|
||||
}
|
||||
.plan-card--complete .plan-current-text{ color:var(--success); }
|
||||
.plan-wait{
|
||||
color:var(--muted2);
|
||||
padding:0 0 4px 15px;
|
||||
font-size:11px;
|
||||
padding:4px 0 2px;
|
||||
font-size:12px;
|
||||
}
|
||||
.plan-items{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0 0 4px 15px;
|
||||
padding:0;
|
||||
}
|
||||
.plan-item{
|
||||
display:flex;
|
||||
gap:6px;
|
||||
gap:10px;
|
||||
align-items:flex-start;
|
||||
margin-top:3px;
|
||||
padding:5px 0;
|
||||
min-height:26px;
|
||||
}
|
||||
.plan-item-mark{
|
||||
flex:0 0 auto;
|
||||
width:1.1em;
|
||||
text-align:center;
|
||||
line-height:1.35;
|
||||
position:relative;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:16px;
|
||||
height:16px;
|
||||
margin-top:2px;
|
||||
border:1px solid var(--line-strong);
|
||||
border-radius:50%;
|
||||
background:var(--card);
|
||||
color:var(--muted);
|
||||
font-size:0;
|
||||
}
|
||||
.plan-item-text{
|
||||
flex:1 1 auto;
|
||||
word-break:break-word;
|
||||
color:var(--txt-2);
|
||||
}
|
||||
.plan-item--done .plan-item-mark{
|
||||
color:var(--success);
|
||||
border-color:color-mix(in srgb, var(--success) 40%, var(--line));
|
||||
background:var(--success-bg);
|
||||
}
|
||||
.plan-item--done .plan-item-mark::after{
|
||||
content:"";
|
||||
width:7px;
|
||||
height:4px;
|
||||
border-left:2px solid currentColor;
|
||||
border-bottom:2px solid currentColor;
|
||||
transform:translateY(-1px) rotate(-45deg);
|
||||
}
|
||||
.plan-item--done .plan-item-mark,
|
||||
.plan-item--done .plan-item-text{
|
||||
color:var(--muted);
|
||||
opacity:.75;
|
||||
opacity:.78;
|
||||
}
|
||||
.plan-item--done .plan-item-text{ text-decoration:line-through; }
|
||||
.plan-item--current .plan-item-mark{ color:var(--accent); }
|
||||
.plan-item--current .plan-item-text{ color:var(--accent); font-weight:500; }
|
||||
.plan-item--pending .plan-item-mark,
|
||||
.plan-item--current .plan-item-mark{
|
||||
border-color:var(--accent);
|
||||
background:var(--accent);
|
||||
box-shadow:0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
}
|
||||
.plan-item--current .plan-item-mark::after{
|
||||
content:"";
|
||||
width:5px;
|
||||
height:5px;
|
||||
border-radius:50%;
|
||||
background:var(--on-accent);
|
||||
}
|
||||
.plan-item--current .plan-item-text{
|
||||
color:var(--txt-strong);
|
||||
font-weight:700;
|
||||
}
|
||||
.plan-item--pending .plan-item-mark::after{
|
||||
content:"";
|
||||
width:4px;
|
||||
height:4px;
|
||||
border-radius:50%;
|
||||
background:var(--line-strong);
|
||||
}
|
||||
.plan-item--pending .plan-item-text{ color:var(--muted); }
|
||||
.plan-item--error .plan-item-mark,
|
||||
.plan-item--error .plan-item-mark{
|
||||
color:var(--danger);
|
||||
border-color:color-mix(in srgb, var(--danger) 38%, var(--line));
|
||||
background:var(--danger-bg);
|
||||
}
|
||||
.plan-item--error .plan-item-mark::before,
|
||||
.plan-item--error .plan-item-mark::after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
width:8px;
|
||||
height:2px;
|
||||
border-radius:999px;
|
||||
background:currentColor;
|
||||
}
|
||||
.plan-item--error .plan-item-mark::before{ transform:rotate(45deg); }
|
||||
.plan-item--error .plan-item-mark::after{ transform:rotate(-45deg); }
|
||||
.plan-item--error .plan-item-text{ color:var(--danger); }
|
||||
.plan-item--warn .plan-item-mark,
|
||||
.plan-item--warn .plan-item-text{ color:#c67600; }
|
||||
.plan-item--warn .plan-item-mark{
|
||||
color:#c67600;
|
||||
border-color:color-mix(in srgb, #c67600 38%, var(--line));
|
||||
background:color-mix(in srgb, #c67600 12%, var(--card));
|
||||
font-size:10px;
|
||||
font-weight:800;
|
||||
}
|
||||
.plan-item--warn .plan-item-mark::after{ content:"!"; }
|
||||
.plan-item--warn .plan-item-text{ color:#9b6100; }
|
||||
html[data-appearance="dark"] .plan-item--warn .plan-item-mark,
|
||||
html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
|
||||
.plan-foot{
|
||||
display:flex;
|
||||
justify-content:flex-end;
|
||||
gap:8px;
|
||||
padding-top:4px;
|
||||
padding-top:10px;
|
||||
border-top:1px solid var(--line-soft);
|
||||
margin-top:4px;
|
||||
margin-top:6px;
|
||||
}
|
||||
.plan-more-hint{
|
||||
flex:1 1 auto;
|
||||
align-self:center;
|
||||
font-size:11px;
|
||||
font-size:12px;
|
||||
color:var(--muted2);
|
||||
text-align:left;
|
||||
}
|
||||
@@ -1673,19 +1789,36 @@ html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
|
||||
align-items:center;
|
||||
gap:8px;
|
||||
max-width:100%;
|
||||
padding:6px 12px;
|
||||
min-height:34px;
|
||||
padding:0 12px 0 10px;
|
||||
border:1px solid var(--line);
|
||||
border-left:3px solid var(--accent);
|
||||
border-radius:999px;
|
||||
background:var(--card);
|
||||
background:color-mix(in srgb, var(--card) 94%, var(--accent) 6%);
|
||||
cursor:pointer;
|
||||
font-size:12px;
|
||||
font-weight:500;
|
||||
font-weight:600;
|
||||
color:var(--txt);
|
||||
box-shadow:var(--shadow);
|
||||
box-shadow:0 10px 24px #14192814, 0 1px 2px #14192810;
|
||||
font-family:var(--font-sans);
|
||||
transition:background .15s, border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.plan-capsule:hover{
|
||||
border-color:var(--line-strong);
|
||||
background:var(--card);
|
||||
box-shadow:0 12px 28px #1419281f, 0 1px 2px #14192812;
|
||||
}
|
||||
.plan-capsule-ic{
|
||||
width:15px;
|
||||
height:15px;
|
||||
flex:0 0 auto;
|
||||
color:var(--muted);
|
||||
}
|
||||
.plan-capsule-chevron{
|
||||
width:12px;
|
||||
height:12px;
|
||||
flex:0 0 auto;
|
||||
color:var(--muted2);
|
||||
}
|
||||
.plan-capsule:hover{ border-color:var(--line-strong); background:var(--field-bg); }
|
||||
.plan-capsule-text{
|
||||
min-width:0;
|
||||
white-space:nowrap;
|
||||
@@ -1698,7 +1831,9 @@ html[data-appearance="dark"] .plan-item--warn .plan-item-text{ color:#f0a020; }
|
||||
color:var(--accent);
|
||||
font-weight:600;
|
||||
}
|
||||
.plan-card--complete .plan-capsule{ border-left-color:var(--success); }
|
||||
.plan-card--complete .plan-capsule{
|
||||
background:color-mix(in srgb, var(--card) 88%, var(--success) 12%);
|
||||
}
|
||||
.plan-card--complete .plan-capsule-text em{ color:var(--success); }
|
||||
|
||||
/* ─── chat composer: Plan/Auto toggle ─── */
|
||||
|
||||
@@ -70,6 +70,24 @@ def strip_final_info_marker(text: Any) -> str:
|
||||
return _FINAL_INFO_RE.sub('', str(text or ''))
|
||||
|
||||
|
||||
def normalize_final_turn_segs(full: str, outputs: Any) -> Optional[List[str]]:
|
||||
if not outputs or not isinstance(outputs, (list, tuple)):
|
||||
return None
|
||||
segs = [strip_final_info_marker(s) for s in outputs]
|
||||
full_text = strip_final_info_marker(full)
|
||||
if not segs:
|
||||
return None
|
||||
joined = "".join(segs)
|
||||
if full_text.strip() == joined.strip():
|
||||
return segs
|
||||
if joined and full_text.startswith(joined):
|
||||
suffix = full_text[len(joined):]
|
||||
if suffix.strip():
|
||||
segs[-1] = segs[-1] + suffix
|
||||
return segs
|
||||
return None
|
||||
|
||||
|
||||
for _s in (sys.stdout, sys.stderr):
|
||||
with contextlib.suppress(Exception):
|
||||
_s.reconfigure(encoding="utf-8", errors="replace")
|
||||
@@ -664,9 +682,8 @@ class AgentManager:
|
||||
_segs[_idx - 1] = str(_outs[-2])
|
||||
if "done" in item:
|
||||
full = strip_final_info_marker(item.get("done") or "")
|
||||
done_outputs = item.get("outputs") # done时=turn_resps.copy()全量轮
|
||||
done_outputs = normalize_final_turn_segs(full, item.get("outputs")) # done时=turn_resps.copy()全量轮
|
||||
if done_outputs:
|
||||
done_outputs = [strip_final_info_marker(s) for s in done_outputs]
|
||||
with self.lock:
|
||||
if sess.partial is not None:
|
||||
sess.partial["content"] = full
|
||||
@@ -696,12 +713,10 @@ class AgentManager:
|
||||
with self.lock:
|
||||
sess.partial = None
|
||||
full = strip_final_info_marker(full)
|
||||
if done_outputs:
|
||||
done_outputs = [strip_final_info_marker(s) for s in done_outputs]
|
||||
import plan_state
|
||||
plan_state.sync_plan_path_from_text(sess, full, sess.cwd or self.ga_root)
|
||||
# 轨道2: 落库时带结构化全量轮(权威turn_segs),前端按轮渲染;content保留兜底
|
||||
_final_segs = [str(s) for s in done_outputs] if done_outputs else None
|
||||
_final_segs = normalize_final_turn_segs(full, done_outputs)
|
||||
if _final_segs:
|
||||
self.add_message(sess, "assistant", full, turn_segs=_final_segs)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user