From 313459b8e42e3807512bf9cca553790437bed865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 16 Jun 2026 23:59:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20UX=E9=98=9F=E5=88=97?= =?UTF-8?q?=E4=B8=89=E4=BB=B6=E5=A5=97(=E6=89=93=E6=96=AD=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E7=BB=AD=E5=8F=91+=E9=98=9F=E5=88=97=E7=BC=96?= =?UTF-8?q?=E8=BE=91+=E7=AB=8B=E5=8D=B3=E5=8F=91=E9=80=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AiChat.vue | 73 +++++++++++++++++++++++++++++++-- src/composables/ai/useAiSend.ts | 38 ++++++++++++++++- src/i18n/en/aiChat.ts | 5 +++ src/i18n/zh-CN/aiChat.ts | 5 +++ 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue index 79f7af5..5afae9f 100644 --- a/src/components/AiChat.vue +++ b/src/components/AiChat.vue @@ -515,9 +515,29 @@
- /{{ item.skill }} - {{ item.text }} - + + +
@@ -1541,6 +1561,40 @@ async function handleForceSend() { await store.tryForceSend(confirmDialog) } +// ── UX-260616-06 队列消息编辑(决策只编 text)+ UX-260616-07 立即发送(决策 a 插队)── + +/** 当前正在编辑的队列项 index;null=非编辑态 */ +const editingQueueIdx = ref(null) +/** 编辑态输入框文本(回车写回 / ESC 取消还原) */ +const editingQueueText = ref('') + +/** 进入编辑态:记录 index + 快照当前 text */ +function startEditQueued(idx: number) { + const item = store.state.queue[idx] + if (!item) return + editingQueueIdx.value = idx + editingQueueText.value = item.text +} + +/** 取消编辑:还原非编辑态,不写回 */ +function cancelEditQueued() { + editingQueueIdx.value = null + editingQueueText.value = '' +} + +/** 保存编辑:回车/失焦触发,委托 store.editQueued 写回 text */ +function saveEditQueued() { + if (editingQueueIdx.value === null) return + store.editQueued(editingQueueIdx.value, editingQueueText.value) + editingQueueIdx.value = null + editingQueueText.value = '' +} + +/** UX-260616-07 立即发送:委托 store.sendQueuedNow(splice+stop+sendMessage) */ +async function handleSendQueuedNow(idx: number) { + await store.sendQueuedNow(idx) +} + // 用户是否已在底部附近(上滑查看历史时不强制拉回) function isNearBottom(): boolean { const el = messagesContainer.value @@ -2919,6 +2973,19 @@ body.ai-sidebar-resizing * { } .ai-queue-x { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0; } .ai-queue-x:hover { color: var(--df-danger); } +/* UX-260616-06 队列项编辑/立即发送按钮 + 编辑态 input */ +.ai-queue-btn { + background: none; border: none; color: var(--df-text-dim); cursor: pointer; + font-size: 11px; line-height: 1; padding: 0 4px; flex-shrink: 0; +} +.ai-queue-btn:hover { color: var(--df-accent); } +.ai-queue-edit-input { + flex: 1; min-width: 0; font-size: 12px; padding: 1px 4px; + background: var(--df-bg-card); color: var(--df-text); + border: 0.5px solid var(--df-accent); + border-radius: calc(var(--df-radius) - 2px); + outline: none; +} /* ── 技能 chip / 联想浮层 ── */ .ai-skill-chip { diff --git a/src/composables/ai/useAiSend.ts b/src/composables/ai/useAiSend.ts index bb00b04..cc58378 100644 --- a/src/composables/ai/useAiSend.ts +++ b/src/composables/ai/useAiSend.ts @@ -359,13 +359,47 @@ function clearQueue() { state.queue = [] } +/** + * UX-260616-06: 编辑队列项的 text(决策只编 text)。 + * + * 设计: + * - 只改 text,skill 编辑态不暴露(skill 来自发送时技能联想,编辑态改 skill 复杂化交互无收益)。 + * - 边界:index 越界 / newText 空白 → 直接忽略(no-op),不抛错(UI 不会触发,防御)。 + * - queue item 类型 { text, skill?, enqueuedAt } 不改。 + */ +function editQueued(index: number, newText: string) { + if (index < 0 || index >= state.queue.length) return + const trimmed = newText.trim() + if (!trimmed) return + state.queue[index].text = trimmed +} + +/** + * UX-260616-07: 队列项立即发送(决策 a:插队=打断当前+发本条,复用 UX-05 stop 链路)。 + * + * 流程(顺序关键): + * 1. 先 splice 出该项(记下 {text, skill})——必须在 stopChat 之前, + * 否则 stopChat→AiCompleted→drainQueue 续发时该项还在队列致重复发。 + * 2. 调 stopChat() 打断当前生成(stopChat UX-05 后已不清队列,故 splice 步骤前置必要)。 + * 3. 直接 sendMessage(splicedItem.text, skill) 立即发这条(不等 drainQueue 轮到)。 + * 当前未完成回复已由 stop 截断保留(agentic.rs:185 save_conversation),无需额外处理。 + */ +async function sendQueuedNow(index: number) { + if (index < 0 || index >= state.queue.length) return + const spliced = state.queue.splice(index, 1)[0] + await stopChat() + await sendMessage(spliced.text, spliced.skill) +} + /** 停止当前生成:本地先复位 streaming,再发停止信号。 * 不依赖后端 AiCompleted 收尾——审批态看门狗已 clear(useAiEvents),若 AiCompleted 竞态丢失则 streaming 永久 true 卡死,故本地兜底。 */ async function stopChat() { state.streaming = false // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死) clearStreamWatchdog() // 清残留看门狗 - state.queue = [] // B-32:用户主动停止,清队列防生成中入队的消息被静默丢弃 + // UX-260616-05 决策 a(逐条续发):不再清队列,保留供 AiCompleted 链式触发 drainQueue 续发。 + // 后端 agentic.rs:190 emit AiCompleted 注释明确「保证前端收事件时后端已可接下一条,队列续发不被拒」; + // 当前未完成回复已入库(agentic.rs:185 save_conversation)保留为截断回复。 await aiApi.stopChat() } @@ -379,6 +413,8 @@ export function useAiSend() { drainQueue, cancelQueued, clearQueue, + editQueued, // UX-260616-06 + sendQueuedNow, // UX-260616-07 stopChat, isQueueTimedOut, tryForceSend, diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts index a24d610..de8fbf7 100644 --- a/src/i18n/en/aiChat.ts +++ b/src/i18n/en/aiChat.ts @@ -64,6 +64,11 @@ export default { // ── Pending queue ── queueTitle: 'Queued {n}/10', clearQueue: 'Clear', + // UX-260616-06/07: queue item edit + send now + queueEdit: 'Edit', + queueSave: 'Save', + queueCancel: 'Cancel', + queueSendNow: 'Send now', // ── Skill ── clearSkill: 'Clear skill', diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts index 79edb71..6cf5b1f 100644 --- a/src/i18n/zh-CN/aiChat.ts +++ b/src/i18n/zh-CN/aiChat.ts @@ -64,6 +64,11 @@ export default { // ── 待发送队列 ── queueTitle: '待发送 {n}/10', clearQueue: '清空', + // UX-260616-06/07:队列项编辑 + 立即发送 + queueEdit: '编辑', + queueSave: '保存', + queueCancel: '取消', + queueSendNow: '立即发送', // ── 技能 ── clearSkill: '取消技能',