优化: 设置域体验完善
- 校验失败红框定位字段,保存反馈改持久化成功后 - 数值越界钳制,双提示队列化不再重叠 - 搜索防抖跳分类,托盘气泡文案进多语言
This commit is contained in:
+26
-14
@@ -29,16 +29,20 @@
|
||||
</template>
|
||||
</AppLayout>
|
||||
|
||||
<!-- 全局错误 toast(根级,主窗口与分离窗口共用)-->
|
||||
<transition name="toast">
|
||||
<div v-if="errorMsg" class="df-toast">⚠ {{ errorMsg }}</div>
|
||||
</transition>
|
||||
<!-- 走查 P3-16:全局 toast 统一收进同一纵向堆叠容器。原错误 toast 与托盘提示气泡
|
||||
各自 fixed 同位(bottom:24px 居中),两者同现时互相压盖、后弹者盖住先弹者。 -->
|
||||
<div class="df-toast-stack">
|
||||
<!-- 全局错误 toast(根级,主窗口与分离窗口共用)-->
|
||||
<transition name="toast">
|
||||
<div v-if="errorMsg" class="df-toast">⚠ {{ errorMsg }}</div>
|
||||
</transition>
|
||||
|
||||
<!-- 关窗后台运行气泡(主窗关闭被拦截且仍有会话运行时,后端 emit tray-hide-notice):
|
||||
告知会话转入后台继续生成、托盘图标可随时唤起,复用 df-toast 形态 -->
|
||||
<transition name="toast">
|
||||
<div v-if="trayNoticeMsg" class="df-toast df-toast--info">{{ trayNoticeMsg }}</div>
|
||||
</transition>
|
||||
<!-- 关窗后台运行气泡(主窗关闭被拦截且仍有会话运行时,后端 emit tray-hide-notice):
|
||||
告知会话转入后台继续生成、托盘图标可随时唤起,复用 df-toast 形态 -->
|
||||
<transition name="toast">
|
||||
<div v-if="trayNoticeMsg" class="df-toast df-toast--info">{{ trayNoticeMsg }}</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 审批待办浮层(应用内右上角,窗口聚焦/可见时作快速提示)。
|
||||
仅在主窗口与 ai-detached 窗口挂载(file-explorer / approval-popup 自身不挂)。
|
||||
@@ -63,6 +67,7 @@ import { useKnowledgeStore } from './stores/knowledge'
|
||||
import { useProjectStore } from './stores/project'
|
||||
import { useAppSettingsStore } from './stores/appSettings'
|
||||
import i18n from './i18n'
|
||||
import { t as ti18n } from './i18n/i18n-helpers'
|
||||
import AiChat from './components/AiChat.vue'
|
||||
import ApprovalOverlay from './components/ai/ApprovalOverlay.vue'
|
||||
import ApprovalDialog from './components/project/ApprovalDialog.vue'
|
||||
@@ -227,7 +232,8 @@ onMounted(async () => {
|
||||
_unlistenTrayHideNotice = await listen<{ running: number }>('tray-hide-notice', (e) => {
|
||||
const running = e.payload?.running ?? 0
|
||||
if (running <= 0) return
|
||||
trayNoticeMsg.value = `${running} 个会话将继续在后台运行,托盘图标可随时唤起`
|
||||
// 走查 P3-15:气泡文案硬编码中文 → 入 i18n(运行数经命名参数注入)
|
||||
trayNoticeMsg.value = ti18n('common.trayNoticeRunning', { n: running })
|
||||
if (trayNoticeTimer) clearTimeout(trayNoticeTimer)
|
||||
trayNoticeTimer = setTimeout(() => { trayNoticeMsg.value = '' }, 3500)
|
||||
})
|
||||
@@ -313,16 +319,22 @@ onUnmounted(() => {
|
||||
.page-enter-from { opacity: 0; transform: translateY(6px); }
|
||||
.page-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
|
||||
/* ═══ 全局错误 toast ═══ */
|
||||
.df-toast {
|
||||
/* ═══ 全局 toast 纵向堆叠容器(走查 P3-16)═══
|
||||
容器独占 fixed 定位,内部 flex 纵排 + 底对齐,多条 toast 依次向上堆叠不再同位互盖。
|
||||
子项保持 .df-toast 原形态(定宽自适应),定位字段从子项上移到容器。 */
|
||||
.df-toast-stack {
|
||||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||
z-index: 9999; max-width: 90vw; pointer-events: none;
|
||||
}
|
||||
.df-toast {
|
||||
background: var(--df-danger); color: #fff;
|
||||
padding: 10px 18px; border-radius: var(--df-radius-sm);
|
||||
font-size: 13px; z-index: 9999; max-width: 90vw;
|
||||
font-size: 13px; max-width: 90vw;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
.toast-enter-active, .toast-leave-active { transition: all 0.25s var(--df-ease); }
|
||||
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, 10px); }
|
||||
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(10px); }
|
||||
/* 后台运行提示:同 df-toast 形态,信息语义用 accent 底色与错误红区分 */
|
||||
.df-toast--info { background: var(--df-accent); }
|
||||
</style>
|
||||
|
||||
@@ -289,6 +289,10 @@ const projectsStore = useProjectStore()
|
||||
// 滚动边沿/跟随意图重置随 MessageList 迁移,由子组件自管。
|
||||
watch(() => store.state.activeConversationId, () => {
|
||||
if (editingMsgId.value) cancelEdit()
|
||||
// UX-P3: 队列编辑态(editingQueueIdx/editingQueueText)绑定旧会话队列在 activeQueue
|
||||
// 视图内的下标,切会话后 activeQueue 已换成新会话的集合,残留 idx/text 会错挂到
|
||||
// 新会话同下标队列项上,随消息编辑态一并重置。
|
||||
if (editingQueueIdx.value !== null) cancelEditQueued()
|
||||
})
|
||||
|
||||
|
||||
@@ -331,7 +335,14 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
||||
// Ctrl+R: 重新生成末条回复(替代浏览器刷新,Tauri 下 preventDefault 可靠)
|
||||
if (ctrl && (e.key === 'r' || e.key === 'R')) {
|
||||
e.preventDefault()
|
||||
if (!store.state.streaming) store.regenerate()
|
||||
// UX-P3: 末条非 assistant 等场景 regenerate() 会 reject(regenerateUnavailable),原样裸调
|
||||
// 成 unhandled rejection 用户无感;对齐 MessageList.handleRetry 的 catch + toast。
|
||||
if (!store.state.streaming) {
|
||||
store.regenerate().catch((e: unknown) => {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
showToast(t('aiChat.regenerateFailed', { msg }), 'error')
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
// Ctrl+L: 清空当前对话(带 confirm;生成中跳过避免删流式中消息)
|
||||
@@ -510,9 +521,14 @@ async function handleSendQueuedNow(idx: number) {
|
||||
* cancelQueued 内部经 activeQueueRealIndex 做过滤视图下标映射,越界 no-op;
|
||||
* 项携带技能时按名找回 SkillInfo 回挂 chip,找不到(技能已卸载)只回填文本,
|
||||
* 与队列原地编辑只保 text 同一取舍。 */
|
||||
function backfillQueued(idx: number) {
|
||||
async function backfillQueued(idx: number) {
|
||||
const item = activeQueue.value[idx]
|
||||
if (!item) return
|
||||
// UX-P3: 输入框已有未发送草稿(文本/图片/技能 chip 任一非空)时回填会静默覆盖,
|
||||
// 先 confirm 确认;取消则直接返回,队列项原样保留(尚未 cancelQueued)。
|
||||
if (chatInputRef.value?.hasUnsentInput()) {
|
||||
if (!await confirmDialog(t('aiChat.backfillOverwriteConfirm'))) return
|
||||
}
|
||||
const skill = item.skill
|
||||
? store.state.skills.find(s => s.name === item.skill) ?? null
|
||||
: null
|
||||
|
||||
@@ -6,20 +6,21 @@
|
||||
</div>
|
||||
<div class="general-settings">
|
||||
<SettingRow ref="rowTheme" :label="$t('settings.labelTheme')" :desc="$t('settings.descTheme')">
|
||||
<select v-model="settings.theme" class="setting-select" @change="markSaved('theme')">
|
||||
<!-- 走查 P3-11:不再 @change 即显「已保存」,反馈移到 persist 完成后(防假成功) -->
|
||||
<select v-model="settings.theme" class="setting-select">
|
||||
<option value="dark">{{ $t('settings.themeDark') }}</option>
|
||||
<option value="light">{{ $t('settings.themeLight') }}</option>
|
||||
<option value="system">{{ $t('settings.themeSystem') }}</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowLanguage" :label="$t('settings.labelLanguage')" :desc="$t('settings.descLanguage')">
|
||||
<select v-model="settings.language" class="setting-select" @change="markSaved('language')">
|
||||
<select v-model="settings.language" class="setting-select">
|
||||
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
||||
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowAiLanguage" :label="$t('settings.labelAiLanguage')" :desc="$t('settings.descAiLanguage')">
|
||||
<select v-model="settings.aiLanguage" class="setting-select" @change="markSaved('aiLanguage')">
|
||||
<select v-model="settings.aiLanguage" class="setting-select">
|
||||
<option value="auto">{{ $t('settings.aiLanguageAuto') }}</option>
|
||||
<option value="zh-CN">{{ $t('settings.aiLanguageZh') }}</option>
|
||||
<option value="en">{{ $t('settings.aiLanguageEn') }}</option>
|
||||
@@ -27,13 +28,13 @@
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowToken" :label="$t('settings.labelShowTokenUsage')" :desc="$t('settings.descShowTokenUsage')">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="settings.showTokenUsage" @change="markSaved('token')" />
|
||||
<input type="checkbox" v-model="settings.showTokenUsage" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowStreamingMd" :label="$t('settings.labelStreamingMd')" :desc="$t('settings.descStreamingMd')">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="settings.streamingMd" @change="markSaved('streamingMd')" />
|
||||
<input type="checkbox" v-model="settings.streamingMd" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</SettingRow>
|
||||
@@ -91,32 +92,42 @@ const rowLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowLanguage
|
||||
const rowAiLanguage = useTemplateRef<InstanceType<typeof SettingRow>>('rowAiLanguage')
|
||||
const rowToken = useTemplateRef<InstanceType<typeof SettingRow>>('rowToken')
|
||||
const rowStreamingMd = useTemplateRef<InstanceType<typeof SettingRow>>('rowStreamingMd')
|
||||
function markSaved(key: 'theme' | 'language' | 'aiLanguage' | 'token' | 'streamingMd') {
|
||||
const map = { theme: rowTheme.value, language: rowLanguage.value, aiLanguage: rowAiLanguage.value, token: rowToken.value, streamingMd: rowStreamingMd.value }
|
||||
map[key]?.markSaved()
|
||||
|
||||
// 走查 P3-11 假成功族:原 @change 立即 markSaved,先于持久化完成,失败无人知晓。
|
||||
// 改为 set 完成后才点亮「已保存」,失败点亮行内红字。注:appSettings.set 当前对落库
|
||||
// 失败仅 console 内吞(stores/appSettings.ts 降级内存态设计),catch 实际不会触发;
|
||||
// 待 store 层把失败上抛后本处失败通道即自动生效(登记项,见批次报告)。
|
||||
async function persist(key: string, value: unknown, row: { value: { markSaved: () => void; markFailed: () => void } | null }) {
|
||||
try {
|
||||
await appSettings.set(key, value)
|
||||
row.value?.markSaved()
|
||||
} catch (e) {
|
||||
console.error(`保存设置 ${key} 失败:`, e)
|
||||
row.value?.markFailed()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => settings.theme, (val) => {
|
||||
applyTheme(val)
|
||||
void appSettings.set('df-theme', val)
|
||||
void persist('df-theme', val, rowTheme)
|
||||
})
|
||||
|
||||
watch(() => settings.aiLanguage, (val) => {
|
||||
void appSettings.set('df-ai-language', val)
|
||||
void persist('df-ai-language', val, rowAiLanguage)
|
||||
})
|
||||
|
||||
watch(() => settings.showTokenUsage, (val) => {
|
||||
void appSettings.set('df-show-token-usage', val)
|
||||
void persist('df-show-token-usage', val, rowToken)
|
||||
})
|
||||
|
||||
watch(() => settings.streamingMd, (val) => {
|
||||
void appSettings.set('df-ai-streaming-md', val)
|
||||
void persist('df-ai-streaming-md', val, rowStreamingMd)
|
||||
})
|
||||
|
||||
watch(() => settings.language, (val) => {
|
||||
void appSettings.set('df-language', val)
|
||||
// 同步 i18n 实例,让界面立即跟随语言切换
|
||||
locale.value = val as 'zh-CN' | 'en'
|
||||
void persist('df-language', val, rowLanguage)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
next-round
|
||||
:label="$t('settings.labelGlobalConcurrency')"
|
||||
:desc="$t('settings.descGlobalConcurrency')">
|
||||
<input type="number" class="setting-number" min="1" style="width:80px"
|
||||
<!-- 走查 P3-14:max=64 前端 sanity 上界(原生 min/max 只约束步进按钮,越界拦截在 JS clamp) -->
|
||||
<input type="number" class="setting-number" min="1" max="64" style="width:80px"
|
||||
v-model.number="settings.llmGlobalConcurrency"
|
||||
@change="onConcurrencyChange" />
|
||||
</SettingRow>
|
||||
@@ -19,7 +20,7 @@
|
||||
next-round
|
||||
:label="$t('settings.labelPerConvConcurrency')"
|
||||
:desc="$t('settings.descPerConvConcurrency')">
|
||||
<input type="number" class="setting-number" min="1" style="width:80px"
|
||||
<input type="number" class="setting-number" min="1" max="64" style="width:80px"
|
||||
v-model.number="settings.llmPerConvConcurrency"
|
||||
@change="onConcurrencyChange" />
|
||||
</SettingRow>
|
||||
@@ -29,7 +30,8 @@
|
||||
:label="$t('settings.labelAgentMaxIterations')"
|
||||
:desc="$t('settings.descAgentMaxIterations')">
|
||||
<label class="unlimited-toggle">
|
||||
<input type="checkbox" v-model="unlimited" @change="markSaved('iter')" />
|
||||
<!-- 走查 P3-11:勾选反馈不再即时显,由 syncAgentMaxIterations 的 IPC 成功回调点亮 -->
|
||||
<input type="checkbox" v-model="unlimited" />
|
||||
<span>{{ $t('settings.labelUnlimited') }}</span>
|
||||
</label>
|
||||
<input type="number" class="setting-number" min="1" max="50" style="width:80px"
|
||||
@@ -52,6 +54,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, computed, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { aiApi } from '@/api'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import SettingRow from './SettingRow.vue'
|
||||
@@ -59,10 +62,16 @@ import SettingRow from './SettingRow.vue'
|
||||
// ============================================================
|
||||
// 性能域 — 本地设置(localStorage):LLM 并发 / 单对话并发 / Agent 轮次 / 流式重试
|
||||
// 拆自 GeneralPanel,本 Section 仅持有性能相关 key + 各自 debounce IPC 同步
|
||||
// 阶段5:SettingRow 封装行布局 + nextRound badge(后端快照锁定边界,下次对话生效);
|
||||
// 4 项变更后调 markSaved 显"已保存"
|
||||
// 阶段5:SettingRow 封装行布局 + nextRound badge(后端快照锁定边界,下次对话生效)
|
||||
// 走查 P3-11:「已保存」改为 IPC 成功回调后点亮,失败点亮行内红字(防假成功)
|
||||
// ============================================================
|
||||
const appSettings = useAppSettingsStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 走查 P3-14:并发越界钳制需要上抛 warning toast,由壳(Settings.vue)的 showToast 消费
|
||||
const emit = defineEmits<{
|
||||
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void
|
||||
}>()
|
||||
|
||||
const settings = reactive({
|
||||
llmGlobalConcurrency: appSettings.get<number>('df-llm-global-concurrency', 3),
|
||||
@@ -73,7 +82,7 @@ const settings = reactive({
|
||||
agentMaxRetries: appSettings.get<number>('df-ai-agent-max-retries', 3),
|
||||
})
|
||||
|
||||
// 即时生效反馈:SettingRow refs
|
||||
// 即时生效反馈:SettingRow refs(成功/失败两态,走查 P3-11)
|
||||
const rowGlobalConcurrency = useTemplateRef<InstanceType<typeof SettingRow>>('rowGlobalConcurrency')
|
||||
const rowPerConvConcurrency = useTemplateRef<InstanceType<typeof SettingRow>>('rowPerConvConcurrency')
|
||||
const rowMaxIterations = useTemplateRef<InstanceType<typeof SettingRow>>('rowMaxIterations')
|
||||
@@ -82,50 +91,73 @@ function markSaved(key: 'global' | 'perConv' | 'iter' | 'retry') {
|
||||
const map = { global: rowGlobalConcurrency.value, perConv: rowPerConvConcurrency.value, iter: rowMaxIterations.value, retry: rowMaxRetries.value }
|
||||
map[key]?.markSaved()
|
||||
}
|
||||
function markFailed(key: 'global' | 'perConv' | 'iter' | 'retry') {
|
||||
const map = { global: rowGlobalConcurrency.value, perConv: rowPerConvConcurrency.value, iter: rowMaxIterations.value, retry: rowMaxRetries.value }
|
||||
map[key]?.markFailed()
|
||||
}
|
||||
|
||||
// SQLite 持久化(watch 触发):落库失败点亮行内红字。注:appSettings.set 当前对落库失败
|
||||
// 仅 console 内吞(降级内存态设计,stores/appSettings.ts),catch 是 store 上抛错误后的
|
||||
// 失败通道,今日实际不触发(登记项见批次报告);IPC 同步失败通道在下方 timer 内,是真实信号。
|
||||
watch(() => settings.llmGlobalConcurrency, (v) => {
|
||||
void appSettings.set('df-llm-global-concurrency', v)
|
||||
appSettings.set('df-llm-global-concurrency', v).catch(() => markFailed('global'))
|
||||
})
|
||||
|
||||
watch(() => settings.llmPerConvConcurrency, (v) => {
|
||||
void appSettings.set('df-llm-per-conv-concurrency', v)
|
||||
appSettings.set('df-llm-per-conv-concurrency', v).catch(() => markFailed('perConv'))
|
||||
})
|
||||
|
||||
watch(() => settings.agentMaxIterations, (v) => {
|
||||
void appSettings.set('df-ai-agent-max-iterations', v)
|
||||
appSettings.set('df-ai-agent-max-iterations', v).catch(() => markFailed('iter'))
|
||||
})
|
||||
|
||||
watch(() => settings.agentMaxRetries, (v) => {
|
||||
void appSettings.set('df-ai-agent-max-retries', v)
|
||||
appSettings.set('df-ai-agent-max-retries', v).catch(() => markFailed('retry'))
|
||||
})
|
||||
|
||||
// onMounted 期间调 sync 不应触发"已保存"反馈(那是启动恢复,非用户改动)
|
||||
let _userInteracting = false
|
||||
|
||||
// 走查 P3-14:并发上界 64 为前端 sanity 值(后端 ai_set_concurrency_config 仅 max(1) 无上界,
|
||||
// 超大并发会让限流器形同虚设、放大上游限流/超时风险);越界时静默修正 + toast 告知修正结果
|
||||
const MAX_CONCURRENCY = 64
|
||||
|
||||
/** 非法输入(NaN/清空)回退默认值,合法值 clamp [1,64] */
|
||||
function clampConcurrency(v: number | undefined, fallback: number): number {
|
||||
const n = typeof v === 'number' && Number.isFinite(v) ? Math.trunc(v) : fallback
|
||||
return Math.min(MAX_CONCURRENCY, Math.max(1, n))
|
||||
}
|
||||
|
||||
// 同步 LLM 并发上限到后端(debounce 300ms,防快速连续调整时频繁 IPC)
|
||||
// 持久化已由上方 watch 写 appSettings(SQLite) 完成;此处只负责同步内存限流器
|
||||
let _concurrencyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function syncConcurrencyConfig() {
|
||||
// clamp 到合理区间,防越界输入(缓存被篡改 / 手动输超限)致限流失效;
|
||||
// 修正后值同步回 v-model(input 显示) + watch 持久化
|
||||
// 下限 1,无上限;约束单对话并发不超过全局并发
|
||||
settings.llmGlobalConcurrency = Math.max(1, settings.llmGlobalConcurrency || 3)
|
||||
settings.llmPerConvConcurrency = Math.max(1, settings.llmPerConvConcurrency || 2)
|
||||
if (settings.llmPerConvConcurrency > settings.llmGlobalConcurrency) {
|
||||
settings.llmPerConvConcurrency = settings.llmGlobalConcurrency
|
||||
// 下限 1,上界 64(前端 sanity);约束单对话并发不超过全局并发
|
||||
let clamped = false
|
||||
const g = clampConcurrency(settings.llmGlobalConcurrency, 3)
|
||||
if (g !== settings.llmGlobalConcurrency) clamped = true
|
||||
settings.llmGlobalConcurrency = g
|
||||
let p = clampConcurrency(settings.llmPerConvConcurrency, 2)
|
||||
if (p > g) { p = g; clamped = true }
|
||||
settings.llmPerConvConcurrency = p
|
||||
if (clamped && _userInteracting) {
|
||||
emit('toast', t('settings.toastConcurrencyClamped', { max: MAX_CONCURRENCY }), 'warning')
|
||||
}
|
||||
if (_concurrencyTimer) clearTimeout(_concurrencyTimer)
|
||||
// 走查 P3-11:interactive 在排程时捕获(debounce 回调执行时 _userInteracting 已复位 false)
|
||||
const interactive = _userInteracting
|
||||
_concurrencyTimer = setTimeout(async () => {
|
||||
try {
|
||||
await aiApi.setConcurrencyConfig(settings.llmGlobalConcurrency, settings.llmPerConvConcurrency)
|
||||
await aiApi.setConcurrencyConfig(g, p)
|
||||
// IPC 成功才显「已保存」,失败显行内红字(原实现先显成功、失败仅 console = 假成功)
|
||||
if (interactive) { markSaved('global'); markSaved('perConv') }
|
||||
} catch (e) {
|
||||
console.error('同步 LLM 并发配置失败:', e)
|
||||
if (interactive) { markFailed('global'); markFailed('perConv') }
|
||||
}
|
||||
}, 300)
|
||||
if (_userInteracting) {
|
||||
markSaved('global')
|
||||
markSaved('perConv')
|
||||
}
|
||||
}
|
||||
|
||||
// wrapper:并发变更(@change 用户触发)同时反馈两项(global/perConv 任一变动都同步两项)
|
||||
@@ -158,28 +190,32 @@ function syncAgentMaxIterations() {
|
||||
settings.agentMaxIterations = Math.min(50, Math.max(1, settings.agentMaxIterations || 10))
|
||||
}
|
||||
if (_agentIterTimer) clearTimeout(_agentIterTimer)
|
||||
const interactive = _userInteracting
|
||||
_agentIterTimer = setTimeout(async () => {
|
||||
try {
|
||||
await aiApi.setAgentMaxIterations(settings.agentMaxIterations)
|
||||
if (interactive) markSaved('iter')
|
||||
} catch (e) {
|
||||
console.error('同步 Agent 最大轮次失败:', e)
|
||||
if (interactive) markFailed('iter')
|
||||
}
|
||||
}, 300)
|
||||
if (_userInteracting) {
|
||||
markSaved('iter')
|
||||
}
|
||||
}
|
||||
|
||||
// 「不限」勾选:agentMaxIterations===0 即不限(后端 0→usize::MAX,loop 靠 stop/收敛/审批退出)
|
||||
const unlimited = computed({
|
||||
get: () => settings.agentMaxIterations === 0,
|
||||
set(v: boolean) {
|
||||
// 走查 P3-11:勾选属用户交互,标记 interactive 让「已保存」等 IPC 成功后再显
|
||||
// (原实现在模板 @change 即时 markSaved,先于持久化 = 假成功)
|
||||
_userInteracting = true
|
||||
if (v) {
|
||||
settings.agentMaxIterations = 0
|
||||
} else if (settings.agentMaxIterations === 0) {
|
||||
settings.agentMaxIterations = 10
|
||||
}
|
||||
syncAgentMaxIterations()
|
||||
_userInteracting = false
|
||||
},
|
||||
})
|
||||
|
||||
@@ -191,16 +227,16 @@ function syncAgentMaxRetries() {
|
||||
// clamp 0-10,防越界
|
||||
settings.agentMaxRetries = Math.min(10, Math.max(0, settings.agentMaxRetries ?? 3))
|
||||
if (_agentRetryTimer) clearTimeout(_agentRetryTimer)
|
||||
const interactive = _userInteracting
|
||||
_agentRetryTimer = setTimeout(async () => {
|
||||
try {
|
||||
await aiApi.setAgentMaxRetries(settings.agentMaxRetries)
|
||||
if (interactive) markSaved('retry')
|
||||
} catch (e) {
|
||||
console.error('同步流式重试次数失败:', e)
|
||||
if (interactive) markFailed('retry')
|
||||
}
|
||||
}, 300)
|
||||
if (_userInteracting) {
|
||||
markSaved('retry')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -67,7 +67,10 @@
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelName') }}</label>
|
||||
<input v-model="providerForm.name" class="setting-input" :placeholder="$t('settings.phProviderName')" />
|
||||
<!-- 走查 P3-10:校验失败定位字段——红框 + @input 清错(示范级,仅本表单) -->
|
||||
<input ref="inputName" v-model="providerForm.name" class="setting-input"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.name }"
|
||||
:placeholder="$t('settings.phProviderName')" @input="fieldErrors.name = false" />
|
||||
</div>
|
||||
<!-- 项 D:厂商预设下拉 — 选中自动填充 providerType/baseUrl/defaultModel(用户可改覆盖)。
|
||||
新建态独立"快捷选择";编辑态回填后切预设仍可覆盖(等价快捷填充),不阻塞手改。
|
||||
@@ -88,12 +91,16 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelBaseUrl') }}</label>
|
||||
<input v-model="providerForm.baseUrl" class="setting-input" :placeholder="$t('settings.phBaseUrl')" />
|
||||
<input ref="inputBaseUrl" v-model="providerForm.baseUrl" class="setting-input"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.baseUrl }"
|
||||
:placeholder="$t('settings.phBaseUrl')" @input="fieldErrors.baseUrl = false" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelApiKey') }}</label>
|
||||
<!-- 走查 P3-9:type 恒 password 无法核对已粘贴密钥,加显隐切换 -->
|
||||
<input v-model="providerForm.apiKey" class="setting-input" :type="showApiKey ? 'text' : 'password'" :placeholder="$t('settings.phApiKey')" />
|
||||
<input ref="inputApiKey" v-model="providerForm.apiKey" class="setting-input" :type="showApiKey ? 'text' : 'password'"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.apiKey }"
|
||||
:placeholder="$t('settings.phApiKey')" @input="fieldErrors.apiKey = false" />
|
||||
<button type="button" class="btn-link" :title="$t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow')"
|
||||
@click="showApiKey = !showApiKey">
|
||||
{{ $t(showApiKey ? 'settings.apiKeyHide' : 'settings.apiKeyShow') }}
|
||||
@@ -104,7 +111,9 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelDefaultModel') }}</label>
|
||||
<input v-model="providerForm.defaultModel" class="setting-input" :placeholder="$t('settings.phDefaultModel')" />
|
||||
<input ref="inputDefaultModel" v-model="providerForm.defaultModel" class="setting-input"
|
||||
:class="{ 'setting-input--invalid': fieldErrors.defaultModel }"
|
||||
:placeholder="$t('settings.phDefaultModel')" @input="fieldErrors.defaultModel = false" />
|
||||
<!-- 方向 A(轻量):defaultModel 仅作"建议起点"——预设 model id 易过时(DeepSeek chat 停用 /
|
||||
Gemini 2.5→3.5 等),提示用户点「测试连接」拉真实模型列表覆盖,降低踩过时默认值的困惑。 -->
|
||||
<div class="form-field-hint">{{ $t('settings.defaultModelHint') }}</div>
|
||||
@@ -112,6 +121,7 @@
|
||||
<!-- 供应商级采样温度覆盖:部分受限模型(如 Kimi 系)仅接受 temperature=1,留空走默认 0.7 -->
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelSamplingTemp') }}</label>
|
||||
<!-- 走查 P3-14:原生 min/max 只约束步进按钮,手输越界由 saveProvider clamp + toast 提示 -->
|
||||
<input type="number" v-model.number="providerForm.temperature" class="setting-input"
|
||||
:placeholder="$t('settings.phSamplingTemp')" step="0.1" min="0" max="2" />
|
||||
</div>
|
||||
@@ -192,7 +202,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { reactive, ref, computed, onMounted, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { aiApi } from '@/api'
|
||||
import type { AiProviderConfig, ModelConfig, Modality, Capability, ProbeSource } from '@/api/types'
|
||||
@@ -228,6 +238,25 @@ const aiProviders = ref<AiProviderConfig[]>([])
|
||||
// 密钥显隐切换(走查 P3-9):默认隐藏;重开表单时复位,防上一家 provider 的显钥状态延续
|
||||
const showApiKey = ref(false)
|
||||
|
||||
// 走查 P3-10:校验失败只 toast 不定位字段——补字段级红框 + 滚动聚焦首个缺失字段。
|
||||
// 仅保存必填四项(name/baseUrl/apiKey/defaultModel)做示范级实现;红框在对应输入框
|
||||
// @input 时即清(用户开始修正就不该继续红着)。
|
||||
const fieldErrors = reactive({ name: false, baseUrl: false, apiKey: false, defaultModel: false })
|
||||
const inputName = useTemplateRef<HTMLInputElement>('inputName')
|
||||
const inputBaseUrl = useTemplateRef<HTMLInputElement>('inputBaseUrl')
|
||||
const inputApiKey = useTemplateRef<HTMLInputElement>('inputApiKey')
|
||||
const inputDefaultModel = useTemplateRef<HTMLInputElement>('inputDefaultModel')
|
||||
|
||||
/** 聚焦首个红框字段(focus 默认行为即滚动到该元素,无需额外 scrollIntoView) */
|
||||
function focusFirstError() {
|
||||
const el = fieldErrors.name ? inputName.value
|
||||
: fieldErrors.baseUrl ? inputBaseUrl.value
|
||||
: fieldErrors.apiKey ? inputApiKey.value
|
||||
: fieldErrors.defaultModel ? inputDefaultModel.value
|
||||
: null
|
||||
el?.focus()
|
||||
}
|
||||
|
||||
function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) }
|
||||
|
||||
const providerForm = reactive({
|
||||
@@ -433,6 +462,7 @@ function openProviderForm(p?: AiProviderConfig) {
|
||||
}
|
||||
providerForm.fetching = false
|
||||
providerForm.saveError = null // 重开表单清旧错误横幅
|
||||
fieldErrors.name = fieldErrors.baseUrl = fieldErrors.apiKey = fieldErrors.defaultModel = false // 重开表单清字段红框
|
||||
showApiKey.value = false // 密钥默认隐藏,不延续上次表单的显钥状态(防窥)
|
||||
providerForm.visible = true
|
||||
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
|
||||
@@ -547,10 +577,28 @@ function classifySaveError(raw: string): { title: string; detail: string } {
|
||||
async function saveProvider() {
|
||||
// apiKey:新建必填;编辑可留空(不改,FR-S1 后端保留原 DB 值)
|
||||
const needKey = !providerForm.editId
|
||||
if (!providerForm.name || !providerForm.baseUrl || (needKey && !providerForm.apiKey) || !providerForm.defaultModel) {
|
||||
// 走查 P3-10:逐字段判定缺失,红框标出并滚动聚焦首个缺失字段(toast 保留,双通道反馈)
|
||||
fieldErrors.name = !providerForm.name
|
||||
fieldErrors.baseUrl = !providerForm.baseUrl
|
||||
fieldErrors.apiKey = needKey && !providerForm.apiKey
|
||||
fieldErrors.defaultModel = !providerForm.defaultModel
|
||||
if (fieldErrors.name || fieldErrors.baseUrl || fieldErrors.apiKey || fieldErrors.defaultModel) {
|
||||
emit('toast', t('settings.toastSaveIncomplete'), 'warning')
|
||||
focusFirstError()
|
||||
return
|
||||
}
|
||||
// 走查 P3-14:采样温度钳制 [0,2](原生 min/max 只约束步进按钮,手输 99 照存)。
|
||||
// 越界有限值 → clamp + toast 告知修正值;NaN/清空残留 → 归一 null(走默认 0.7,同步清显示)
|
||||
let temperatureOverride: number | undefined
|
||||
if (typeof providerForm.temperature === 'number' && Number.isFinite(providerForm.temperature)) {
|
||||
if (providerForm.temperature < 0 || providerForm.temperature > 2) {
|
||||
providerForm.temperature = Math.min(2, Math.max(0, providerForm.temperature))
|
||||
emit('toast', t('settings.toastTempClamped', { v: providerForm.temperature }), 'warning')
|
||||
}
|
||||
temperatureOverride = providerForm.temperature
|
||||
} else {
|
||||
providerForm.temperature = null
|
||||
}
|
||||
providerForm.saving = true
|
||||
try {
|
||||
await aiApi.saveProvider({
|
||||
@@ -561,11 +609,7 @@ async function saveProvider() {
|
||||
apiKey: providerForm.apiKey,
|
||||
defaultModel: providerForm.defaultModel,
|
||||
modelConfigs: providerForm.models,
|
||||
// 采样温度非空且为有效数字才带(清空输入 v-model.number 可能残留 ''/NaN,不过滤会污染后端 config)
|
||||
temperatureOverride:
|
||||
typeof providerForm.temperature === 'number' && Number.isFinite(providerForm.temperature)
|
||||
? providerForm.temperature
|
||||
: undefined,
|
||||
temperatureOverride,
|
||||
})
|
||||
// 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
||||
providerForm.saveError = null
|
||||
@@ -788,6 +832,9 @@ onMounted(loadProviders)
|
||||
/* 输入/下拉在 form-field 内 flex 填满(基础样式走全局 settings.css .setting-input/.setting-select) */
|
||||
.form-field .setting-input,
|
||||
.form-field .setting-select { flex: 1; min-width: 0; }
|
||||
/* 走查 P3-10:校验缺失字段红框(@input 即清,用户开始修正就不再红) */
|
||||
.setting-input--invalid { border-color: var(--df-danger) !important; }
|
||||
.setting-input--invalid:focus { border-color: var(--df-danger) !important; box-shadow: 0 0 0 1px var(--df-danger); }
|
||||
.setting-input--sm { flex: 0 0 auto; min-width: 0; width: 70px; padding: 4px 8px; font-size: 12px; }
|
||||
|
||||
/* Toggle 小号变体(本面板负载均衡池用;.toggle 基础走全局) */
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
<div class="setting-control">
|
||||
<slot />
|
||||
<Transition name="saved-fade">
|
||||
<span v-if="savedVisible" class="saved-hint">{{ $t('settings.savedHint') }}</span>
|
||||
<!-- 走查 P3-11 假成功族:持久化/IPC 有成功/失败两态,反馈位同理补失败红字,
|
||||
防止「已保存」承诺落空后用户无从察觉 -->
|
||||
<span v-if="hintState !== 'none'" class="saved-hint" :class="{ 'saved-hint--fail': hintState === 'failed' }">
|
||||
{{ hintState === 'saved' ? $t('settings.savedHint') : $t('settings.saveFailedHint') }}
|
||||
</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,9 +25,10 @@
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
// ============================================================
|
||||
// 统一设置行:label/desc 入 props,控件走默认 slot,变更后父级调 markSaved() 显反馈。
|
||||
// 统一设置行:label/desc 入 props,控件走默认 slot,变更后父级调 markSaved() 显反馈,
|
||||
// 持久化失败时调 markFailed() 显红字(走查 P3-11:只报成功不报失败 = 半个反馈位)。
|
||||
// nextRound=true 显"下次对话生效" badge(性能域并发/轮次/重试白名单项)。
|
||||
// savedVisible 用 shallowRef(布尔翻转无深响应需求);timer ref 持有供连续触发时清旧。
|
||||
// hintState 用 shallowRef(字符串翻转无深响应需求);timer ref 持有供连续触发时清旧。
|
||||
// ============================================================
|
||||
defineProps<{
|
||||
/** 设置项标题 */
|
||||
@@ -34,30 +39,41 @@ defineProps<{
|
||||
nextRound?: boolean
|
||||
}>()
|
||||
|
||||
const savedVisible = shallowRef(false)
|
||||
const hintState = shallowRef<'none' | 'saved' | 'failed'>('none')
|
||||
const timer = shallowRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
/** 显"✓已保存"行内提示,2s 后消失。连续触发清旧 timer 防提前消失。 */
|
||||
function markSaved() {
|
||||
savedVisible.value = true
|
||||
/** 点亮行内反馈提示,2s 后消失。连续触发清旧 timer 防提前消失。 */
|
||||
function flashHint(state: 'saved' | 'failed') {
|
||||
hintState.value = state
|
||||
if (timer.value) clearTimeout(timer.value)
|
||||
timer.value = setTimeout(() => {
|
||||
savedVisible.value = false
|
||||
hintState.value = 'none'
|
||||
timer.value = null
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
defineExpose({ markSaved })
|
||||
/** 显"✓已保存"行内提示(持久化/IPC 成功后才调,防假成功) */
|
||||
function markSaved() {
|
||||
flashHint('saved')
|
||||
}
|
||||
|
||||
/** 显"✗ 保存失败"行内提示(持久化/IPC 失败路径调用) */
|
||||
function markFailed() {
|
||||
flashHint('failed')
|
||||
}
|
||||
|
||||
defineExpose({ markSaved, markFailed })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 反馈位"✓已保存" — 行内紧贴控件右侧,2s 淡出 */
|
||||
/* 反馈位"✓已保存" — 行内紧贴控件右侧,2s 淡出;失败态红字区分 */
|
||||
.saved-hint {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--df-accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.saved-hint--fail { color: var(--df-danger); }
|
||||
/* badge"下次对话生效" — label 右侧小标 */
|
||||
.next-round-badge {
|
||||
display: inline-block;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<!-- ═══ 设置左导航(master-detail 左栏)═══
|
||||
阶段3 UX 重构:6 类导航(外观/AI模型/知识库/性能/安全/高级)+ emoji 图标。
|
||||
阶段4 UX 重构:顶部搜索框 → 匹配 label/desc → 高亮匹配类 + 选中首匹配类
|
||||
阶段4 UX 重构:顶部搜索框 → 匹配 label/desc → 即时高亮匹配类 + 防抖后跳首匹配类
|
||||
+ emit scroll-target(父 Settings.vue 滚动到对应 setting-row)。
|
||||
走查 P3-17:跳转防抖 350ms + 仅命中才跳,避免每键入强制切分类打断输入。
|
||||
activeCategory 受控(双向),v-model 由 Settings.vue 绑定 + appSettings 持久化。
|
||||
底部留导入导出占位(阶段6 接入)。 -->
|
||||
<nav class="settings-nav" :class="{ 'is-horizontal': horizontal }">
|
||||
@@ -52,7 +53,7 @@
|
||||
// advanced 高级(autoExecuteMode/logLevel + 连接)
|
||||
// ============================================================
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SETTINGS_INDEX, type SettingsCategory, type SettingIndexEntry } from './searchIndex'
|
||||
|
||||
@@ -126,26 +127,47 @@ function isMatchedCategory(cat: SettingsCategory): boolean {
|
||||
return matchedCategories.value.includes(cat)
|
||||
}
|
||||
|
||||
/** 关键词变化 → 选中首匹配类 + 上抛首匹配项 key(触发右侧滚动定位)。
|
||||
* 空关键词时不改 activeCategory(用户清空搜索应留在当前类),也不上抛。 */
|
||||
/** 关键词变化 → (防抖后)选中首匹配类 + 上抛首匹配项 key(触发右侧滚动定位)。
|
||||
* 空关键词时不改 activeCategory(用户清空搜索应留在当前类),也不上抛。
|
||||
* 走查 P3-17:原实现每敲一键立即切分类+滚动,浏览 A 类打字被反复拽到 B 类打断输入。
|
||||
* 改为 350ms 防抖——连续键入只更新左侧命中高亮(即时,computed),停顿后才跳转定位;
|
||||
* 且仅当有命中时才跳(无命中保持原地,不打断)。 */
|
||||
const JUMP_DEBOUNCE_MS = 350
|
||||
let _jumpTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function cancelPendingJump() {
|
||||
if (_jumpTimer) {
|
||||
clearTimeout(_jumpTimer)
|
||||
_jumpTimer = null
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => searchKeyword.value,
|
||||
(kw) => {
|
||||
const trimmed = kw.trim()
|
||||
if (!trimmed) return
|
||||
const first = matchedEntries.value[0]
|
||||
if (!first) return
|
||||
if (props.modelValue !== first.category) {
|
||||
emit('update:modelValue', first.category)
|
||||
}
|
||||
// 分类切换后需等右侧 Section 挂载,父组件 watch scroll-target 时自带 nextTick
|
||||
emit('scroll-target', first.key)
|
||||
() => {
|
||||
cancelPendingJump()
|
||||
if (!searchKeyword.value.trim()) return
|
||||
_jumpTimer = setTimeout(() => {
|
||||
_jumpTimer = null
|
||||
// 防抖窗口后关键词可能又变了,重读当前值;无命中不跳(仅命中时跳)
|
||||
const trimmed = searchKeyword.value.trim()
|
||||
if (!trimmed) return
|
||||
const first = matchedEntries.value[0]
|
||||
if (!first) return
|
||||
if (props.modelValue !== first.category) {
|
||||
emit('update:modelValue', first.category)
|
||||
}
|
||||
// 分类切换后需等右侧 Section 挂载,父组件 watch scroll-target 时自带 nextTick
|
||||
emit('scroll-target', first.key)
|
||||
}, JUMP_DEBOUNCE_MS)
|
||||
},
|
||||
)
|
||||
|
||||
// 卸载清防抖 timer,防 unmount 后旧 timer fire 时 emit 到已销毁组件
|
||||
onUnmounted(cancelPendingJump)
|
||||
|
||||
defineExpose({
|
||||
/** 清空搜索框(父组件切分类时可调用,但当前保留搜索状态便于用户继续搜) */
|
||||
clearSearch() {
|
||||
cancelPendingJump()
|
||||
searchKeyword.value = ''
|
||||
},
|
||||
})
|
||||
|
||||
@@ -89,6 +89,19 @@ export default {
|
||||
truncated: 'truncated',
|
||||
failed: 'failed',
|
||||
meta: '{rounds} rounds · {tokens} tokens',
|
||||
// S3: finished card is clickable to open the read-only trace (hover hint)
|
||||
viewHint: 'Click to view execution trace',
|
||||
},
|
||||
// S3 sub-agent trace overlay (opened from the finished card, read-only simple render,
|
||||
// discarded on close)
|
||||
agentTrace: {
|
||||
title: 'Sub-agent Trace',
|
||||
empty: 'No messages recorded',
|
||||
loadFail: 'Failed to load sub-agent trace',
|
||||
roleUser: 'User',
|
||||
roleAssistant: 'Assistant',
|
||||
roleTool: 'Tool result',
|
||||
roleSystem: 'System',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -292,5 +292,10 @@ export default {
|
||||
sendErrRateLimit: 'Rate limit reached, please retry later (error: {msg})',
|
||||
sendErrQuota: 'AI quota or balance insufficient, check your provider account (error: {msg})',
|
||||
sendErrNetwork: 'Network error or timeout, check your connection and retry (error: {msg})',
|
||||
|
||||
// ── UX-P3 chat polish additions ──
|
||||
editEmptyBlocked: 'Edit content is empty, submission cancelled',
|
||||
skillNoMatchQuery: 'No skill matches "{q}", send blocked',
|
||||
backfillOverwriteConfirm: 'The input box has an unsent draft; backfilling will overwrite it. Continue?',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,5 +24,7 @@ export default {
|
||||
perPage: 'Per page',
|
||||
total: '{n} total',
|
||||
},
|
||||
// Close-to-tray notice bubble (App.vue consumes the tray-hide-notice event; moved from hardcoded Chinese)
|
||||
trayNoticeRunning: '{n} conversation(s) keep running in the background — the tray icon brings the window back anytime',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,7 +17,15 @@ export default {
|
||||
|
||||
// ===== Instant feedback (phase 5 SettingRow) =====
|
||||
savedHint: '✓ Saved',
|
||||
// False-success family: red inline feedback for persist/IPC failure
|
||||
saveFailedHint: '✗ Save failed',
|
||||
nextRoundBadge: 'Next message',
|
||||
// Out-of-range clamp notices (concurrency 1-64 + per-conv ≤ global; temperature reports the corrected value)
|
||||
toastConcurrencyClamped: 'Value out of range, corrected automatically (concurrency 1-{max}; per-conversation capped at global)',
|
||||
toastTempClamped: 'Sampling temperature must be within 0-2, corrected to {v}',
|
||||
// Prompt caching toggle save feedback (previously silent)
|
||||
toastAnthropicCacheSaved: 'Prompt caching setting saved',
|
||||
toastAnthropicCacheFail: 'Failed to save the prompt caching setting',
|
||||
|
||||
// ===== Connection mgmt (phase 7: experimental badge, not wired to AI yet) =====
|
||||
experimentalBadge: 'Experimental · not wired to AI tools',
|
||||
|
||||
@@ -91,5 +91,7 @@ export default {
|
||||
descSaveFailed: 'Failed to save description: {msg}',
|
||||
saveDesc: 'Save',
|
||||
cancelEdit: 'Cancel',
|
||||
// S4 related conversations (reverse lookup of main sessions that ran task tools, click to jump)
|
||||
relatedConversations: 'Conversations',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -88,6 +88,18 @@ export default {
|
||||
truncated: '已截断',
|
||||
failed: '失败',
|
||||
meta: '{rounds} 轮 · {tokens} tokens',
|
||||
// S3:完成卡可点击查看只读轨迹(hover 提示)
|
||||
viewHint: '点击查看执行轨迹',
|
||||
},
|
||||
// S3 子代理轨迹浮层(完成卡点击展开,只读简版渲染,关即弃)
|
||||
agentTrace: {
|
||||
title: '子代理执行轨迹',
|
||||
empty: '暂无消息记录',
|
||||
loadFail: '拉取子代理轨迹失败',
|
||||
roleUser: '用户',
|
||||
roleAssistant: '助手',
|
||||
roleTool: '工具结果',
|
||||
roleSystem: '系统',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -293,5 +293,10 @@ export default {
|
||||
sendErrRateLimit: '发送频率超限,请稍后重试(原始错误:{msg})',
|
||||
sendErrQuota: 'AI 服务额度或余额不足,请检查 Provider 账户(原始错误:{msg})',
|
||||
sendErrNetwork: '网络异常或请求超时,请检查网络后重试(原始错误:{msg})',
|
||||
|
||||
// ── UX-P3 对话域打磨追加 ──
|
||||
editEmptyBlocked: '编辑内容为空,已取消提交',
|
||||
skillNoMatchQuery: '未找到技能「{q}」,已阻止发送',
|
||||
backfillOverwriteConfirm: '输入框已有未发送草稿,回填将覆盖,确定继续?',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,5 +24,7 @@ export default {
|
||||
perPage: '每页',
|
||||
total: '共 {n} 条',
|
||||
},
|
||||
// 关窗后台运行气泡(App.vue 消费 tray-hide-notice 事件;走查 P3-15 由硬编码中文迁入)
|
||||
trayNoticeRunning: '{n} 个会话将继续在后台运行,托盘图标可随时唤起',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,7 +17,15 @@ export default {
|
||||
|
||||
// ===== 即时生效反馈(阶段5 SettingRow) =====
|
||||
savedHint: '✓ 已保存',
|
||||
// 走查 P3-11 假成功族:持久化/IPC 失败的红字反馈位
|
||||
saveFailedHint: '✗ 保存失败',
|
||||
nextRoundBadge: '下次对话生效',
|
||||
// 走查 P3-14 越界钳制提示(并发 1-64 + 单对话不超全局;采样温度单独提示修正值)
|
||||
toastConcurrencyClamped: '输入超出有效范围,已自动修正(并发 1-{max},单对话不超过全局)',
|
||||
toastTempClamped: '采样温度须在 0-2 范围内,已修正为 {v}',
|
||||
// 走查 P3-11:Prompt 缓存开关保存反馈(原切换零反馈)
|
||||
toastAnthropicCacheSaved: 'Prompt 缓存设置已保存',
|
||||
toastAnthropicCacheFail: 'Prompt 缓存设置保存失败',
|
||||
|
||||
// ===== 连接管理(阶段7:实验功能 badge 明示暂未接入 AI) =====
|
||||
experimentalBadge: '实验功能·暂未接入 AI 工具',
|
||||
|
||||
@@ -91,5 +91,7 @@ export default {
|
||||
descSaveFailed: '保存描述失败: {msg}',
|
||||
saveDesc: '保存',
|
||||
cancelEdit: '取消',
|
||||
// S4 关联会话(审计表反查发起过任务工具的主会话,点击跳转)
|
||||
relatedConversations: '关联会话',
|
||||
},
|
||||
}
|
||||
|
||||
+20
-3
@@ -44,7 +44,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="anthropicCache"
|
||||
@change="anthropicCache = ($event.target as HTMLInputElement).checked"
|
||||
@change="onAnthropicCacheChange(($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
@@ -57,8 +57,11 @@
|
||||
:ai-providers="aiProviders"
|
||||
/>
|
||||
|
||||
<!-- 性能:global/per-conv concurrency/max-iterations/max-retries -->
|
||||
<PerformanceSection v-else-if="activeCategory === 'performance'" />
|
||||
<!-- 性能:global/per-conv concurrency/max-iterations/max-retries(@toast 接走查 P3-14 越界修正提示) -->
|
||||
<PerformanceSection
|
||||
v-else-if="activeCategory === 'performance'"
|
||||
@toast="showToast"
|
||||
/>
|
||||
|
||||
<!-- 安全:allowed_dirs -->
|
||||
<AllowedDirsPanel
|
||||
@@ -153,6 +156,20 @@ onBeforeRouteLeave(async () => {
|
||||
// 值落库为 JSON 布尔,后端 "true"/"1" 判定兼容)
|
||||
const anthropicCache = appSettings.useSetting<boolean>('df-ai-anthropic-cache', false)
|
||||
|
||||
// 走查 P3-11:原开关切换零反馈,用户无从得知是否生效。改为 set 完成后 toast 反馈成败。
|
||||
// 注:appSettings.set 当前落库失败仅 console 内吞(降级内存态),catch 是 store 上抛错误后
|
||||
// 的失败通道,今日实际不触发(登记项见批次报告),成功反馈先补齐。
|
||||
async function onAnthropicCacheChange(checked: boolean) {
|
||||
anthropicCache.value = checked
|
||||
try {
|
||||
await appSettings.set('df-ai-anthropic-cache', checked)
|
||||
showToast(ti18n('settings.toastAnthropicCacheSaved'), 'success')
|
||||
} catch (e) {
|
||||
console.error('保存 Prompt 缓存开关失败:', e)
|
||||
showToast(ti18n('settings.toastAnthropicCacheFail'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 窄屏响应式:监听窗口宽度,nav 折叠为横向 tab(避免依赖仅 CSS 的 :has,
|
||||
// 并让 SettingsNav 按横向模式渲染 list)
|
||||
const isNarrowScreen = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user