新增: 多会话并发前端 UI 阶段3/4(F-09 收尾)
后端 per-conv 并发已就绪(d899c58 + CR-03~07),前端呈现层收尾:
阶段3 多会话生成态:
- stores/ai.ts generatingConvId 单值 → generatingConvs:Set + add/remove/isGenerating helper
- useAiEvents 5 处迁移(Completed/Error/路由标记改 per-conv Set 操作,
AiError 精确清出错会话非全局清空,修原 = null 误清其他会话)
- ConversationSidebar 多会话并行生成脉冲指示(isGenerating 各会话独立)
阶段4 独立窗口多会话:
- useAiWindow label ai-detached → ai-detached-{convId}(per-conv 独立窗口)
- localStorage 单 key → per-conv key(防多会话串扰,读路径兼容旧 key 回退)
vue-tsc EXIT 0 + generatingConvId 零代码残留(仅注释)
This commit is contained in:
@@ -242,7 +242,7 @@ async function confirmClearChat() {
|
||||
* 新建对话(UX-08 §3.4):生成中点新建 → 弹 ConfirmDialog「当前正在生成,确定中断并新建?」,
|
||||
* 确认走原软复位流程(B-260615-10 store.newConversation,中断 + 新建);取消不动(保持生成)。
|
||||
* 非生成中直接走原新建流程(零行为变化)。仅「当前视图正在生成」才弹确认 ——
|
||||
* 切走后台生成时(streaming && generatingConvId !== activeConversationId)不算中断当前视图。
|
||||
* 切走后台生成时(streaming && !isGenerating(activeConversationId))不算中断当前视图。
|
||||
*/
|
||||
async function confirmNewConversation() {
|
||||
if (isViewingGenerating.value) {
|
||||
@@ -277,8 +277,10 @@ const convSidebarRef = ref<InstanceType<typeof ConversationSidebar> | null>(null
|
||||
// (顶部工具栏 + provider/model 选择逻辑,零行为变更,store 单例共享 modelOverride)。
|
||||
|
||||
// 当前视图是否正在生成(切走后台生成时光标不显示)
|
||||
// F-09: 多会话并发 — 用 isGenerating(activeConversationId) 替代单值比对,
|
||||
// 当前会话在生成 Set 中即算"视图正在生成"(其他会话后台生成不影响本视图判定)
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.state.streaming && store.state.generatingConvId === store.state.activeConversationId
|
||||
store.state.streaming && store.isGenerating(store.state.activeConversationId)
|
||||
)
|
||||
|
||||
/** UX-09:进入编辑态 — 置 editingMsgId(透传 ChatInput prop)+ 委托 ChatInput.startEditFocus 回填/聚焦。 */
|
||||
@@ -806,6 +808,22 @@ body.ai-sidebar-resizing * {
|
||||
font-size: 9px;
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
/* F-09: 多会话并发生成指示器(会话项标题行内脉冲点,多会话可同时显示) */
|
||||
.ai-conv-gen-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--df-primary, #4f8cff);
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
animation: ai-conv-gen-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ai-conv-gen-pulse {
|
||||
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
||||
50% { opacity: 1; transform: scale(1.15); }
|
||||
}
|
||||
.ai-conv-item-actions {
|
||||
display: none;
|
||||
align-items: center;
|
||||
|
||||
@@ -45,7 +45,10 @@
|
||||
@click="onSelectSearchResult(conv.id)"
|
||||
>
|
||||
<div class="ai-conv-item-main">
|
||||
<span class="ai-conv-item-title" :title="conv.title || ''">{{ conv.title || t('aiChat.newConversation') }}</span>
|
||||
<span class="ai-conv-item-title" :title="conv.title || ''">
|
||||
{{ conv.title || t('aiChat.newConversation') }}
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="ai-conv-item-actions">
|
||||
@@ -125,6 +128,8 @@
|
||||
class="ai-conv-item-title-badge"
|
||||
width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
|
||||
><polyline points="20 6 9 17 4 12"/></svg>
|
||||
<!-- F-09: 多会话并发生成指示器(后台/当前会话生成中均显脉冲点,多会话可同时显示) -->
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
</div>
|
||||
@@ -167,7 +172,10 @@
|
||||
@click="store.switchConversation(conv.id)"
|
||||
>
|
||||
<div class="ai-conv-item-main">
|
||||
<span class="ai-conv-item-title" :title="conv.title || ''">{{ conv.title || t('aiChat.newConversation') }}</span>
|
||||
<span class="ai-conv-item-title" :title="conv.title || ''">
|
||||
{{ conv.title || t('aiChat.newConversation') }}
|
||||
<span v-if="store.isGenerating(conv.id)" class="ai-conv-gen-dot" :title="t('aiChat.generating')"></span>
|
||||
</span>
|
||||
<span class="ai-conv-item-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="ai-conv-item-actions">
|
||||
|
||||
@@ -46,8 +46,9 @@ const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 当前视图是否正在生成(切走后台生成时光标不显示,与 MaxRoundsCard 一致守卫)
|
||||
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.state.streaming && store.state.generatingConvId === store.state.activeConversationId,
|
||||
store.state.streaming && store.isGenerating(store.state.activeConversationId),
|
||||
)
|
||||
|
||||
// pendingDirAuth(AiDirAuthRequired 事件置)+ 当前视图正在生成(防切走后误显)双重守卫。
|
||||
|
||||
@@ -38,8 +38,9 @@ const store = useAiStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 当前视图是否正在生成(切走后台生成时光标不显示)
|
||||
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.state.streaming && store.state.generatingConvId === store.state.activeConversationId,
|
||||
store.state.streaming && store.isGenerating(store.state.activeConversationId),
|
||||
)
|
||||
|
||||
// pendingMaxRounds(达 max 事件置)+ 当前视图正在生成(防切走后误显)双重守卫。
|
||||
|
||||
@@ -192,8 +192,9 @@ const messagesContainer = ref<HTMLDivElement>()
|
||||
const toolCardListRef = ref<InstanceType<typeof ToolCardList>>()
|
||||
|
||||
// 当前视图是否正在生成(切走后台生成时光标不显示)
|
||||
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
|
||||
const isViewingGenerating = computed(() =>
|
||||
store.state.streaming && store.state.generatingConvId === store.state.activeConversationId
|
||||
store.state.streaming && store.isGenerating(store.state.activeConversationId)
|
||||
)
|
||||
|
||||
function isLastAi(msg: AiMessage): boolean {
|
||||
@@ -347,10 +348,10 @@ watch(() => store.state.streaming, (s) => {
|
||||
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
|
||||
if (s) {
|
||||
// UX-260616-03: 新一轮生成开始 → 收起上一轮已完成的旧工具卡,聚焦当前生成内容。
|
||||
// 仅当前视图正在生成时触发(generatingConvId 对齐 activeConversationId),
|
||||
// 仅当前视图正在生成时触发(isGenerating(activeConversationId)),
|
||||
// 切走后台生成/恢复会话(streaming 初值已 true 的 onMounted 路径)不误触。
|
||||
// buildActiveToolIds/collapseAllToolLists 是 function 声明,提升可用。
|
||||
if (store.state.generatingConvId === store.state.activeConversationId) {
|
||||
if (store.isGenerating(store.state.activeConversationId)) {
|
||||
collapseAllToolLists(buildActiveToolIds(store.state.messages))
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user