优化: AI Chat 渲染与无障碍 + 文件查看 + 同步 IO 收口
- 渲染: 结果/过程分离(气泡只放最终结果,过程轮叙述+工具卡收进轨迹卡默认收起可展开——治『流式后正文消失』且不再 N 气泡打扰) - 渲染 P0: v-memo 定向 dep(只有真正渲染流式块的 item 失效,其余取占位) + renderItems 桶 memo(签名覆盖全消费输入,item 引用稳定) + 指纹 watch 降为 O(1) toolEpoch - TopBar 三处 O(n) messages computed 合并单次遍历;md 缓存改有界 LRU(除满即全清抖动);useTurnTrace 补 never 穷尽 + default;MessageItem 模板去重 - 无障碍(四批累计): 可点容器键盘化(含 role/tabindex/keydown/aria-expanded) + icon-only aria-label 约 60 处 + 装饰图标 aria-hidden + 52 个滚动容器 overscroll-behavior:contain + 设置开关聚焦可见环 + outline 零补偿站点补 :focus-visible - UX 走查项: 灵感 IDEA-10/11、知识 KNOW-6/7/9、项目详情 O8、审计 SA-4、设置 SA-6/26/27/28(脏表单/冗余 save/clamp/删除二次确认) - 文件查看专项: 切工程状态残留 F5/F6、FileTree 去重与展开反馈 F7/F15/F17、切文件头部残留 F18、R 状态着色 F19;FilePreview diff 文件头(---/+++)误当增删渲染修复 + 大纲行高亮死样式修复 + 图片失败文案 i18n 化 - 同步 IO 收口(异步上下文内阻塞): unattended 看板读写改 tokio::fs、fetch_url 探活改 spawn_blocking、http/generate_image 清理改 tokio::fs - 子代理 P3 批量: 任务书钉扎上限/截断文案区分父子语境/环境画像日期/temperature 可覆盖/model 回退上报/lang 透传 - crate 清理: df-execute·df-types 注释与格式、df-ideas 过时注释、df-nodes subflow 夸大描述、df-project 死变量
This commit is contained in:
@@ -70,33 +70,41 @@
|
||||
idle(或 conv_state 未收到回退旧 streaming=false)显发送按钮。双轨过渡不强制全替旧 bool。 -->
|
||||
<button
|
||||
v-if="stopBtnState === 'stopping'"
|
||||
type="button"
|
||||
class="ai-send-btn ai-send-btn--stop ai-send-btn--stopping"
|
||||
:title="$t('aiChat.stopping')"
|
||||
:aria-label="$t('aiChat.stopping')"
|
||||
disabled
|
||||
>
|
||||
<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="stopBtnState === 'retry'"
|
||||
type="button"
|
||||
class="ai-send-btn ai-send-btn--stop ai-send-btn--retry"
|
||||
:title="$t('aiChat.stopFailedRetry')"
|
||||
:aria-label="$t('aiChat.stopFailedRetry')"
|
||||
@click="store.stopChat"
|
||||
>
|
||||
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="stopBtnState === 'stop'"
|
||||
type="button"
|
||||
class="ai-send-btn ai-send-btn--stop"
|
||||
:title="$t('aiChat.stopGenerating')"
|
||||
:aria-label="$t('aiChat.stopGenerating')"
|
||||
@click="store.stopChat"
|
||||
>
|
||||
<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="ai-send-btn"
|
||||
:class="{ 'ai-send-btn--active': canSend }"
|
||||
:disabled="!canSend"
|
||||
:aria-label="$t('aiChat.queueSendNow')"
|
||||
@click="handleSend"
|
||||
>
|
||||
<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
||||
@@ -128,7 +136,9 @@ import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { useProjectStore } from '../../stores/project'
|
||||
import { useAppSettingsStore } from '../../stores/appSettings'
|
||||
import { getConvState } from '../../composables/ai/useAiEvents'
|
||||
import { continueList, exitListOnBackspace } from '../../composables/ai/useListContinuation'
|
||||
import { textIdle } from '../../composables/ai/useAiPendingState'
|
||||
import { getConvDraft, setConvDraft, classifySendError, type SendErrorKind } from '../../composables/ai/aiShared'
|
||||
import { extractImageUrlParts } from '../../composables/ai/utils'
|
||||
@@ -158,6 +168,7 @@ const emit = defineEmits<{
|
||||
|
||||
const store = useAiStore()
|
||||
const projectsStore = useProjectStore()
|
||||
const appSettings = useAppSettingsStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const inputText = ref('')
|
||||
@@ -803,6 +814,33 @@ function selectMention(item: MentionItem) {
|
||||
})
|
||||
}
|
||||
|
||||
// ── 列表续写开关(设计:docs/02-架构设计/专项设计/输入框列表预览与自动编号-设计-2026-10-10.md,P1=V0 零视觉) ──
|
||||
// appSettings 键 `df-ai-list-input`,默认关:关=完全回退现状 textarea 行为,零残留。
|
||||
// 逻辑层为纯函数(useListContinuation),此处只做接线(键分支 + 应用编辑 + @span 平移)。
|
||||
const listInputEnabled = computed(() => appSettings.get<boolean>('df-ai-list-input', false))
|
||||
|
||||
/** 当前光标位(textarea 未挂载时退化为文末) */
|
||||
function caretOffset(): number {
|
||||
return inputEl.value?.selectionStart ?? inputText.value.length
|
||||
}
|
||||
|
||||
/** 应用列表续写/退出的编辑结果:改文本 → 显式平移 @span(程序赋值不派发 input)→ 回光标 + 重算高度 */
|
||||
function applyListEdit(r: { text: string; caret: number }): void {
|
||||
const old = inputText.value
|
||||
inputText.value = r.text
|
||||
// 程序赋值不触发 input 事件(见 onInputText 说明),mention span 须手动平移,同 shiftMentionSpans 调用范式
|
||||
shiftMentionSpans(old, r.text)
|
||||
nextTick(() => {
|
||||
const el = inputEl.value
|
||||
if (el) {
|
||||
el.focus()
|
||||
el.setSelectionRange(r.caret, r.caret)
|
||||
el.style.height = 'auto'
|
||||
el.style.height = Math.min(el.scrollHeight, INPUT_MAX_HEIGHT) + 'px'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// P1-1 输入法组词守卫:拼音/双拼组词未上屏时 Enter=选词、Esc=取消组词,均非用户发送/取消意图,
|
||||
// 直接放行给 IME。isComposing 覆盖标准浏览器;keyCode===229 兜部分环境 keydown 阶段不上报 isComposing。
|
||||
@@ -895,6 +933,28 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 列表续写(开关 df-ai-list-input,默认关):空项退格退出 / Shift+Enter 续写补号。
|
||||
// 插在技能·@ 浮层导航之后(浮层开时不拦,用户意图是选实体)、Enter 发送之前;
|
||||
// 只绑 Shift+Enter(换行链),Enter 发送语义不变。关闭时整段短路 → 零行为变更。
|
||||
if (listInputEnabled.value && !skillOpen.value && !mentionOpen.value) {
|
||||
const withMod = e.ctrlKey || e.metaKey || e.altKey
|
||||
if (e.key === 'Backspace' && !e.shiftKey && !withMod) {
|
||||
const r = exitListOnBackspace(inputText.value, caretOffset())
|
||||
if (r) {
|
||||
e.preventDefault()
|
||||
applyListEdit(r)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && e.shiftKey && !withMod) {
|
||||
const r = continueList(inputText.value, caretOffset())
|
||||
if (r) {
|
||||
e.preventDefault()
|
||||
applyListEdit(r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
|
||||
Reference in New Issue
Block a user