From 3e3c6f4d0fc8cc7fe36bd36e5ab2125b66a428b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sat, 3 Oct 2026 16:28:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E5=AE=8C=E5=96=84=E4=B8=8E=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 审批卡尊重滚动意图,等长切换回底,排队超时按钮修活 - 带图编辑提示不静默丢,删除目标回写,切换文案消歧 - 会话键盘可达+搜索态重命名,筛选计数口径对齐 - 子任务提示正名,看板加载态 - 主题跟随系统,表单脏态拦截,密钥显隐留空提示 --- .gitignore | 1 + src/components/AiChat.vue | 12 +- src/components/ToolCardList.vue | 39 ++++++- src/components/ai/ChatInput.vue | 9 ++ src/components/ai/ConversationSidebar.vue | 105 +++++++++++++++++- src/components/ai/MessageList.vue | 12 ++ src/components/ai/TopBar.vue | 7 +- src/components/settings/AppearanceSection.vue | 15 ++- src/components/settings/ProviderPanel.vue | 14 ++- src/components/task/TaskDrawer.vue | 3 +- src/i18n/en/ai.ts | 2 + src/i18n/en/aiChat.ts | 8 ++ src/i18n/en/settings.ts | 4 + src/i18n/en/taskDetail.ts | 2 + src/i18n/en/tasks.ts | 2 + src/i18n/zh-CN/ai.ts | 2 + src/i18n/zh-CN/aiChat.ts | 8 ++ src/i18n/zh-CN/settings.ts | 4 + src/i18n/zh-CN/taskDetail.ts | 2 + src/i18n/zh-CN/tasks.ts | 2 + src/views/Settings.vue | 24 +++- src/views/Tasks.vue | 29 ++++- 22 files changed, 294 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 2c2ab52..c8d2ae5 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ devflow-dev.db-wal # 微信小程序 appid(真实 appid 本地保留,不提交仓库) apps/df-miniapp/src/manifest.json # 微信小程序 appid(真实 appid 本地保留,不提交仓库) +temp/ diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue index 32bc0c1..acb6b29 100644 --- a/src/components/AiChat.vue +++ b/src/components/AiChat.vue @@ -413,9 +413,6 @@ const activeQueue = computed(() => store.state.queue.filter(q => q.conversationId === store.state.activeConversationId), ) -/** 队首排队是否已超 30s(仅看当前活跃会话队列) */ -const queueTimedOut = computed(() => store.isQueueTimedOut(store.state.activeConversationId)) - // ── V7 排队时钟:响应式 tick 驱动排队项已等待时长 ── // 原 queueWaitSeconds 在 computed 内直接取 Date.now(),非响应式依赖:纯等待期(无 store 变更) // 永不重算,秒数冻结;且只覆盖队首。改为有排队时起 1s interval 推进 nowTick,展示函数读 @@ -423,6 +420,15 @@ const queueTimedOut = computed(() => store.isQueueTimedOut(store.state.activeCon const nowTick = ref(Date.now()) let queueTickTimer: ReturnType | null = null +/** 队首排队是否已超 30s(仅看当前活跃会话队列)。 + * UX-P2-A1:isQueueTimedOut 内取 Date.now() 非响应式,纯等待期(无 store 变更)本 computed + * 永不重算,超时横幅/强制发送按钮延迟到下一次无关 store 变更才出现。显式读 nowTick + * (V7 排队时钟,有排队时 1s interval 推进)建立每秒依赖,超时判定随 tick 每秒重算。 */ +const queueTimedOut = computed(() => { + void nowTick.value + return store.isQueueTimedOut(store.state.activeConversationId) +}) + watch( () => activeQueue.value.length > 0, (waiting) => { diff --git a/src/components/ToolCardList.vue b/src/components/ToolCardList.vue index c006ebb..d027c40 100644 --- a/src/components/ToolCardList.vue +++ b/src/components/ToolCardList.vue @@ -65,6 +65,11 @@ const emit = defineEmits<{ * 调 store.sendMessage 触发 AI 装流式。仅 fetch_url 工具会发此事件。 */ 'install-obscura': [] + /** + * 审批卡到达但用户不在底部附近(上滚读历史中):不强拉滚动, + * 由父(MessageList)出「有新审批」轻提示,尊重用户阅读位置。 + */ + 'pending-out-of-view': [] }>() // 根元素 ref(scrollToFirstPending 查 DOM 用) @@ -306,6 +311,28 @@ let _unlistenPending: UnlistenFn | null = null // 论证 workflow 回归中优:防卸载竞态——await listen 解析前组件已卸载(HMR/快速切会话)时, // onBeforeUnmount 跑到 _unlistenPending 仍 null 跳过 unlisten → 孤儿监听器泄漏。disposed 标志兜底。 let _disposed = false + +/** 距滚动容器底部 <150px 视为"在底部附近",审批卡到达才跟随滚动(宽于 useMessageScroll 的 80px 在底判定) */ +const PENDING_FOLLOW_PX = 150 +/** 轻提示去重阈值:审批卡短时间连发多条时 5s 内只提示一次 */ +const PENDING_HINT_INTERVAL_MS = 5000 +// 模块级(跨实例共享):ai-pending-arrived 广播所有 ToolCardList 实例都会收到,去重须超组件实例 +let _lastPendingHintTs = 0 + +/** 滚动容器(.ai-messages,MessageList 持有)是否在底部附近 */ +function isContainerNearBottom(el: Element): boolean { + const he = el as HTMLElement + return he.scrollHeight - he.scrollTop - he.clientHeight < PENDING_FOLLOW_PX +} + +/** 用户不在底部时的轻提示:不拉滚,经父转发 toast(5s 去重防连发刷屏) */ +function notifyPendingOutOfView(): void { + const now = Date.now() + if (now - _lastPendingHintTs < PENDING_HINT_INTERVAL_MS) return + _lastPendingHintTs = now + emit('pending-out-of-view') +} + onMounted(async () => { const fn = await listen('ai-pending-arrived', () => { // BUG-260624-01 残留优化:本实例无 pending 卡时短路返回,避免大列表 N 个 ToolCardList 实例 @@ -313,7 +340,17 @@ onMounted(async () => { // 内 querySelector 返 null no-op 天然过滤(功能正确),此处显式 props 过滤把无 pending 实例挡在 // 进入函数前。含 pending 的实例再走 nextTick + scrollToFirstPending(展开折叠组 + scroll)。 if (!props.toolCalls.some(tc => tc.status === 'pending_approval')) return - nextTick(() => scrollToFirstPending()) + nextTick(() => { + // UX 走查-对话域 B1:尊重用户滚动意图。nextTick 等审批卡渲染完再测距底距离: + // 在底部附近(含本就贴底)才跟随滚到审批卡;用户上滚读历史时不强拉,改轻提示。 + // 容器找不到(极端:分离窗口 DOM 层级变化)按可滚处理,保留原展开+滚动兜底。 + const container = rootEl.value?.closest('.ai-messages') + if (!container || isContainerNearBottom(container)) { + scrollToFirstPending() + } else { + notifyPendingOutOfView() + } + }) }) // 解析时若已卸载(HMR/极速 mount/unmount),立即 unlisten 刚拿到的句柄,防孤儿监听器。 if (_disposed) { fn(); return } diff --git a/src/components/ai/ChatInput.vue b/src/components/ai/ChatInput.vue index 1ae349e..653cda5 100644 --- a/src/components/ai/ChatInput.vue +++ b/src/components/ai/ChatInput.vue @@ -894,6 +894,15 @@ async function sendExamplePrompt(i18nKey: string) { /** UX-09 进入编辑态:回填末条 user 内容到输入框,聚焦,光标置末 */ function startEditFocus(msg: AiMessage): void { + // UX 走查-对话域 B3:带图消息禁止进入编辑态。editMessage IPC(ai_chat_edit)只回填/替换 + // content 文本,不接受 parts——编辑重发后图片静默丢失(重载后图消失),无任何提示。 + // 后端契约未扩前,入口挡住并提示,杜绝静默丢数据。父已先置 editingMsgId 再调本方法, + // 故 emit cancel-edit 让父复位编辑态,状态保持一致。 + if (msg.parts?.some(p => p.type === 'image')) { + emit('cancel-edit') + emit('error', { msg: t('aiChat.editImageBlocked'), type: 'warning' }) + return + } inputText.value = msg.content nextTick(() => { inputEl.value?.focus() diff --git a/src/components/ai/ConversationSidebar.vue b/src/components/ai/ConversationSidebar.vue index ed9279d..5fd08fd 100644 --- a/src/components/ai/ConversationSidebar.vue +++ b/src/components/ai/ConversationSidebar.vue @@ -28,6 +28,7 @@ :placeholder="t('aiChat.searchPlaceholder')" :title="t('aiChat.searchShortcutHint')" type="text" + @keydown="onSearchKeydown" /> {{ t('aiChat.searchShortcutHint') }} @@ -44,10 +45,28 @@ 'ai-conv-item--archived': conv.archived, 'ai-conv-item--pinned': conv.pinned, }" + tabindex="0" @click="onSelectSearchResult(conv.id)" + @keydown="onItemKeydown($event, conv.id)" >
- + + + {{ conv.title || t('aiChat.newConversation') }} @@ -61,6 +80,10 @@
+
+
+ + +
{{ convToast.msg }}
+
@@ -243,6 +279,9 @@ import { useI18n } from 'vue-i18n' import { useAiStore } from '../../stores/ai' // V1 只读消费:跨会话待审批真相源(state.pendingApprovals 仅是活跃会话投影,后台会话挂起不进投影) import { convPendingApprovals } from '../../composables/ai/aiShared' +// UX走查 P2-7 只读订阅:rename/archive/pin 失败通知由 useAiConversations 发布,本组件弹就近 toast +import { convOpFailNotice } from '../../composables/ai/useAiConversations' +import { useToast } from '../../composables/useToast' import { formatRelative } from '../../utils/time' import type { AiConversationSummary } from '../../api/types' @@ -324,6 +363,27 @@ function onRenameKeydown(e: KeyboardEvent, convId: string) { } } +// UX走查 P3-1: 搜索框 Esc 清空(对齐 Tasks 搜索框行为)。同款 IME 守卫:组词中按 Esc 是 +// 取消组词,keyup 阶段 isComposing 已复位拦不住,故挂 keydown +function onSearchKeydown(e: KeyboardEvent) { + if (e.isComposing || e.keyCode === 229) return + if (e.key === 'Escape') { + e.preventDefault() + store.state.searchQuery = '' + } +} + +// UX走查 P2-6: 会话项键盘可达(tabindex=0)。Enter/Space 等价点击选中;仅焦点落在本项 div +// 自身时处理——焦点在内部按钮/重命名输入框时 target≠currentTarget,避免与控件自身语义双触发 +function onItemKeydown(e: KeyboardEvent, convId: string) { + if (e.isComposing || e.keyCode === 229) return + if (e.target !== e.currentTarget) return + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + beginSwitch(convId) + } +} + // ── 会话项「更多」操作菜单(置顶/归档/删除)── const actionsOpenId = ref(null) @@ -343,6 +403,20 @@ function onActionsOutsideClick(e: MouseEvent) { actionsOpenId.value = null } +// UX走查 P2-6: 操作菜单原仅外部点击可关,键盘用户开菜单后无 Esc 逃逸;IME 守卫防组词取消误关 +function onActionsEscClose(e: KeyboardEvent) { + if (!actionsOpenId.value) return + if (e.isComposing || e.keyCode === 229) return + if (e.key === 'Escape') actionsOpenId.value = null +} + +// UX走查 P2-7: rename/archive/pin 失败反馈改侧栏就近 toast(原以错误气泡推进聊天流,视线错位)。 +// 每次失败发布新对象,watch 即重触发;useToast 自动重置计时与卸载清理 +const { toast: convToast, showToast: showConvToast } = useToast() +watch(convOpFailNotice, (n) => { + if (n) showConvToast(t(`ai.${n.key}`, n.params ?? {}), 'error') +}) + // ── 会话切换过渡态(UX走查 2026-10-03 P1):点击即预高亮目标项(乐观),IPC 返回后随 // activeConversationId 落定接管高亮;失败/过期时 finally 清 pending,高亮自动回滚到原活跃项。 // 治:大历史会话 IPC 数百 ms 期间点击项不高亮、原高亮不动、无任何进行时反馈,快速连点像失灵。 @@ -511,12 +585,15 @@ function onResizeStart(e: MouseEvent) { onMounted(() => { // 点击外部关闭更多操作菜单 document.addEventListener('click', onActionsOutsideClick) + // 操作菜单 Esc 关闭(P2-6,与外部点击同生命周期) + document.addEventListener('keydown', onActionsEscClose) // V1:后台会话待审批变化兜底轮询(见 pendingCountByConv 注) pendingPollTimer = setInterval(() => { pendingTick.value++ }, 2500) }) onBeforeUnmount(() => { document.removeEventListener('click', onActionsOutsideClick) + document.removeEventListener('keydown', onActionsEscClose) // 释放待审批徽标轮询计时器 if (pendingPollTimer) { clearInterval(pendingPollTimer); pendingPollTimer = null } // 拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移) @@ -774,6 +851,11 @@ body.ai-sidebar-resizing * { .ai-conv-item:hover .ai-conv-item-actions { display: flex; } +/* UX走查 P2-6: 键盘 Tab 进会话项(项自身 tabindex=0,或焦点在项内按钮/输入框)时同样浮出 + 操作按钮——原仅 :hover 渲染,键盘用户永远够不到「...」菜单触发点 */ +.ai-conv-item:focus-within .ai-conv-item-actions { + display: flex; +} /* 菜单展开时强制 actions 可见(鼠标移到菜单上 item 不再 hover,防菜单消失) */ .ai-conv-item:has(.ai-conv-actions-menu) .ai-conv-item-actions { display: flex; @@ -853,6 +935,27 @@ body.ai-sidebar-resizing * { color: var(--df-text-dim); } +/* UX走查 P2-7: 会话操作失败就近 toast(仅 error 一型)。视觉对齐 AiChat .ai-toast, + 但 AiChat 版是 scoped 样式渗透不进来,故自持一份;侧栏窄,max-width 收 85% */ +.ai-conv-toast { + position: absolute; + top: 16px; + left: 50%; + transform: translateX(-50%); + padding: 8px 14px; + border-radius: var(--df-radius); + font-size: 12px; + z-index: 30; /* 高于操作菜单(20),菜单展开时失败反馈不被盖 */ + max-width: 85%; + overflow-wrap: anywhere; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + background: var(--df-danger-bg); + color: var(--df-danger); + border: 0.5px solid var(--df-danger); +} +.conv-toast-enter-active, .conv-toast-leave-active { transition: opacity 0.2s, transform 0.2s; } +.conv-toast-enter-from, .conv-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); } + /* 侧边栏滑入动画 */ .sidebar-slide-enter-active { transition: width 0.2s var(--df-ease), opacity 0.15s; } .sidebar-slide-leave-active { transition: width 0.15s var(--df-ease), opacity 0.1s; } diff --git a/src/components/ai/MessageList.vue b/src/components/ai/MessageList.vue index 811e68a..47047cd 100644 --- a/src/components/ai/MessageList.vue +++ b/src/components/ai/MessageList.vue @@ -326,6 +326,12 @@ function scrollToFirstPending(): void { } } +/** 审批卡到达但用户不在底部(上滚读历史中):不强拉滚动,出「有新审批」轻提示。 + * ToolCardList 侧已做 5s 去重,此处仅转发父 toast(单一出口)。 */ +function onPendingOutOfView(): void { + emit('toast', { msg: t('aiChat.newApprovalPending'), type: 'info' }) +} + /** * 移除 deep:true,改在 watch source 中计算指纹(消息数 + toolCalls 的 id/status), * 避免 deep 每帧全量深遍历长会话数组引起掉帧。流式 delta 只改 content 不影响指纹, @@ -1108,6 +1114,10 @@ watch(() => store.state.activeConversationId, () => { } // 切会话同步清流式块级 memo 缓存(_blockCache),防旧会话已缓存块被新会话命中。 resetStreamCache() + // UX 走查-对话域 B2:切会话显式回底。仅 watch messages.length 时,两个等长消息数的会话 + // 互切不触发任何滚动,scrollTop 残留旧会话位置(可能停在中段)。setFollowing(true)已声明 + // 跟随意图,此处补上实际滚动;nextTick 等新会话消息渲染出真实 scrollHeight 再滚。 + nextTick(() => scrollToBottom()) }) // 流式 rAF 清理:组件卸载时若仍有 pending rAF(streaming 中途切走/关窗),取消防泄漏。 @@ -1205,6 +1215,7 @@ defineExpose({ @approve="({ id, approved, decision }) => store.approveToolCall(id, approved, decision)" @batch-approve="(decision) => store.batchApprove(decision)" @install-obscura="onInstallObscura" + @pending-out-of-view="onPendingOutOfView" />
@@ -1459,6 +1470,7 @@ defineExpose({ @approve="({ id, approved, decision }) => store.approveToolCall(id, approved, decision)" @batch-approve="(decision) => store.batchApprove(decision)" @install-obscura="onInstallObscura" + @pending-out-of-view="onPendingOutOfView" /> diff --git a/src/components/ai/TopBar.vue b/src/components/ai/TopBar.vue index 2bd2e64..ae9ea4e 100644 --- a/src/components/ai/TopBar.vue +++ b/src/components/ai/TopBar.vue @@ -26,7 +26,7 @@ class="ai-provider-bar" :class="{ 'ai-provider-bar--switchable': store.state.providers.length > 1 }" v-if="store.state.providers.length > 0" - :title="store.state.providers.length > 1 ? $t('aiChat.clickToSwitchProvider') : ''" + :title="store.state.providers.length > 1 ? $t('aiChat.providerSwitchSetsDefault') : ''" @click="cycleProvider" > @@ -536,6 +536,11 @@ async function removeGoal(i: number) { if (next.length === 0) goalsExpanded.value = false // API 接受 string[] 或 GoalEntry[] await aiApi.updateConversationGoals(convId, next.map(g => g.text)) + // UX 走查-对话域 B4:删除后同步回写 store 的 conv.pinned_goals(对齐 AiCompleted 回写 + // useAiLifecycleEvents:126-129 的结构)。不回写则 loadGoals(:519)从 store 重读旧列表, + // 已删目标在下次 messages.length watch 触发刷新时"复活"。 + const conv = store.state.conversations.find(c => c.id === convId) + if (conv) conv.pinned_goals = next } // 循环切换 Provider(provider bar 点击) diff --git a/src/components/settings/AppearanceSection.vue b/src/components/settings/AppearanceSection.vue index 0da24dd..df87e52 100644 --- a/src/components/settings/AppearanceSection.vue +++ b/src/components/settings/AppearanceSection.vue @@ -42,7 +42,7 @@ diff --git a/src/components/settings/ProviderPanel.vue b/src/components/settings/ProviderPanel.vue index 0c58514..2457cb3 100644 --- a/src/components/settings/ProviderPanel.vue +++ b/src/components/settings/ProviderPanel.vue @@ -92,7 +92,15 @@
- + + + + +
{{ $t('settings.apiKeyKeepHint') }}
@@ -217,6 +225,9 @@ const props = defineProps<{ const aiProviders = ref([]) +// 密钥显隐切换(走查 P3-9):默认隐藏;重开表单时复位,防上一家 provider 的显钥状态延续 +const showApiKey = ref(false) + function errMsg(e: unknown): string { return e instanceof Error ? e.message : String(e) } const providerForm = reactive({ @@ -422,6 +433,7 @@ function openProviderForm(p?: AiProviderConfig) { } providerForm.fetching = false providerForm.saveError = null // 重开表单清旧错误横幅 + showApiKey.value = false // 密钥默认隐藏,不延续上次表单的显钥状态(防窥) providerForm.visible = true // L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。 providerForm.savedSnapshot = formBusinessSnapshot() diff --git a/src/components/task/TaskDrawer.vue b/src/components/task/TaskDrawer.vue index 3a36055..f7ab2ae 100644 --- a/src/components/task/TaskDrawer.vue +++ b/src/components/task/TaskDrawer.vue @@ -323,7 +323,8 @@ async function confirmSubtask() { subtaskTitle.value = '' subtaskDesc.value = '' await loadChildren() - showToast(t('tasks.batch.advanceResult', { n: 1 }), 'success') + // UX走查 P2-3:新建子任务成功此前误复用批量推进文案「已推进 N 个任务」,语义误导 + showToast(t('taskDetail.subtaskCreated'), 'success') } catch (e: any) { showToast(e?.toString() ?? t('common.unknownError'), 'error') } finally { diff --git a/src/i18n/en/ai.ts b/src/i18n/en/ai.ts index 8f8ec8a..a864957 100644 --- a/src/i18n/en/ai.ts +++ b/src/i18n/en/ai.ts @@ -43,6 +43,8 @@ export default { queueTimedOut: 'Queue timed out', approvalNotDelivered: 'Approval operation failed to reach the backend: {error}', queuedSendFailed: 'Failed to send queued message: {error}', + // UX-P2-A2: toast when queued messages get dropped by error/regenerate/edit (was silent) + queueCleared: 'Cleared {n} queued message(s)', approvalTimeout: '⚰ Approval timed out, automatically rejected: {toolName}', // conversation load/switch failure feedback (network jitter/IPC down → empty list/blank, was silent) loadConvFail: 'Failed to load conversations. Please check your network or backend connection.', diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts index d9ee3b5..95ae4f3 100644 --- a/src/i18n/en/aiChat.ts +++ b/src/i18n/en/aiChat.ts @@ -16,6 +16,8 @@ export default { unarchive: 'Unarchive', pinConversation: 'Pin', unpinConversation: 'Unpin', + // UX走查 P3-1/P2-6: rename entry in the "..." menu (search-mode parity + keyboard reach) + renameConversation: 'Rename', moreActions: 'More actions', archived: 'Archived', archivedCount: 'Archived ({n})', @@ -36,6 +38,8 @@ export default { maximize: 'Maximize', pendingApprovalCount: '{n} pending', pendingApprovalHint: 'Click to jump to the first pending approval', + // UX walkthrough B1: approval card arrived while user scrolled up; no forced scroll, hint only (5s dedup in ToolCardList) + newApprovalPending: 'New approval pending', restoreSidebar: 'Restore sidebar', sidebarResizeHint: 'Drag to resize sidebar', detachWindow: 'Detach window', @@ -60,6 +64,8 @@ export default { // ── Provider status ── clickToSwitchProvider: 'Click to switch provider', + // UX walkthrough B5: topbar switching shares the same IPC as Settings "set as default" (ai_set_provider rewrites is_default), label it honestly + providerSwitchSetsDefault: 'Click to switch provider (also sets it as the global default)', providerNotConfigured: 'No AI provider configured. Please add one in settings.', notConfigured: 'Not configured', @@ -164,6 +170,8 @@ export default { editMessage: 'Edit', editMessageFailed: 'Edit failed: {msg}', editNotLastUser: 'Only the last user message can be edited', + // UX walkthrough B3: editMessage IPC takes no parts; editing would silently drop images, block at entry + editImageBlocked: 'This message contains images and cannot be edited (regenerating would lose them)', placeholderEditing: 'Edit message then Enter to regenerate (Esc to cancel)', // ── Error bubble actions (UX-03 / ) ── diff --git a/src/i18n/en/settings.ts b/src/i18n/en/settings.ts index 1bbcf8b..5b9717a 100644 --- a/src/i18n/en/settings.ts +++ b/src/i18n/en/settings.ts @@ -54,6 +54,10 @@ export default { phProviderName: 'e.g. DeepSeek, GLM-4', phBaseUrl: 'https://api.deepseek.com', phApiKey: 'sk-...', + // UX-P2 walkthrough: API key show/hide toggle + "leave empty = keep" note + apiKeyShow: 'Show', + apiKeyHide: 'Hide', + apiKeyKeepHint: 'Leave empty while editing to keep the saved API key unchanged', phDefaultModel: 'deepseek-chat', // Direction A (lightweight): defaultModel is only a "suggested starting point"; // guide users to hit "Test connection" to pull the vendor's real model list and overwrite, diff --git a/src/i18n/en/taskDetail.ts b/src/i18n/en/taskDetail.ts index d192baa..8690dd1 100644 --- a/src/i18n/en/taskDetail.ts +++ b/src/i18n/en/taskDetail.ts @@ -76,6 +76,8 @@ export default { addSubtask: '+ Add Subtask', progressTitle: 'Progress', addSubtaskFailed: 'Failed to create subtask: {msg}', + // Subtask created (UX review P2-3): previously reused the batch-advance wording "Advanced N tasks" + subtaskCreated: 'Subtask created', // P2-9 subtask hint subtaskHint: 'This is a subtask of: {title}', // P2-7 subtask creation validation diff --git a/src/i18n/en/tasks.ts b/src/i18n/en/tasks.ts index 9f0fdff..6e07465 100644 --- a/src/i18n/en/tasks.ts +++ b/src/i18n/en/tasks.ts @@ -85,6 +85,8 @@ export default { emptyColumn: 'No tasks', dragHandle: 'Drag to move', parentNoDrag: 'Parent status is aggregated from subtasks and cannot be dragged', + // Illegal transition rejected on drag (UX review P2-5): toast the reason when the card bounces back + illegalMove: 'Cannot move: transition from "{from}" to "{to}" is not allowed', }, // New task modal diff --git a/src/i18n/zh-CN/ai.ts b/src/i18n/zh-CN/ai.ts index e119314..46c4e35 100644 --- a/src/i18n/zh-CN/ai.ts +++ b/src/i18n/zh-CN/ai.ts @@ -43,6 +43,8 @@ export default { queueTimedOut: '排队超时', approvalNotDelivered: '审批操作未送达后端:{error}', queuedSendFailed: '队列消息发送失败:{error}', + // UX-P2-A2:出错/重生成/编辑清排队消息的 toast 提示(原静默丢弃无任何反馈) + queueCleared: '已清 {n} 条排队消息', approvalTimeout: '⚰ 审批超时,已自动拒绝:{toolName}', // 对话加载/切换失败反馈(网络抖动/IPC 断开 → 列表空/切换空白,原静默无反馈) loadConvFail: '对话列表加载失败,请检查网络或后端连接', diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts index 8ada021..e0c5e3d 100644 --- a/src/i18n/zh-CN/aiChat.ts +++ b/src/i18n/zh-CN/aiChat.ts @@ -16,6 +16,8 @@ export default { unarchive: '取消归档', pinConversation: '置顶', unpinConversation: '取消置顶', + // UX走查 P3-1/P2-6: 会话「...」菜单重命名入口(搜索态无双击降级补齐 + 键盘可达的重命名路径) + renameConversation: '重命名', moreActions: '更多操作', archived: '已归档', archivedCount: '已归档 ({n})', @@ -36,6 +38,8 @@ export default { maximize: '最大化', pendingApprovalCount: '{n} 项待审批', pendingApprovalHint: '点击跳转到第一个待审批项', + // UX 走查 B1:审批卡到达但用户上滚读历史,不强拉滚动,只出轻提示(ToolCardList 5s 去重) + newApprovalPending: '有新的审批待处理', restoreSidebar: '还原侧栏', sidebarResizeHint: '拖拽调整侧栏宽度', detachWindow: '分离窗口', @@ -60,6 +64,8 @@ export default { // ── Provider 状态 ── clickToSwitchProvider: '点击切换 Provider', + // UX 走查 B5:顶栏切换与设置页「设为默认」同一条 IPC(ai_set_provider 互斥写 is_default),文案如实标注 + providerSwitchSetsDefault: '点击切换 Provider(切换即设为全局默认)', providerNotConfigured: '未配置 AI 提供商,请在设置中添加', notConfigured: '未配置', @@ -165,6 +171,8 @@ export default { editMessage: '编辑', editMessageFailed: '编辑失败:{msg}', editNotLastUser: '只能编辑最后一条用户消息', + // UX 走查 B3:editMessage IPC 不接 parts,编辑重发会静默丢图,入口禁止并提示 + editImageBlocked: '该消息包含图片,暂不支持编辑(重新生成会丢失图片)', placeholderEditing: '编辑消息后回车重新生成(Esc 取消)', // ── 错误气泡操作入口(UX-03 / ) ── diff --git a/src/i18n/zh-CN/settings.ts b/src/i18n/zh-CN/settings.ts index 79a676d..d26f2e0 100644 --- a/src/i18n/zh-CN/settings.ts +++ b/src/i18n/zh-CN/settings.ts @@ -54,6 +54,10 @@ export default { phProviderName: '如 DeepSeek、GLM-4', phBaseUrl: 'https://api.deepseek.com', phApiKey: 'sk-...', + // 走查 P3-9:密钥显隐切换 + 编辑态「留空=不改」说明 + apiKeyShow: '显示', + apiKeyHide: '隐藏', + apiKeyKeepHint: '编辑时留空表示沿用已保存的密钥,不会清空', phDefaultModel: 'deepseek-chat', // 方向 A(轻量):defaultModel 仅作"建议起点",引导用户点「测试连接」拉真实模型列表覆盖, // 避免预设 model id 过时(DeepSeek chat 停用 / Gemini 2.5→3.5 等)误导。 diff --git a/src/i18n/zh-CN/taskDetail.ts b/src/i18n/zh-CN/taskDetail.ts index 746037e..45596c2 100644 --- a/src/i18n/zh-CN/taskDetail.ts +++ b/src/i18n/zh-CN/taskDetail.ts @@ -76,6 +76,8 @@ export default { addSubtask: '+ 添加子任务', progressTitle: '完成进度', addSubtaskFailed: '创建子任务失败: {msg}', + // 子任务创建成功(UX走查 P2-3):此前误复用批量推进文案「已推进 N 个任务」 + subtaskCreated: '子任务已创建', // P2-9 子任务提示 subtaskHint: '该任务为子任务,隶属于:{title}', // P2-7 子任务创建校验 diff --git a/src/i18n/zh-CN/tasks.ts b/src/i18n/zh-CN/tasks.ts index 050cb48..d14207a 100644 --- a/src/i18n/zh-CN/tasks.ts +++ b/src/i18n/zh-CN/tasks.ts @@ -85,6 +85,8 @@ export default { emptyColumn: '暂无任务', dragHandle: '拖动以移动', parentNoDrag: '父任务状态由子任务聚合,不能直接拖动', + // 拖拽非法流转被拒(UX走查 P2-5):卡片弹回时 toast 说明原因,不再静默 + illegalMove: '无法移动:状态不允许从「{from}」流转到「{to}」', }, // 新建任务模态 diff --git a/src/views/Settings.vue b/src/views/Settings.vue index e70ba1d..62cb813 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -82,6 +82,7 @@