优化: 对话交互修复与界面完善
- 修输入法组词回车直发/分离窗误关/出错后按钮死锁 - 新建任务失败弹窗内联提示,会话切换乐观高亮 - 状态多选联动刷新不再冲空可见行 - 测试连接保留已配开关与权重,防抖按提供商分键 - 代码块复制/长回复折叠带量/队列回填/排队时钟 - 子代理完成卡渲染,死链卡片与引用清理 - 截图渲染换 pro 版依赖,样式 token 化收尾
This commit is contained in:
@@ -116,6 +116,7 @@ import { useAiStore } from '../../stores/ai'
|
||||
import { useProjectStore } from '../../stores/project'
|
||||
import { getConvState } from '../../composables/ai/useAiEvents'
|
||||
import { textIdle } from '../../composables/ai/useAiPendingState'
|
||||
import { getConvDraft, setConvDraft, classifySendError, type SendErrorKind } from '../../composables/ai/aiShared'
|
||||
import { extractImageUrlParts } from '../../composables/ai/utils'
|
||||
import { uploadFile } from '../../composables/ai/fileUpload'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
@@ -292,7 +293,12 @@ const stopBtnState = computed<'idle' | 'stop' | 'stopping' | 'retry'>(() => {
|
||||
const convId = store.state.activeConversationId
|
||||
const cs = getConvState(convId)
|
||||
if (cs === 'stopping') return 'stopping'
|
||||
if (cs === 'error') return 'retry'
|
||||
// P1-3: conv_state='error' 是 AiError 收尾落的终态(useAiLifecycleEvents cleanup 先
|
||||
// setStreaming(false) 再置 error),此时本会话 loop 已死——「重试停止」按钮点 stopChat 对死
|
||||
// loop 无意义,却顶掉发送位,出错后鼠标用户点不了发送(Enter 不看按钮态仍可发,语义错位)。
|
||||
// 仅流式仍在跑(停止失败等 loop 未断的 error)时保留 retry 语义,否则回发送按钮,
|
||||
// 与 Enter/handleSend 对齐。
|
||||
if (cs === 'error') return store.state.streaming ? 'retry' : 'idle'
|
||||
const textActive = store.state.streaming && (cs === 'generating' || !textIdle.value)
|
||||
return textActive ? 'stop' : 'idle'
|
||||
})
|
||||
@@ -676,21 +682,29 @@ function selectMention(item: MentionItem) {
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// P1-1 输入法组词守卫:拼音/双拼组词未上屏时 Enter=选词、Esc=取消组词,均非用户发送/取消意图,
|
||||
// 直接放行给 IME。isComposing 覆盖标准浏览器;keyCode===229 兜部分环境 keydown 阶段不上报 isComposing。
|
||||
if (e.isComposing || e.keyCode === 229) return
|
||||
// 各 Esc 消费分支 stopPropagation:P1-2 双层防,分离窗口 AiChat 的 window 级 Esc 会关整个窗口,
|
||||
// 内层已消费的 Esc 不再外抛(主防线是 AiChat 侧的 e.target 输入控件守卫,此处为辅)
|
||||
// 编辑态 Escape 取消编辑(最高优先级)
|
||||
if (props.editingMsgId && e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
emit('cancel-edit')
|
||||
return
|
||||
}
|
||||
// Escape 关闭技能联想浮层(无论是否有匹配项)
|
||||
if (skillOpen.value && e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
skillOpen.value = false
|
||||
return
|
||||
}
|
||||
// 已选技能 chip 态下 Escape 取消选中(浮层已关闭,这里兜底 chip 可取消路径)
|
||||
if (pendingSkill.value && e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
pendingSkill.value = null
|
||||
return
|
||||
}
|
||||
@@ -713,6 +727,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
skillOpen.value = false
|
||||
return
|
||||
}
|
||||
@@ -736,6 +751,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
mentionOpen.value = false
|
||||
mentionStart.value = -1
|
||||
return
|
||||
@@ -859,9 +875,10 @@ async function handleSend() {
|
||||
if (snapshotSpans.length) {
|
||||
pendingMentionSpans.value = snapshotSpans
|
||||
}
|
||||
// 用户可见提示(原仅 console.error,用户无感);Tauri IPC 错误是字符串非 Error 对象
|
||||
// 用户可见提示(原仅 console.error,用户无感);Tauri IPC 错误是字符串非 Error 对象。
|
||||
// I5:按错误内容分类映射友好文案,原始错误串留在 {msg} 细节里不丢排查线索
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
emit('error', { msg: t('aiChat.toastSendFail', { msg }), type: 'error' })
|
||||
emit('error', { msg: t(SEND_ERR_KEY[classifySendError(msg)], { msg }), type: 'error' })
|
||||
}
|
||||
await nextTick()
|
||||
emit('sent')
|
||||
@@ -902,11 +919,65 @@ function clearInput(): void {
|
||||
if (inputEl.value) inputEl.value.style.height = 'auto'
|
||||
}
|
||||
|
||||
/** I4 队列回填:父把取出的队列项文本填回输入框重新编辑。
|
||||
* 可选 skill:队列项携带技能名时由父按名找回 SkillInfo 回挂 chip;找回失败传 null 仅丢
|
||||
* chip(与队列原地编辑只保 text 同一取舍)。清 mention 区间/浮层态,光标置尾并聚焦。 */
|
||||
function fillDraft(text: string, skill: SkillInfo | null = null): void {
|
||||
pendingSkill.value = skill
|
||||
inputText.value = text
|
||||
pendingMentionSpans.value = []
|
||||
mentionOpen.value = false
|
||||
mentionStart.value = -1
|
||||
nextTick(() => {
|
||||
inputEl.value?.focus()
|
||||
const el = inputEl.value
|
||||
if (el) {
|
||||
const len = el.value.length
|
||||
el.setSelectionRange(len, len)
|
||||
el.style.height = 'auto'
|
||||
el.style.height = Math.min(el.scrollHeight, 120) + 'px'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 聚焦输入框 */
|
||||
function focus(): void {
|
||||
inputEl.value?.focus()
|
||||
}
|
||||
|
||||
// ── V8 per-conv 草稿:切会话保存/恢复,防 A 会话未发送草稿串到 B 输入框 ──
|
||||
// 草稿真相源在 aiShared.convDrafts(内存 Map,不落 localStorage)。父组件 AiChat 的
|
||||
// activeConversationId watcher 注册更早先执行:编辑态切换时父先 cancelEdit 清输入,
|
||||
// 本 watcher 后执行,旧会话保存到的是清空后的空串,与既有「切会话丢弃编辑缓冲」语义一致。
|
||||
watch(() => store.state.activeConversationId, (nv, ov) => {
|
||||
if (ov) setConvDraft(ov, inputText.value)
|
||||
inputText.value = getConvDraft(nv)
|
||||
// 随输入框的临时态不跨会话携带:mention 区间偏移只对旧文本有效,照搬到恢复的文本会渲染错位
|
||||
// chip;技能 chip 与待发图片同理,残留到新会话会静默混入下一条消息
|
||||
pendingMentionSpans.value = []
|
||||
mentionOpen.value = false
|
||||
mentionStart.value = -1
|
||||
pendingSkill.value = null
|
||||
pendingImages.value = []
|
||||
skillOpen.value = false
|
||||
// 恢复文本会触发输入框自身 watch 重算联想(文本以 @ 结尾时按旧光标位误拉起浮层),
|
||||
// nextTick 在该 watch 之后统一收口,并按恢复文本重算输入框高度
|
||||
nextTick(() => {
|
||||
mentionOpen.value = false
|
||||
mentionStart.value = -1
|
||||
autoResize()
|
||||
})
|
||||
})
|
||||
|
||||
// ── I5 发送失败错误分类文案:类别到 i18n 键的映射(分类逻辑在 aiShared.classifySendError)。
|
||||
// other 复用既有 toastSendFail 文案,行为与改造前一致 ──
|
||||
const SEND_ERR_KEY: Record<SendErrorKind, string> = {
|
||||
rate_limit: 'aiChat.sendErrRateLimit',
|
||||
quota: 'aiChat.sendErrQuota',
|
||||
network: 'aiChat.sendErrNetwork',
|
||||
other: 'aiChat.toastSendFail',
|
||||
}
|
||||
|
||||
/**
|
||||
* 未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
|
||||
* 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
|
||||
@@ -923,6 +994,7 @@ defineExpose({
|
||||
focus,
|
||||
startEditFocus,
|
||||
clearInput,
|
||||
fillDraft,
|
||||
hasUnsentInput,
|
||||
sendExamplePrompt,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user