- 全局样式类(df-sec-h/df-toast族)替换散装实现,主题token去硬编码兜底 - 拆分子组件:ai-chat/queue-panel(队列时钟与编辑随迁)、 conversation-sidebar/ConversationList、settings/provider-panel - 父组件死副本清理(队列编辑/时钟/键位表,TS6133清零)并补 fill-draft父侧转发(重构半成品断链修复) - 验收:vue-tsc EXIT 0
124 lines
5.5 KiB
Vue
124 lines
5.5 KiB
Vue
<template>
|
|
<!-- L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):断路器熔断后展示的操作卡。
|
|
触发:后端断路器连续同类失败达阈值 → emit AiHelpRequired(已 guard.reset,loop 终止)→
|
|
useAiEvents handleLifecycleEvent 翻 pendingHelp(模块级 ref)→ 本卡渲染。
|
|
复用 MaxRoundsCard 模式:per-conv 守卫(pendingHelp.conversationId === active)+ 行为已记入本地 ref。
|
|
显 reason(原因)+ context(最近错误 sample)+ options 按钮供用户选(换思路/授权路径/人工接管)。
|
|
选 option → 仅关闭求助卡(后端 loop 已终止,用户重新发消息即"换思路/人工接管"入口,
|
|
路径授权走 DirAuthDialog 既有链路)。机制优先 prompt 说教:代码强制熔断 + 结构化求助,
|
|
非"教 AI 失败就问用户"(LLM 不可靠)。 -->
|
|
<div v-if="helpActive" ref="helpRef" class="ai-help-required">
|
|
<span class="ai-help-required-text">{{ helpActive.reason }}</span>
|
|
<span class="ai-help-required-hint">{{ helpActive.context }}</span>
|
|
<div class="ai-help-required-options">
|
|
<button
|
|
v-for="opt in helpActive.options"
|
|
:key="opt"
|
|
type="button"
|
|
class="ai-help-required-btn"
|
|
@click="handleSelectOption(opt)"
|
|
>{{ opt }}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch, nextTick } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAiStore } from '../../stores/ai'
|
|
import { pendingHelp } from '../../composables/ai/useAiPendingState'
|
|
|
|
// toast 经 emit 转父(保持单一 toast 源,与 MaxRoundsCard 一致)
|
|
const emit = defineEmits<{
|
|
(e: 'toast', payload: { msg: string; type: 'error' | 'warning' | 'info' }): void
|
|
}>()
|
|
|
|
const store = useAiStore()
|
|
const { t } = useI18n()
|
|
|
|
// 当前挂起的求助(有值即后端发过 AiHelpRequired)。pendingHelp 由 useAiEvents 翻:
|
|
// AiHelpRequired 设值;AiCompleted/AiError 清值(重新发送或错误中断后求助卡消失)。
|
|
//
|
|
// helpActive 既做守卫(挂起会话===当前展示会话)又做模板取值源:返回 PendingHelp 非空或 null,
|
|
// 模板 v-if="helpActive" 取真值后 Vue 类型收窄为非 null,可直接访问 .reason/.context/.options。
|
|
// 显示守卫:仅当挂起求助属当前展示会话时显(F-09 多会话并发不串台)。
|
|
// 注:不依赖 streaming——求助事件后端已 guard.reset(generating=false),若依赖 isGenerating
|
|
// 会导致卡片不显(对齐 MaxRoundsCard F-260620 去 streaming 依赖的根治理)。
|
|
const helpActive = computed(() => {
|
|
const p = pendingHelp.value
|
|
if (p && p.conversationId === store.state.activeConversationId) return p
|
|
return null
|
|
})
|
|
|
|
// M8:卡片根 ref。断路器熔断常发生在多轮迭代后,消息流已长,求助卡被挤出视口用户看不到 → 求助无声
|
|
// 卡死。卡片显示时 scrollIntoView 让 option 按钮组立即进入视口。
|
|
const helpRef = ref<HTMLElement | null>(null)
|
|
|
|
/** M8:helpActive 从 null→非 null(求助挂起出现)时 scrollIntoView 让操作入口立即可见。
|
|
* 仅监 truthy 翻转,不监 reason/context 变化避免已可见时反复跳。 */
|
|
watch(helpActive, async (p) => {
|
|
if (!p) return
|
|
await nextTick()
|
|
helpRef.value?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
})
|
|
|
|
/** 选 option:关求助卡即可。后端 loop 已在 AiHelpRequired 时终止(guard.reset + return),
|
|
* 用户选 option 后的"换思路/授权路径/人工接管"由用户重新发消息/操作触发(无独立 IPC)。
|
|
* 点"授权路径"提示用户路径授权链路(DirAuthDialog 自动触发,无需此卡代发)。
|
|
* 选后清 pendingHelp,卡片隐藏(对齐 MaxRoundsCard 点继续/停止后由事件清的语义,此处无续跑事件故立即清)。 */
|
|
function handleSelectOption(opt: string): void {
|
|
void opt // option 仅区分按钮文案,后端 loop 已终止无独立 IPC 续跑,选即清卡片
|
|
pendingHelp.value = null
|
|
emit('toast', { msg: t('aiChat.helpRequiredAck'), type: 'info' })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* wave2(2026-10-18):统一透明彩底卡范式,danger 系(断路器熔断=最高警示级)——
|
|
边框 color-mix 30% + 底 8% + 标题同色 12px/600 + 正文 secondary 12px + 透明底描边钮 */
|
|
.ai-help-required {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
margin-bottom: 6px;
|
|
padding: 6px 8px;
|
|
background: color-mix(in srgb, var(--df-danger) 8%, transparent);
|
|
border: 0.5px solid color-mix(in srgb, var(--df-danger) 30%, transparent);
|
|
border-radius: var(--df-radius);
|
|
}
|
|
.ai-help-required-text {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--df-danger);
|
|
}
|
|
.ai-help-required-hint {
|
|
font-size: 12px;
|
|
color: var(--df-text-secondary);
|
|
word-break: break-all;
|
|
/* M7:context 含错误堆栈,长栈撑高卡片。等宽字体利堆栈行对齐;max-height + overflow-y:auto
|
|
短内容自动不显滚动条,长堆栈滚动扫看(比折叠实用,无需展开按钮)。 */
|
|
font-family: var(--df-font-mono);
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
.ai-help-required-options {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-top: 2px;
|
|
}
|
|
.ai-help-required-btn {
|
|
border: 0.5px solid color-mix(in srgb, var(--df-danger) 40%, transparent);
|
|
background: color-mix(in srgb, var(--df-danger) 10%, transparent);
|
|
color: var(--df-danger);
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 3px 10px;
|
|
border-radius: calc(var(--df-radius) - 2px);
|
|
transition: background 0.15s var(--df-ease);
|
|
}
|
|
.ai-help-required-btn:hover { background: color-mix(in srgb, var(--df-danger) 16%, transparent); }
|
|
</style>
|