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,292 @@
|
|
|
1
|
+
/** 前缀以单文档编辑;模板对应可跟随编辑变化的区间,只读区间由 changeFilter 拦截。 */
|
|
2
|
+
|
|
3
|
+
import { EditorState, StateEffect, StateField, type Extension, type Range } from '@codemirror/state';
|
|
4
|
+
import {
|
|
5
|
+
Decoration, EditorView, keymap, lineNumbers, type DecorationSet,
|
|
6
|
+
} from '@codemirror/view';
|
|
7
|
+
import { S } from './strings.ts';
|
|
8
|
+
|
|
9
|
+
/** 一块 = 一份可编辑模板(或一段只读文本)在文档里占的那一截。 */
|
|
10
|
+
export interface PrefixBlock {
|
|
11
|
+
title: string;
|
|
12
|
+
/** 可编辑源的 key;无 = 这一块来自代码,只读 */
|
|
13
|
+
sourceKey?: string;
|
|
14
|
+
/** 按来源分档,决定底色 */
|
|
15
|
+
tone: 'persona' | 'world' | 'derived';
|
|
16
|
+
/** 这一块此刻的文本 */
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 块在文档里的位置。`from`/`to` 随编辑实时更新。 */
|
|
21
|
+
interface BlockRange {
|
|
22
|
+
block: PrefixBlock;
|
|
23
|
+
from: number;
|
|
24
|
+
to: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 整份重排(重新载入时用)。 */
|
|
28
|
+
const setBlocks = StateEffect.define<BlockRange[]>();
|
|
29
|
+
/** 鼠标此刻停在第几块上(-1 = 不在任何块上)。纯装饰用,不影响文档。 */
|
|
30
|
+
const setHover = StateEffect.define<number>();
|
|
31
|
+
|
|
32
|
+
const hoverField = StateField.define<number>({
|
|
33
|
+
create: () => -1,
|
|
34
|
+
update(value, tr) {
|
|
35
|
+
for (const e of tr.effects) if (e.is(setHover)) return e.value;
|
|
36
|
+
return value;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** 保存状态。`idle` = 这次打开还没动过,什么也不提示。 */
|
|
41
|
+
export type SaveState = 'idle' | 'dirty' | 'saving' | 'saved';
|
|
42
|
+
const setSaveState = StateEffect.define<SaveState>();
|
|
43
|
+
|
|
44
|
+
const SAVE_LABEL: Record<SaveState, string> = {
|
|
45
|
+
idle: '',
|
|
46
|
+
dirty: S.saveHintDirty,
|
|
47
|
+
saving: S.saving,
|
|
48
|
+
saved: S.saved,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 保存状态,外加它落在哪一行(`at` = 换状态那一刻的光标位置)。
|
|
53
|
+
*
|
|
54
|
+
* **文档一变就是 `dirty`**,不等页面来通知——提示的时效性不该押在上层的回调链上;
|
|
55
|
+
* 页面只在保存前后覆盖成 `saving` / `saved`。
|
|
56
|
+
*/
|
|
57
|
+
const saveField = StateField.define<{ state: SaveState; at: number }>({
|
|
58
|
+
create: () => ({ state: 'idle', at: 0 }),
|
|
59
|
+
update(value, tr) {
|
|
60
|
+
for (const e of tr.effects) {
|
|
61
|
+
if (e.is(setSaveState)) return { state: e.value, at: tr.state.selection.main.head };
|
|
62
|
+
}
|
|
63
|
+
if (tr.docChanged) return { state: 'dirty', at: tr.state.selection.main.head };
|
|
64
|
+
return value;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** 编辑时映射块边界:to 的 assoc=1、from 的 assoc=-1,使边界插入内容归入该块。 */
|
|
69
|
+
const blockField = StateField.define<BlockRange[]>({
|
|
70
|
+
create: () => [],
|
|
71
|
+
update(value, tr) {
|
|
72
|
+
for (const e of tr.effects) if (e.is(setBlocks)) return e.value;
|
|
73
|
+
if (!tr.docChanged) return value;
|
|
74
|
+
return value.map((r) => ({
|
|
75
|
+
block: r.block,
|
|
76
|
+
from: tr.changes.mapPos(r.from, -1),
|
|
77
|
+
to: tr.changes.mapPos(r.to, 1),
|
|
78
|
+
}));
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/** 悬停与光标高亮覆盖所属块的全部行。 */
|
|
83
|
+
const blockDecorations = EditorView.decorations.compute(
|
|
84
|
+
[blockField, hoverField, saveField, 'doc', 'selection'],
|
|
85
|
+
(state) => {
|
|
86
|
+
const ranges: Range<Decoration>[] = [];
|
|
87
|
+
const blocks = state.field(blockField);
|
|
88
|
+
const hovered = state.field(hoverField);
|
|
89
|
+
const caret = state.selection.main.head;
|
|
90
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
91
|
+
const r = blocks[i];
|
|
92
|
+
const first = state.doc.lineAt(Math.min(r.from, state.doc.length)).number;
|
|
93
|
+
const last = state.doc.lineAt(Math.max(r.from, Math.min(r.to, state.doc.length))).number;
|
|
94
|
+
const active = r.block.sourceKey && caret >= r.from && caret <= r.to;
|
|
95
|
+
for (let n = first; n <= last; n++) {
|
|
96
|
+
const line = state.doc.line(n);
|
|
97
|
+
const cls = `cm-block cm-block-${r.block.tone}`
|
|
98
|
+
+ (n === first ? ' cm-block-first' : '')
|
|
99
|
+
+ (r.block.sourceKey ? '' : ' cm-block-readonly')
|
|
100
|
+
+ (i === hovered ? ' cm-block-hover' : '')
|
|
101
|
+
+ (active ? ' cm-block-active' : '');
|
|
102
|
+
// 块首行挂上序号:左侧标签与右栏靠它读**真实**位置(见 blockGeometry)
|
|
103
|
+
ranges.push(Decoration.line(
|
|
104
|
+
n === first ? { class: cls, attributes: { 'data-block': String(i) } } : { class: cls },
|
|
105
|
+
).range(line.from));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/* 保存提示使用属性与 CSS 伪元素,不插入 contentEditable 节点,以保留空行退格行为。已保存提示仅留在保存时的光标行。 */
|
|
109
|
+
const save = state.field(saveField);
|
|
110
|
+
const line = state.doc.lineAt(Math.min(caret, state.doc.length));
|
|
111
|
+
const pinned = save.state === 'saved'
|
|
112
|
+
&& state.doc.lineAt(Math.min(save.at, state.doc.length)).number !== line.number;
|
|
113
|
+
const label = pinned ? '' : SAVE_LABEL[save.state];
|
|
114
|
+
if (label) {
|
|
115
|
+
ranges.push(Decoration.line({
|
|
116
|
+
class: `cm-save-${save.state}`,
|
|
117
|
+
attributes: { 'data-save-hint': label },
|
|
118
|
+
}).range(line.from));
|
|
119
|
+
}
|
|
120
|
+
return Decoration.set(ranges, true) as DecorationSet;
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/** 鼠标移动时算出停在第几块上。只在结果变了时 dispatch,免得每帧一次事务。 */
|
|
125
|
+
const hoverTracker = EditorView.domEventHandlers({
|
|
126
|
+
mousemove(event, view) {
|
|
127
|
+
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
|
|
128
|
+
const blocks = view.state.field(blockField);
|
|
129
|
+
let idx = -1;
|
|
130
|
+
if (pos != null) idx = blocks.findIndex((r) => pos >= r.from && pos <= r.to);
|
|
131
|
+
if (idx !== view.state.field(hoverField)) view.dispatch({ effects: setHover.of(idx) });
|
|
132
|
+
return false;
|
|
133
|
+
},
|
|
134
|
+
mouseleave(_event, view) {
|
|
135
|
+
if (view.state.field(hoverField) !== -1) view.dispatch({ effects: setHover.of(-1) });
|
|
136
|
+
return false;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/* 块标签由页面按 blockGeometry 绝对定位。 */
|
|
141
|
+
|
|
142
|
+
/** 落在只读块里的改动一律拦掉。 */
|
|
143
|
+
const readonlyGuard = EditorState.changeFilter.of((tr) => {
|
|
144
|
+
const blocks = tr.startState.field(blockField, false);
|
|
145
|
+
if (!blocks) return true;
|
|
146
|
+
let ok = true;
|
|
147
|
+
tr.changes.iterChangedRanges((fromA, toA) => {
|
|
148
|
+
for (const r of blocks) {
|
|
149
|
+
if (r.block.sourceKey) continue;
|
|
150
|
+
// 只读块内部(不含两端边界)被碰到就否决整笔改动
|
|
151
|
+
if (fromA < r.to && toA > r.from) ok = false;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return ok;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export interface PrefixEditorOptions {
|
|
158
|
+
parent: HTMLElement;
|
|
159
|
+
/** 文档变化时报出每块的当前文本(已按区间切好)。只报可编辑块。 */
|
|
160
|
+
onChange(changed: Array<{ sourceKey: string; text: string }>): void;
|
|
161
|
+
/** Ctrl/Cmd+S。**不自动保存**,写盘只从这里发起。 */
|
|
162
|
+
onSave?(): void;
|
|
163
|
+
/** 几何变化(行数、滚动、尺寸)时通知,用来重排右栏。 */
|
|
164
|
+
onGeometry?(): void;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface PrefixEditor {
|
|
168
|
+
view: EditorView;
|
|
169
|
+
/** 整份重载:重新拼文档、重设区间。 */
|
|
170
|
+
setBlocks(blocks: PrefixBlock[]): void;
|
|
171
|
+
/** 覆盖行尾提示的状态(保存前后各一次)。打字会自动回到 `dirty`。 */
|
|
172
|
+
setSaveState(state: SaveState): void;
|
|
173
|
+
/** 内容区顶边的视口坐标。左右两层据此换算到与正文同一条基线上。 */
|
|
174
|
+
contentTop(): number;
|
|
175
|
+
/** 某一块此刻在视口里的垂直位置(相对编辑器内容顶部),用来对齐左侧标签与右栏。 */
|
|
176
|
+
blockGeometry(): Array<{
|
|
177
|
+
sourceKey?: string;
|
|
178
|
+
title: string;
|
|
179
|
+
tone: PrefixBlock['tone'];
|
|
180
|
+
top: number;
|
|
181
|
+
height: number;
|
|
182
|
+
}>;
|
|
183
|
+
dispose(): void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 块文本拼成一份文档:块之间**恰好一个换行**,那一格属于分隔本身。 */
|
|
187
|
+
function joinBlocks(blocks: PrefixBlock[]): { doc: string; ranges: BlockRange[] } {
|
|
188
|
+
const ranges: BlockRange[] = [];
|
|
189
|
+
let doc = '';
|
|
190
|
+
for (const block of blocks) {
|
|
191
|
+
const from = doc.length;
|
|
192
|
+
doc += block.text;
|
|
193
|
+
ranges.push({ block, from, to: doc.length });
|
|
194
|
+
doc += '\n';
|
|
195
|
+
}
|
|
196
|
+
return { doc: doc.replace(/\n$/, ''), ranges };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createPrefixEditor(opts: PrefixEditorOptions): PrefixEditor {
|
|
200
|
+
let last = new Map<string, string>();
|
|
201
|
+
|
|
202
|
+
const view = new EditorView({
|
|
203
|
+
parent: opts.parent,
|
|
204
|
+
state: EditorState.create({
|
|
205
|
+
doc: '',
|
|
206
|
+
extensions: [
|
|
207
|
+
blockField,
|
|
208
|
+
hoverField,
|
|
209
|
+
saveField,
|
|
210
|
+
blockDecorations,
|
|
211
|
+
hoverTracker,
|
|
212
|
+
lineNumbers(),
|
|
213
|
+
readonlyGuard,
|
|
214
|
+
keymap.of([{
|
|
215
|
+
key: 'Mod-s',
|
|
216
|
+
preventDefault: true,
|
|
217
|
+
run: () => { opts.onSave?.(); return true; },
|
|
218
|
+
}]),
|
|
219
|
+
EditorView.lineWrapping,
|
|
220
|
+
EditorView.updateListener.of((u) => {
|
|
221
|
+
if (u.docChanged) {
|
|
222
|
+
const changed: Array<{ sourceKey: string; text: string }> = [];
|
|
223
|
+
for (const r of u.state.field(blockField)) {
|
|
224
|
+
const key = r.block.sourceKey;
|
|
225
|
+
if (!key) continue;
|
|
226
|
+
const text = u.state.doc.sliceString(
|
|
227
|
+
Math.min(r.from, u.state.doc.length),
|
|
228
|
+
Math.min(r.to, u.state.doc.length),
|
|
229
|
+
);
|
|
230
|
+
if (last.get(key) !== text) {
|
|
231
|
+
last.set(key, text);
|
|
232
|
+
changed.push({ sourceKey: key, text });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (changed.length) opts.onChange(changed);
|
|
236
|
+
}
|
|
237
|
+
if (u.docChanged || u.geometryChanged || u.viewportChanged) opts.onGeometry?.();
|
|
238
|
+
}),
|
|
239
|
+
] as Extension[],
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
view,
|
|
245
|
+
setBlocks(blocks) {
|
|
246
|
+
const { doc, ranges } = joinBlocks(blocks);
|
|
247
|
+
last = new Map(blocks.filter((b) => b.sourceKey).map((b) => [b.sourceKey!, b.text]));
|
|
248
|
+
view.dispatch({
|
|
249
|
+
// 载入不是"改动":`setSaveState` 与 `setBlocks` 同批发,把 docChanged
|
|
250
|
+
// 顺带置上的 dirty 压回去,否则一进页面就挂着"按 Ctrl+S 保存"
|
|
251
|
+
changes: { from: 0, to: view.state.doc.length, insert: doc },
|
|
252
|
+
effects: [setBlocks.of(ranges), setSaveState.of('idle')],
|
|
253
|
+
});
|
|
254
|
+
opts.onGeometry?.();
|
|
255
|
+
},
|
|
256
|
+
setSaveState(state) {
|
|
257
|
+
view.dispatch({ effects: setSaveState.of(state) });
|
|
258
|
+
},
|
|
259
|
+
contentTop: () => view.contentDOM.getBoundingClientRect().top,
|
|
260
|
+
/** 优先测量已渲染块首行的 DOM 位置;不可见行回退到 lineBlockAt 估算值。 */
|
|
261
|
+
blockGeometry() {
|
|
262
|
+
const out: Array<{
|
|
263
|
+
sourceKey?: string; title: string; tone: PrefixBlock['tone']; top: number; height: number;
|
|
264
|
+
}> = [];
|
|
265
|
+
const len = view.state.doc.length;
|
|
266
|
+
const blocks = view.state.field(blockField);
|
|
267
|
+
const base = view.contentDOM.getBoundingClientRect().top;
|
|
268
|
+
const topOf = (i: number, pos: number): number => {
|
|
269
|
+
const el = view.contentDOM.querySelector(`[data-block="${i}"]`);
|
|
270
|
+
if (el) return el.getBoundingClientRect().top - base;
|
|
271
|
+
return view.lineBlockAt(Math.min(pos, len)).top;
|
|
272
|
+
};
|
|
273
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
274
|
+
const r = blocks[i];
|
|
275
|
+
const top = topOf(i, r.from);
|
|
276
|
+
// 高度取到下一块的起点;最后一块吃到内容底部
|
|
277
|
+
const next = i + 1 < blocks.length
|
|
278
|
+
? topOf(i + 1, blocks[i + 1].from)
|
|
279
|
+
: view.contentDOM.getBoundingClientRect().height;
|
|
280
|
+
out.push({
|
|
281
|
+
sourceKey: r.block.sourceKey,
|
|
282
|
+
title: r.block.title,
|
|
283
|
+
tone: r.block.tone,
|
|
284
|
+
top,
|
|
285
|
+
height: Math.max(next - top, 0),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
},
|
|
290
|
+
dispose: () => view.destroy(),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统提示词以单个编辑器中的一份文档呈现,分段表示为行区间。段与模板源的映射来自后端 PrefixSegment.sourceKey。
|
|
3
|
+
* 编辑不自动保存,用户通过 Ctrl+S 写回;未保存状态在界面显示。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { get, post } from '../../core/api.ts';
|
|
7
|
+
import { pageIntro } from '../../ui/page.ts';
|
|
8
|
+
import type { FeatureContext, FrameworkFeature } from '../feature.ts';
|
|
9
|
+
import { estTok } from '../live/context.ts';
|
|
10
|
+
import { createPrefixEditor, type PrefixBlock } from './editor.ts';
|
|
11
|
+
import { S } from './strings.ts';
|
|
12
|
+
import { varWarnings, type PromptDoc, type PromptVar } from './view.ts';
|
|
13
|
+
|
|
14
|
+
const PROMPTS_ROUTE = 'prompts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 两侧栏宽度保存在模块作用域,切换控制台页面时保留,浏览器刷新后恢复默认值。
|
|
18
|
+
*/
|
|
19
|
+
const colWidth = { flags: 158, rail: 190 };
|
|
20
|
+
const COL_MIN = 96;
|
|
21
|
+
const COL_MAX = 460;
|
|
22
|
+
|
|
23
|
+
interface PrefixSegment {
|
|
24
|
+
title: string;
|
|
25
|
+
text: string;
|
|
26
|
+
sourceKey?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 段按来源分三档上色:人格自有 / World 环境 / 运行时现拼。 */
|
|
30
|
+
function segmentTone(sourceKey: string | undefined): PrefixBlock['tone'] {
|
|
31
|
+
if (!sourceKey) return 'derived';
|
|
32
|
+
return sourceKey.startsWith('worlds.') ? 'world' : 'persona';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function mountPrompts(ctx: FeatureContext, opts: { embedded?: boolean } = {}): Promise<void> {
|
|
36
|
+
const { ui, root } = ctx;
|
|
37
|
+
const intro = pageIntro(ui, S.title, S.introDesc);
|
|
38
|
+
const status = ui.msgline('');
|
|
39
|
+
const toolbar = ui.h('div', 'prompt-toolbar');
|
|
40
|
+
toolbar.append(status, ui.h('span', 'grow'));
|
|
41
|
+
|
|
42
|
+
// 标签、分割线与右栏均按 blockGeometry 定位。
|
|
43
|
+
const stage = ui.h('div', 'prefix-stage');
|
|
44
|
+
const flags = ui.h('div', 'prefix-flags');
|
|
45
|
+
const host = ui.h('div', 'prefix-host');
|
|
46
|
+
const rail = ui.h('div', 'prefix-rail');
|
|
47
|
+
const gripL = ui.h('div', 'prefix-grip');
|
|
48
|
+
const gripR = ui.h('div', 'prefix-grip');
|
|
49
|
+
// 分割线单独一层:它要从旗尖一路延伸到右栏末端,横跨三列,谁的内部都装不下
|
|
50
|
+
const dividers = ui.h('div', 'prefix-dividers');
|
|
51
|
+
stage.append(flags, gripL, host, gripR, rail, dividers);
|
|
52
|
+
if (ctx.capabilities.sessionControl === true) {
|
|
53
|
+
const reload = ui.button(S.reloadSession, {
|
|
54
|
+
size: 'sm',
|
|
55
|
+
onClick: () => {
|
|
56
|
+
const lock = ui.disable(reload);
|
|
57
|
+
status.textContent = S.reloading;
|
|
58
|
+
void post<{ result?: string }>('/api/session/reload-prefix', undefined, { signal: ctx.signal })
|
|
59
|
+
.then(async (out) => {
|
|
60
|
+
status.textContent = out?.result || S.prefixReloaded;
|
|
61
|
+
await load();
|
|
62
|
+
})
|
|
63
|
+
.catch((err: unknown) => {
|
|
64
|
+
if ((err as { name?: string } | null)?.name === 'AbortError') return;
|
|
65
|
+
ctx.onError(err);
|
|
66
|
+
status.className = 'msgline bad';
|
|
67
|
+
status.textContent = err instanceof Error ? err.message : String(err);
|
|
68
|
+
})
|
|
69
|
+
.finally(() => lock.dispose());
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
toolbar.appendChild(reload);
|
|
73
|
+
}
|
|
74
|
+
if (!opts.embedded) root.appendChild(intro);
|
|
75
|
+
root.append(toolbar, stage);
|
|
76
|
+
|
|
77
|
+
/** key → 那份模板的声明与 revision(保存要带 baseRevision 做乐观锁)。 */
|
|
78
|
+
const docs = new Map<string, PromptDoc>();
|
|
79
|
+
/** 改过但还没写盘的块文本(按 key 覆盖)。Ctrl+S 之前它只躺在这儿。 */
|
|
80
|
+
const dirty = new Map<string, string>();
|
|
81
|
+
|
|
82
|
+
function setStatus(text: string, bad?: boolean): void {
|
|
83
|
+
status.className = bad ? 'msgline bad' : 'msgline';
|
|
84
|
+
status.textContent = text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 未存的改动别让它随手翻页丢掉——这一页没有自动保存兜底。 */
|
|
88
|
+
ctx.lifecycle.own(ctx.router.addLeaveGuard(
|
|
89
|
+
() => (dirty.size ? S.leaveUnsaved : null),
|
|
90
|
+
));
|
|
91
|
+
|
|
92
|
+
async function flushSaves(): Promise<void> {
|
|
93
|
+
const batch = [...dirty.entries()];
|
|
94
|
+
dirty.clear();
|
|
95
|
+
if (!batch.length) return;
|
|
96
|
+
setStatus(S.saving);
|
|
97
|
+
editor.setSaveState('saving');
|
|
98
|
+
const done: string[] = [];
|
|
99
|
+
for (const [key, text] of batch) {
|
|
100
|
+
const doc = docs.get(key);
|
|
101
|
+
if (!doc) continue;
|
|
102
|
+
try {
|
|
103
|
+
const out = await post<{ error?: string; result?: string; revision?: string }>(
|
|
104
|
+
'/api/prompts',
|
|
105
|
+
{ key, content: text, baseRevision: doc.revision },
|
|
106
|
+
{ signal: ctx.signal },
|
|
107
|
+
);
|
|
108
|
+
if (out?.error) throw new Error(out.error);
|
|
109
|
+
doc.content = text;
|
|
110
|
+
doc.revision = out?.revision;
|
|
111
|
+
done.push(doc.title);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if ((err as { name?: string } | null)?.name === 'AbortError') return;
|
|
114
|
+
ctx.onError(err);
|
|
115
|
+
// 保存失败的改动留在待保存队列。
|
|
116
|
+
dirty.set(key, text);
|
|
117
|
+
editor.setSaveState('dirty');
|
|
118
|
+
setStatus(S.saveFailedFor(doc.title, String((err as Error)?.message ?? err)), true);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
editor.setSaveState('saved');
|
|
123
|
+
setStatus(S.savedList(done.join(S.listSep)));
|
|
124
|
+
refreshRail();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const editor = createPrefixEditor({
|
|
128
|
+
parent: host,
|
|
129
|
+
onChange: (changed) => {
|
|
130
|
+
for (const c of changed) dirty.set(c.sourceKey, c.text);
|
|
131
|
+
setStatus(S.dirtyCount(dirty.size));
|
|
132
|
+
refreshRail();
|
|
133
|
+
},
|
|
134
|
+
onSave: () => { void flushSaves(); },
|
|
135
|
+
onGeometry: () => positionSides(),
|
|
136
|
+
});
|
|
137
|
+
ctx.lifecycle.own({ dispose: () => editor.dispose() });
|
|
138
|
+
|
|
139
|
+
// ── 右栏:每块一格,列它那份模板的占位符;点开是浮层 ─────────────────
|
|
140
|
+
let openPop: HTMLElement | null = null;
|
|
141
|
+
/** 捕获阶段关闭的块键,用于将同键点击处理为关闭。 */
|
|
142
|
+
let justClosed: string | null = null;
|
|
143
|
+
function closePop(): void { openPop?.remove(); openPop = null; }
|
|
144
|
+
ctx.lifecycle.own({ dispose: closePop });
|
|
145
|
+
|
|
146
|
+
// 点浮层以外的任何地方都立刻收起来。**用捕获阶段**:chip 自己的 onClick 在冒泡阶段
|
|
147
|
+
// 才跑,所以这里先收、它再决定要不要开,于是"再点一次同一个"照样是收起。
|
|
148
|
+
const onDocClick = (ev: Event): void => {
|
|
149
|
+
if (!openPop) return;
|
|
150
|
+
const t = ev.target as Node | null;
|
|
151
|
+
if (t && openPop.contains(t)) return;
|
|
152
|
+
// 只有点的**正是打开它的那颗 chip** 才算"收起";点别处纯粹是关掉,
|
|
153
|
+
// 不能留下标记——否则下次点那颗 chip 会被当成收起而打不开。
|
|
154
|
+
const chip = t instanceof Element ? t.closest('.varchip') : null;
|
|
155
|
+
const key = openPop.getAttribute('data-var');
|
|
156
|
+
justClosed = chip?.getAttribute('data-chip') === key ? key : null;
|
|
157
|
+
closePop();
|
|
158
|
+
};
|
|
159
|
+
document.addEventListener('click', onDocClick, true);
|
|
160
|
+
ctx.lifecycle.own({ dispose: () => document.removeEventListener('click', onDocClick, true) });
|
|
161
|
+
const onEsc = (ev: KeyboardEvent): void => { if (ev.key === 'Escape') closePop(); };
|
|
162
|
+
document.addEventListener('keydown', onEsc);
|
|
163
|
+
ctx.lifecycle.own({ dispose: () => document.removeEventListener('keydown', onEsc) });
|
|
164
|
+
|
|
165
|
+
function varPopup(v: PromptVar): HTMLElement {
|
|
166
|
+
const pop = ui.h('div', 'varpop');
|
|
167
|
+
pop.appendChild(ui.h('div', 'varpop-name mono', `{{${v.name}}}`));
|
|
168
|
+
pop.appendChild(ui.h('div', 'varpop-desc', v.description));
|
|
169
|
+
pop.appendChild(ui.h('div', 'varpop-label', v.multiline ? S.fillNowMultiline : S.fillNow));
|
|
170
|
+
const val = ui.h('pre', 'mono varpop-value');
|
|
171
|
+
val.textContent = v.value === undefined
|
|
172
|
+
? S.varUnreported
|
|
173
|
+
: v.value === ''
|
|
174
|
+
? S.varEmpty
|
|
175
|
+
: v.value;
|
|
176
|
+
pop.appendChild(val);
|
|
177
|
+
return pop;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 按块几何重排;测量编辑器内容与 stage 的坐标差以换算位置。 */
|
|
181
|
+
function positionSides(): void {
|
|
182
|
+
const geo = editor.blockGeometry();
|
|
183
|
+
const off = editor.contentTop() - stage.getBoundingClientRect().top;
|
|
184
|
+
for (let i = 0; i < rail.children.length && i < geo.length; i++) {
|
|
185
|
+
const el = rail.children[i] as HTMLElement;
|
|
186
|
+
el.style.top = `${geo[i].top + off}px`;
|
|
187
|
+
el.style.minHeight = `${geo[i].height}px`;
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < flags.children.length && i < geo.length; i++) {
|
|
190
|
+
(flags.children[i] as HTMLElement).style.top = `${geo[i].top + off}px`;
|
|
191
|
+
}
|
|
192
|
+
for (let i = 0; i < dividers.children.length && i < geo.length; i++) {
|
|
193
|
+
const el = dividers.children[i] as HTMLElement;
|
|
194
|
+
el.style.top = `${geo[i].top + off}px`;
|
|
195
|
+
el.style.left = `${flags.offsetWidth}px`;
|
|
196
|
+
}
|
|
197
|
+
const h = `${host.scrollHeight}px`;
|
|
198
|
+
rail.style.height = h;
|
|
199
|
+
flags.style.height = h;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 两侧栏宽度落到 DOM 上。 */
|
|
203
|
+
function applyWidths(): void {
|
|
204
|
+
flags.style.flexBasis = `${colWidth.flags}px`;
|
|
205
|
+
rail.style.flexBasis = `${colWidth.rail}px`;
|
|
206
|
+
positionSides();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** document 接收拖动与松手事件,监听随生命周期释放。 */
|
|
210
|
+
function bindGrip(grip: HTMLElement, side: 'flags' | 'rail'): void {
|
|
211
|
+
grip.addEventListener('pointerdown', (ev: Event) => {
|
|
212
|
+
const startX = (ev as MouseEvent).clientX;
|
|
213
|
+
const start = colWidth[side];
|
|
214
|
+
ev.preventDefault();
|
|
215
|
+
grip.className = 'prefix-grip grip-dragging';
|
|
216
|
+
const move = (e: Event): void => {
|
|
217
|
+
const d = (e as MouseEvent).clientX - startX;
|
|
218
|
+
// 右栏在右边:往左拖是变宽,所以它的符号是反的
|
|
219
|
+
const next = start + (side === 'flags' ? d : -d);
|
|
220
|
+
colWidth[side] = Math.max(COL_MIN, Math.min(COL_MAX, next));
|
|
221
|
+
applyWidths();
|
|
222
|
+
};
|
|
223
|
+
const up = (): void => {
|
|
224
|
+
document.removeEventListener('pointermove', move);
|
|
225
|
+
document.removeEventListener('pointerup', up);
|
|
226
|
+
grip.className = 'prefix-grip';
|
|
227
|
+
};
|
|
228
|
+
document.addEventListener('pointermove', move);
|
|
229
|
+
document.addEventListener('pointerup', up);
|
|
230
|
+
}, { signal: ctx.signal });
|
|
231
|
+
}
|
|
232
|
+
bindGrip(gripL, 'flags');
|
|
233
|
+
bindGrip(gripR, 'rail');
|
|
234
|
+
applyWidths();
|
|
235
|
+
window.addEventListener('resize', positionSides, { signal: ctx.signal });
|
|
236
|
+
|
|
237
|
+
/** 重建左侧标签与右栏(块集合或警告变了时)。 */
|
|
238
|
+
function refreshRail(): void {
|
|
239
|
+
closePop();
|
|
240
|
+
rail.replaceChildren();
|
|
241
|
+
flags.replaceChildren();
|
|
242
|
+
dividers.replaceChildren();
|
|
243
|
+
for (const g of editor.blockGeometry()) {
|
|
244
|
+
const flag = ui.h('div', `prefix-flag tone-${g.tone}${g.sourceKey ? '' : ' flag-readonly'}`);
|
|
245
|
+
flag.appendChild(ui.h('span', 'prefix-flag-text', g.title));
|
|
246
|
+
const origin = g.sourceKey ? docs.get(g.sourceKey)?.origin : undefined;
|
|
247
|
+
flag.title = g.sourceKey
|
|
248
|
+
? S.flagEditable(g.sourceKey, origin)
|
|
249
|
+
: S.flagReadonly;
|
|
250
|
+
flags.appendChild(flag);
|
|
251
|
+
dividers.appendChild(ui.h('div', `prefix-divider tone-${g.tone}`));
|
|
252
|
+
|
|
253
|
+
const cell = ui.h('div', 'prefix-railcell');
|
|
254
|
+
const doc = g.sourceKey ? docs.get(g.sourceKey) : undefined;
|
|
255
|
+
if (!doc) {
|
|
256
|
+
cell.appendChild(ui.h('span', 'prefix-railnote', g.sourceKey ? S.sourceMissing(g.sourceKey) : S.fromCode));
|
|
257
|
+
rail.appendChild(cell);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const text = dirty.get(doc.key) ?? doc.content;
|
|
261
|
+
if (doc.origin) {
|
|
262
|
+
cell.appendChild(ui.h('span', 'prefix-railnote',
|
|
263
|
+
doc.origin === 'deployment' ? S.originDeployment
|
|
264
|
+
: doc.origin === 'package' ? S.originPackage
|
|
265
|
+
: S.originWorld));
|
|
266
|
+
}
|
|
267
|
+
for (const v of doc.vars ?? []) {
|
|
268
|
+
const chip = ui.button(v.name, {
|
|
269
|
+
size: 'sm',
|
|
270
|
+
onClick: () => {
|
|
271
|
+
// 捕获阶段的 onDocClick 已经把浮层收了;这里靠 justClosed 认出"再点同一个"
|
|
272
|
+
if (justClosed === `${doc.key}:${v.name}`) { justClosed = null; return; }
|
|
273
|
+
const pop = varPopup(v);
|
|
274
|
+
pop.setAttribute('data-var', `${doc.key}:${v.name}`);
|
|
275
|
+
cell.appendChild(pop);
|
|
276
|
+
openPop = pop;
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
chip.className += ' varchip mono';
|
|
280
|
+
chip.setAttribute('data-chip', `${doc.key}:${v.name}`);
|
|
281
|
+
// 模板里没用到的占位符标灰:它声明了,但这一块此刻不引用它
|
|
282
|
+
if (!text.includes(`{{${v.name}`)) chip.className += ' varchip-unused';
|
|
283
|
+
cell.appendChild(chip);
|
|
284
|
+
}
|
|
285
|
+
const warns = varWarnings(text, doc.vars ?? []);
|
|
286
|
+
if (warns.length) {
|
|
287
|
+
const w = ui.h('div', 'prefix-railwarn');
|
|
288
|
+
w.textContent = warns.join(S.warnSep);
|
|
289
|
+
cell.appendChild(w);
|
|
290
|
+
}
|
|
291
|
+
rail.appendChild(cell);
|
|
292
|
+
}
|
|
293
|
+
positionSides();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function load(): Promise<void> {
|
|
297
|
+
try {
|
|
298
|
+
const [docsRes, prefixRes] = await Promise.all([
|
|
299
|
+
get<{ prompts?: PromptDoc[] }>('/api/prompts', { signal: ctx.signal }),
|
|
300
|
+
get<{ segments?: PrefixSegment[] }>('/api/prompts/prefix', { signal: ctx.signal })
|
|
301
|
+
.catch(() => null),
|
|
302
|
+
]);
|
|
303
|
+
docs.clear();
|
|
304
|
+
for (const d of docsRes?.prompts ?? []) docs.set(d.key, d);
|
|
305
|
+
const segments = prefixRes?.segments ?? [];
|
|
306
|
+
|
|
307
|
+
if (!segments.length) {
|
|
308
|
+
setStatus(prefixRes ? S.prefixEmpty : S.prefixUnavailable, true);
|
|
309
|
+
editor.setBlocks([]);
|
|
310
|
+
refreshRail();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 可编辑块装**模板原文**(带 {{}}),只读块装渲染后的文本——后者没有源可给。
|
|
315
|
+
editor.setBlocks(segments.map((seg): PrefixBlock => {
|
|
316
|
+
const doc = seg.sourceKey ? docs.get(seg.sourceKey) : undefined;
|
|
317
|
+
return {
|
|
318
|
+
title: seg.title,
|
|
319
|
+
tone: segmentTone(seg.sourceKey),
|
|
320
|
+
...(doc ? { sourceKey: doc.key } : {}),
|
|
321
|
+
text: (doc ? doc.content : seg.text).replace(/\s+$/, ''),
|
|
322
|
+
};
|
|
323
|
+
}));
|
|
324
|
+
// 字数不是这条前缀真正的成本,token 才是。口径与截断判据同源(`estTok`),
|
|
325
|
+
// 所以这里报的数和「上下文」页、和真去截断时算的是同一套。
|
|
326
|
+
const tok = estTok(segments.map((s) => s.text).join(''));
|
|
327
|
+
setStatus(S.prefixStats(segments.length, tok.toLocaleString()));
|
|
328
|
+
refreshRail();
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if ((err as { name?: string } | null)?.name === 'AbortError') return;
|
|
331
|
+
ctx.onError(err);
|
|
332
|
+
setStatus(S.loadFailed(String((err as Error)?.message ?? err)), true);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await load();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export const promptsFeature: FrameworkFeature = {
|
|
340
|
+
route: PROMPTS_ROUTE,
|
|
341
|
+
label: S.title,
|
|
342
|
+
icon: 'text',
|
|
343
|
+
navMode: 'persona',
|
|
344
|
+
needsAny: ['prompts'],
|
|
345
|
+
mount: mountPrompts,
|
|
346
|
+
};
|