优化: AI Chat 体验全面升级(图标/审批/目标提取/窗口管理)

- AI 入口图标:灯泡→星花 sparkles+发光,与灵感入口区分
- 目标提取根本性改造:用户消息规则→工具调用推理(infer_goal_from_tool_calls)
  - Vec<String>→Vec<GoalEntry>(text+status),active→completed 状态流转
  - 多条/轮提取,system_prompt 仅注入 active 目标
  - 代码拆分到 helpers.rs(解决 brace 嵌套致 pub(crate) 不可见)
- 审批修复:patch_file 全档位自动放行,批量审批防抖,乐观更新竞态保护
- 审批通知浮层:全局右上角 ApprovalOverlay,窗口遮挡时可见
- 设置导入导出移除:价值低占用空间,跨设备同步建议复制 SQLite
- 分离窗口默认置顶+置顶状态持久化(刷新后保持)
- header 菜单聚合:清空/压缩上下文收入 ... popout
- LLM 文字重叠防御:delta 重复检测+丢弃
- 置顶图标:星→大头针 pushpin
- 文档/注释更新:过期 Vec<String>/chat.rs 提取描述修正
This commit is contained in:
lxy
2026-07-03 13:07:03 +08:00
parent aa0701a2ea
commit 0dfaeb0f5b
26 changed files with 929 additions and 753 deletions
+201
View File
@@ -0,0 +1,201 @@
<template>
<!-- ═══ 审批待办浮层(屏幕右上角,置顶到所有窗口上面)═══
设计目的:当 aichat 面板被遮挡/隐藏/最小化,有 pending 审批时,
用户能立刻看到通知并一键激活 AI 面板处理。
实现:
- 监听 store.state.pendingApprovals(由 AiApprovalRequired 事件驱动)
- 位置 position:fixed top:16px right:16px,z-index 高于面板
- 窗口本身 alwaysOnTop 时浮层自然置顶到所有桌面窗口上面
- 点击浮层 → emit 'activate'(App.vue 决定激活主面板/分离窗口)
- 用户关闭后 60s 内同会话不再弹(防审批流期间反复打扰),新审批追加会再显 -->
<Transition name="approval-overlay">
<div
v-if="visible"
class="approval-overlay"
role="alert"
@click="onActivate"
>
<div class="approval-overlay-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4" />
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
</svg>
<span v-if="count > 1" class="approval-overlay-badge">{{ count }}</span>
</div>
<div class="approval-overlay-body">
<div class="approval-overlay-title">{{ $t('aiChat.approvalWaitingTitle') }}</div>
<div class="approval-overlay-desc">{{ $t('aiChat.approvalWaitingDesc') }}</div>
</div>
<button
class="approval-overlay-close"
:title="$t('common.close')"
@click.stop="onDismiss"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</Transition>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useAiStore } from '@/stores/ai'
const store = useAiStore()
const emit = defineEmits<{
/** 用户点击浮层主体:激活 AI 面板/分离窗口 */
(e: 'activate'): void
}>()
// 待审批数(0 = 无)
const count = computed(() => store.state.pendingApprovals.length)
// 用户主动 dismiss 后 60s 内不再弹(同会话审批流期反复打扰防护)
const dismissedAt = ref<number>(0)
const DISMISS_COOLDOWN_MS = 60_000
// 显隐:有 pending + 未在冷却期
const visible = computed(() => {
if (count.value === 0) return false
if (dismissedAt.value && Date.now() - dismissedAt.value < DISMISS_COOLDOWN_MS) return false
return true
})
// 审批清空时复位 dismiss(下次有新审批会重新弹)
watch(count, (c) => {
if (c === 0) dismissedAt.value = 0
})
function onActivate() {
emit('activate')
}
function onDismiss() {
dismissedAt.value = Date.now()
}
</script>
<style scoped>
/* 浮层 — 右上角 fixed,z-index 高于 AI 面板与其他模态 */
.approval-overlay {
position: fixed;
top: 16px;
right: 16px;
z-index: 10000;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
min-width: 260px;
max-width: 360px;
background: var(--df-bg-card);
border: 0.5px solid var(--df-accent);
border-left: 3px solid var(--df-accent);
border-radius: var(--df-radius-md);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(123, 111, 240, 0.2);
cursor: pointer;
transition: all 0.18s var(--df-ease);
user-select: none;
}
.approval-overlay:hover {
transform: translateX(-2px);
border-color: var(--df-accent);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5), 0 0 0 2px var(--df-accent);
}
.approval-overlay-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--df-accent-bg);
color: var(--df-accent);
flex-shrink: 0;
}
.approval-overlay-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 9px;
background: var(--df-danger);
color: #fff;
font-size: 10px;
font-weight: 600;
line-height: 18px;
text-align: center;
border: 1.5px solid var(--df-bg-card);
}
.approval-overlay-body {
flex: 1;
min-width: 0;
}
.approval-overlay-title {
font-size: 13px;
font-weight: 600;
color: var(--df-text);
margin-bottom: 2px;
}
.approval-overlay-desc {
font-size: 11px;
color: var(--df-text-dim);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.approval-overlay-close {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border: none;
border-radius: var(--df-radius-sm);
background: transparent;
color: var(--df-text-dim);
cursor: pointer;
flex-shrink: 0;
transition: all 0.15s var(--df-ease);
}
.approval-overlay-close:hover {
background: var(--df-sidebar-hover);
color: var(--df-text);
}
/* 进出动画:从右侧滑入 */
.approval-overlay-enter-active,
.approval-overlay-leave-active {
transition: all 0.25s var(--df-ease);
}
.approval-overlay-enter-from,
.approval-overlay-leave-to {
opacity: 0;
transform: translateX(40px);
}
/* 响应式:窄屏占满右侧 */
@media (max-width: 480px) {
.approval-overlay {
top: 8px;
right: 8px;
left: 8px;
max-width: none;
}
}
</style>