优化: L2统一状态机完整(ConvState持久化+事件暴露+前端status联合)
1a PerConvState.conv_state持久化(入口/reset/drop写收敛,CONV_STATE_ENABLED门控) 1b AiChatEvent加AiConvStateChanged变体,guard持app_handle在迁移点emit 1c 前端convStates真相源+停止按钮三态(status union)+MaxRoundsCard读conv_state 双轨过渡:enum视图与generating bool并行,off降级可回退
This commit is contained in:
@@ -84,8 +84,26 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- L2 状态机停止按钮三态(批2 1c):conv_state 派生 generating/stopping/error 三态,
|
||||
idle(或 conv_state 未收到回退旧 streaming=false)显发送按钮。双轨过渡不强制全替旧 bool。 -->
|
||||
<button
|
||||
v-if="store.state.streaming"
|
||||
v-if="stopBtnState === 'stopping'"
|
||||
class="ai-send-btn ai-send-btn--stop ai-send-btn--stopping"
|
||||
:title="$t('aiChat.stopping')"
|
||||
disabled
|
||||
>
|
||||
<svg 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'"
|
||||
class="ai-send-btn ai-send-btn--stop ai-send-btn--retry"
|
||||
:title="$t('aiChat.stopFailedRetry')"
|
||||
@click="store.stopChat"
|
||||
>
|
||||
<svg 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'"
|
||||
class="ai-send-btn ai-send-btn--stop"
|
||||
:title="$t('aiChat.stopGenerating')"
|
||||
@click="store.stopChat"
|
||||
@@ -119,6 +137,7 @@ import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { useProjectStore } from '../../stores/project'
|
||||
import { getConvState } from '../../composables/ai/useAiEvents'
|
||||
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord, MentionSpan } from '../../api/types'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -237,6 +256,28 @@ async function onDrop(e: DragEvent): Promise<void> {
|
||||
/** 发送可用:有文本或图片或已选技能(纯技能调用允许空文本) */
|
||||
const canSend = computed(() => inputText.value.trim().length > 0 || pendingImages.value.length > 0 || !!pendingSkill.value)
|
||||
|
||||
// L2 状态机停止按钮三态(批2 1c):从 conv_state 派生,旧 streaming/generating bool 双轨过渡兜底。
|
||||
//
|
||||
// 三态映射(对齐后端 ConvState 语义):
|
||||
// - 'stopping':ConvState=stopping,停止中 → 显"停止中" disabled(防重复点);
|
||||
// - 'stop': ConvState=generating(含审批挂起/压缩派生等活跃态)→ 显停止按钮可点;
|
||||
// - 'retry': ConvState=error,停止失败可重试 → 显重试按钮(再发 stop 信号);
|
||||
// - 'idle': 空闲(或 conv_state 未收到回退旧 bool=false)→ 显发送按钮。
|
||||
//
|
||||
// 兜底:conv_state 未追踪过(getConvState 返回 null,CONV_STATE_ENABLED=off 或老后端)时,
|
||||
// 回退旧 store.state.streaming 判断(generating→显停止,否则 idle)。不强制全替旧 bool,防回归。
|
||||
// 注:旧 streaming 是全局单值(非 per-conv),仅当前会话生成时为 true;F-09 多会话并发下 conv_state
|
||||
// 更精确,但回退路径用 streaming 对单会话场景语义等价。
|
||||
const stopBtnState = computed<'idle' | 'stop' | 'stopping' | 'retry'>(() => {
|
||||
const convId = store.state.activeConversationId
|
||||
const cs = getConvState(convId)
|
||||
if (cs === 'stopping') return 'stopping'
|
||||
if (cs === 'error') return 'retry'
|
||||
if (cs === 'generating' || cs === 'compressed') return 'stop'
|
||||
// conv_state 未收到(idle 或 null)→ 回退旧 streaming bool
|
||||
return store.state.streaming ? 'stop' : 'idle'
|
||||
})
|
||||
|
||||
// ── 技能 `/` 联想 ──
|
||||
const skillOpen = ref(false)
|
||||
const skillIndex = ref(0)
|
||||
@@ -912,6 +953,18 @@ defineExpose({
|
||||
.ai-send-btn--stop:hover {
|
||||
background: #d63a3f;
|
||||
}
|
||||
/* L2 停止中态:disabled 灰显(防重复点),复用 :disabled 通用规则(opacity 0.4) */
|
||||
.ai-send-btn--stopping {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* L2 停止失败可重试态:橙色边框提示(区别于红色停止),hover 加深 */
|
||||
.ai-send-btn--retry {
|
||||
background: #e5484d;
|
||||
color: #fff;
|
||||
}
|
||||
.ai-send-btn--retry:hover {
|
||||
background: #d63a3f;
|
||||
}
|
||||
.ai-input-hint {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAiStore } from '../../stores/ai'
|
||||
import { pendingMaxRounds } from '../../composables/ai/useAiEvents'
|
||||
import { pendingMaxRounds, getConvState } from '../../composables/ai/useAiEvents'
|
||||
import { aiApi } from '../../api'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -47,6 +47,20 @@ const isViewingGenerating = computed(() =>
|
||||
store.isGenerating(store.state.activeConversationId),
|
||||
)
|
||||
|
||||
// L2 状态机(批2 1c):达 max 可靠弹判断改读 conv_state(generating),诊断痛②看门狗误清 streaming
|
||||
// 致卡片不弹的根解。conv_state 作新增派生源,旧 isGenerating 双轨过渡兜底——conv_state 未收到
|
||||
// 时(CONV_STATE_ENABLED=off 或老后端)回退 isGenerating 判断,不强制全替防回归。
|
||||
// 注:达 max 时后端仍 generating(conv_state=generating,审批挂起期亦是 generating),故读
|
||||
// conv_state===generating 覆盖挂起态;compressed(压缩派生)也视为活跃可弹(达 max 与压缩互斥,
|
||||
// 理论不并存,此处宽放以防边界)。
|
||||
const isViewingGeneratingState = computed(() => {
|
||||
const cs = getConvState(store.state.activeConversationId)
|
||||
if (cs === 'generating' || cs === 'compressed') return true
|
||||
if (cs === 'idle' || cs === 'error' || cs === 'stopping') return false
|
||||
// conv_state 未追踪过(null)→ 回退旧 isGenerating bool 判断
|
||||
return isViewingGenerating.value
|
||||
})
|
||||
|
||||
// pendingMaxRounds(达 max 事件置的挂起 convId)+ 当前视图正在生成(防切走后误显)双重守卫。
|
||||
// TD-260621-02 per-conv:pendingMaxRounds 存挂起 convId(string|null),精确比对
|
||||
// activeConversationId —— 仅当挂起会话 === 当前展示会话时显操作卡,F-09 并发下不会错显于其他会话。
|
||||
@@ -56,7 +70,7 @@ const maxRoundsActing = ref(false)
|
||||
const showMaxRoundsCard = computed(() =>
|
||||
!!pendingMaxRounds.value
|
||||
&& pendingMaxRounds.value === store.state.activeConversationId
|
||||
&& isViewingGenerating.value,
|
||||
&& isViewingGeneratingState.value,
|
||||
)
|
||||
|
||||
/** 点继续:调 ai_continue_loop。后端续 max_iterations 轮(iteration 从 0 重计)。
|
||||
|
||||
Reference in New Issue
Block a user