优化: 聊天体验批与Web规范合规
This commit is contained in:
+154
-2
@@ -25,6 +25,8 @@
|
||||
@close-panel="store.togglePanel()"
|
||||
@dock="store.dockDetached()"
|
||||
@toggle-always-on-top="toggleAlwaysOnTop"
|
||||
@reattach="onReattachToMain"
|
||||
@show-shortcuts="showShortcuts"
|
||||
@close-detached="store.closeDetachedWindow()"
|
||||
@provider-switched="(name: string) => showToast(t('aiChat.providerSwitched', { model: name }), 'info')"
|
||||
/>
|
||||
@@ -46,6 +48,8 @@
|
||||
|
||||
<!-- Input -->
|
||||
<div class="ai-input-area">
|
||||
<!-- B4: 自动压缩完成提示(一次性 4s 淡出;治「上下文何时被压缩」零感知,不弹 toast 不打断流) -->
|
||||
<div v-if="autoCompressedFlash" class="ai-autocompressed-hint">{{ t('aiChat.autoCompressed') }}</div>
|
||||
<!-- 无人值守边界配置卡(默认收起=现状零变化):发起时一次授权,生成中锁编辑 -->
|
||||
<TaskBoundaryCard @toast="(p) => showToast(p.msg, p.type)" />
|
||||
<!-- AE-2025-07: Agentic 循环进度条(C 状态语义化)
|
||||
@@ -131,6 +135,24 @@
|
||||
<Transition name="ai-toast">
|
||||
<div v-if="toast.visible" class="ai-toast" :class="'ai-toast-' + toast.type">{{ toast.msg }}</div>
|
||||
</Transition>
|
||||
<!-- I12(2026-10-05):键盘快捷键速查。Teleport 到 body 防 .ai-panel overflow 裁剪;
|
||||
主窗/分离窗各自实例独立渲染,入口在 TopBar ... 菜单(emit show-shortcuts) -->
|
||||
<Teleport to="body">
|
||||
<div v-if="shortcutsVisible" class="ai-shortcuts-mask" @click.self="shortcutsVisible = false">
|
||||
<div class="ai-shortcuts-panel" role="dialog" :aria-label="t('aiChat.shortcutsTitle')">
|
||||
<div class="ai-shortcuts-head">
|
||||
<span class="ai-shortcuts-title">{{ t('aiChat.shortcutsTitle') }}</span>
|
||||
<button class="ai-shortcuts-close" :aria-label="t('aiChat.scCloseOverlay')" @click="shortcutsVisible = false">✕</button>
|
||||
</div>
|
||||
<ul class="ai-shortcuts-list">
|
||||
<li v-for="s in shortcuts" :key="s.key" class="ai-shortcuts-row">
|
||||
<span class="ai-shortcuts-key">{{ s.key }}</span>
|
||||
<span class="ai-shortcuts-act">{{ s.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -154,6 +176,8 @@ import { useConfirm } from '../composables/useConfirm'
|
||||
import { useToast } from '../composables/useToast'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { collectTurnStats, turnMessages } from '../composables/ai/useTurnTrace'
|
||||
import { useAiContext } from '../composables/ai/useAiContext'
|
||||
import { emitReattachToMain, registerMainReattachListener } from '@/composables/ai/useAiWindow'
|
||||
import type { AiMessage } from '../api/types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -164,6 +188,8 @@ const props = defineProps<{
|
||||
const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
// B4: 自动压缩完成一次性可见信号(输入区上方 hint 行短暂显示,4s 自灭)
|
||||
const { autoCompressedFlash } = useAiContext()
|
||||
|
||||
// path_auth 审批链阶段3b(统一审批模型):df-ai-unified-approval 开关响应式 ref。
|
||||
// 开(true,默认):path 挂起归一进 ToolCard 内联审批(once/always/deny),不挂 DirAuthDialog。
|
||||
@@ -278,7 +304,9 @@ async function confirmNewConversation() {
|
||||
await store.newConversation()
|
||||
} catch (e) {
|
||||
console.error('[AI] 新建对话失败:', e)
|
||||
// 用错误气泡反馈用户,不静默吞掉
|
||||
// #16(2026-10-05 注释纠偏):原注释称「用错误气泡反馈用户」,实际 catch 内仅 console.error,
|
||||
// UI 无反馈;失败兜底由 store.newConversation 内部 withConvOp toast 承接,此处不再重复提示,
|
||||
// 保留 console.error 留痕。注释与实现对齐,避免后续维护误导。
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,9 +366,19 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
||||
const el = e.target as HTMLElement | null
|
||||
if (ctrl && !!el?.closest?.('input, textarea, [contenteditable]')) return
|
||||
// Ctrl+N: 新建对话(Tauri 下 preventDefault 可靠,无浏览器新窗口冲突)
|
||||
// V10(2026-10-05):撤 streaming 守卫,对齐按钮侧 handleNewConversation 的 F-09 真并发
|
||||
// 语义——新建不杀旧 loop,旧会话后台继续,无需「生成中禁新建」;原守卫静默吞键(无反馈)
|
||||
// 与按钮可点自相矛盾。真并发下新旧会话事件按 conversation_id 路由,无串台风险。
|
||||
if (ctrl && (e.key === 'n' || e.key === 'N')) {
|
||||
e.preventDefault()
|
||||
if (!store.state.streaming) store.newConversation()
|
||||
store.newConversation()
|
||||
return
|
||||
}
|
||||
// I13(2026-10-05):Ctrl+I 聚焦输入框(输入焦点守卫已让路——焦点已在输入控件内不重复抢);
|
||||
// Ctrl+K 归会话搜索,聚焦输入框选 I(无浏览器默认冲突,IME 安全)。
|
||||
if (ctrl && (e.key === 'i' || e.key === 'I')) {
|
||||
e.preventDefault()
|
||||
chatInputRef.value?.focus()
|
||||
return
|
||||
}
|
||||
// Ctrl+R: 重新生成末条回复(替代浏览器刷新,Tauri 下 preventDefault 可靠)
|
||||
@@ -386,6 +424,12 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
||||
answerConfirm(false) // 视为取消,resolve Promise 打断 confirmClearChat/confirmDelete
|
||||
return
|
||||
}
|
||||
// I12:快捷键速查面板开时 Esc 仅关面板(优先级高于主面板/分离窗关闭,防误关整窗)
|
||||
if (shortcutsVisible.value) {
|
||||
e.preventDefault()
|
||||
shortcutsVisible.value = false
|
||||
return
|
||||
}
|
||||
// P1-2 Esc 仲裁:事件源落在内层 Esc 消费者(输入框/侧栏重命名框/队列编辑框等输入控件,
|
||||
// 或技能/@联想浮层与技能 chip)时,Esc 语义归内层(关浮层/取消编辑/取消输入法组词),
|
||||
// 不关分离窗——否则输入框按 Esc 关技能浮层的同时窗口也进入关闭流程,一次按键双重效果。
|
||||
@@ -612,6 +656,9 @@ onMounted(async () => {
|
||||
_unlistenAutoApproved = await listen<{ tool: string; dir: string }>('ai-tool-auto-approved-toast', (e) => {
|
||||
showToast(t('aiChat.autoApprovedToast', { tool: e.payload.tool, dir: e.payload.dir }), 'info')
|
||||
})
|
||||
// V11b: 主窗口注册跨窗收回分离窗监听(label 判定在内部,分离窗调用为 no-op);
|
||||
// reattachPanel 由主窗 store 执行(关全部 ai-detached-* 窗 + 恢复主面板)。
|
||||
registerMainReattachListener(() => store.reattachPanel())
|
||||
// P1-e-d: 审批失败 toast(useAiApproval.approveToolCall 审批 IPC 未送达经事件总线广播,
|
||||
// 本组件 listen 弹本地 toast。主窗口/分离窗口各挂一份 AiChat,各自消费)
|
||||
_unlistenAiToast = await listen<{ msg: string; type: 'info' | 'error' | 'warning' | 'success' }>('ai-toast', (e) => {
|
||||
@@ -674,6 +721,30 @@ async function toggleAlwaysOnTop() {
|
||||
appSettingsStore.set('df-ai-always-on-top', alwaysOnTop.value)
|
||||
}
|
||||
|
||||
// ── V11b 收回分离窗 + I12 快捷键速查面板 ──
|
||||
// V11b(2026-10-05):分离窗 TopBar「收回」按钮 emit 链——分离窗与主窗是独立 JS context,
|
||||
// 不能直调主窗 store.reattachPanel;走 Tauri 全局事件广播(emitReattachToMain),主窗 AiChat
|
||||
// 挂载时注册的 registerMainReattachListener(useAiWindow.ts,按 label==='main' 判定)消费执行。
|
||||
// I12(2026-10-05):顶部 ... 菜单入口(TopBar emit show-shortcuts)→ 此处 Teleport 展示速查浮层。
|
||||
// 键位表 shortcuts 与 onGlobalKeydown 同文件同居——键区调整时表随手同步,防速查与真实键位漂移。
|
||||
const shortcutsVisible = ref(false)
|
||||
// computed:随 locale 即时翻译(仅面板打开时消费,开销可忽略);t 已在本组件上方 useI18n 解构
|
||||
const shortcuts = computed(() => [
|
||||
{ key: 'Ctrl+N', label: t('aiChat.scNewConv') },
|
||||
{ key: 'Ctrl+I', label: t('aiChat.scFocusInput') },
|
||||
{ key: 'Ctrl+R', label: t('aiChat.scRegenerate') },
|
||||
{ key: 'Ctrl+L', label: t('aiChat.scClearChat') },
|
||||
{ key: 'Ctrl+K', label: t('aiChat.scSearch') },
|
||||
{ key: 'Ctrl+B', label: t('aiChat.scToggleSidebar') },
|
||||
{ key: 'Esc', label: t('aiChat.scCloseOverlay') },
|
||||
])
|
||||
function showShortcuts() {
|
||||
shortcutsVisible.value = true
|
||||
}
|
||||
async function onReattachToMain() {
|
||||
await emitReattachToMain()
|
||||
}
|
||||
|
||||
// ── F-15 阶段2: 手动上下文管理(清空 / 压缩) ──
|
||||
// ChatMessage.status 渲染语义(types.ts AiMessage.status 字面量联合定义):
|
||||
// null | undefined | 'active' → 正常消息(现状)
|
||||
@@ -833,6 +904,21 @@ async function initContextEventListeners(): Promise<void> {
|
||||
flex-shrink: 0;
|
||||
animation: ai-agentic-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
/* B4: 自动压缩完成提示(一次性 4s;视觉克制度对齐 agentic 进度条,弱存在感) */
|
||||
.ai-autocompressed-hint {
|
||||
margin-bottom: 6px;
|
||||
padding: 3px 8px;
|
||||
background: color-mix(in srgb, var(--df-accent) 8%, transparent);
|
||||
border-radius: var(--df-radius);
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
user-select: none;
|
||||
animation: ai-autocompressed-fade 4s ease-in-out forwards;
|
||||
}
|
||||
@keyframes ai-autocompressed-fade {
|
||||
0%, 70% { opacity: 1; }
|
||||
100% { opacity: 0.35; }
|
||||
}
|
||||
.ai-agentic-progress-text {
|
||||
font-family: var(--df-font-mono);
|
||||
white-space: nowrap;
|
||||
@@ -930,4 +1016,70 @@ async function initContextEventListeners(): Promise<void> {
|
||||
.ai-toast-enter-active, .ai-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
||||
.ai-toast-enter-from, .ai-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
|
||||
|
||||
/* ═══ I12 快捷键速查面板(Teleport 到 body,scoped 对 Teleport 内容仍生效——
|
||||
Vue scoped 把 data 属性打在编译期节点上,与挂载位置无关) ═══ */
|
||||
.ai-shortcuts-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ai-shortcuts-panel {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg, 8px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
width: 320px;
|
||||
max-width: calc(100vw - 48px);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.ai-shortcuts-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ai-shortcuts-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--df-text);
|
||||
}
|
||||
.ai-shortcuts-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--df-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ai-shortcuts-close:hover { color: var(--df-text); background: var(--df-bg-secondary); }
|
||||
.ai-shortcuts-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.ai-shortcuts-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ai-shortcuts-key {
|
||||
font-family: var(--df-mono, monospace);
|
||||
font-size: 11px;
|
||||
background: var(--df-bg-secondary);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
color: var(--df-text);
|
||||
}
|
||||
.ai-shortcuts-act { color: var(--df-text-secondary); }
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user