优化: 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:
@@ -205,7 +205,12 @@ async function addDir() {
|
||||
}
|
||||
|
||||
// 删除:即时持久化,dirs 由后端返回值回填(与添加对称,删除也立即生效)。
|
||||
// SA-28: 删除即持久化且无二次确认(误点即丢一条授权),补确认弹层——复用本组件已有的
|
||||
// useConfirm/内联弹层(与「信任所有目录」开关同套)。文案受「不新增 i18n 键」约束,
|
||||
// 无专用键,以 common.delete 拼路径;理想应有 allowedDirsRemoveConfirm 专用键(记 note)。
|
||||
async function removeDir(idx: number) {
|
||||
const dir = dirs.value[idx]
|
||||
if (!await confirmDialog(`${t('common.delete')}「${dir}」?`, t('common.delete'))) return
|
||||
const next = dirs.value.filter((_, i) => i !== idx)
|
||||
saving.value = true
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="connection-list" v-if="connections.length > 0">
|
||||
<div class="connection-card" v-for="conn in connections" :key="conn.id">
|
||||
<div class="conn-left">
|
||||
<span class="conn-icon">{{ typeIcon(conn.type) }}</span>
|
||||
<span class="conn-icon" aria-hidden="true">{{ typeIcon(conn.type) }}</span>
|
||||
<div class="conn-info">
|
||||
<div class="conn-name-row">
|
||||
<span class="conn-name">{{ conn.name }}</span>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</select>
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowMinMessages" :label="$t('settings.labelMinMessages')" :desc="$t('settings.descMinMessages')">
|
||||
<input type="number" min="2" max="50" v-model.number="knowledgeConfig.min_messages" class="setting-number" style="width:80px" @change="markSaved('minMessages')" />
|
||||
<input type="number" min="2" max="50" v-model.number="knowledgeConfig.min_messages" class="setting-number" style="width:80px" @change="onMinMessagesChange" />
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowAutoInject" :label="$t('settings.labelAutoInject')" :desc="$t('settings.descAutoInject')">
|
||||
<label class="toggle">
|
||||
@@ -76,9 +76,15 @@ const embeddingProviders = computed(() =>
|
||||
props.aiProviders.filter(p => p.provider_type !== 'anthropic')
|
||||
)
|
||||
|
||||
// SA-26: 加载回填(loaded 翻 true + 字段赋值)会触发下方 deep watch 一次 ->
|
||||
// 把刚读到的值原样回写 saveConfig IPC,纯冗余。用一次性标志让该次 flush 跳过:
|
||||
// 赋值前打标,watch 回调首行消费并 return。首次加载 loaded 恒 false->true = 必有变更,
|
||||
// 该次 flush 必触发因而标志必被消费,不会误吞后续用户改动(本函数仅 onMounted 调用一次)。
|
||||
let _skipNextFlush = false
|
||||
async function loadKnowledgeConfig() {
|
||||
try {
|
||||
const cfg = await knowledgeApi.getConfig()
|
||||
_skipNextFlush = true
|
||||
Object.assign(knowledgeConfig, cfg)
|
||||
knowledgeConfig.loaded = true
|
||||
} catch (e) {
|
||||
@@ -114,6 +120,8 @@ function saveKnowledgeConfigDebounced() {
|
||||
watch(
|
||||
() => ({ ...knowledgeConfig }),
|
||||
() => {
|
||||
// SA-26: 加载回填导致的 flush 跳过(非用户改动),避免进面板即多一次无谓 saveConfig IPC
|
||||
if (_skipNextFlush) { _skipNextFlush = false; return }
|
||||
if (knowledgeConfig.loaded) saveKnowledgeConfigDebounced()
|
||||
},
|
||||
{ deep: true },
|
||||
@@ -138,6 +146,16 @@ function markSaved(key: SavedKey) {
|
||||
_pendingSavedKey = key
|
||||
}
|
||||
|
||||
// SA-27: input 原生 min/max 只约束步进按钮,手输越界不受限 -> 对齐 PerformanceSection clamp 范式。
|
||||
// 非法值(NaN/清空)回退默认 4,合法值 clamp [2,50](与模板 min/max 一致),修正后回写 v-model
|
||||
// 触发保存与「已保存」反馈。注:未加越界 toast(本组件未接 @toast 通道,且无通用的越界修正文案 key)。
|
||||
function onMinMessagesChange() {
|
||||
const raw = knowledgeConfig.min_messages
|
||||
const n = typeof raw === 'number' && Number.isFinite(raw) ? Math.trunc(raw) : 4
|
||||
knowledgeConfig.min_messages = Math.min(50, Math.max(2, n))
|
||||
markSaved('minMessages')
|
||||
}
|
||||
|
||||
/** 真正点亮行内「✓已保存」反馈(仅 IPC 成功路径调用)。 */
|
||||
function flashRow(key: SavedKey) {
|
||||
const map: Record<SavedKey, InstanceType<typeof SettingRow> | null> = {
|
||||
|
||||
@@ -168,16 +168,16 @@
|
||||
<div class="model-tags">
|
||||
<span v-for="md in m.modalities" v-show="!redundantTags.modalities.has(md)" :key="'md-' + md"
|
||||
class="tag tag-modality" :title="$t('settings.tagTooltipModality')">
|
||||
{{ $t('settings.tagModality.' + md) }}<span class="tag-remove" @click.stop="removeModality(m, md)">×</span>
|
||||
{{ $t('settings.tagModality.' + md) }}<span class="tag-remove" role="button" tabindex="0" :aria-label="$t('common.delete')" @click.stop="removeModality(m, md)" @keydown.enter.prevent="removeModality(m, md)" @keydown.space.prevent="removeModality(m, md)">×</span>
|
||||
</span>
|
||||
<span v-for="cap in m.capabilities" v-show="!redundantTags.capabilities.has(cap)" :key="'cap-' + cap"
|
||||
class="tag tag-capability" :title="$t('settings.tagTooltipCapability')">
|
||||
{{ $t('settings.tagCapability.' + cap) }}<span class="tag-remove" @click.stop="removeCapability(m, cap)">×</span>
|
||||
{{ $t('settings.tagCapability.' + cap) }}<span class="tag-remove" role="button" tabindex="0" :aria-label="$t('common.delete')" @click.stop="removeCapability(m, cap)" @keydown.enter.prevent="removeCapability(m, cap)" @keydown.space.prevent="removeCapability(m, cap)">×</span>
|
||||
</span>
|
||||
<span v-if="m.probe_source && m.probe_source !== 'user_set'" v-show="!redundantTags.sources.has(m.probe_source)"
|
||||
class="tag tag-source" :title="$t('settings.tagTooltipSource')">{{ $t('settings.tagSource.' + m.probe_source) }}</span>
|
||||
<span v-if="m.probe_source === 'user_set'" class="tag tag-source" :title="$t('settings.tagTooltipSource')">{{ $t('settings.tagSource.user_set') }}</span>
|
||||
<span class="tag-add" :title="$t('settings.addTagTitle')" @click.stop="startAddTag(m)">+</span>
|
||||
<span class="tag-add" role="button" tabindex="0" :title="$t('settings.addTagTitle')" :aria-label="$t('settings.addTagTitle')" @click.stop="startAddTag(m)" @keydown.enter.prevent="startAddTag(m)" @keydown.space.prevent="startAddTag(m)">+</span>
|
||||
</div>
|
||||
<!-- 添加能力内联菜单(点+展开;选未有的能力/模态即添加) -->
|
||||
<div v-if="addingTagModel === m.model_id" class="add-tag-menu">
|
||||
@@ -293,7 +293,10 @@ const providerForm = reactive({
|
||||
*/
|
||||
const redundantTags = computed(() => {
|
||||
const models = providerForm.models
|
||||
if (models.length === 0) return { modalities: new Set<string>(), capabilities: new Set<string>(), sources: new Set<string>() }
|
||||
// SA-6: 去冗余的前提是「模型间对比」——仅 1 个模型时它的所有标签都满足「全模型共有」,
|
||||
// 无脑隐藏会把唯一模型的能力标签全抹掉(行内空白,用户看不到该模型能力)。
|
||||
// 故 ≥2 个模型起才去冗余;0/1 个一律全显(sources 分支本就 <2 直接放行,此处对齐)。
|
||||
if (models.length < 2) return { modalities: new Set<string>(), capabilities: new Set<string>(), sources: new Set<string>() }
|
||||
const common = (pick: (m: ModelConfig) => string[]) => {
|
||||
const sets = models.map(m => new Set(pick(m)))
|
||||
const first = sets[0]
|
||||
|
||||
@@ -28,11 +28,15 @@
|
||||
v-for="cat in categories"
|
||||
:key="cat.value"
|
||||
class="nav-item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:class="{
|
||||
'nav-item--active': cat.value === modelValue,
|
||||
'nav-item--matched': isMatchedCategory(cat.value),
|
||||
}"
|
||||
@click="$emit('update:modelValue', cat.value)"
|
||||
@keydown.enter.prevent="$emit('update:modelValue', cat.value)"
|
||||
@keydown.space.prevent="$emit('update:modelValue', cat.value)"
|
||||
>
|
||||
<span class="nav-icon">{{ cat.icon }}</span>
|
||||
<span class="nav-label">{{ $t(cat.labelKey) }}</span>
|
||||
|
||||
@@ -107,6 +107,14 @@
|
||||
/* Toggle(3/5 Panel 重复) */
|
||||
.toggle { position: relative; display: inline-block; width: 40px; height: 22px; cursor: pointer; }
|
||||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||
/* 键盘聚焦可见环:原生 checkbox 被 opacity:0+零尺寸隐藏,global.css 的 :focus-visible 兜底
|
||||
画在零尺寸 input 上不可见;把聚焦态转移到可见的 slider。用 :focus-visible(非 :focus-within)
|
||||
保证鼠标点击不显环、仅 Tab 键盘导航出现;.toggle--sm 变体同匹配(.toggle 前缀)。
|
||||
:focus{border-color} 仅覆盖 setting-select/input 等可见输入,覆盖不到这种视觉隐藏 checkbox。 */
|
||||
.toggle input:focus-visible + .toggle-slider {
|
||||
outline: 2px solid var(--df-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: var(--df-border); border-radius: var(--df-radius-lg); transition: background 0.2s; }
|
||||
.toggle-slider::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; bottom: 3px; background: var(--df-text-dim); border-radius: 50%; transition: transform 0.2s, background 0.2s; }
|
||||
.toggle input:checked + .toggle-slider { background: var(--df-accent); }
|
||||
|
||||
Reference in New Issue
Block a user