cortico 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按标准 Open Responses Item 渲染 session 时间线。
|
|
3
|
+
* 连续的同 responseId assistant 侧 Item 合为一组;无 responseId 的连续 Item 也合组,
|
|
4
|
+
* 回执或输入结束当前组。function_call_output 按 call_id 回填,未匹配时单独显示。
|
|
5
|
+
* 原始 Item 包含 phase、status 与加密载荷;明文计字数,加密载荷计字符数。
|
|
6
|
+
* #n 对应 session 记录 index;合成开头不落盘且无序号。
|
|
7
|
+
* 打字机由 lifecycle.frame 管理,监听使用 signal,滚动保持由 shouldStick 判断。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ConsoleUi, Disposable } from '../../../shared/client-panel.ts';
|
|
11
|
+
import type { Lifecycle } from '../../core/lifecycle.ts';
|
|
12
|
+
import { shouldStick } from '../../ui/index.ts';
|
|
13
|
+
import { estTok } from './context.ts';
|
|
14
|
+
import { S } from './strings.ts';
|
|
15
|
+
import type { ContextRecord, Item } from '../../../../protocol/open-responses/context.ts';
|
|
16
|
+
|
|
17
|
+
/** 距底部在此像素阈值以内时保持尾部粘滞。 */
|
|
18
|
+
const STICK_PX = 60;
|
|
19
|
+
/** 超过这个长度的正文折起来,折叠行只印字数。 */
|
|
20
|
+
const FOLD_CHARS = 500;
|
|
21
|
+
/** 打字机每帧推进的字数。 */
|
|
22
|
+
const TYPE_CHARS_PER_FRAME = 8;
|
|
23
|
+
/** 事件投递帧的工具名(与 core/loop.ts 的 EXTERNAL_EVENT_FRAME 同名;那边不对浏览器包导出)。 */
|
|
24
|
+
const EXTERNAL_EVENT_FRAME = 'external_event_frame';
|
|
25
|
+
/** bot 头像。不带缓存破坏参数:一条时间线上几十个组共用同一份缓存。 */
|
|
26
|
+
const AVATAR_URL = '/api/avatar';
|
|
27
|
+
|
|
28
|
+
function toggleClass(el: HTMLElement, cls: string, on: boolean): void {
|
|
29
|
+
const set = new Set(el.className.split(' ').filter((s) => s !== ''));
|
|
30
|
+
if (on) set.add(cls);
|
|
31
|
+
else set.delete(cls);
|
|
32
|
+
el.className = [...set].join(' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type MessageItem = Extract<Item, { type: 'message' }>;
|
|
36
|
+
type ReasoningItem = Extract<Item, { type: 'reasoning' }>;
|
|
37
|
+
type CallItem = Extract<Item, { type: 'function_call' }>;
|
|
38
|
+
type OutputItem = Extract<Item, { type: 'function_call_output' }>;
|
|
39
|
+
|
|
40
|
+
/** 文本原样显示,其他内容部件显示类型标记。 */
|
|
41
|
+
function partsText(content: MessageItem['content'] | OutputItem['output']): string {
|
|
42
|
+
if (typeof content === 'string') return content;
|
|
43
|
+
return content.map((part) => {
|
|
44
|
+
const p = part as { type?: string; text?: string; refusal?: string };
|
|
45
|
+
if (typeof p.text === 'string') return p.text;
|
|
46
|
+
if (typeof p.refusal === 'string') return p.refusal;
|
|
47
|
+
return `[${p.type ?? 'part'}]`;
|
|
48
|
+
}).join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAssistantSide(item: Item): boolean {
|
|
52
|
+
return item.type === 'reasoning' || item.type === 'function_call'
|
|
53
|
+
|| (item.type === 'message' && item.role === 'assistant');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPrefix(item: Item): boolean {
|
|
57
|
+
return item.type === 'message' && (item.role === 'system' || item.role === 'developer');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isEventFrame(item: Item): boolean {
|
|
61
|
+
return item.type === 'function_call' && item.name === EXTERNAL_EVENT_FRAME;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Turn {
|
|
65
|
+
responseId: string;
|
|
66
|
+
grp: HTMLElement;
|
|
67
|
+
body: HTMLElement;
|
|
68
|
+
status: HTMLElement;
|
|
69
|
+
entries: ContextRecord[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 合成首轮的 Item 没有 session 序号。 */
|
|
73
|
+
type Ordinal = number | null;
|
|
74
|
+
|
|
75
|
+
export interface TimelineDeps {
|
|
76
|
+
ui: ConsoleUi;
|
|
77
|
+
lifecycle: Lifecycle;
|
|
78
|
+
signal: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface TimelineView {
|
|
82
|
+
el: HTMLElement;
|
|
83
|
+
/**
|
|
84
|
+
* 整份重画。`note` 是顶上一条分隔说明,`banner` 是置顶横幅(fork 视图用)。
|
|
85
|
+
*
|
|
86
|
+
* `keepScroll` 给轮询用:用户正往上翻历史时,一次例行重画不该把他弹回底部。
|
|
87
|
+
*/
|
|
88
|
+
rebuild(
|
|
89
|
+
messages: readonly ContextRecord[],
|
|
90
|
+
opts?: {
|
|
91
|
+
note?: string | null;
|
|
92
|
+
banner?: HTMLElement | null;
|
|
93
|
+
empty?: string;
|
|
94
|
+
keepScroll?: boolean;
|
|
95
|
+
/** 合成开头显示在开头 system 之后;空数组或未提供时不显示。 */
|
|
96
|
+
head?: readonly ContextRecord[] | null;
|
|
97
|
+
},
|
|
98
|
+
): void;
|
|
99
|
+
/** 追加一条(实时帧)。`live` 决定要不要动画与打字机。 */
|
|
100
|
+
append(m: ContextRecord, index: number, live: boolean): void;
|
|
101
|
+
/** 说话人展示名。头像图片加载不到时,ASSISTANT 组左栏的占位圆里印它的首字;下一次画到组时生效。 */
|
|
102
|
+
setSpeaker(name: string): void;
|
|
103
|
+
/** 是否隐藏系统前缀,下一次重画生效。 */
|
|
104
|
+
setHideSystem(hide: boolean): void;
|
|
105
|
+
/** 在滚动区顶部挂一块外部内容,随时间线一起滚;传 null 取下。重画不动它。 */
|
|
106
|
+
setHeader(node: HTMLElement | null): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createTimeline(deps: TimelineDeps): TimelineView {
|
|
110
|
+
const { ui, lifecycle, signal } = deps;
|
|
111
|
+
|
|
112
|
+
const el = ui.h('div', 'tlwrap');
|
|
113
|
+
const scroll = ui.h('div', 'tlscroll');
|
|
114
|
+
const inner = ui.h('div', 'tlinner');
|
|
115
|
+
scroll.appendChild(inner);
|
|
116
|
+
const think = ui.h('div', 'tlthink hidden');
|
|
117
|
+
think.append(ui.h('span', 'pulse'), ui.h('span', null, S.thinking));
|
|
118
|
+
const jump = ui.button(S.jumpBottom, {
|
|
119
|
+
size: 'sm',
|
|
120
|
+
onClick: () => {
|
|
121
|
+
autoScroll = true;
|
|
122
|
+
stick(true);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
jump.className = 'btn sm tljump hidden';
|
|
126
|
+
el.append(scroll, think, jump);
|
|
127
|
+
|
|
128
|
+
/** call_id → 那颗卡上等结果的槽位。每次重画清空。 */
|
|
129
|
+
const toolCalls = new Map<string, HTMLElement>();
|
|
130
|
+
/** 正在往里添 Item 的那个 ASSISTANT 组。任何非 assistant 侧的 Item 都会结束它。 */
|
|
131
|
+
let turn: Turn | null = null;
|
|
132
|
+
/** 头像占位圆里的字。 */
|
|
133
|
+
let speakerInitial = 'B';
|
|
134
|
+
let hideSystem = false;
|
|
135
|
+
/** 挂在滚动区顶部的外部内容(开场引导)。 */
|
|
136
|
+
let header: HTMLElement | null = null;
|
|
137
|
+
let autoScroll = true;
|
|
138
|
+
/** 正在跑的打字机。重画时全部收掉——否则它们会往脱离文档的节点里继续写。 */
|
|
139
|
+
const typing = new Set<Disposable>();
|
|
140
|
+
|
|
141
|
+
const stick = (force?: boolean): void => {
|
|
142
|
+
if (autoScroll || force) scroll.scrollTop = scroll.scrollHeight;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
scroll.addEventListener(
|
|
146
|
+
'scroll',
|
|
147
|
+
() => {
|
|
148
|
+
autoScroll = shouldStick(scroll.scrollTop, scroll.scrollHeight, scroll.clientHeight, STICK_PX);
|
|
149
|
+
toggleClass(jump, 'hidden', autoScroll);
|
|
150
|
+
},
|
|
151
|
+
{ signal },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const stopTyping = (): void => {
|
|
155
|
+
for (const t of [...typing]) t.dispose();
|
|
156
|
+
typing.clear();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** 打字机点击后立即完成;帧句柄由 lifecycle 管理。 */
|
|
160
|
+
const typewriter = (target: HTMLElement, text: string): void => {
|
|
161
|
+
toggleClass(target, 'typing', true);
|
|
162
|
+
let i = 0;
|
|
163
|
+
let handle: Disposable | null = null;
|
|
164
|
+
const finish = (): void => {
|
|
165
|
+
target.textContent = text;
|
|
166
|
+
toggleClass(target, 'typing', false);
|
|
167
|
+
if (handle) {
|
|
168
|
+
typing.delete(handle);
|
|
169
|
+
handle.dispose();
|
|
170
|
+
handle = null;
|
|
171
|
+
}
|
|
172
|
+
stick();
|
|
173
|
+
};
|
|
174
|
+
handle = lifecycle.frame(() => {
|
|
175
|
+
i += TYPE_CHARS_PER_FRAME;
|
|
176
|
+
if (i >= text.length) {
|
|
177
|
+
finish();
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
target.textContent = text.slice(0, i);
|
|
181
|
+
stick();
|
|
182
|
+
return undefined;
|
|
183
|
+
});
|
|
184
|
+
typing.add(handle);
|
|
185
|
+
target.addEventListener('click', finish, { signal });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
const clps = (body: HTMLElement, open: boolean): HTMLElement => {
|
|
190
|
+
const w = ui.h('div', open ? 'clps open' : 'clps');
|
|
191
|
+
const b = ui.h('div');
|
|
192
|
+
b.appendChild(body);
|
|
193
|
+
w.appendChild(b);
|
|
194
|
+
return w;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const bindToggle = (head: HTMLElement, box: HTMLElement, chev?: HTMLElement | null): void => {
|
|
198
|
+
head.addEventListener(
|
|
199
|
+
'click',
|
|
200
|
+
() => {
|
|
201
|
+
const open = !box.className.split(' ').includes('open');
|
|
202
|
+
toggleClass(box, 'open', open);
|
|
203
|
+
if (chev) toggleClass(chev, 'up', open);
|
|
204
|
+
},
|
|
205
|
+
{ signal },
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** 短的直接铺,长的折起来,折叠行上只印字数。 */
|
|
210
|
+
const foldedPre = (content: string): HTMLElement => {
|
|
211
|
+
const box = ui.h('div');
|
|
212
|
+
if ((content || '').length <= FOLD_CHARS) {
|
|
213
|
+
box.appendChild(ui.h('pre', 'mono', content));
|
|
214
|
+
return box;
|
|
215
|
+
}
|
|
216
|
+
const head = ui.h('div', 'foldhead', S.foldHead(false, content.length));
|
|
217
|
+
const pre = ui.h('pre', 'mono', content);
|
|
218
|
+
const wrap = clps(pre, false);
|
|
219
|
+
head.addEventListener(
|
|
220
|
+
'click',
|
|
221
|
+
() => {
|
|
222
|
+
const open = !wrap.className.split(' ').includes('open');
|
|
223
|
+
toggleClass(wrap, 'open', open);
|
|
224
|
+
head.textContent = S.foldHead(open, content.length);
|
|
225
|
+
},
|
|
226
|
+
{ signal },
|
|
227
|
+
);
|
|
228
|
+
box.append(head, wrap);
|
|
229
|
+
return box;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** 每个画出来的块都带上它的 Item 身份,时间线上任何一块都能对回记录。 */
|
|
233
|
+
const tag = (node: HTMLElement, entry: ContextRecord): HTMLElement => {
|
|
234
|
+
node.setAttribute('data-item-type', entry.item.type ?? 'item_reference');
|
|
235
|
+
node.setAttribute('data-item-id', entry.item.id ?? '');
|
|
236
|
+
return node;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const prettyArgs = (raw: string): string => {
|
|
240
|
+
try {
|
|
241
|
+
return JSON.stringify(JSON.parse(raw || '{}'), null, 2);
|
|
242
|
+
} catch {
|
|
243
|
+
return raw; // 参数还在流式拼装中就可能不是合法 JSON;照原文显示
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/** 序号:这条 Item 在 session 记录里的位置。合成首轮没有序号,返回 null。 */
|
|
248
|
+
const ordinal = (index: Ordinal): HTMLElement | null => {
|
|
249
|
+
if (index === null) return null;
|
|
250
|
+
const s = ui.h('span', 'meta ordinal', `#${index}`);
|
|
251
|
+
s.title = S.ordinalTitle(index);
|
|
252
|
+
return s;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const kv = (key: string, value?: string, label?: string): HTMLElement => {
|
|
256
|
+
const box = ui.h('span', `kv kv-${key}`);
|
|
257
|
+
box.appendChild(ui.h('span', 'kv-k', label ?? key));
|
|
258
|
+
if (value !== undefined) box.appendChild(ui.h('span', 'kv-v', value));
|
|
259
|
+
return box;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const gutterGlyph = (glyph: string, title: string): HTMLElement => {
|
|
263
|
+
const box = ui.h('div', 'gutter');
|
|
264
|
+
box.appendChild(ui.h('span', 'glyph', glyph));
|
|
265
|
+
box.title = title;
|
|
266
|
+
return box;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** ASSISTANT 组左栏的头像:图片加载到就用图片,否则占位圆里印说话人首字。 */
|
|
270
|
+
const avatar = (): HTMLElement => {
|
|
271
|
+
const box = ui.h('div', 'gutter avatar');
|
|
272
|
+
const image = ui.h('img', 'avatar-image hidden');
|
|
273
|
+
image.alt = '';
|
|
274
|
+
const fallback = ui.h('span', 'avatar-fallback', speakerInitial);
|
|
275
|
+
image.addEventListener('load', () => {
|
|
276
|
+
toggleClass(image, 'hidden', false);
|
|
277
|
+
toggleClass(fallback, 'hidden', true);
|
|
278
|
+
}, { signal });
|
|
279
|
+
image.addEventListener('error', () => {
|
|
280
|
+
toggleClass(image, 'hidden', true);
|
|
281
|
+
toggleClass(fallback, 'hidden', false);
|
|
282
|
+
}, { signal });
|
|
283
|
+
image.src = AVATAR_URL;
|
|
284
|
+
box.append(image, fallback);
|
|
285
|
+
return box;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const group = (cls: string, gutter: HTMLElement, right: boolean): { grp: HTMLElement; col: HTMLElement } => {
|
|
289
|
+
const grp = ui.h('div', right ? `${cls} grp r` : `${cls} grp`);
|
|
290
|
+
const col = ui.h('div', right ? 'gcol r' : 'gcol');
|
|
291
|
+
if (right) grp.append(col, gutter);
|
|
292
|
+
else grp.append(gutter, col);
|
|
293
|
+
return { grp, col };
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
const renderSystem = (entry: ContextRecord, live: boolean): HTMLElement => {
|
|
298
|
+
const item = entry.item as MessageItem;
|
|
299
|
+
const text = partsText(item.content);
|
|
300
|
+
const card = tag(ui.h('div', live ? 'tcard syscard anim-in' : 'tcard syscard'), entry);
|
|
301
|
+
const head = ui.h('div', 'cardhead');
|
|
302
|
+
const chev = ui.h('span', 'chev', '▼');
|
|
303
|
+
const firstLine = text.split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
|
|
304
|
+
head.append(
|
|
305
|
+
ui.h('span', 'badge', item.role.toUpperCase()),
|
|
306
|
+
ui.h('span', 'headtxt', firstLine),
|
|
307
|
+
ui.h('span', 'meta', `~${ui.fmt.count(estTok(text))} tok`),
|
|
308
|
+
chev,
|
|
309
|
+
);
|
|
310
|
+
const box = clps(ui.h('pre', 'mono', text || S.empty), false);
|
|
311
|
+
bindToggle(head, box, chev);
|
|
312
|
+
card.append(head, box);
|
|
313
|
+
return card;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/** user 消息=内部系统文本(事件到达通知这类),靠右的 USER 组。 */
|
|
317
|
+
const renderWorld = (entry: ContextRecord, index: Ordinal, live: boolean): HTMLElement => {
|
|
318
|
+
const item = entry.item as MessageItem;
|
|
319
|
+
const { grp, col } = group(
|
|
320
|
+
live ? 'usergrp anim-in' : 'usergrp',
|
|
321
|
+
gutterGlyph('>_', S.userGlyphTitle),
|
|
322
|
+
true,
|
|
323
|
+
);
|
|
324
|
+
tag(grp, entry);
|
|
325
|
+
const head = ui.h('div', 'ghead');
|
|
326
|
+
const ord = ordinal(index);
|
|
327
|
+
if (ord) head.appendChild(ord);
|
|
328
|
+
head.appendChild(ui.h('span', 'badge', 'USER'));
|
|
329
|
+
const world = ui.h('div', 'world');
|
|
330
|
+
for (const line of partsText(item.content).split('\n')) {
|
|
331
|
+
world.appendChild(ui.h('div', live ? 'world-line slide-in' : 'world-line', line));
|
|
332
|
+
}
|
|
333
|
+
col.append(head, world);
|
|
334
|
+
return grp;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/** reasoning 明文、摘要与加密载荷分别显示;三者皆空时不显示。 */
|
|
338
|
+
const renderReasoning = (entry: ContextRecord, live: boolean): HTMLElement | null => {
|
|
339
|
+
const item = entry.item as ReasoningItem;
|
|
340
|
+
const text = (item.content ?? [])
|
|
341
|
+
.map((part) => (part.type === 'reasoning_text' ? part.text : `[${part.type}]`)).join('\n\n');
|
|
342
|
+
const summary = item.summary
|
|
343
|
+
.map((part) => (part.type === 'summary_text' ? part.text : `[${part.type}]`)).join('\n\n');
|
|
344
|
+
const body = text || summary;
|
|
345
|
+
if (!body && item.encrypted_content == null) return null;
|
|
346
|
+
// 头上按字面报长度:明文按字数,加密载荷按字符数(上下文记录里没有逐条 token 数)
|
|
347
|
+
const block = tag(ui.h('div', 'think'), entry);
|
|
348
|
+
const head = ui.h('div', 'think-head');
|
|
349
|
+
if (body) head.appendChild(ui.h('span', 'think-label', S.visibleReasoning(body.length)));
|
|
350
|
+
if (item.encrypted_content != null) {
|
|
351
|
+
if (body) head.appendChild(ui.h('span', 'meta', '·'));
|
|
352
|
+
head.appendChild(ui.h('span', 'think-label think-enc', S.encryptedReasoning(item.encrypted_content.length)));
|
|
353
|
+
}
|
|
354
|
+
if (!body) {
|
|
355
|
+
block.appendChild(head);
|
|
356
|
+
return block;
|
|
357
|
+
}
|
|
358
|
+
const chev = ui.h('span', 'chev up', '▼');
|
|
359
|
+
head.appendChild(chev);
|
|
360
|
+
const content = ui.h('div');
|
|
361
|
+
const main = ui.h('div', 'think-body');
|
|
362
|
+
content.appendChild(main);
|
|
363
|
+
if (text && summary) {
|
|
364
|
+
const sub = ui.h('div', 'think-sub');
|
|
365
|
+
sub.append(ui.h('span', 'think-label', S.summary), ui.h('div', 'think-body', summary));
|
|
366
|
+
content.appendChild(sub);
|
|
367
|
+
}
|
|
368
|
+
const box = clps(content, true);
|
|
369
|
+
bindToggle(head, box, chev);
|
|
370
|
+
block.append(head, box);
|
|
371
|
+
if (live) typewriter(main, body);
|
|
372
|
+
else main.textContent = body;
|
|
373
|
+
return block;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/** assistant 正文是直接输出,未发送到 World;拒绝与非 final_answer 的 phase 显示标签。 */
|
|
377
|
+
const renderMonolog = (entry: ContextRecord): HTMLElement[] => {
|
|
378
|
+
const item = entry.item as MessageItem;
|
|
379
|
+
const phase = 'phase' in item && typeof item.phase === 'string' ? item.phase : '';
|
|
380
|
+
const parts = typeof item.content === 'string'
|
|
381
|
+
? [{ type: 'output_text', text: item.content }]
|
|
382
|
+
: item.content;
|
|
383
|
+
const out: HTMLElement[] = [];
|
|
384
|
+
for (const part of parts) {
|
|
385
|
+
const p = part as { type: string; text?: string; refusal?: string };
|
|
386
|
+
const refusal = typeof p.refusal === 'string';
|
|
387
|
+
const text = typeof p.text === 'string' ? p.text : refusal ? p.refusal! : '';
|
|
388
|
+
if (!text.trim()) continue;
|
|
389
|
+
const mono = tag(ui.h('div', refusal ? 'monolog refusal' : 'monolog'), entry);
|
|
390
|
+
mono.title = S.monologTitle(phase);
|
|
391
|
+
const label = refusal ? S.refusal : p.type !== 'output_text' ? p.type : phase && phase !== 'final_answer' ? phase : '';
|
|
392
|
+
if (label) mono.appendChild(ui.h('div', 'monolog-label', label));
|
|
393
|
+
mono.appendChild(ui.h('div', 'monolog-body', text));
|
|
394
|
+
out.push(mono);
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const toolCard = (entry: ContextRecord, cls: string, synthetic: boolean): HTMLElement => {
|
|
400
|
+
const item = entry.item as CallItem;
|
|
401
|
+
const card = tag(ui.h('div', cls), entry);
|
|
402
|
+
const head = ui.h('div', 'toolhead');
|
|
403
|
+
head.append(kv('tool_name', item.name || '?'), kv('call_id', item.call_id));
|
|
404
|
+
// class 键钉死为「合成」(styles.css 的 `.kv-合成` 与测试都按它找),只有印出来的字随语言走
|
|
405
|
+
if (synthetic) head.appendChild(kv('合成', undefined, S.synthetic));
|
|
406
|
+
if (item.status && item.status !== 'completed') head.appendChild(ui.h('span', 'meta', item.status));
|
|
407
|
+
const ab = ui.h('div', 'toolargs');
|
|
408
|
+
ab.appendChild(foldedPre(prettyArgs(item.arguments)));
|
|
409
|
+
const slot = ui.h('div', 'toolresult pending', '…');
|
|
410
|
+
card.append(head, ab, slot);
|
|
411
|
+
toolCalls.set(item.call_id, slot);
|
|
412
|
+
return card;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/** external_event_frame 合成调用对与普通调用同组,并标为合成。 */
|
|
416
|
+
const renderToolCall = (entry: ContextRecord, live: boolean): HTMLElement => {
|
|
417
|
+
const synthetic = isEventFrame(entry.item);
|
|
418
|
+
const cls = ['toolcall'];
|
|
419
|
+
if (synthetic) cls.push('evframe');
|
|
420
|
+
if (live) cls.push('anim-in');
|
|
421
|
+
return toolCard(entry, cls.join(' '), synthetic);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/** 工具回执:能对上号就填回那颗卡的槽位(返回 null 表示不新建卡片)。 */
|
|
425
|
+
const renderToolResult = (entry: ContextRecord, index: Ordinal, live: boolean): HTMLElement | null => {
|
|
426
|
+
const item = entry.item as OutputItem;
|
|
427
|
+
const text = partsText(item.output);
|
|
428
|
+
const slot = toolCalls.get(item.call_id);
|
|
429
|
+
if (slot) {
|
|
430
|
+
toggleClass(slot, 'pending', false);
|
|
431
|
+
slot.textContent = '';
|
|
432
|
+
tag(slot, entry);
|
|
433
|
+
slot.appendChild(foldedPre(text || S.empty));
|
|
434
|
+
stick();
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const card = tag(ui.h('div', live ? 'tcard standalone anim-in' : 'tcard standalone'), entry);
|
|
438
|
+
const head = ui.h('div', 'cardhead');
|
|
439
|
+
head.append(ui.h('span', 'badge', 'function_call_output'), kv('call_id', item.call_id));
|
|
440
|
+
const ord = ordinal(index);
|
|
441
|
+
if (ord) head.appendChild(ord);
|
|
442
|
+
card.append(head, foldedPre(text || S.empty));
|
|
443
|
+
return card;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const renderOther = (entry: ContextRecord, index: Ordinal, live: boolean): HTMLElement => {
|
|
447
|
+
const item = entry.item;
|
|
448
|
+
const card = tag(ui.h('div', live ? 'tcard standalone anim-in' : 'tcard standalone'), entry);
|
|
449
|
+
const head = ui.h('div', 'cardhead');
|
|
450
|
+
head.appendChild(ui.h('span', 'badge', (item.type ?? 'item_reference').toUpperCase()));
|
|
451
|
+
const ord = ordinal(index);
|
|
452
|
+
if (ord) head.appendChild(ord);
|
|
453
|
+
card.appendChild(head);
|
|
454
|
+
if (item.type === 'compaction') {
|
|
455
|
+
head.appendChild(ui.h('span', 'think-label think-enc', S.encryptedPayload(item.encrypted_content.length)));
|
|
456
|
+
} else {
|
|
457
|
+
card.appendChild(foldedPre(JSON.stringify(item, null, 2)));
|
|
458
|
+
}
|
|
459
|
+
return card;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/** 连续同 Response 的 assistant 侧 Item 共用一组;缺 responseId 时按连续性分组。可展开原始 Item。 */
|
|
463
|
+
const ensureTurn = (entry: ContextRecord, index: Ordinal, live: boolean): Turn => {
|
|
464
|
+
const responseId = entry.context.responseId ?? '';
|
|
465
|
+
if (turn && turn.responseId === responseId) return turn;
|
|
466
|
+
const { grp, col } = group(live ? 'tcard turn anim-in' : 'tcard turn', avatar(), false);
|
|
467
|
+
if (responseId) grp.setAttribute('data-response-id', responseId);
|
|
468
|
+
const head = ui.h('div', 'ghead turnhead');
|
|
469
|
+
const status = ui.h('span', 'meta turnstatus');
|
|
470
|
+
head.appendChild(ui.h('span', 'badge', 'ASSISTANT'));
|
|
471
|
+
const ord = ordinal(index);
|
|
472
|
+
if (ord) head.appendChild(ord);
|
|
473
|
+
head.appendChild(status);
|
|
474
|
+
head.appendChild(ui.h('span', 'grow'));
|
|
475
|
+
const rawToggle = ui.h('span', 'meta rawtoggle', S.rawItems);
|
|
476
|
+
rawToggle.setAttribute('role', 'button');
|
|
477
|
+
head.appendChild(rawToggle);
|
|
478
|
+
const body = ui.h('div', 'turnbody');
|
|
479
|
+
const rawPre = ui.h('pre', 'mono');
|
|
480
|
+
const rawBox = clps(rawPre, false);
|
|
481
|
+
toggleClass(rawBox, 'turnraw', true);
|
|
482
|
+
col.append(head, body, rawBox);
|
|
483
|
+
const next: Turn = { responseId, grp, body, status, entries: [] };
|
|
484
|
+
rawToggle.addEventListener('click', () => {
|
|
485
|
+
const open = !rawBox.className.split(' ').includes('open');
|
|
486
|
+
if (open) rawPre.textContent = JSON.stringify(next.entries.map((e) => e.item), null, 2);
|
|
487
|
+
toggleClass(rawBox, 'open', open);
|
|
488
|
+
}, { signal });
|
|
489
|
+
inner.appendChild(grp);
|
|
490
|
+
turn = next;
|
|
491
|
+
return next;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const renderOne = (entry: ContextRecord, index: Ordinal, live: boolean): void => {
|
|
495
|
+
const item = entry.item;
|
|
496
|
+
if (isAssistantSide(item)) {
|
|
497
|
+
if (live) hideThinking();
|
|
498
|
+
const t = ensureTurn(entry, index, live);
|
|
499
|
+
t.entries.push(entry);
|
|
500
|
+
const status = entry.context.responseStatus;
|
|
501
|
+
t.status.textContent = status && status !== 'completed' ? status : '';
|
|
502
|
+
if (item.type === 'reasoning') {
|
|
503
|
+
const block = renderReasoning(entry, live);
|
|
504
|
+
if (block) t.body.appendChild(block);
|
|
505
|
+
} else if (item.type === 'function_call') t.body.appendChild(renderToolCall(entry, live));
|
|
506
|
+
else t.body.append(...renderMonolog(entry));
|
|
507
|
+
// 一段里全是空 Item(空 reasoning、空正文)时组头孤零零一行,标个「空」
|
|
508
|
+
toggleClass(t.grp, 'empty', t.body.children.length === 0);
|
|
509
|
+
stick();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
turn = null;
|
|
513
|
+
let node: HTMLElement | null;
|
|
514
|
+
if (item.type === 'message' && item.role === 'user') {
|
|
515
|
+
node = renderWorld(entry, index, live);
|
|
516
|
+
if (live) showThinking();
|
|
517
|
+
} else if (isPrefix(item)) node = hideSystem ? null : renderSystem(entry, live);
|
|
518
|
+
else if (item.type === 'function_call_output') node = renderToolResult(entry, index, live);
|
|
519
|
+
else node = renderOther(entry, index, live);
|
|
520
|
+
if (node) inner.appendChild(node);
|
|
521
|
+
stick();
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const renderHead = (entries: readonly ContextRecord[]): void => {
|
|
525
|
+
turn = null;
|
|
526
|
+
inner.appendChild(ui.h('div', 'divider sessionhead', S.headStart));
|
|
527
|
+
for (const entry of entries) renderOne(entry, null, false);
|
|
528
|
+
turn = null;
|
|
529
|
+
inner.appendChild(ui.h('div', 'divider sessionhead', S.headEnd));
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
function showThinking(): void {
|
|
533
|
+
toggleClass(think, 'hidden', false);
|
|
534
|
+
stick();
|
|
535
|
+
}
|
|
536
|
+
function hideThinking(): void {
|
|
537
|
+
toggleClass(think, 'hidden', true);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
el,
|
|
542
|
+
rebuild(messages, opts) {
|
|
543
|
+
const wasStuck = autoScroll;
|
|
544
|
+
const keepTop = scroll.scrollTop;
|
|
545
|
+
stopTyping();
|
|
546
|
+
hideThinking();
|
|
547
|
+
toolCalls.clear();
|
|
548
|
+
turn = null;
|
|
549
|
+
inner.replaceChildren();
|
|
550
|
+
if (opts?.banner) inner.appendChild(opts.banner);
|
|
551
|
+
if (opts?.note) inner.appendChild(ui.h('div', 'divider', opts.note));
|
|
552
|
+
if (!messages.length) {
|
|
553
|
+
inner.appendChild(ui.placeholder(opts?.empty ?? S.sessionEmpty));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// 合成开头插在开头的 system 卡之后(与请求体里的位置一致)。
|
|
557
|
+
const head = opts?.head?.length ? opts.head : null;
|
|
558
|
+
let headDone = !head;
|
|
559
|
+
messages.forEach((entry, i) => {
|
|
560
|
+
if (!headDone && !isPrefix(entry.item)) {
|
|
561
|
+
renderHead(head!);
|
|
562
|
+
headDone = true;
|
|
563
|
+
}
|
|
564
|
+
renderOne(entry, i, false);
|
|
565
|
+
});
|
|
566
|
+
if (!headDone) renderHead(head!);
|
|
567
|
+
if (opts?.keepScroll && !wasStuck) {
|
|
568
|
+
scroll.scrollTop = keepTop;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
autoScroll = true;
|
|
572
|
+
toggleClass(jump, 'hidden', true);
|
|
573
|
+
stick(true);
|
|
574
|
+
},
|
|
575
|
+
append(entry, index, live) {
|
|
576
|
+
renderOne(entry, index, live);
|
|
577
|
+
},
|
|
578
|
+
setHideSystem(hide) {
|
|
579
|
+
hideSystem = hide;
|
|
580
|
+
},
|
|
581
|
+
setHeader(node) {
|
|
582
|
+
header?.remove();
|
|
583
|
+
header = node;
|
|
584
|
+
if (node) scroll.insertBefore(node, inner);
|
|
585
|
+
},
|
|
586
|
+
setSpeaker(name) {
|
|
587
|
+
speakerInitial = name.trim().slice(0, 1).toUpperCase() || 'B';
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
}
|