Compare commits
5 Commits
1ca5e15f8e
...
89bcca744d
| Author | SHA1 | Date | |
|---|---|---|---|
| 89bcca744d | |||
| af01030424 | |||
| 10a2763f00 | |||
| 04770e0af9 | |||
| f5bfab4056 |
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: sub-agent-guide
|
name: sub-agent-guide
|
||||||
description: 子智能体使用指南。创建子智能体之前必须阅读此技能。子智能体仅用于并行处理独立的“非代码编写”任务(如项目初步探索、网络搜索与信息收集、程序测试执行)。关键限制:子智能体之间无法通信,看不到主对话历史。严格禁止将子智能体用于项目代码编写、代码修改、重构或实现功能。**如果运行模式为多智能体模式,禁止阅读并参考该 skill。**
|
description: 子智能体使用指南。创建子智能体之前必须阅读此技能。子智能体用于并行处理独立任务(项目探索、网络搜索与信息收集、程序测试执行等)。代码修改视情况而定:一般性代码编写由主智能体完成;大规模、重复性高、边界与任务明确的代码修改可由子智能体承担,但必须明确工作范围防止冲突,最终由主智能体验证。关键限制:子智能体之间无法通信,看不到主对话历史。**如果运行模式为多智能体模式,禁止阅读并参考该 skill。**
|
||||||
---
|
---
|
||||||
|
|
||||||
# 子智能体使用指南
|
# 子智能体使用指南
|
||||||
@ -11,7 +11,19 @@ description: 子智能体使用指南。创建子智能体之前必须阅读此
|
|||||||
|
|
||||||
子智能体拥有完整工具能力,但与主智能体和其他子智能体完全隔离。它们看不到你的对话历史,无法互相通信,只能通过文件系统共享信息。
|
子智能体拥有完整工具能力,但与主智能体和其他子智能体完全隔离。它们看不到你的对话历史,无法互相通信,只能通过文件系统共享信息。
|
||||||
|
|
||||||
**硬性限制:子智能体严格禁止用于项目代码编写/修改。**
|
**代码修改纪律(视情况而定)**
|
||||||
|
|
||||||
|
子智能体**不是**代码编写的主力——一般性代码编写、重构、功能实现仍由主智能体承担。但当任务**同时满足**以下条件时,**允许子智能体修改核心代码**:
|
||||||
|
|
||||||
|
1. **大规模、重复性高**:改动量大或模式重复(如批量迁移、统一错误处理、机械式重构),交给子智能体执行效率远高于主智能体逐文件处理
|
||||||
|
2. **边界与任务明确**:任务描述清楚指定要操作的文件/目录范围,无歧义
|
||||||
|
3. **范围防冲突**:与主智能体、其他子智能体的工作范围不重叠
|
||||||
|
4. **最终验证**:子智能体的智能程度取决于用户配置的模型,其改动**必须由主智能体审查验证**(读 diff / 跑测试),不能直接信以为真
|
||||||
|
|
||||||
|
**禁止场景(不可放宽):**
|
||||||
|
- 修改任务描述明确范围之外的文件
|
||||||
|
- 影响面不可控的架构级改动、跨模块协调
|
||||||
|
- 子智能体无法自主验证正确性的改动
|
||||||
|
|
||||||
## 何时使用
|
## 何时使用
|
||||||
|
|
||||||
@ -21,13 +33,14 @@ description: 子智能体使用指南。创建子智能体之前必须阅读此
|
|||||||
- **重要**:进行大范围搜索或调查前,必须先自己进行一次浅层搜索/调查,摸清关键词、主要来源和任务边界,再创建子智能体并给出精确指导。
|
- **重要**:进行大范围搜索或调查前,必须先自己进行一次浅层搜索/调查,摸清关键词、主要来源和任务边界,再创建子智能体并给出精确指导。
|
||||||
- **程序测试与验证**:执行测试命令、收集日志、汇总失败用例与错误模式
|
- **程序测试与验证**:执行测试命令、收集日志、汇总失败用例与错误模式
|
||||||
- **并行独立的文档/分析任务**:生成说明文档、运行报告、排查清单
|
- **并行独立的文档/分析任务**:生成说明文档、运行报告、排查清单
|
||||||
|
- **大规模、重复性高的代码修改**:批量迁移、统一错误处理、机械式重构等边界明确的任务(任务描述必须精确指定文件/目录范围,完成后审查验证改动)
|
||||||
|
|
||||||
**不适合:**
|
**不适合:**
|
||||||
- 任务之间需要频繁协调(子智能体无法互相通信)
|
- 任务之间需要频繁协调(子智能体无法互相通信)
|
||||||
- 简单快速的任务(创建子智能体有开销)
|
- 简单快速的任务(创建子智能体有开销)
|
||||||
- 需要你参与决策的任务(子智能体是自主运行的)
|
- 需要你参与决策的任务(子智能体是自主运行的)
|
||||||
- 任何项目代码编写、代码修改、代码重构、功能实现任务(必须由主智能体处理)
|
- 边界模糊、影响面不可控的代码修改(必须先由主智能体划定范围)
|
||||||
- 任何会直接改动项目源码的开发工作
|
- 需要跨模块协调或反复验证的架构级改动
|
||||||
|
|
||||||
## 任务描述的艺术
|
## 任务描述的艺术
|
||||||
|
|
||||||
@ -71,17 +84,19 @@ description: 子智能体使用指南。创建子智能体之前必须阅读此
|
|||||||
|
|
||||||
## 交付目录设计
|
## 交付目录设计
|
||||||
|
|
||||||
`deliverables_dir` 必须是**不存在的新目录**,子智能体会把所有成果放在这里。
|
`deliverables_dir` 可以**留空**:留空时系统自动在 `.astrion/sub_agent_results/` 下创建(如 `agent_1`,撞名自动加后缀),并把最终路径作为工具结果返回。该目录是运行时内部目录,不进入 git 版本控制。
|
||||||
|
|
||||||
**命名建议:**
|
显式传入时:必须是相对项目根目录的**不存在的新目录**,已存在会报错。
|
||||||
|
|
||||||
|
**命名建议**(显式传入时,用清晰描述任务的相对路径):
|
||||||
```
|
```
|
||||||
sub_agent_results/task_description/ # 清晰描述任务
|
research/python312/ # 清晰描述任务
|
||||||
sub_agent_results/module_a_docs/ # 按模块组织
|
docs/module_a/ # 按模块组织
|
||||||
sub_agent_results/test_auth_report/ # 按测试/报告组织
|
analysis/frontend_auth/ # 按功能分析组织
|
||||||
```
|
```
|
||||||
|
|
||||||
**避免:**
|
**避免:**
|
||||||
- ❌ 使用已存在的目录(会报错)
|
- ❌ 传入已存在的目录(会报错)
|
||||||
- ❌ 使用绝对路径(必须是相对于项目根目录的相对路径)
|
- ❌ 使用绝对路径(必须是相对于项目根目录的相对路径)
|
||||||
- ❌ 使用通用名称如 "output" "result"(多个子智能体会冲突)
|
- ❌ 使用通用名称如 "output" "result"(多个子智能体会冲突)
|
||||||
|
|
||||||
@ -251,7 +266,7 @@ create_sub_agent(task="""
|
|||||||
- 梳理认证相关入口、调用链和关键配置
|
- 梳理认证相关入口、调用链和关键配置
|
||||||
- 运行现有测试并记录失败项
|
- 运行现有测试并记录失败项
|
||||||
- 生成诊断报告(auth_diagnosis.md)
|
- 生成诊断报告(auth_diagnosis.md)
|
||||||
- 不允许修改任何项目源码
|
- 本任务为只读诊断,不修改任何文件
|
||||||
""")
|
""")
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -286,7 +301,7 @@ create_sub_agent(agent_id=1, ...) # 失败了
|
|||||||
result = create_sub_agent(agent_id=1, run_in_background=false, ...)
|
result = create_sub_agent(agent_id=1, run_in_background=false, ...)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
# 读取交付目录中的文件
|
# 读取交付目录中的文件(显式传入或自动创建的路径都在这里)
|
||||||
deliverables = result["deliverables_dir"]
|
deliverables = result["deliverables_dir"]
|
||||||
# 处理生成的文件...
|
# 处理生成的文件...
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -133,7 +133,7 @@ class ToolsDefinitionAgentToolsMixin:
|
|||||||
},
|
},
|
||||||
"deliverables_dir": {
|
"deliverables_dir": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "交付文件夹的相对路径(相对于项目根目录)。子智能体会将所有结果文件放在此目录。\n\n要求:必须是不存在的新目录;若目录不存在会自动创建;若目录已存在(无论是否为空)将报错。\n\n留空则使用默认路径:sub_agent_results/agent_{agent_id}\n\n示例:'docs/api'、'reports/performance'、'tests/generated'"
|
"description": "交付文件夹的相对路径(相对于项目根目录)。子智能体会将所有结果文件放在此目录。\n\n显式传入时:必须是不存在的新目录,若已存在(无论是否为空)将报错。\n\n留空时:自动在 .astrion/sub_agent_results/ 下创建(如 agent_{agent_id}),最终路径会作为工具结果返回。\n\n示例:'docs/api'、'reports/performance'、'tests/generated'"
|
||||||
},
|
},
|
||||||
"run_in_background": {
|
"run_in_background": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@ -149,7 +149,7 @@ class ToolsDefinitionAgentToolsMixin:
|
|||||||
"description": "子智能体思考模式,根据任务复杂度选择:\n\nfast(快速模式)- 适合简单明确的任务:\n- 网络信息搜集和整理\n- 批量文件读取和简单处理\n- 执行已知的命令序列\n- 生成简单的文档或报告\n- 数据格式转换\n\nthinking(思考模式)- 适合复杂任务:\n- 代码架构分析和重构设计\n- 复杂算法实现和优化\n- 多步骤问题诊断和调试\n- 技术方案选型和对比\n- 需要深度推理的代码审查\n\n不填则使用默认模式。"
|
"description": "子智能体思考模式,根据任务复杂度选择:\n\nfast(快速模式)- 适合简单明确的任务:\n- 网络信息搜集和整理\n- 批量文件读取和简单处理\n- 执行已知的命令序列\n- 生成简单的文档或报告\n- 数据格式转换\n\nthinking(思考模式)- 适合复杂任务:\n- 代码架构分析和重构设计\n- 复杂算法实现和优化\n- 多步骤问题诊断和调试\n- 技术方案选型和对比\n- 需要深度推理的代码审查\n\n不填则使用默认模式。"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"required": ["agent_id", "summary", "task", "deliverables_dir", "thinking_mode"]
|
"required": ["agent_id", "summary", "task", "thinking_mode"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -103,22 +103,22 @@ class SubAgentCreationMixin:
|
|||||||
return "任务摘要不能为空"
|
return "任务摘要不能为空"
|
||||||
if not task or not task.strip():
|
if not task or not task.strip():
|
||||||
return "任务详情不能为空"
|
return "任务详情不能为空"
|
||||||
# 多智能体模式不需要交付目录
|
# 交付目录不再必填:传统模式未指定时自动创建(见 _resolve_deliverables_dir)
|
||||||
if not multi_agent_mode and target_dir is None:
|
|
||||||
return "指定文件夹不能为空"
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _generate_task_id(self, agent_id: int) -> str:
|
def _generate_task_id(self, agent_id: int) -> str:
|
||||||
suffix = uuid.uuid4().hex[:6]
|
suffix = uuid.uuid4().hex[:6]
|
||||||
return f"sub_{agent_id}_{int(time.time())}_{suffix}"
|
return f"sub_{agent_id}_{int(time.time())}_{suffix}"
|
||||||
|
|
||||||
def _resolve_deliverables_dir(self, relative_dir: Optional[str], *, multi_agent_mode: bool = False) -> Path:
|
def _resolve_deliverables_dir(self, relative_dir: Optional[str], *, agent_id: int = 0, multi_agent_mode: bool = False) -> Path:
|
||||||
relative_dir = (relative_dir or "").strip()
|
relative_dir = (relative_dir or "").strip()
|
||||||
# 多智能体模式:没有交付目录概念,直接使用项目根目录
|
# 多智能体模式:没有交付目录概念,直接使用项目根目录
|
||||||
if multi_agent_mode and not relative_dir:
|
if multi_agent_mode and not relative_dir:
|
||||||
return self.project_path.resolve()
|
return self.project_path.resolve()
|
||||||
if not relative_dir:
|
if not relative_dir:
|
||||||
raise ValueError(tr("sub_agent_creation.deliverables_dir_required"))
|
# 未显式指定交付目录:在运行时目录 .astrion/sub_agent_results/ 下自动创建
|
||||||
|
#(撞名自动加后缀,保证总能建成),实际路径由 create_sub_agent 结果返回
|
||||||
|
return self._auto_deliverables_dir(agent_id)
|
||||||
deliverables_path = (self.project_path / relative_dir).resolve()
|
deliverables_path = (self.project_path / relative_dir).resolve()
|
||||||
if not str(deliverables_path).startswith(str(self.project_path)):
|
if not str(deliverables_path).startswith(str(self.project_path)):
|
||||||
raise ValueError(tr("sub_agent_creation.deliverables_dir_outside"))
|
raise ValueError(tr("sub_agent_creation.deliverables_dir_outside"))
|
||||||
@ -126,3 +126,15 @@ class SubAgentCreationMixin:
|
|||||||
raise ValueError(tr("sub_agent_creation.deliverables_dir_not_new"))
|
raise ValueError(tr("sub_agent_creation.deliverables_dir_not_new"))
|
||||||
deliverables_path.mkdir(parents=True, exist_ok=True)
|
deliverables_path.mkdir(parents=True, exist_ok=True)
|
||||||
return deliverables_path
|
return deliverables_path
|
||||||
|
|
||||||
|
def _auto_deliverables_dir(self, agent_id: int) -> Path:
|
||||||
|
"""未指定交付目录时自动创建:.astrion/sub_agent_results/agent_{id}(撞名追加后缀)。"""
|
||||||
|
base = (self.project_path / ".astrion" / "sub_agent_results").resolve()
|
||||||
|
base.mkdir(parents=True, exist_ok=True)
|
||||||
|
candidate = base / f"agent_{agent_id}"
|
||||||
|
suffix = 2
|
||||||
|
while candidate.exists():
|
||||||
|
candidate = base / f"agent_{agent_id}_{suffix}"
|
||||||
|
suffix += 1
|
||||||
|
candidate.mkdir(parents=True, exist_ok=True)
|
||||||
|
return candidate
|
||||||
|
|||||||
@ -284,7 +284,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
task_root.mkdir(parents=True, exist_ok=True)
|
task_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
deliverables_path = self._resolve_deliverables_dir(deliverables_dir, multi_agent_mode=multi_agent_mode)
|
deliverables_path = self._resolve_deliverables_dir(deliverables_dir, agent_id=agent_id, multi_agent_mode=multi_agent_mode)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
# 回滚已创建的任务目录,避免残留状态(交付目录校验失败时 deliverables 不会创建)
|
# 回滚已创建的任务目录,避免残留状态(交付目录校验失败时 deliverables 不会创建)
|
||||||
shutil.rmtree(task_root, ignore_errors=True)
|
shutil.rmtree(task_root, ignore_errors=True)
|
||||||
|
|||||||
@ -413,6 +413,12 @@ class SubAgentTask:
|
|||||||
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
|
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
|
||||||
compressed = self._deep_compress_messages()
|
compressed = self._deep_compress_messages()
|
||||||
if compressed:
|
if compressed:
|
||||||
|
# 写入进度文件:前端活动窗口在压缩发生的位置显示压缩提示
|
||||||
|
self.emit("progress", {
|
||||||
|
"subtype": "compression",
|
||||||
|
"round": self._compress_round,
|
||||||
|
"ts": int(time.time() * 1000),
|
||||||
|
})
|
||||||
ma_debug(
|
ma_debug(
|
||||||
"sub_agent_deep_compressed",
|
"sub_agent_deep_compressed",
|
||||||
task_id=self.task_id,
|
task_id=self.task_id,
|
||||||
@ -477,6 +483,9 @@ class SubAgentTask:
|
|||||||
progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
|
progress_id = tool_call.get("id") or f"tool_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
|
||||||
|
|
||||||
if name == "finish_task":
|
if name == "finish_task":
|
||||||
|
# finish_task 在模型流式阶段已 emit「calling」进度事件(_call_model),
|
||||||
|
# 此处必须补终态,否则前端活动窗口该条目永远停留在「调用中」转圈
|
||||||
|
self.emit("progress", {"id": progress_id, "tool": name, "status": "completed", "args": args, "ts": int(time.time() * 1000)})
|
||||||
await self._write_finish(args, elapsed)
|
await self._write_finish(args, elapsed)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,13 @@
|
|||||||
- 你的工作范围应该与其他子智能体不重叠
|
- 你的工作范围应该与其他子智能体不重叠
|
||||||
- 不要修改任务描述之外的文件
|
- 不要修改任务描述之外的文件
|
||||||
|
|
||||||
|
## 代码修改纪律(视情况而定)
|
||||||
|
- 你有完整的读写工具,主智能体可能分配给你修改核心代码的任务(典型:大规模、重复性高、边界明确的工作)
|
||||||
|
- 允许修改代码的前提:任务描述明确划定了文件/目录范围,且改动面在该范围之内
|
||||||
|
- 修改前先阅读相关代码,理解上下文再动手;只做任务要求的改动,不顺手扩大改动面
|
||||||
|
- 修改完成后,在 finish_task 的 summary 中列出改动清单(文件、改动点),由主智能体审查验证
|
||||||
|
- 任务未指定代码修改时,保持只读分析,不要擅自改动代码
|
||||||
|
|
||||||
## 效率性
|
## 效率性
|
||||||
- 直接开始工作,不要过度解释
|
- 直接开始工作,不要过度解释
|
||||||
- 合理使用工具,避免重复操作
|
- 合理使用工具,避免重复操作
|
||||||
|
|||||||
@ -30,15 +30,23 @@
|
|||||||
v-for="item in timelineItems"
|
v-for="item in timelineItems"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="qd-feed-row"
|
class="qd-feed-row"
|
||||||
:class="[item.kind === 'tool' ? 'feed-tool' : 'feed-text', { 'is-new': animatedKeys.has(item.key) }]"
|
:class="[item.kind === 'tool' ? 'feed-tool' : item.kind === 'compression' ? 'feed-compressed' : 'feed-text', { 'is-new': animatedKeys.has(item.key) }]"
|
||||||
>
|
>
|
||||||
<template v-if="item.kind === 'tool'">
|
<template v-if="item.kind === 'tool'">
|
||||||
<span v-if="item.state === 'running' || item.state === 'calling'" class="qd-tool-spinner"></span>
|
|
||||||
<span v-else class="qd-tool-done">{{ item.state === 'failed' ? '✕' : '✓' }}</span>
|
|
||||||
<span class="tool-name">{{ item.toolName }}</span>
|
<span class="tool-name">{{ item.toolName }}</span>
|
||||||
<span class="tool-param" :title="item.text">{{ item.text }}</span>
|
<span class="tool-param" :title="item.text">{{ item.text }}</span>
|
||||||
<span class="tool-result" :class="{ 'is-error': item.state === 'failed' }">
|
<span class="tool-status" :class="{ 'is-error': item.state === 'failed' }">
|
||||||
{{ item.stateLabel }}
|
<span v-if="item.state === 'running' || item.state === 'calling'" class="qd-tool-spinner"></span>
|
||||||
|
<span v-else class="qd-tool-done">{{ item.state === 'failed' ? '✕' : '✓' }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.kind === 'compression'">
|
||||||
|
<span class="feed-compressed__label">
|
||||||
|
<svg class="feed-compressed__icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||||
|
<path d="M8 1.5 13.5 4v4.5c0 3-2.2 5-5.5 6-3.3-1-5.5-3-5.5-6V4L8 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.5 8.2 7.2 9.9 11 5.8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
{{ t('quickdock.contextCompressed', { round: item.round }) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>{{ item.content }}</template>
|
<template v-else>{{ item.content }}</template>
|
||||||
@ -283,14 +291,6 @@ function isEntryTerminal(status?: string) {
|
|||||||
].includes(normalized);
|
].includes(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 工具条目状态标签:映射表存 key、使用处解析(规范 §3.3,模块顶层禁止调 t()) */
|
|
||||||
const TOOL_STATE_LABEL_KEYS: Record<string, string> = {
|
|
||||||
completed: 'quickdock.toolStateDone',
|
|
||||||
failed: 'quickdock.toolStateFailed',
|
|
||||||
calling: 'quickdock.toolStateCalling',
|
|
||||||
running: 'quickdock.toolStateProgress'
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildText(entry: any): string {
|
function buildText(entry: any): string {
|
||||||
const tool = entry.tool || '';
|
const tool = entry.tool || '';
|
||||||
const args = entry.args || {};
|
const args = entry.args || {};
|
||||||
@ -309,7 +309,6 @@ interface ToolTimelineItem {
|
|||||||
kind: 'tool';
|
kind: 'tool';
|
||||||
key: string;
|
key: string;
|
||||||
state: string;
|
state: string;
|
||||||
stateLabel: string;
|
|
||||||
toolName: string;
|
toolName: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
@ -320,10 +319,16 @@ interface OutputTimelineItem {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() => {
|
interface CompressionTimelineItem {
|
||||||
|
kind: 'compression';
|
||||||
|
key: string;
|
||||||
|
round: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem | CompressionTimelineItem)[]>(() => {
|
||||||
void currentLocale.value;
|
void currentLocale.value;
|
||||||
const entries = activityEntries.value || [];
|
const entries = activityEntries.value || [];
|
||||||
const rawItems: ({ kind: 'tool'; key: string; entry: any } | OutputTimelineItem)[] = [];
|
const rawItems: ({ kind: 'tool'; key: string; entry: any } | OutputTimelineItem | CompressionTimelineItem)[] = [];
|
||||||
let currentToolGroup: { kind: 'tool'; key: string; entry: any } | null = null;
|
let currentToolGroup: { kind: 'tool'; key: string; entry: any } | null = null;
|
||||||
|
|
||||||
const flushToolGroup = () => {
|
const flushToolGroup = () => {
|
||||||
@ -361,6 +366,17 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上下文压缩提示:在压缩发生的位置插入一条提示条目
|
||||||
|
if (entry?.type === 'progress' && entry?.subtype === 'compression') {
|
||||||
|
flushToolGroup();
|
||||||
|
rawItems.push({
|
||||||
|
kind: 'compression',
|
||||||
|
key: `compression-${entry.ts || index}`,
|
||||||
|
round: Number(entry.round) || 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!entry || entry.type !== 'progress' || !entry.tool) {
|
if (!entry || entry.type !== 'progress' || !entry.tool) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -401,7 +417,7 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
|
|||||||
flushToolGroup();
|
flushToolGroup();
|
||||||
|
|
||||||
return rawItems.map((item) => {
|
return rawItems.map((item) => {
|
||||||
if (item.kind === 'output') {
|
if (item.kind === 'output' || item.kind === 'compression') {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
const state = normalizeStatus(item.entry.status);
|
const state = normalizeStatus(item.entry.status);
|
||||||
@ -409,7 +425,6 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
|
|||||||
kind: 'tool' as const,
|
kind: 'tool' as const,
|
||||||
key: item.key,
|
key: item.key,
|
||||||
state,
|
state,
|
||||||
stateLabel: t(TOOL_STATE_LABEL_KEYS[state] || 'quickdock.toolStateProgress'),
|
|
||||||
toolName: item.entry.tool || t('common.tool'),
|
toolName: item.entry.tool || t('common.tool'),
|
||||||
text: buildText(item.entry)
|
text: buildText(item.entry)
|
||||||
};
|
};
|
||||||
|
|||||||
@ -697,15 +697,16 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qd-feed-row.feed-tool .tool-result {
|
.qd-feed-row.feed-tool .tool-status {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 11.5px;
|
display: inline-flex;
|
||||||
color: var(--text-tertiary);
|
align-items: center;
|
||||||
font-variant-numeric: tabular-nums;
|
justify-content: center;
|
||||||
|
min-width: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qd-feed-row.feed-tool .tool-result.is-error {
|
.qd-feed-row.feed-tool .tool-status.is-error .qd-tool-done {
|
||||||
color: var(--state-danger);
|
color: var(--state-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -733,6 +734,25 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qd-feed-row.feed-compressed {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qd-feed-row.feed-compressed .feed-compressed__label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qd-feed-row.feed-compressed .feed-compressed__icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.qd-feed-row.feed-term {
|
.qd-feed-row.feed-term {
|
||||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
937
static/src/components/personalization/styles/settings-shared.css
Normal file
937
static/src/components/personalization/styles/settings-shared.css
Normal file
@ -0,0 +1,937 @@
|
|||||||
|
/*
|
||||||
|
* 个人空间抽屉共享样式(骨架 / 设置行 / 控件 / 浮层 / 响应式)
|
||||||
|
* 从 PersonalizationDrawer.vue 抽出:scoped 样式无法穿透 tab 子组件,故改为全局 CSS。
|
||||||
|
* 所有类名均为 settings-* / personal-* 前缀,全局冲突风险低。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 卡片尺寸/圆角/内边距的唯一事实源在全局 .personal-page-card(_overlays.scss);
|
||||||
|
这里只保留 redesign 特有的变量、容器查询上下文与内部布局 */
|
||||||
|
.settings-redesign-card {
|
||||||
|
--settings-floating-menu-bg: color-mix(in srgb, var(--surface-soft) 90%, var(--surface-raised));
|
||||||
|
--settings-floating-menu-hover: var(--hover-bg);
|
||||||
|
--settings-floating-menu-shadow: none;
|
||||||
|
--settings-tab-hover-bg: var(--theme-tab-active);
|
||||||
|
--settings-tab-active-bg: var(--theme-tab-active);
|
||||||
|
position: relative;
|
||||||
|
/* 容器查询上下文:内部布局按卡片自身宽度响应挤压,与视口脱耦 */
|
||||||
|
container-type: inline-size;
|
||||||
|
container-name: settings-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .settings-redesign-card,
|
||||||
|
body[data-theme='dark'] .settings-redesign-card {
|
||||||
|
--settings-floating-menu-bg: var(--surface-panel);
|
||||||
|
--settings-floating-menu-hover: var(--hover-bg);
|
||||||
|
--settings-floating-menu-shadow: none;
|
||||||
|
--settings-tab-hover-bg: var(--hover-bg);
|
||||||
|
--settings-tab-active-bg: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端关闭按钮位于侧栏顶部流内(不再是绝对定位大圆钮) */
|
||||||
|
.settings-nav-head {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-close-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.settings-close-button:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-close-button svg {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端顶栏:桌面端隐藏 */
|
||||||
|
.settings-mobile-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-chevron {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-body,
|
||||||
|
.settings-redesign-form {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-layout {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-nav {
|
||||||
|
flex: 0 0 184px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--theme-control-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-tabs {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 4px 10px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-tab {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: -0.006em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.settings-redesign-tab:hover {
|
||||||
|
background: var(--settings-tab-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-tab.active {
|
||||||
|
background: var(--settings-tab-active-bg);
|
||||||
|
font-weight: 640;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-icon {
|
||||||
|
--icon-size: 18px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与其他图标外框统一 18px,不再特殊放大(图标外框对齐规范) */
|
||||||
|
.settings-tab-icon--chat {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-icon--chat svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-content {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-content-header {
|
||||||
|
height: 58px;
|
||||||
|
flex: 0 0 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid var(--theme-control-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-content-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 19px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-scroll {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 2px 20px 96px;
|
||||||
|
scroll-padding-bottom: 96px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-tabs::-webkit-scrollbar,
|
||||||
|
.settings-redesign-scroll::-webkit-scrollbar,
|
||||||
|
.settings-floating-menu::-webkit-scrollbar,
|
||||||
|
.role-editor-body::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-floating-menu {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-page {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-row,
|
||||||
|
.settings-input-row,
|
||||||
|
.settings-list-row,
|
||||||
|
.settings-action-row,
|
||||||
|
.settings-toggle-row {
|
||||||
|
min-height: 0;
|
||||||
|
border-bottom: 1px solid var(--theme-control-border);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 13px 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list-row.tall,
|
||||||
|
.settings-input-row.stackable {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list-row.tall {
|
||||||
|
grid-template-columns: minmax(180px, 1fr) minmax(0, 360px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row-copy {
|
||||||
|
min-width: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row-title {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 550;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 3px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-button {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 0;
|
||||||
|
height: 32px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px 0 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.settings-select-button:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-button .select-chevron {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-button .select-chevron::before {
|
||||||
|
content: '';
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-right: 1.5px solid currentColor;
|
||||||
|
border-bottom: 1.5px solid currentColor;
|
||||||
|
transform: translateY(-1.5px) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-wrap.open .settings-select-button .select-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-floating-menu {
|
||||||
|
position: fixed;
|
||||||
|
right: auto;
|
||||||
|
top: auto;
|
||||||
|
width: 300px;
|
||||||
|
max-height: calc(34px * 4 + 12px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--settings-floating-menu-bg);
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
box-shadow: var(--settings-floating-menu-shadow);
|
||||||
|
opacity: 1;
|
||||||
|
backdrop-filter: none;
|
||||||
|
z-index: 2147483647;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-wrap.open .settings-floating-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-floating-menu.dark {
|
||||||
|
background: var(--surface-panel) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.settings-floating-menu.dark .settings-menu-option:hover {
|
||||||
|
background: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .settings-floating-menu,
|
||||||
|
body[data-theme='dark'] .settings-floating-menu {
|
||||||
|
background: var(--surface-panel) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
html[data-theme='dark'] .settings-menu-option:hover,
|
||||||
|
body[data-theme='dark'] .settings-menu-option:hover {
|
||||||
|
background: var(--hover-bg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-option {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 22px;
|
||||||
|
row-gap: 0;
|
||||||
|
column-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 9px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.settings-menu-option:hover {
|
||||||
|
background: var(--settings-floating-menu-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-option:disabled {
|
||||||
|
opacity: 0.48;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-option strong {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
display: block;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 550;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-option span {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.15;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-option svg {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-self: center;
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--text-primary);
|
||||||
|
stroke-width: 2.25;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-option.selected svg {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-row input,
|
||||||
|
.settings-add-row input,
|
||||||
|
.settings-number-row input,
|
||||||
|
.settings-number-input,
|
||||||
|
.settings-compression-grid input {
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0 11px;
|
||||||
|
outline: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-number-input {
|
||||||
|
width: 120px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 18px 0 6px;
|
||||||
|
}
|
||||||
|
.settings-section-divider__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.settings-section-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
margin-left: 10px;
|
||||||
|
background: var(--theme-control-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-row > input {
|
||||||
|
width: min(220px, 100%);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-row input:focus,
|
||||||
|
.settings-add-row input:focus,
|
||||||
|
.settings-number-row input:focus,
|
||||||
|
.settings-number-input:focus,
|
||||||
|
.settings-compression-grid input:focus {
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* width 用绝对值:min(280px, 100%) 的百分比在 grid auto 列固有尺寸计算中会失效,
|
||||||
|
导致列宽被内容(chips 排)撑到 404px、label 列被挤成 0px 竖排 */
|
||||||
|
.settings-input-stack {
|
||||||
|
width: 280px;
|
||||||
|
max-width: 100%;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-stack.wide {
|
||||||
|
width: 240px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-stack input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-chip-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-chip-row.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-chip-row button {
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-number-row button,
|
||||||
|
.settings-add-row button,
|
||||||
|
.settings-secondary-button,
|
||||||
|
.settings-primary-button {
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.settings-chip-row button:hover,
|
||||||
|
.settings-number-row button:hover,
|
||||||
|
.settings-add-row button:hover,
|
||||||
|
.settings-secondary-button:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-chip-row button.active,
|
||||||
|
.settings-primary-button {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-secondary-button.danger {
|
||||||
|
color: var(--state-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-inline-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-inline-actions.right {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mini-status {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mini-status.warning {
|
||||||
|
color: var(--state-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-add-row,
|
||||||
|
.settings-number-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-add-row input,
|
||||||
|
.settings-number-row input {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-textarea-row {
|
||||||
|
min-height: 0;
|
||||||
|
border-bottom: 1px solid var(--theme-control-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-textarea-row textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 96px;
|
||||||
|
max-height: 320px;
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 12px;
|
||||||
|
outline: none;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
resize: vertical;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-textarea-row textarea:focus {
|
||||||
|
border-color: var(--text-secondary);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-textarea-row textarea::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle-row {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle-row input[type='checkbox'] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group-block {
|
||||||
|
border-bottom: 1px solid var(--theme-control-border);
|
||||||
|
padding: 14px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group-title {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle-row.inner {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 7px 0 7px 18px;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-check-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-compression-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-compression-grid label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-save-bar {
|
||||||
|
position: absolute;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
min-height: 54px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-save-bar > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-status-group {
|
||||||
|
min-height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
color: var(--state-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error,
|
||||||
|
.usage-summary-error {
|
||||||
|
color: var(--state-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 注:卡片常态最大宽度 648px,以下断点均在此基础上进一步响应。
|
||||||
|
窗口拖窄(≤520px 卡片):侧栏再收窄、内容留白再压缩、输入框收窄 */
|
||||||
|
@container settings-card (max-width: 520px) {
|
||||||
|
.settings-redesign-nav {
|
||||||
|
flex-basis: 164px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-content-header {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-scroll {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-row,
|
||||||
|
.settings-input-row,
|
||||||
|
.settings-list-row,
|
||||||
|
.settings-action-row,
|
||||||
|
.settings-toggle-row {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-row > input {
|
||||||
|
width: min(180px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input-stack,
|
||||||
|
.settings-input-stack.wide {
|
||||||
|
width: 220px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 极限挤压(手机全屏 / 窗口极窄):输入类行上下堆叠,控件全宽;开关行保持同行(iOS 设置式) */
|
||||||
|
@container settings-card (max-width: 440px) {
|
||||||
|
.settings-select-row,
|
||||||
|
.settings-input-row,
|
||||||
|
.settings-list-row,
|
||||||
|
.settings-action-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-wrap,
|
||||||
|
.settings-input-stack,
|
||||||
|
.settings-input-stack.wide,
|
||||||
|
.settings-input-row > input {
|
||||||
|
justify-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select-wrap .settings-select-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-check-grid,
|
||||||
|
.settings-compression-grid,
|
||||||
|
.usage-summary-grid--tokens,
|
||||||
|
.usage-summary-grid--counts {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ 手机端 / 窄屏(≤760px):全屏 + iOS 设置式「列表 → 子页」导航 ═══ */
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
/* 卡片全屏化由全局 _overlays.scss 同断点媒体查询负责(统一断点 760px) */
|
||||||
|
|
||||||
|
/* 导航列表与内容子页并排,直接切换(触屏不做平移动画,避免看到上一屏滑走) */
|
||||||
|
.settings-redesign-layout {
|
||||||
|
width: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-card.mobile-in-sub .settings-redesign-layout {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-nav,
|
||||||
|
.settings-redesign-content {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS 点击默认灰色高亮闪(tap-highlight),观感类似 hover 粘滞,禁用 */
|
||||||
|
.settings-redesign-tab {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 窄屏布局下禁用选项 hover 态:桌面窄窗口 hover:hover 仍匹配,
|
||||||
|
触屏已由 @media (hover: hover) 包裹兑底,这里按布局模式再禁一层 */
|
||||||
|
.settings-redesign-tab:hover,
|
||||||
|
.settings-menu-option:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端列表态不需要「当前选中项」常驻高亮(iOS 设置式列表无此概念) */
|
||||||
|
.settings-redesign-tab.active {
|
||||||
|
background: transparent;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-floating-menu.dark .settings-menu-option:hover,
|
||||||
|
html[data-theme='dark'] .settings-menu-option:hover,
|
||||||
|
body[data-theme='dark'] .settings-menu-option:hover {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-nav {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端头部隐藏,移动端顶栏接管 */
|
||||||
|
.settings-nav-head,
|
||||||
|
.settings-redesign-content-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px;
|
||||||
|
border-bottom: 1px solid var(--theme-control-border);
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-bar-title {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-bar-cell {
|
||||||
|
width: 44px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-bar-btn {
|
||||||
|
height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-bar-btn:active {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-bar-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-mobile-back {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航变为分组列表样式:48px 触控目标 + 右箭头 */
|
||||||
|
.settings-redesign-tabs {
|
||||||
|
padding: 8px 12px calc(20px + env(safe-area-inset-bottom, 0px));
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-tab {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-icon {
|
||||||
|
--icon-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-icon--chat {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-icon--chat svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-chevron {
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-redesign-scroll {
|
||||||
|
padding: 4px 16px calc(48px + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-save-bar {
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-floating-menu {
|
||||||
|
width: min(340px, calc(100vw - 48px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 语音模型下载进度条 ── */
|
||||||
82
static/src/components/personalization/tabs/AdminTab.vue
Normal file
82
static/src/components/personalization/tabs/AdminTab.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'AdminTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
openAdminPanel,
|
||||||
|
openApiAdmin,
|
||||||
|
openCustomTools,
|
||||||
|
openMcpConfig,
|
||||||
|
showMcpConfigEntry
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page admin-monitor-page">
|
||||||
|
<div class="settings-action-row" v-if="showMcpConfigEntry">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.mcpConfigTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.mcpConfigDesc') }}</span
|
||||||
|
></span
|
||||||
|
><button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="openMcpConfig"
|
||||||
|
>
|
||||||
|
{{ $t('personalization.open') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.adminMonitorTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.adminMonitorDesc') }}</span
|
||||||
|
></span
|
||||||
|
><button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="openAdminPanel"
|
||||||
|
>
|
||||||
|
{{ $t('personalization.open') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.apiAdminTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.apiAdminDesc') }}</span></span
|
||||||
|
><button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="openApiAdmin"
|
||||||
|
>
|
||||||
|
{{ $t('personalization.open') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.customToolsAdminTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.customToolsAdminDesc') }}</span></span
|
||||||
|
><button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="openCustomTools"
|
||||||
|
>
|
||||||
|
{{ $t('personalization.open') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-monitor-page {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
375
static/src/components/personalization/tabs/AppearanceTab.vue
Normal file
375
static/src/components/personalization/tabs/AppearanceTab.vue
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'AppearanceTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
activeDropdown,
|
||||||
|
activeTheme,
|
||||||
|
blockDisplayLabel,
|
||||||
|
blockDisplayOptions,
|
||||||
|
compactMessageDisplayLabel,
|
||||||
|
compactMessageDisplayOptions,
|
||||||
|
currentBlockDisplayMode,
|
||||||
|
currentCompactMessageDisplay,
|
||||||
|
floatingMenuStyle,
|
||||||
|
form,
|
||||||
|
handleMinimalExpandHeightLimitedChange,
|
||||||
|
handleStackedHideBordersChange,
|
||||||
|
locale,
|
||||||
|
localeLabel,
|
||||||
|
localeOptions,
|
||||||
|
minimalExpandHeightLimited,
|
||||||
|
personalization,
|
||||||
|
selectBlockDisplayMode,
|
||||||
|
selectCompactMessageDisplay,
|
||||||
|
selectLocaleOption,
|
||||||
|
selectThemeOption,
|
||||||
|
stackedHideBorders,
|
||||||
|
themeLabel,
|
||||||
|
themeOptions,
|
||||||
|
toggleDropdown
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.themeTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.themeDesc') }}</span></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'theme' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('theme')"
|
||||||
|
>
|
||||||
|
{{ themeLabel }} <span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in themeOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: activeTheme === option.id }"
|
||||||
|
@click="selectThemeOption(option.id)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('common.settingsLanguage') }}</span
|
||||||
|
><span class="settings-row-desc">{{
|
||||||
|
$t('common.settingsLanguageDesc')
|
||||||
|
}}</span></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'language' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('language')"
|
||||||
|
>
|
||||||
|
{{ localeLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in localeOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: locale === option.id }"
|
||||||
|
@click="selectLocaleOption(option.id)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.groupByWorkspaceTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.groupByWorkspaceDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.group_sidebar_by_workspace"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'group_sidebar_by_workspace',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.group_sidebar_by_workspace" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.newChatBlankTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.newChatBlankDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.new_chat_button_behavior === 'route'"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'new_chat_button_behavior',
|
||||||
|
value: $event.target.checked ? 'route' : 'blank'
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.new_chat_button_behavior === 'route'" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.useCustomNamesTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.useCustomNamesDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.use_custom_names"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'use_custom_names',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.use_custom_names" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.enhancedToolDisplayTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.enhancedToolDisplayDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.enhanced_tool_display"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'enhanced_tool_display',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.enhanced_tool_display" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.showStatusAvatarTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.showStatusAvatarDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.show_status_avatar"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'show_status_avatar',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.show_status_avatar" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.showGitStatusBarTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.showGitStatusBarDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.show_git_status_bar"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'show_git_status_bar',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.show_git_status_bar" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.autoOpenTerminalTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.autoOpenTerminalDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.auto_open_terminal_panel"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'auto_open_terminal_panel',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.auto_open_terminal_panel" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.quickDockAutoExpandTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.quickDockAutoExpandDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.quick_dock_auto_expand"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'quick_dock_auto_expand',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.quick_dock_auto_expand" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.editSummaryLiveTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.editSummaryLiveDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.edit_summary_live_display"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'edit_summary_live_display',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.edit_summary_live_display" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.filePreviewWrapTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.filePreviewWrapDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.file_preview_auto_wrap"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'file_preview_auto_wrap',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.file_preview_auto_wrap" /></label>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.blockDisplayModeTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.blockDisplayModeDesc') }}</span></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'block-display' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('block-display')"
|
||||||
|
>
|
||||||
|
{{ blockDisplayLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in blockDisplayOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: currentBlockDisplayMode === option.value }"
|
||||||
|
@click="selectBlockDisplayMode(option.value)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="currentBlockDisplayMode === 'stacked'"
|
||||||
|
class="settings-toggle-row"
|
||||||
|
>
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.hideBlockBordersTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.hideBlockBordersDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="stackedHideBorders"
|
||||||
|
@change="handleStackedHideBordersChange($event)"
|
||||||
|
/>
|
||||||
|
<FancyCheck :checked="stackedHideBorders" />
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="currentBlockDisplayMode === 'minimal'"
|
||||||
|
class="settings-toggle-row"
|
||||||
|
>
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.minimalExpandHeightTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.minimalExpandHeightDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="minimalExpandHeightLimited"
|
||||||
|
@change="handleMinimalExpandHeightLimitedChange($event)"
|
||||||
|
/>
|
||||||
|
<FancyCheck :checked="minimalExpandHeightLimited" />
|
||||||
|
</label>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.compactMessageTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.compactMessageDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'compact-message' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('compact-message')"
|
||||||
|
>
|
||||||
|
{{ compactMessageDisplayLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in compactMessageDisplayOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: currentCompactMessageDisplay === option.value }"
|
||||||
|
@click="selectCompactMessageDisplay(option.value)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
203
static/src/components/personalization/tabs/ContextTab.vue
Normal file
203
static/src/components/personalization/tabs/ContextTab.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ContextTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
commitProjectMemoryInjectLimitInput,
|
||||||
|
commitRecentConversationsPromptLimitInput,
|
||||||
|
form,
|
||||||
|
handleCompressionNumberInput,
|
||||||
|
handleProjectMemoryInjectLimitInput,
|
||||||
|
handleRecentConversationsPromptLimitInput,
|
||||||
|
personalization,
|
||||||
|
projectMemoryInjectLimitMin,
|
||||||
|
recentConversationsPromptLimitRange,
|
||||||
|
restoreCompressionDefaults,
|
||||||
|
restoreProjectMemoryInjectLimit,
|
||||||
|
restoreRecentConversationsPromptLimit
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.recentConversationsTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.recentConversationsDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.recent_conversations_prompt_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'recent_conversations_prompt_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.recent_conversations_prompt_enabled" /></label>
|
||||||
|
<div
|
||||||
|
class="settings-input-row"
|
||||||
|
v-if="form.recent_conversations_prompt_enabled"
|
||||||
|
>
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.recentConversationsCountTitle') }}</span>
|
||||||
|
<div class="settings-number-row">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:min="recentConversationsPromptLimitRange.min"
|
||||||
|
:max="recentConversationsPromptLimitRange.max"
|
||||||
|
:value="form.recent_conversations_prompt_limit"
|
||||||
|
@input="handleRecentConversationsPromptLimitInput"
|
||||||
|
@blur="commitRecentConversationsPromptLimitInput"
|
||||||
|
/><button type="button" @click="restoreRecentConversationsPromptLimit">{{ $t('personalization.restoreDefault') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.maxMemoryInjectTitle') }}</span>
|
||||||
|
<div class="settings-number-row">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
:min="projectMemoryInjectLimitMin"
|
||||||
|
:value="form.project_memory_inject_limit ?? ''"
|
||||||
|
::placeholder="$t('personalization.noLimitPlaceholder')"
|
||||||
|
@input="handleProjectMemoryInjectLimitInput"
|
||||||
|
@blur="commitProjectMemoryInjectLimitInput"
|
||||||
|
/><button type="button" @click="restoreProjectMemoryInjectLimit">{{ $t('personalization.restoreDefault') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group-block">
|
||||||
|
<div class="settings-group-title">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.compressionStrategyTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.compressionStrategyDesc') }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.autoShallowCompressTitle') }}</span></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.auto_shallow_compress_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'auto_shallow_compress_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.auto_shallow_compress_enabled" /></label>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.autoDeepCompressTitle') }}</span></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.auto_deep_compress_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'auto_deep_compress_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.auto_deep_compress_enabled" /></label>
|
||||||
|
<div class="settings-compression-grid">
|
||||||
|
<label
|
||||||
|
><span>{{ $t('personalization.shallowTriggerTokens') }}</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
:value="form.shallow_compress_trigger_tokens ?? ''"
|
||||||
|
@input="
|
||||||
|
handleCompressionNumberInput(
|
||||||
|
'shallow_compress_trigger_tokens',
|
||||||
|
$event
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
><span>{{ $t('personalization.shallowKeepRecentTools') }}</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
:value="form.shallow_compress_keep_recent_tools ?? ''"
|
||||||
|
@input="
|
||||||
|
handleCompressionNumberInput(
|
||||||
|
'shallow_compress_keep_recent_tools',
|
||||||
|
$event
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
><span>{{ $t('personalization.shallowKeepTurnTools') }}</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
:value="form.shallow_compress_keep_user_turn_tools ?? ''"
|
||||||
|
@input="
|
||||||
|
handleCompressionNumberInput(
|
||||||
|
'shallow_compress_keep_user_turn_tools',
|
||||||
|
$event
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
><span>{{ $t('personalization.shallowMaxReplace') }}</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
:value="form.shallow_compress_max_replace_per_round ?? ''"
|
||||||
|
@input="
|
||||||
|
handleCompressionNumberInput(
|
||||||
|
'shallow_compress_max_replace_per_round',
|
||||||
|
$event
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
><span>{{ $t('personalization.shallowToolInterval') }}</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
|
||||||
|
@input="
|
||||||
|
handleCompressionNumberInput(
|
||||||
|
'shallow_compress_trigger_tool_calls_interval',
|
||||||
|
$event
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
><span>{{ $t('personalization.deepTriggerTokens') }}</span
|
||||||
|
><input
|
||||||
|
type="number"
|
||||||
|
:value="form.deep_compress_trigger_tokens ?? ''"
|
||||||
|
@input="
|
||||||
|
handleCompressionNumberInput('deep_compress_trigger_tokens', $event)
|
||||||
|
"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.deepCompressInjectTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.deepCompressInjectDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.deep_compress_form === 'inject'"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'deep_compress_form',
|
||||||
|
value: $event.target.checked ? 'inject' : 'file'
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.deep_compress_form === 'inject'" /></label>
|
||||||
|
<div class="settings-inline-actions right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="restoreCompressionDefaults"
|
||||||
|
>{{ $t('personalization.restoreCompressionDefaults') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
182
static/src/components/personalization/tabs/DataTab.vue
Normal file
182
static/src/components/personalization/tabs/DataTab.vue
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DataTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
personalization,
|
||||||
|
fetchUsageSummary,
|
||||||
|
formatTokenCount,
|
||||||
|
usageError,
|
||||||
|
usageLoading,
|
||||||
|
usageSummary,
|
||||||
|
usageUpdatedText
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="settings-page usage-summary-page"
|
||||||
|
data-tutorial="personal-page-usage"
|
||||||
|
>
|
||||||
|
<div class="usage-summary-card settings-stats-card">
|
||||||
|
<div class="usage-summary-header">
|
||||||
|
<div>
|
||||||
|
<p class="usage-summary-eyebrow">{{ $t('personalization.usageEyebrow') }}</p>
|
||||||
|
<h3>{{ $t('personalization.usageTitle') }}</h3>
|
||||||
|
<p class="usage-summary-desc">{{ $t('personalization.usageDesc') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-grid usage-summary-grid--tokens">
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">{{ $t('personalization.usageInputLabel') }}</div>
|
||||||
|
<div class="value value--success">
|
||||||
|
{{ formatTokenCount(usageSummary.total_input_tokens) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">{{ $t('personalization.usageOutputLabel') }}</div>
|
||||||
|
<div class="value value--warning">
|
||||||
|
{{ formatTokenCount(usageSummary.total_output_tokens) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-grid usage-summary-grid--counts">
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">{{ $t('personalization.usageConversationsLabel') }}</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ formatTokenCount(usageSummary.total_conversations) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">{{ $t('personalization.usageMessagesLabel') }}</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ formatTokenCount(usageSummary.total_user_messages) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-item">
|
||||||
|
<div class="label">{{ $t('personalization.usageToolsLabel') }}</div>
|
||||||
|
<div class="value">
|
||||||
|
{{ formatTokenCount(usageSummary.total_tools) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usage-summary-meta">
|
||||||
|
<span v-if="usageError" class="usage-summary-error">{{ usageError }}</span
|
||||||
|
><span v-else-if="usageLoading">{{ $t('personalization.usageSyncing') }}</span
|
||||||
|
><span v-else>{{ $t('personalization.usageUpdated', { time: usageUpdatedText }) }}</span
|
||||||
|
><button
|
||||||
|
type="button"
|
||||||
|
class="usage-summary-refresh"
|
||||||
|
@click="fetchUsageSummary"
|
||||||
|
:disabled="usageLoading"
|
||||||
|
>
|
||||||
|
{{ usageLoading ? $t('personalization.usageRefreshing') : $t('personalization.usageRefresh') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.usage-summary-page {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-stats-card,
|
||||||
|
.usage-summary-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-header h3 {
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-desc,
|
||||||
|
.usage-summary-note,
|
||||||
|
.usage-summary-meta {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-grid--tokens {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-grid--counts {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-item {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-item .label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-item .value {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-item .value--success {
|
||||||
|
color: var(--state-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-item .value--warning {
|
||||||
|
color: var(--state-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-summary-refresh {
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
static/src/components/personalization/tabs/FilesTab.vue
Normal file
69
static/src/components/personalization/tabs/FilesTab.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FilesTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
personalization,
|
||||||
|
form,
|
||||||
|
activeDropdown,
|
||||||
|
floatingMenuStyle,
|
||||||
|
imageCompressionLabel,
|
||||||
|
imageCompressionOptions,
|
||||||
|
selectImageCompression,
|
||||||
|
toggleDropdown,
|
||||||
|
activeTheme
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="settings-page"
|
||||||
|
data-tutorial="personal-page-image"
|
||||||
|
>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.imageCompressionTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.imageCompressionDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'image' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('image')"
|
||||||
|
>
|
||||||
|
{{ imageCompressionLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in imageCompressionOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.image_compression === option.id }"
|
||||||
|
@click="selectImageCompression(option.id)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
101
static/src/components/personalization/tabs/GeneralTab.vue
Normal file
101
static/src/components/personalization/tabs/GeneralTab.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'GeneralTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
personalization,
|
||||||
|
form,
|
||||||
|
startTutorial,
|
||||||
|
isAppShell,
|
||||||
|
appCurrentVersionText,
|
||||||
|
appUpdateCheckedText,
|
||||||
|
appHasUpdate,
|
||||||
|
appUpdateStateText,
|
||||||
|
appUpdateChecking,
|
||||||
|
checkAppUpdate,
|
||||||
|
downloadLatestApp
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<label class="settings-toggle-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.autoTitleTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.autoTitleDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.auto_generate_title"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'auto_generate_title',
|
||||||
|
value: ($event.target as HTMLInputElement).checked
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<FancyCheck :checked="form.auto_generate_title" />
|
||||||
|
</label>
|
||||||
|
<div class="settings-action-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.tutorialTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.tutorialDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="settings-secondary-button" @click="startTutorial">
|
||||||
|
{{ $t('personalization.tutorialButton') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-row" v-if="isAppShell">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.appUpdateTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{
|
||||||
|
$t('personalization.appUpdateDesc', {
|
||||||
|
version: appCurrentVersionText,
|
||||||
|
checked: appUpdateCheckedText
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
<div class="settings-inline-actions">
|
||||||
|
<span class="settings-mini-status" :class="{ warning: appHasUpdate }">{{
|
||||||
|
appUpdateStateText
|
||||||
|
}}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
:disabled="appUpdateChecking"
|
||||||
|
@click="checkAppUpdate"
|
||||||
|
>
|
||||||
|
{{ appUpdateChecking ? $t('personalization.appChecking') : $t('personalization.appCheckUpdate') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="appHasUpdate"
|
||||||
|
type="button"
|
||||||
|
class="settings-primary-button"
|
||||||
|
@click="downloadLatestApp"
|
||||||
|
>
|
||||||
|
{{ $t('common.download') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-row danger-zone">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.logoutTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.logoutDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button danger"
|
||||||
|
@click="personalization.logout()"
|
||||||
|
>
|
||||||
|
{{ $t('personalization.logoutTitle') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
159
static/src/components/personalization/tabs/ModelTab.vue
Normal file
159
static/src/components/personalization/tabs/ModelTab.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ModelTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
personalization,
|
||||||
|
form,
|
||||||
|
activeDropdown,
|
||||||
|
defaultModelLabel,
|
||||||
|
filteredModelOptions,
|
||||||
|
floatingMenuStyle,
|
||||||
|
isEffortActive,
|
||||||
|
isRunModeActive,
|
||||||
|
reasoningEffortLabel,
|
||||||
|
runModeLabel,
|
||||||
|
selectDefaultModel,
|
||||||
|
selectDefaultReasoningEffort,
|
||||||
|
selectDefaultRunMode,
|
||||||
|
toggleDropdown,
|
||||||
|
runModeOptions,
|
||||||
|
reasoningEffortOptions,
|
||||||
|
activeTheme
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="settings-page"
|
||||||
|
data-tutorial="personal-page-model"
|
||||||
|
>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.defaultModelTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.defaultModelDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'model' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('model')"
|
||||||
|
>
|
||||||
|
{{ defaultModelLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in filteredModelOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{
|
||||||
|
selected: form.default_model === option.value,
|
||||||
|
disabled: option.disabled
|
||||||
|
}"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
@click="selectDefaultModel(option.value)"
|
||||||
|
>
|
||||||
|
<strong>{{ option.label }}</strong
|
||||||
|
><span>{{ option.disabled ? $t('personalization.modelDisabled') : option.desc }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.defaultRunModeTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.defaultRunModeDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'run-mode' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('run-mode')"
|
||||||
|
>
|
||||||
|
{{ runModeLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in runModeOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: isRunModeActive(option.value) }"
|
||||||
|
@click="selectDefaultRunMode(option.value)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.reasoningEffortTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.reasoningEffortDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'reasoning-effort' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('reasoning-effort')"
|
||||||
|
>
|
||||||
|
{{ reasoningEffortLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in reasoningEffortOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: isEffortActive(option.value) }"
|
||||||
|
@click="selectDefaultReasoningEffort(option.value)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
276
static/src/components/personalization/tabs/PreferencesTab.vue
Normal file
276
static/src/components/personalization/tabs/PreferencesTab.vue
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'PreferencesTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
personalization,
|
||||||
|
form,
|
||||||
|
tonePresets,
|
||||||
|
toggleUpdating,
|
||||||
|
activeDropdown,
|
||||||
|
toggleDropdown,
|
||||||
|
floatingMenuStyle,
|
||||||
|
activeTheme,
|
||||||
|
communicationStyleLabel,
|
||||||
|
conversationContinuityLabel,
|
||||||
|
currentBlockDisplayMode,
|
||||||
|
stackedHideBorders,
|
||||||
|
handleStackedHideBordersChange,
|
||||||
|
minimalExpandHeightLimited,
|
||||||
|
handleMinimalExpandHeightLimitedChange,
|
||||||
|
selectCommunicationStyle,
|
||||||
|
selectConversationContinuity
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="settings-page"
|
||||||
|
data-tutorial="personal-page-preferences"
|
||||||
|
>
|
||||||
|
<label class="settings-toggle-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.preferencesTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.preferencesDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.enabled"
|
||||||
|
:disabled="toggleUpdating"
|
||||||
|
@change="personalization.toggleEnabled()"
|
||||||
|
/>
|
||||||
|
<FancyCheck :checked="form.enabled" />
|
||||||
|
</label>
|
||||||
|
<label class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.selfIdentifyTitle') }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="form.self_identify"
|
||||||
|
maxlength="20"
|
||||||
|
:placeholder="$t('personalization.selfIdentifyPlaceholder')"
|
||||||
|
@input="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'self_identify',
|
||||||
|
value: $event.target.value
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@focus="personalization.clearFeedback()"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.userNameTitle') }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="form.user_name"
|
||||||
|
maxlength="20"
|
||||||
|
:placeholder="$t('personalization.userNamePlaceholder')"
|
||||||
|
@input="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'user_name',
|
||||||
|
value: $event.target.value
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@focus="personalization.clearFeedback()"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.professionTitle') }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="form.profession"
|
||||||
|
maxlength="20"
|
||||||
|
:placeholder="$t('personalization.professionPlaceholder')"
|
||||||
|
@input="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'profession',
|
||||||
|
value: $event.target.value
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@focus="personalization.clearFeedback()"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="settings-input-row stackable">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.toneTitle') }}</span>
|
||||||
|
<div class="settings-input-stack">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="form.tone"
|
||||||
|
maxlength="20"
|
||||||
|
:placeholder="$t('personalization.tonePlaceholder')"
|
||||||
|
@input="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'tone',
|
||||||
|
value: $event.target.value
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@focus="personalization.clearFeedback()"
|
||||||
|
/>
|
||||||
|
<div class="settings-chip-row">
|
||||||
|
<button
|
||||||
|
v-for="preset in tonePresets"
|
||||||
|
:key="preset"
|
||||||
|
type="button"
|
||||||
|
@click.prevent="personalization.applyTonePreset(preset)"
|
||||||
|
>
|
||||||
|
{{ preset }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-textarea-row">
|
||||||
|
<div class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.considerationsTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.considerationsDesc') }}</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
:value="form.considerations"
|
||||||
|
rows="6"
|
||||||
|
maxlength="2000"
|
||||||
|
:placeholder="$t('personalization.considerationsPlaceholder')"
|
||||||
|
@input="personalization.updateConsiderations($event.target.value)"
|
||||||
|
@focus="personalization.clearFeedback()"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="currentBlockDisplayMode === 'stacked'"
|
||||||
|
class="settings-toggle-row"
|
||||||
|
>
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.hideBlockBordersTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.hideBlockBordersDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="stackedHideBorders"
|
||||||
|
@change="handleStackedHideBordersChange($event)"
|
||||||
|
/>
|
||||||
|
<FancyCheck :checked="stackedHideBorders" />
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="currentBlockDisplayMode === 'minimal'"
|
||||||
|
class="settings-toggle-row"
|
||||||
|
>
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.minimalExpandHeightTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.minimalExpandHeightDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="minimalExpandHeightLimited"
|
||||||
|
@change="handleMinimalExpandHeightLimitedChange($event)"
|
||||||
|
/>
|
||||||
|
<FancyCheck :checked="minimalExpandHeightLimited" />
|
||||||
|
</label>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.communicationStyleTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.communicationStyleDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'communication' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('communication')"
|
||||||
|
>
|
||||||
|
{{ communicationStyleLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.communication_style === 'default' }"
|
||||||
|
@click="selectCommunicationStyle('default')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.communicationDefault') }}</strong><span>{{ $t('personalization.communicationDefaultDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.communication_style === 'human_like' }"
|
||||||
|
@click="selectCommunicationStyle('human_like')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.communicationHumanLike') }}</strong><span>{{ $t('personalization.communicationHumanLikeDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.communication_style === 'auto' }"
|
||||||
|
@click="selectCommunicationStyle('auto')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.communicationAuto') }}</strong><span>{{ $t('personalization.communicationAutoDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.conversationContinuityTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.conversationContinuityDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'conversation-continuity' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('conversation-continuity')"
|
||||||
|
>
|
||||||
|
{{ conversationContinuityLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.conversation_continuity === 'high' }"
|
||||||
|
@click="selectConversationContinuity('high')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.continuityHigh') }}</strong><span>{{ $t('personalization.continuityHighDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.conversation_continuity === 'medium' }"
|
||||||
|
@click="selectConversationContinuity('medium')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.continuityMedium') }}</strong><span>{{ $t('personalization.continuityMediumDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.conversation_continuity === 'low' }"
|
||||||
|
@click="selectConversationContinuity('low')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.continuityLow') }}</strong><span>{{ $t('personalization.continuityLowDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
170
static/src/components/personalization/tabs/ReviewAgentsTab.vue
Normal file
170
static/src/components/personalization/tabs/ReviewAgentsTab.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ReviewAgentsTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
activeDropdown,
|
||||||
|
activeTheme,
|
||||||
|
closeDropdown,
|
||||||
|
floatingMenuStyle,
|
||||||
|
reviewAgentDefs,
|
||||||
|
reviewAgentOf,
|
||||||
|
subAgentModels,
|
||||||
|
toggleDropdown,
|
||||||
|
updateReviewAgent,
|
||||||
|
updateReviewAgentInt
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--text-secondary); font-size: 13px; line-height: 1.6">{{ $t('personalization.reviewIntro') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-for="agent in reviewAgentDefs" :key="agent.key">
|
||||||
|
<div class="settings-section-divider">
|
||||||
|
<span class="settings-section-divider__label">{{ $t(agent.nameKey) }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin: -4px 0 12px; color: var(--text-secondary); font-size: 12px; line-height: 1.5">
|
||||||
|
{{ $t(agent.descKey) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.modelTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.reviewModelEmptyDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === `review-model-${agent.key}` }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown(`review-model-${agent.key}`)"
|
||||||
|
>
|
||||||
|
{{ reviewAgentOf(agent.key).model || $t('personalization.defaultModelOption') }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown === `review-model-${agent.key}` ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: !reviewAgentOf(agent.key).model }"
|
||||||
|
@click="updateReviewAgent(agent.key, { model: '' }); closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.defaultModelOption') }}</strong><span>{{ $t('personalization.reviewDefaultModelDesc') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="m in subAgentModels"
|
||||||
|
:key="m.name"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: reviewAgentOf(agent.key).model === m.name }"
|
||||||
|
@click="updateReviewAgent(agent.key, { model: m.name }); closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || $t('personalization.textOnly') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.thinkingModeTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.reviewThinkingDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === `review-thinking-${agent.key}` }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown(`review-thinking-${agent.key}`)"
|
||||||
|
>
|
||||||
|
{{ reviewAgentOf(agent.key).thinking ? 'thinking' : 'fast' }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown === `review-thinking-${agent.key}` ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: !reviewAgentOf(agent.key).thinking }"
|
||||||
|
@click="updateReviewAgent(agent.key, { thinking: false }); closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>fast</strong><span>{{ $t('personalization.fastResponseMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: reviewAgentOf(agent.key).thinking }"
|
||||||
|
@click="updateReviewAgent(agent.key, { thinking: true }); closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>thinking</strong><span>{{ $t('personalization.thinkingReasoningMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.timeoutTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.timeoutDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
min="5"
|
||||||
|
max="3600"
|
||||||
|
:value="reviewAgentOf(agent.key).timeout_seconds"
|
||||||
|
@change="updateReviewAgentInt(agent.key, 'timeout_seconds', ($event.target as HTMLInputElement).value, 5, 3600)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.maxRoundsTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.maxRoundsDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
min="1"
|
||||||
|
max="50"
|
||||||
|
:value="reviewAgentOf(agent.key).max_rounds"
|
||||||
|
@change="updateReviewAgentInt(agent.key, 'max_rounds', ($event.target as HTMLInputElement).value, 1, 50)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-select-row" style="margin-bottom: 8px">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.commandTimeoutTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.commandTimeoutDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
min="1"
|
||||||
|
max="600"
|
||||||
|
:value="reviewAgentOf(agent.key).max_command_timeout"
|
||||||
|
@change="updateReviewAgentInt(agent.key, 'max_command_timeout', ($event.target as HTMLInputElement).value, 1, 600)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
442
static/src/components/personalization/tabs/SubAgentsTab.vue
Normal file
442
static/src/components/personalization/tabs/SubAgentsTab.vue
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SubAgentsTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
activeDropdown,
|
||||||
|
activeTheme,
|
||||||
|
closeDropdown,
|
||||||
|
closeRoleEditor,
|
||||||
|
deleteRole,
|
||||||
|
editingRole,
|
||||||
|
handleRoleEditorOverlayPressEnd,
|
||||||
|
handleRoleEditorOverlayPressStart,
|
||||||
|
openRoleEditor,
|
||||||
|
personalization,
|
||||||
|
roleEditorOpen,
|
||||||
|
roleForm,
|
||||||
|
roleSaving,
|
||||||
|
saveRole,
|
||||||
|
saveSubAgentSettings,
|
||||||
|
subAgentCompressThreshold,
|
||||||
|
subAgentMaxTurns,
|
||||||
|
subAgentModels,
|
||||||
|
subAgentRoles,
|
||||||
|
subAgentRolesLoading,
|
||||||
|
subAgentSettingsSaving,
|
||||||
|
toggleDropdown
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--text-secondary); font-size: 13px; line-height: 1.6">{{ $t('personalization.subAgentsIntro') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 压缩阈值配置 -->
|
||||||
|
<div class="settings-action-row" style="margin-bottom: 16px">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.compressThresholdTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.compressThresholdDesc') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div style="display: flex; gap: 6px; align-items: center">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
v-model.number="subAgentCompressThreshold"
|
||||||
|
:min="10000"
|
||||||
|
:step="10000"
|
||||||
|
style="width: 120px"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
:disabled="subAgentSettingsSaving"
|
||||||
|
@click="saveSubAgentSettings"
|
||||||
|
>
|
||||||
|
{{ subAgentSettingsSaving ? $t('personalization.saving') : $t('common.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最大执行轮次配置 -->
|
||||||
|
<div class="settings-action-row" style="margin-bottom: 16px">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.maxTurnsTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.maxTurnsDesc') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div style="display: flex; gap: 6px; align-items: center">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
v-model.number="subAgentMaxTurns"
|
||||||
|
:min="0"
|
||||||
|
:step="10"
|
||||||
|
placeholder="50"
|
||||||
|
style="width: 120px"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
:disabled="subAgentSettingsSaving"
|
||||||
|
@click="saveSubAgentSettings"
|
||||||
|
>
|
||||||
|
{{ subAgentSettingsSaving ? $t('personalization.saving') : $t('common.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 角色列表 -->
|
||||||
|
<div class="settings-section-header" style="margin-bottom: 8px">
|
||||||
|
<span class="settings-section-title">{{ $t('personalization.roleListTitle') }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="openRoleEditor()"
|
||||||
|
>
|
||||||
|
{{ $t('personalization.newRole') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="subAgentRolesLoading" class="settings-empty-hint">{{ $t('common.loading') }}</div>
|
||||||
|
<div v-else-if="subAgentRoles.length === 0" class="settings-empty-hint">{{ $t('personalization.noRoles') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="sub-agent-role-list">
|
||||||
|
<div
|
||||||
|
v-for="role in subAgentRoles"
|
||||||
|
:key="role.role_id"
|
||||||
|
class="sub-agent-role-card"
|
||||||
|
>
|
||||||
|
<div class="sub-agent-role-info">
|
||||||
|
<div class="sub-agent-role-name">
|
||||||
|
{{ role.name }}
|
||||||
|
<span
|
||||||
|
class="sub-agent-role-tag"
|
||||||
|
:class="role.is_custom ? 'tag-custom' : 'tag-preset'"
|
||||||
|
>
|
||||||
|
{{ role.is_custom ? $t('personalization.customTag') : $t('personalization.presetTag') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="sub-agent-role-desc">{{ role.description || $t('personalization.noDescription') }}</div>
|
||||||
|
<div class="sub-agent-role-meta">
|
||||||
|
{{ $t('personalization.roleMetaId', { id: role.role_id, mode: role.thinking_mode }) }}<template v-if="role.model_key">{{ $t('personalization.roleMetaModel', { model: role.model_key }) }}</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sub-agent-role-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
@click="openRoleEditor(role)"
|
||||||
|
>{{ $t('personalization.edit') }}</button>
|
||||||
|
<button
|
||||||
|
v-if="role.is_custom"
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button danger"
|
||||||
|
@click="deleteRole(role)"
|
||||||
|
>{{ $t('common.delete') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 角色编辑弹窗(与个人空间同尺寸) -->
|
||||||
|
<transition name="personal-page-fade" appear>
|
||||||
|
<div
|
||||||
|
v-if="roleEditorOpen"
|
||||||
|
class="role-editor-drawer-overlay"
|
||||||
|
@click="closeDropdown"
|
||||||
|
@mousedown="handleRoleEditorOverlayPressStart"
|
||||||
|
@mouseup="handleRoleEditorOverlayPressEnd"
|
||||||
|
>
|
||||||
|
<div class="role-editor-drawer-card settings-redesign-card" @click.stop>
|
||||||
|
<div class="role-editor-header">
|
||||||
|
<h2>{{ editingRole ? $t('personalization.roleEditorEditTitle') : $t('personalization.roleEditorCreateTitle') }}</h2>
|
||||||
|
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">{{ $t('common.close') }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="role-editor-body" @click="closeDropdown">
|
||||||
|
<label class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.roleIdTitle') }}</span>
|
||||||
|
<input
|
||||||
|
v-if="!editingRole"
|
||||||
|
type="text"
|
||||||
|
:value="roleForm.role_id"
|
||||||
|
maxlength="40"
|
||||||
|
::placeholder="$t('personalization.roleIdPlaceholder')"
|
||||||
|
@input="roleForm.role_id = ($event.target as HTMLInputElement).value"
|
||||||
|
/>
|
||||||
|
<span v-else class="settings-row-desc" style="text-align: right; font-size: 14px;">{{ roleForm.role_id }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.roleNameTitle') }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="roleForm.name"
|
||||||
|
maxlength="40"
|
||||||
|
::placeholder="$t('personalization.roleNamePlaceholder')"
|
||||||
|
@input="roleForm.name = ($event.target as HTMLInputElement).value"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="settings-input-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.roleDescTitle') }}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="roleForm.description"
|
||||||
|
maxlength="100"
|
||||||
|
::placeholder="$t('personalization.roleDescPlaceholder')"
|
||||||
|
@input="roleForm.description = ($event.target as HTMLInputElement).value"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.thinkingModeTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.roleThinkingDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'role-thinking' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('role-thinking')"
|
||||||
|
>
|
||||||
|
{{ roleForm.thinking_mode === 'thinking' ? 'thinking' : 'fast' }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown === 'role-thinking' ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: roleForm.thinking_mode === 'fast' }"
|
||||||
|
@click="roleForm.thinking_mode = 'fast'; closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>fast</strong><span>{{ $t('personalization.fastResponseMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: roleForm.thinking_mode === 'thinking' }"
|
||||||
|
@click="roleForm.thinking_mode = 'thinking'; closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>thinking</strong><span>{{ $t('personalization.thinkingReasoningMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.modelTitle') }}</span>>
|
||||||
|
<span class="settings-row-desc">{{ $t('personalization.roleModelEmptyDesc') }}</span>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'role-model' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('role-model')"
|
||||||
|
>
|
||||||
|
{{ roleForm.model_key || $t('personalization.defaultModelOption') }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown === 'role-model' ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: !roleForm.model_key }"
|
||||||
|
@click="roleForm.model_key = ''; closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.defaultModelOption') }}</strong><span>{{ $t('personalization.roleDefaultModelDesc') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="m in subAgentModels"
|
||||||
|
:key="m.name"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: roleForm.model_key === m.name }"
|
||||||
|
@click="roleForm.model_key = m.name; closeDropdown()"
|
||||||
|
>
|
||||||
|
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || $t('personalization.textOnly') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-textarea-row">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.promptBodyTitle') }}</span>
|
||||||
|
<textarea
|
||||||
|
:value="roleForm.body_prompt"
|
||||||
|
::placeholder="$t('personalization.promptBodyPlaceholder')"
|
||||||
|
@input="roleForm.body_prompt = ($event.target as HTMLTextAreaElement).value"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="role-editor-footer">
|
||||||
|
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">{{ $t('common.cancel') }}</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-primary-button"
|
||||||
|
:disabled="roleSaving || !roleForm.role_id || !roleForm.name || !roleForm.body_prompt"
|
||||||
|
@click="saveRole"
|
||||||
|
>
|
||||||
|
{{ roleSaving ? $t('personalization.saving') : $t('common.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sub-agent-role-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.sub-agent-role-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 11px 0;
|
||||||
|
border-bottom: 1px solid var(--theme-control-border);
|
||||||
|
}
|
||||||
|
.sub-agent-role-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.sub-agent-role-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 550;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.sub-agent-role-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.sub-agent-role-tag.tag-preset {
|
||||||
|
background: color-mix(in srgb, var(--text-secondary) 15%, transparent);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.sub-agent-role-tag.tag-custom {
|
||||||
|
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.sub-agent-role-desc {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 3px;
|
||||||
|
line-height: 1.38;
|
||||||
|
}
|
||||||
|
.sub-agent-role-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.sub-agent-role-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 角色编辑弹窗 — 复用 settings-redesign-card */
|
||||||
|
.role-editor-drawer-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--overlay-scrim);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2147483647;
|
||||||
|
}
|
||||||
|
.role-editor-drawer-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
width: min(648px, calc(100vw - 40px));
|
||||||
|
height: min(646px, calc(100vh - 40px));
|
||||||
|
height: min(646px, calc(100dvh - 40px));
|
||||||
|
padding: 22px 24px 18px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--theme-surface-soft);
|
||||||
|
border: 1px solid var(--theme-control-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.role-editor-drawer-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 0;
|
||||||
|
padding: calc(14px + env(safe-area-inset-top, 0px)) 16px calc(18px + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.role-editor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.role-editor-header h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.role-editor-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
min-height: 0;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.role-editor-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.settings-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.settings-section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 550;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.settings-empty-hint {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 24px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
170
static/src/components/personalization/tabs/ToolsTab.vue
Normal file
170
static/src/components/personalization/tabs/ToolsTab.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ToolsTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
personalization,
|
||||||
|
skillsCatalog,
|
||||||
|
toggleCategory,
|
||||||
|
toolCategories
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.silentToolDisableTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.silentToolDisableDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.silent_tool_disable"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'silent_tool_disable',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.silent_tool_disable" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.hideToolApprovalTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.hideToolApprovalDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.hide_tool_approval_panel"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'hide_tool_approval_panel',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.hide_tool_approval_panel" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.toolIntentTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.toolIntentDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.tool_intent_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'tool_intent_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.tool_intent_enabled" /></label>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.skillHintsTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.skillHintsDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.skill_hints_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'skill_hints_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.skill_hints_enabled" /></label>
|
||||||
|
<div class="settings-group-block">
|
||||||
|
<div class="settings-group-title">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.strictSkillTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.strictSkillDesc') }}</span>
|
||||||
|
</div>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.terminalToolsTitle') }}</span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.skill_strict_terminal_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'skill_strict_terminal_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.skill_strict_terminal_enabled" /></label>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.subAgentToolsTitle') }}</span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.skill_strict_sub_agent_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'skill_strict_sub_agent_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.skill_strict_sub_agent_enabled" /></label>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.runCommandFgTitle') }}</span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.skill_strict_run_command_foreground_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'skill_strict_run_command_foreground_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.skill_strict_run_command_foreground_enabled" /></label>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.runCommandBgTitle') }}</span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.skill_strict_run_command_background_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'skill_strict_run_command_background_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.skill_strict_run_command_background_enabled" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group-block" v-if="skillsCatalog.length">
|
||||||
|
<div class="settings-group-title">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.availableSkillsTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.availableSkillsDesc') }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="settings-check-grid">
|
||||||
|
<label
|
||||||
|
v-for="skill in skillsCatalog"
|
||||||
|
:key="skill.id"
|
||||||
|
class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-title">{{ skill.label }}</span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.enabled_skills.includes(skill.id)"
|
||||||
|
@change="personalization.toggleSkill(skill.id)" /><FancyCheck :checked="form.enabled_skills.includes(skill.id)" /></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-group-block" v-if="toolCategories.length">
|
||||||
|
<div class="settings-group-title">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.disabledToolCategoriesTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.disabledToolCategoriesDesc') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-check-grid">
|
||||||
|
<label
|
||||||
|
v-for="category in toolCategories"
|
||||||
|
:key="category.id"
|
||||||
|
class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-title">{{ category.label }}</span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.disabled_tool_categories.includes(category.id)"
|
||||||
|
@change="toggleCategory(category.id)" /><FancyCheck :checked="form.disabled_tool_categories.includes(category.id)" /></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
114
static/src/components/personalization/tabs/VoiceTab.vue
Normal file
114
static/src/components/personalization/tabs/VoiceTab.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'VoiceTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
deleteVoiceModel,
|
||||||
|
downloadVoiceModel,
|
||||||
|
voiceDownloadMsg,
|
||||||
|
voiceDownloadPercent,
|
||||||
|
voiceDownloading,
|
||||||
|
voiceModelPartial,
|
||||||
|
voiceModelReady
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page">
|
||||||
|
<div class="settings-section" style="margin-bottom: 16px">
|
||||||
|
<p
|
||||||
|
class="settings-section-desc"
|
||||||
|
style="
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
"
|
||||||
|
>{{ $t('personalization.voiceModelIntro') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-row">
|
||||||
|
<span class="settings-row-copy">
|
||||||
|
<span class="settings-row-title">{{ $t('personalization.voiceModelTitle') }}</span>
|
||||||
|
<span class="settings-row-desc">
|
||||||
|
<template v-if="voiceModelReady">{{ $t('personalization.voiceModelReady') }}</template>
|
||||||
|
<template v-else-if="voiceDownloading"
|
||||||
|
>{{ $t('personalization.voiceModelDownloading', { percent: voiceDownloadPercent, msg: voiceDownloadMsg }) }}</template
|
||||||
|
>
|
||||||
|
<template v-else-if="voiceModelPartial"
|
||||||
|
>{{ $t('personalization.voiceModelPartial') }}</template
|
||||||
|
>
|
||||||
|
<template v-else>{{ $t('personalization.voiceModelNotDownloaded') }}</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div style="display: flex; gap: 6px">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
:disabled="voiceDownloading"
|
||||||
|
@click="downloadVoiceModel"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
voiceDownloading
|
||||||
|
? $t('personalization.voiceDownloadingBtn')
|
||||||
|
: voiceModelReady
|
||||||
|
? $t('personalization.voiceRedownload')
|
||||||
|
: $t('personalization.voiceDownloadModel')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="voiceModelReady || voiceModelPartial"
|
||||||
|
type="button"
|
||||||
|
class="settings-secondary-button"
|
||||||
|
style="color: var(--state-error)"
|
||||||
|
:disabled="voiceDownloading"
|
||||||
|
@click="deleteVoiceModel"
|
||||||
|
>
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="voiceDownloading"
|
||||||
|
class="voice-download-bar"
|
||||||
|
style="margin: 8px 0 0"
|
||||||
|
>
|
||||||
|
<div class="voice-download-track">
|
||||||
|
<div
|
||||||
|
class="voice-download-fill"
|
||||||
|
:style="{ width: voiceDownloadPercent + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span style="font-size: 11px; color: var(--text-secondary); margin-top: 4px"
|
||||||
|
>{{ voiceDownloadPercent }}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.voice-download-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.voice-download-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--surface-muted);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.voice-download-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
317
static/src/components/personalization/tabs/WorkspaceTab.vue
Normal file
317
static/src/components/personalization/tabs/WorkspaceTab.vue
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue';
|
||||||
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'WorkspaceTab' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入。
|
||||||
|
* 解构出的名称与主文件 script 顶层绑定一致,模板可直接引用。
|
||||||
|
*/
|
||||||
|
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
|
||||||
|
const {
|
||||||
|
activeDropdown,
|
||||||
|
floatingMenuStyle,
|
||||||
|
form,
|
||||||
|
goalTokenLimitEnabled,
|
||||||
|
permissionModeLabel,
|
||||||
|
personalization,
|
||||||
|
selectDefaultPermissionMode,
|
||||||
|
selectDefaultWorkMode,
|
||||||
|
selectVersioningBackupMode,
|
||||||
|
toggleDropdown,
|
||||||
|
toggleGoalTokenLimit,
|
||||||
|
versioningBackupModeLabel,
|
||||||
|
workModeLabel,
|
||||||
|
permissionModeOptions,
|
||||||
|
workModeOptions,
|
||||||
|
// 清单外顶层绑定(模板引用):activeTheme 已在 drawerContext 中提供;
|
||||||
|
// clampGoalMaxTurns / clampGoalMaxTokens 为顶层 const 但当前不在 drawerContext 内(详见 report.md)
|
||||||
|
activeTheme,
|
||||||
|
clampGoalMaxTurns,
|
||||||
|
clampGoalMaxTokens
|
||||||
|
} = ctx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-page" data-tutorial="personal-page-workspace">
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.defaultPermissionTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.defaultPermissionDesc') }}</span></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'permission' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('permission')"
|
||||||
|
>
|
||||||
|
{{ permissionModeLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in permissionModeOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.default_permission_mode === option.id }"
|
||||||
|
@click="selectDefaultPermissionMode(option.id)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.defaultRunModeTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.workRunModeDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'work-mode' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('work-mode')"
|
||||||
|
>
|
||||||
|
{{ workModeLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="option in workModeOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.default_work_mode === option.id }"
|
||||||
|
@click="selectDefaultWorkMode(option.id)"
|
||||||
|
>
|
||||||
|
<strong>{{ $t(option.labelKey) }}</strong
|
||||||
|
><span>{{ $t(option.descKey) }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.agentsMdInjectTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.agentsMdInjectDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.agents_md_auto_inject"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'agents_md_auto_inject',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.agents_md_auto_inject" /></label>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.claudeMdInjectTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.claudeMdInjectDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.claude_md_auto_inject"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'claude_md_auto_inject',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.claude_md_auto_inject" /></label>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.agentsSkillsScanTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.agentsSkillsScanDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.agents_skills_scan_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'agents_skills_scan_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.agents_skills_scan_enabled" /></label>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.modifyHistoryTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.modifyHistoryDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.modify_history_enabled"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'modify_history_enabled',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.modify_history_enabled" /></label>
|
||||||
|
|
||||||
|
<div class="settings-section-divider">
|
||||||
|
<span class="settings-section-divider__label">{{ $t('personalization.versionControlDivider') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.versioningByDefaultTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.versioningByDefaultDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.versioning_enabled_by_default"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'versioning_enabled_by_default',
|
||||||
|
value: $event.target.checked
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.versioning_enabled_by_default" /></label>
|
||||||
|
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.backupModeTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.backupModeDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="settings-select-wrap"
|
||||||
|
:class="{ open: activeDropdown === 'versioning-backup-mode' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-select-button"
|
||||||
|
@click="toggleDropdown('versioning-backup-mode')"
|
||||||
|
>
|
||||||
|
{{ versioningBackupModeLabel }}
|
||||||
|
<span class="select-chevron" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
|
||||||
|
:style="activeDropdown ? floatingMenuStyle : undefined"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.versioning_backup_mode === 'shallow' }"
|
||||||
|
@click="selectVersioningBackupMode('shallow')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.shallowBackup') }}</strong><span>{{ $t('personalization.shallowBackupDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="settings-menu-option"
|
||||||
|
:class="{ selected: form.versioning_backup_mode === 'full' }"
|
||||||
|
@click="selectVersioningBackupMode('full')"
|
||||||
|
>
|
||||||
|
<strong>{{ $t('personalization.fullBackup') }}</strong><span>{{ $t('personalization.fullBackupDesc') }}</span
|
||||||
|
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section-divider">
|
||||||
|
<span class="settings-section-divider__label">{{ $t('personalization.goalModeDivider') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.goalReviewActiveTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.goalReviewActiveDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.goal_review_mode === 'active'"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'goal_review_mode',
|
||||||
|
value: $event.target.checked ? 'active' : 'readonly'
|
||||||
|
})
|
||||||
|
" /><FancyCheck :checked="form.goal_review_mode === 'active'" /></label>
|
||||||
|
|
||||||
|
<div class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.goalMaxTurnsTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.goalMaxTurnsDesc') }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
:value="form.goal_max_turns"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'goal_max_turns',
|
||||||
|
value: clampGoalMaxTurns($event.target.value)
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="settings-toggle-row"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.goalTokenLimitTitle') }}</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>{{ $t('personalization.goalTokenLimitDesc') }}</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="goalTokenLimitEnabled"
|
||||||
|
@change="toggleGoalTokenLimit($event.target.checked)" /><FancyCheck :checked="goalTokenLimitEnabled" /></label>
|
||||||
|
|
||||||
|
<div v-if="goalTokenLimitEnabled" class="settings-select-row">
|
||||||
|
<span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">{{ $t('personalization.goalTokenLimitValueTitle') }}</span
|
||||||
|
><span class="settings-row-desc">{{ $t('personalization.goalTokenLimitValueDesc') }}</span></span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="settings-number-input"
|
||||||
|
min="1000"
|
||||||
|
step="1000"
|
||||||
|
:value="form.goal_max_tokens || 100000"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'goal_max_tokens',
|
||||||
|
value: clampGoalMaxTokens($event.target.value)
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@ -28,6 +28,8 @@ export default {
|
|||||||
tool: 'Tool',
|
tool: 'Tool',
|
||||||
versioning: 'Versioning',
|
versioning: 'Versioning',
|
||||||
unset: 'Not set',
|
unset: 'Not set',
|
||||||
|
settings: 'Settings',
|
||||||
|
back: 'Back',
|
||||||
settingsLanguage: 'Language',
|
settingsLanguage: 'Language',
|
||||||
settingsLanguageDesc: 'Choose the display language',
|
settingsLanguageDesc: 'Choose the display language',
|
||||||
|
|
||||||
|
|||||||
@ -39,10 +39,9 @@ export default {
|
|||||||
toolRunCommand: 'Run command {command}',
|
toolRunCommand: 'Run command {command}',
|
||||||
toolEditFile: 'Edit {path}',
|
toolEditFile: 'Edit {path}',
|
||||||
toolReadMedia: 'Read media file {path}',
|
toolReadMedia: 'Read media file {path}',
|
||||||
toolStateDone: 'Done',
|
|
||||||
toolStateFailed: 'Failed',
|
// —— Sub-agent detail panel: context compression notice ——
|
||||||
toolStateCalling: 'Calling...',
|
contextCompressed: 'Context compressed (round {round})',
|
||||||
toolStateProgress: 'In progress',
|
|
||||||
|
|
||||||
// —— File preview panel ——
|
// —— File preview panel ——
|
||||||
resizeWidthHint: 'Drag to resize width',
|
resizeWidthHint: 'Drag to resize width',
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export default {
|
|||||||
tool: '工具',
|
tool: '工具',
|
||||||
versioning: '版本管理',
|
versioning: '版本管理',
|
||||||
unset: '未设置',
|
unset: '未设置',
|
||||||
|
settings: '设置',
|
||||||
|
back: '返回',
|
||||||
settingsLanguage: '界面语言',
|
settingsLanguage: '界面语言',
|
||||||
settingsLanguageDesc: '选择界面显示语言',
|
settingsLanguageDesc: '选择界面显示语言',
|
||||||
|
|
||||||
|
|||||||
@ -46,10 +46,9 @@ export default {
|
|||||||
toolRunCommand: '运行命令 {command}',
|
toolRunCommand: '运行命令 {command}',
|
||||||
toolEditFile: '编辑 {path}',
|
toolEditFile: '编辑 {path}',
|
||||||
toolReadMedia: '读取媒体文件 {path}',
|
toolReadMedia: '读取媒体文件 {path}',
|
||||||
toolStateDone: '完成',
|
|
||||||
toolStateFailed: '失败',
|
// —— 子智能体详情面板:上下文压缩提示 ——
|
||||||
toolStateCalling: '调用中',
|
contextCompressed: '上下文已压缩(第 {round} 轮)',
|
||||||
toolStateProgress: '进行中',
|
|
||||||
|
|
||||||
// —— 文件预览面板(FilePreviewPanel) ——
|
// —— 文件预览面板(FilePreviewPanel) ——
|
||||||
resizeWidthHint: '拖拽调整宽度',
|
resizeWidthHint: '拖拽调整宽度',
|
||||||
|
|||||||
@ -15,14 +15,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.personal-page-card {
|
.personal-page-card {
|
||||||
width: min(96vw, 1020px);
|
width: min(648px, calc(100vw - 40px));
|
||||||
height: calc(100vh - 40px);
|
height: min(646px, calc(100vh - 40px));
|
||||||
max-height: 760px;
|
height: min(646px, calc(100dvh - 40px));
|
||||||
background: var(--theme-surface-card);
|
background: var(--theme-surface-soft);
|
||||||
border-radius: 24px;
|
border-radius: 14px;
|
||||||
border: 1px solid var(--theme-control-border);
|
border: 1px solid var(--theme-control-border);
|
||||||
box-shadow: var(--theme-shadow-strong);
|
box-shadow: none;
|
||||||
padding: 40px;
|
padding: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -329,11 +329,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手机端完整适配 */
|
/* 手机端完整适配(与个人空间组件移动端断点 760px 统一) */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 760px) {
|
||||||
.personal-page-overlay {
|
.personal-page-overlay {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
/* 盖过移动端顶栏 mobile-panel-trigger (1500) 及其下拉菜单 (1600) */
|
||||||
|
z-index: 1700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.personal-page-card {
|
.personal-page-card {
|
||||||
@ -341,8 +343,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 16px;
|
|
||||||
padding-top: 60px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -2555,6 +2555,21 @@
|
|||||||
transform: translateY(-26px);
|
transform: translateY(-26px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 触屏(≤760px)不做内容切换过渡:mode=out-in 的离场动画在手机上观感拖沓。
|
||||||
|
必须放在原规则之后(同特异性后者胜)。
|
||||||
|
离场元素直接 display:none:窄屏下 transition 组件为默认同时模式,
|
||||||
|
旧内容瞬间消失、新内容瞬间出现,不会先闪旧内容。 */
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.personal-page-vertical-enter-active,
|
||||||
|
.personal-page-vertical-leave-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-page-vertical-leave-active {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.personal-page-fade-enter-active,
|
.personal-page-fade-enter-active,
|
||||||
.personal-page-fade-leave-active {
|
.personal-page-fade-leave-active {
|
||||||
transition:
|
transition:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user