|
@@ -490,7 +490,8 @@ export class AssistantTurnComponent implements OnDestroy {
|
|
|
effect(() => {
|
|
effect(() => {
|
|
|
const t = this.turn();
|
|
const t = this.turn();
|
|
|
const targets = this.targetsOf(t);
|
|
const targets = this.targetsOf(t);
|
|
|
- untracked(() => this.advance(targets));
|
|
|
|
|
|
|
+ const settled = this.settledKeys(t);
|
|
|
|
|
+ untracked(() => this.advance(targets, settled));
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
/*
|
|
/*
|
|
@@ -537,6 +538,28 @@ export class AssistantTurnComponent implements OnDestroy {
|
|
|
return out;
|
|
return out;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 已结束的块 key 集合(2026-11-11 用户反馈)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用户原话:「打开其他的对话记录的时候,文字还会有动画效果,这个是不对的。
|
|
|
|
|
+ * 只有我们在输出的时候才会有动画;查看会话历史记录时应该直接显示。」
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 根因
|
|
|
|
|
+ *
|
|
|
|
|
+ * 组件是新建的,`shownMap` 恒为空 → `advance()` 把所有块都当「新出现的块」
|
|
|
|
|
+ * 从 0 开始推 → **历史会话打开时整篇逐字重放**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 修法
|
|
|
|
|
+ *
|
|
|
|
|
+ * 推进时,**不属于 `live` 的块直接给满格**,只有 live 块才从 0 演。
|
|
|
|
|
+ * 这样:
|
|
|
|
|
+ * · 打开历史会话 → 所有块都已结束 → 一次到位,无动画
|
|
|
|
|
+ * · 发新消息 → 新块 live → 正常逐段生成
|
|
|
|
|
+ */
|
|
|
|
|
+ private settledKeys(t: AssistantTurn): Set<string> {
|
|
|
|
|
+ return new Set(t.blocks.filter((b) => !b.live).map((b) => b.key));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 推进各块的显示长度。
|
|
* 推进各块的显示长度。
|
|
|
*
|
|
*
|
|
@@ -556,18 +579,33 @@ export class AssistantTurnComponent implements OnDestroy {
|
|
|
* · 不是实时输出(历史消息、刷新后回放)→ 一次到位
|
|
* · 不是实时输出(历史消息、刷新后回放)→ 一次到位
|
|
|
* · 内容变短(重新生成)→ 直接跳转,不倒退
|
|
* · 内容变短(重新生成)→ 直接跳转,不倒退
|
|
|
*/
|
|
*/
|
|
|
- private advance(targets: Record<string, number>): void {
|
|
|
|
|
|
|
+ private advance(targets: Record<string, number>, settled: Set<string> = new Set()): void {
|
|
|
this.cancel();
|
|
this.cancel();
|
|
|
|
|
|
|
|
const live = this.turn().live;
|
|
const live = this.turn().live;
|
|
|
const cur = this.shownMap();
|
|
const cur = this.shownMap();
|
|
|
|
|
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 🔴 回合一结束 → **所有块**一次到位,不逐字重放(2026-11-11)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用户反馈:「打开别的对话记录时文字还在动画,只有输出时才该有动画。」
|
|
|
|
|
+ * 除了整回合判 live,下面还有一层按块的兜底(`settled`),
|
|
|
|
|
+ * 两者叠加保证:历史会话里任何已结束的块都不会演动画。
|
|
|
|
|
+ */
|
|
|
if (this.reduceMotion || !live) {
|
|
if (this.reduceMotion || !live) {
|
|
|
- // 历史回合直接完整显示,不逐字重放
|
|
|
|
|
this.shownMap.set({ ...targets });
|
|
this.shownMap.set({ ...targets });
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 块级兜底:即使回合整体仍 live(比如同回合里另一块还在跑),
|
|
|
|
|
+ * **已经结束的块**也直接满格 —— 不能因为旁边在动就把历史内容重放一遍。
|
|
|
|
|
+ */
|
|
|
|
|
+ const preSettled: Record<string, number> = {};
|
|
|
|
|
+ for (const k of settled) {
|
|
|
|
|
+ if (targets[k] !== undefined) preSettled[k] = targets[k];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/*
|
|
/*
|
|
|
* 🔴 实时回合的**首次**求值:从 0 开始逐字推进(2026-11-08 修复)。
|
|
* 🔴 实时回合的**首次**求值:从 0 开始逐字推进(2026-11-08 修复)。
|
|
|
*
|
|
*
|
|
@@ -582,10 +620,17 @@ export class AssistantTurnComponent implements OnDestroy {
|
|
|
if (Object.keys(cur).length === 0) {
|
|
if (Object.keys(cur).length === 0) {
|
|
|
const zero: Record<string, number> = {};
|
|
const zero: Record<string, number> = {};
|
|
|
for (const k of Object.keys(targets)) zero[k] = 0;
|
|
for (const k of Object.keys(targets)) zero[k] = 0;
|
|
|
|
|
+ /* 已结束的块从满格起,只有 live 块从 0 开始演 */
|
|
|
|
|
+ Object.assign(zero, preSettled);
|
|
|
this.shownMap.set(zero);
|
|
this.shownMap.set(zero);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const base = this.shownMap();
|
|
const base = this.shownMap();
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 已结束的块无论之前推到哪,一律对齐到目标长度(不残留半截)。
|
|
|
|
|
+ * 放在 base 之后、reset 之前,保证下面的「变短不收缩」不会把它们截回去。
|
|
|
|
|
+ */
|
|
|
|
|
+ Object.assign(base, preSettled);
|
|
|
|
|
|
|
|
/*
|
|
/*
|
|
|
* 目标变化:增长要动画;变短**不收缩**(2026-11-08)。
|
|
* 目标变化:增长要动画;变短**不收缩**(2026-11-08)。
|