@world-engines/create-project 0.1.0-alpha.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.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 这是独立 opt-in 示例,不进入空白项目的运行路径。它只操作显式指定、仍为空白的项目。
3
+ */
4
+ export const INTERACTION_DEMO_CONFIG = String.raw `/**
5
+ * 仅在 examples/interaction-demo 的安装脚本成功后复制到 view/src/view-config.ts。
6
+ * 这些名称和 action 与示范 TMW source/manifest 完全一致。
7
+ */
8
+ export const viewConfig = {
9
+ variables: [] as readonly { name: string; label: string }[],
10
+ unlocks: [{ target: "node", id: "demo-gate", label: "示范关卡状态" }] as const,
11
+ actions: [
12
+ { eventType: "demo.gate.close", label: "关闭示范关卡", payload: {} },
13
+ { eventType: "demo.gate.open", label: "打开示范关卡", payload: {} },
14
+ ] as const,
15
+ };
16
+ `;
17
+ export const INTERACTION_DEMO_INSTALLER = String.raw `#!/usr/bin/env node
18
+ import { readFile } from "node:fs/promises";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+ import { LocalProjectHost } from "@world-engines/project-host";
22
+ import { decodeScenarioPartWesp, encodeWesp } from "@world-engines/project-format";
23
+
24
+ // @world-engines/tmw 与 protocol-ts 是 project-host 的已声明运行依赖,不向作者项目根依赖表扩散内部包。
25
+ const hostEntry = fileURLToPath(import.meta.resolve("@world-engines/project-host"));
26
+ async function hostDependency(packageName, entrypoint) {
27
+ for (let current = dirname(hostEntry), parent = ""; current !== parent; parent = current, current = dirname(current)) {
28
+ const candidate = join(current, "node_modules", ...packageName.split("/"));
29
+ try { await readFile(join(candidate, "package.json")); return pathToFileURL(join(candidate, entrypoint)).href; }
30
+ catch { /* 继续沿 Host 的模块查找路径寻找已声明依赖。 */ }
31
+ }
32
+ throw new Error("project-host 依赖缺失:" + packageName);
33
+ }
34
+ const { ScenarioPartSourceV1 } = await import(await hostDependency("@world-engines/protocol-ts", "dist/gen/v1/scenario_part_source.js"));
35
+ const { digestViewInteractionManifestV3, parseViewInteractionManifestV3 } = await import(await hostDependency("@world-engines/tmw", "dist/index.js"));
36
+
37
+ const [flag, target] = process.argv.slice(2);
38
+ if (flag !== "--project" || !target || process.argv.length !== 4) {
39
+ throw new Error("用法:node install.mjs --project <新建且尚未编辑的项目绝对路径>");
40
+ }
41
+
42
+ const projectRoot = resolve(target);
43
+ const host = await LocalProjectHost.open(projectRoot);
44
+ try {
45
+ const before = await host.readSourceReadback();
46
+ const source = ScenarioPartSourceV1.decode(await decodeScenarioPartWesp(before.source));
47
+ const sourceIsBlank = source.schema_version === 1 && [source.metadata, source.logical_graph, source.trigger_semantics, source.layout, source.resource_refs, source.prompt_retrieval, source.spatial_world]
48
+ .every((section) => section === undefined || section.length === 0);
49
+ if (!sourceIsBlank) throw new Error("示范只接受新建且未编辑的 source.wes;请新建一个项目作为示范项目。");
50
+ const schema = await host.dispatchTool({ name: "trigger_schema", input: {} });
51
+ const document = {
52
+ ...schema.empty_document,
53
+ events: [
54
+ { kind: "event", stable_id: "event.demo.gate.close", event_type: "demo.gate.close", hard_fact: true,
55
+ effective_time: { domain: "logical_seq", due_value: 0, effective_value: 0, crossing_policy: "forward_only", real_time_clock: null, wall_clock_policy: null }, selector: null },
56
+ { kind: "event", stable_id: "event.demo.gate.open", event_type: "demo.gate.open", hard_fact: true,
57
+ effective_time: { domain: "logical_seq", due_value: 0, effective_value: 0, crossing_policy: "forward_only", real_time_clock: null, wall_clock_policy: null }, selector: null },
58
+ ],
59
+ conditions: [],
60
+ effects: [
61
+ { kind: "effect", stable_id: "effect.demo.gate.close", capability_id: "node.set_unlock", selector: null,
62
+ arguments: { nodeId: "demo-gate", unlocked: false },
63
+ effective_time: { domain: "logical_seq", due_value: 0, effective_value: 0, crossing_policy: "forward_only", real_time_clock: null, wall_clock_policy: null }, execution_policy: "attempt_once", rewrite_policy: "normal_only", priority: 0, idempotency_key: "demo.unlock" },
64
+ { kind: "effect", stable_id: "effect.demo.gate.open", capability_id: "node.set_unlock", selector: null,
65
+ arguments: { nodeId: "demo-gate", unlocked: true },
66
+ effective_time: { domain: "logical_seq", due_value: 0, effective_value: 0, crossing_policy: "forward_only", real_time_clock: null, wall_clock_policy: null }, execution_policy: "attempt_once", rewrite_policy: "normal_only", priority: 0, idempotency_key: "demo.lock" },
67
+ ],
68
+ presentations: [],
69
+ edges: [
70
+ { from_stable_id: "event.demo.gate.close", to_stable_id: "effect.demo.gate.close" },
71
+ { from_stable_id: "event.demo.gate.open", to_stable_id: "effect.demo.gate.open" },
72
+ ],
73
+ };
74
+ const validation = await host.dispatchTool({ name: "trigger_validate", input: { candidate_document: document } });
75
+ if (validation.ok !== true || typeof validation.plan_sha256 !== "string") throw new Error("示范 TMW document 未通过正式 trigger_validate。");
76
+ const manifest = {
77
+ schema: "view-interaction-manifest.v3", generation: 3, epoch: 1, artifact_digest: validation.plan_sha256,
78
+ runtime_contract_hash: document.runtime_contract_hash, bootstrap_url: "http://localhost:43123/runtime.js",
79
+ capabilities: ["node.set_unlock"],
80
+ events: [
81
+ { event_id: "event.demo.gate.close", event_type: "demo.gate.close", capability: "node.set_unlock",
82
+ payload_schema: { type: "object", additional_properties: false, properties: {}, max_bytes: 64 } },
83
+ { event_id: "event.demo.gate.open", event_type: "demo.gate.open", capability: "node.set_unlock",
84
+ payload_schema: { type: "object", additional_properties: false, properties: {}, max_bytes: 64 } },
85
+ ],
86
+ };
87
+ if (parseViewInteractionManifestV3(manifest) === null) throw new Error("示范 manifest 未通过正式 schema 校验。");
88
+ const manifestDigest = await digestViewInteractionManifestV3(manifest);
89
+ const next = {
90
+ ...source,
91
+ metadata: new TextEncoder().encode(JSON.stringify({ metadata: { initial_scene: { schemaVersion: 1, sceneId: "interaction-demo", revision: 0, worldTime: 0, presentEntityNodeIds: [], spatialRef: null, manuscriptNotes: [] } } })),
92
+ logical_graph: new TextEncoder().encode(JSON.stringify({ entities: [{ entity_id: "demo-gate", name: ["示范关卡"], kind: "location", props: {} }], relations: [], history: [] })),
93
+ trigger_semantics: new TextEncoder().encode(JSON.stringify(document)),
94
+ layout: new TextEncoder().encode(JSON.stringify({ view_interaction_mobile: null, view_interaction_pc: { schema: "view-interaction-source.v3", generation: 3, manifest, manifest_digest: manifestDigest } })),
95
+ };
96
+ const receipt = await host.writeScenario({ operation_id: "install-interaction-demo-v1", expected_project_revision: before.project_revision, expected_source_sha256: before.source_sha256, source: await encodeWesp(ScenarioPartSourceV1.encode(next).finish()) });
97
+ if (receipt.readback_verified !== true) throw new Error("示范 source 写入未获得 readback 回执。");
98
+ const written = await host.readSourceReadback();
99
+ const writtenSource = ScenarioPartSourceV1.decode(await decodeScenarioPartWesp(written.source));
100
+ const writtenLayout = JSON.parse(new TextDecoder().decode(writtenSource.layout));
101
+ if (written.project_revision !== receipt.next_project_revision || writtenLayout.view_interaction_pc?.manifest_digest !== manifestDigest) throw new Error("示范 source readback 与 manifest 不一致。");
102
+ const readback = await host.dispatchTool({ name: "trigger_read", input: {} });
103
+ const simulation = await host.dispatchTool({ name: "trigger_simulate", input: { event: { type: "demo.gate.close", id: "demo.gate.close.verify", payload: {} }, state: {} } });
104
+ const gameplay = simulation.state?.gameplayState;
105
+ if (JSON.stringify(readback.document) !== JSON.stringify(document) || simulation.ok !== true
106
+ || gameplay?.entity_state_stacks?.["node:demo-gate"]?.kind !== "unavailable") throw new Error("示范 source 写后核验失败。");
107
+ process.stdout.write(JSON.stringify({ status: "installed", receipt, manifest_digest: manifestDigest, simulation }, null, 2) + "\n");
108
+ } finally {
109
+ await host.close();
110
+ }
111
+ `;
112
+ export const INTERACTION_DEMO_README = String.raw `# 可选互动与状态示范
113
+
114
+ 该目录不会被默认 View 或默认空白剧本加载。它只用于单独新建的示范项目:脚本先要求该项目的 \`source.wes\` 仍为空,再以一次带 revision/source SHA CAS 的正式 \`writeScenario\` 写入 TMW document 和 View manifest;任一前置条件不满足会退出,不会改写已有剧本。manifest 的 \`bootstrap_url\` 是当前正式 manifest 所需的 artifact fence;示范只使用本地 Preview 既有 \`http://localhost:43123/runtime.js\` 形式校验它,并不宣称该 URL 是可下载的作者脚本。
115
+
116
+ 1. 用 \`create-worldengine-project\` 新建一个专门的空项目,并完成根依赖安装。
117
+ 2. 在这个项目中执行(路径必须是该空项目的绝对路径):
118
+
119
+ \`\`\`powershell
120
+ node examples/interaction-demo/install.mjs --project D:\\Proj\\interaction-demo
121
+ Copy-Item examples/interaction-demo/view-config.ts view/src/view-config.ts
122
+ npm run snapshot
123
+ npm run smoke:view
124
+ \`\`\`
125
+
126
+ 脚本会执行真实 \`trigger_validate\`、严格的 \`view-interaction-manifest.v3\` parse/digest、CAS source 写入、\`trigger_read\` 与 \`trigger_simulate\`。成功输出中的 \`readback_verified: true\` 是 source 写入回执;\`simulation.ok: true\` 且 node 状态被读回,证明正式 authoring TMW 路径成立。示范关卡初始为“已解锁”;“关闭示范关卡”将它设为“未解锁”,“打开示范关卡”再设回“已解锁”。两种 action 都可重复提交,不会因重复创建变量而失败。当前已安装 project-host 的公开入口不导出可编程的 LocalPreviewRuntime event executor,因此示范不会假装以内部路径完成 Preview runtime 读回;请在 GUI Preview 点击这两个按钮完成该最后一跳。View 只会显示“互动请求已发送”,不会把该同步请求冒充为故事已完成。支持该通知的 Host 会以 \`interactionResult\` 的 \`interactionId\`、\`nonce\`、\`status: committed|failed|uncertain\`(及可选 \`code\`)反馈 activation 是否 durable;\`committed\` 不等于 AI 完成或后续 delivery。\`tmw.delivery\` 只是业务通知,不能作为某次点击的关联回执。
127
+
128
+ 示范 action 是 manifest 声明的 \`demo.gate.close\` 和 \`demo.gate.open\`;它们分别关闭和打开 node \`demo-gate\`。状态面板通过公开只读 \`getUnlock("node", "demo-gate")\` 显示真实保存态。不要把示范的固定标识直接合并进已有剧本;请在自己的 TMW document 与 manifest 同时声明相同的 event、payload schema 和解锁目标后,再复制或改写配置。
129
+ `.replaceAll("\\`", "`").replaceAll("\\\\", "\\");
@@ -0,0 +1 @@
1
+ export declare const DEFAULT_CHAT_PLAYER_PANEL: string;
@@ -0,0 +1,311 @@
1
+ export const DEFAULT_CHAT_PLAYER_PANEL = String.raw `import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { TmwInteractionResult } from "@world-engines/view-sdk";
4
+ import { emitConfiguredAction, optionalRead, publicError, type ChatMessage, type ChatPlayConversationBridge } from "./chatplay";
5
+ import { viewConfig } from "./view-config";
6
+
7
+ export type PlayerPanelKind = "model" | "worldline" | "progress" | "assets" | "resources" | "state" | "actions" | "search";
8
+
9
+ const panelTitles: Record<PlayerPanelKind, string> = {
10
+ model: "模型设置", worldline: "世界线", progress: "剧情进度", assets: "资源", resources: "已解锁内容", state: "角色状态", actions: "互动选择", search: "历史搜索",
11
+ };
12
+
13
+ export function PlayerPanel({ kind, bridge, messages, onWorldlineChange, onClose }: {
14
+ kind: PlayerPanelKind;
15
+ bridge: ChatPlayConversationBridge;
16
+ messages: readonly ChatMessage[];
17
+ onWorldlineChange: () => void;
18
+ onClose: () => void;
19
+ }) {
20
+ const [revision, setRevision] = useState(0);
21
+ const [body, setBody] = useState<ReactNode>("正在读取…");
22
+ const request = useRef(0);
23
+ const dialog = useRef<HTMLElement>(null);
24
+
25
+ useLayoutEffect(() => {
26
+ const node = dialog.current;
27
+ if (!node) return;
28
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
29
+ const focusable = () => [...node.querySelectorAll<HTMLElement>("button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href], [tabindex]:not([tabindex=\"-1\"])")];
30
+ focusable()[0]?.focus();
31
+ const trap = (event: KeyboardEvent) => {
32
+ if (event.key !== "Tab") return;
33
+ const items = focusable();
34
+ if (!items.length) { event.preventDefault(); return; }
35
+ const first = items[0]; const last = items.at(-1)!;
36
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
37
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
38
+ };
39
+ node.addEventListener("keydown", trap);
40
+ return () => { node.removeEventListener("keydown", trap); previous?.focus(); };
41
+ }, []);
42
+
43
+ useEffect(() => {
44
+ let active = true;
45
+ let stop = () => {};
46
+ void bridge.ready.then(() => {
47
+ if (!active) return;
48
+ const refresh = () => setRevision((value) => value + 1);
49
+ const stops = [bridge.on("stateChanged", refresh), bridge.on("resourceUnlocked", refresh), bridge.on("tmwDelivery", refresh)];
50
+ stop = () => stops.forEach((end) => end());
51
+ }).catch(() => {});
52
+ return () => { active = false; stop(); };
53
+ }, [bridge, kind]);
54
+
55
+ useEffect(() => {
56
+ const generation = ++request.current;
57
+ let active = true;
58
+ const show = (next: ReactNode) => { if (active && generation === request.current) setBody(next); };
59
+ const unavailable = (reason: unknown, fallback: string) => publicError(reason, fallback) ?? "当前游戏暂不支持此功能。";
60
+ const load = async () => {
61
+ if (kind === "model") { show(<ModelSettings bridge={bridge} />); return; }
62
+ if (kind === "worldline") {
63
+ const result = await optionalRead(() => bridge.worldline.list());
64
+ show(result.value ? <Worldlines bridge={bridge} initialCatalog={result.value} messages={messages} onChanged={onWorldlineChange} /> : unavailable(result.error, "世界线暂不可用"));
65
+ return;
66
+ }
67
+ if (kind === "progress") {
68
+ const [meta, progress] = await Promise.all([optionalRead(() => bridge.getScenarioMeta()), optionalRead(() => bridge.getProgress())]);
69
+ show(typeof progress.value === "number" ? <div><h3>{meta.value?.title || "当前剧情"}</h3><p>当前进度 {Math.round(Math.max(0, Math.min(1, progress.value)) * 100)}%</p></div> : unavailable(progress.error, "剧情进度暂不可用"));
70
+ return;
71
+ }
72
+ if (kind === "assets") {
73
+ const result = await optionalRead(() => bridge.listAssets());
74
+ const labels = result.value ? assetLabels(result.value) : [];
75
+ show(result.value ? (result.value.length ? <div className="chat-gallery">{result.value.map((asset, index) => <Asset key={asset.id} bridge={bridge} asset={asset} label={labels[index]} />)}</div> : "暂无资源。") : unavailable(result.error, "资源暂不可用"));
76
+ return;
77
+ }
78
+ if (kind === "resources") {
79
+ const result = await optionalRead(() => bridge.getOwnedResources());
80
+ const resources = result.value ? ownedResourceRows(result.value) : undefined;
81
+ show(resources ? (resources.length ? <ul className="chat-detail-list">{resources.map((item) => <li key={item.key}><span>{item.kind}</span><span>{item.label}</span></li>)}</ul> : "暂无已解锁内容。") : unavailable(result.error, "已解锁内容暂不可用"));
82
+ return;
83
+ }
84
+ if (kind === "state") { show(await readState(bridge, unavailable)); return; }
85
+ if (kind === "actions") { show(<ConfiguredActions bridge={bridge} />); return; }
86
+ show(<History bridge={bridge} initialMessages={messages} />);
87
+ };
88
+ void load().catch((reason) => show(unavailable(reason, "内容暂不可用")));
89
+ return () => { active = false; };
90
+ }, [bridge, kind, messages, onWorldlineChange, revision]);
91
+
92
+ const headingId = "chat-player-panel-title";
93
+ return <section className="chat-player-panel" ref={dialog} role="dialog" aria-modal="true" aria-labelledby={headingId}>
94
+ <header className="chat-panel-header"><h2 id={headingId}>{panelTitles[kind]}</h2><button className="chat-panel-close" type="button" aria-label="关闭面板" onClick={onClose}>×</button></header>
95
+ <div className="chat-panel-content">{body}</div>
96
+ </section>;
97
+ }
98
+
99
+ async function readState(bridge: ChatPlayConversationBridge, unavailable: (reason: unknown, fallback: string) => string): Promise<ReactNode> {
100
+ const context = bridge.context();
101
+ const canReadCustomState = !context || context.capabilities.customStateMaxBytes > 0;
102
+ const keysResult = canReadCustomState ? await optionalRead(() => bridge.customState.keys()) : { value: [] as string[] };
103
+ const custom = await Promise.all((keysResult.value ?? []).map(async (key) => ({ label: key, result: await optionalRead(() => bridge.customState.get(key)) })));
104
+ const variables = await Promise.all(viewConfig.variables.map(async (item) => ({ label: item.label, result: await optionalRead(() => bridge.getVariable(item.name)) })));
105
+ const unlocks = await Promise.all(viewConfig.unlocks.map(async (item) => ({ label: item.label, result: await optionalRead(() => bridge.getUnlock(item.target, item.id)) })));
106
+ const rows = [
107
+ ...custom.map((item) => ({ label: item.label, text: customStateText(item.result.value), error: item.result.error })),
108
+ ...variables.map((item) => ({ label: item.label, text: variableValue(item.result.value), error: item.result.error })),
109
+ ...unlocks.map((item) => ({ label: item.label, text: unlockText(item.result.value), error: item.result.error })),
110
+ ];
111
+ if (!rows.length) return keysResult.error ? unavailable(keysResult.error, "角色状态暂不可用") : "暂无可显示的角色状态。";
112
+ return <ul className="chat-detail-list">{rows.map((row) => <li key={row.label}><span>{row.label}</span><span>{row.error ? unavailable(row.error, "读取失败") : row.text}</span></li>)}</ul>;
113
+ }
114
+
115
+ function ModelSettings({ bridge }: { bridge: ChatPlayConversationBridge }) {
116
+ const [busy, setBusy] = useState(false);
117
+ const [notice, setNotice] = useState<string | null>(null);
118
+ const pending = useRef(false);
119
+ const open = async () => {
120
+ if (pending.current) return;
121
+ pending.current = true;
122
+ setBusy(true); setNotice(null);
123
+ try {
124
+ if (typeof bridge.openModelSettings !== "function") throw new Error("E_CAPABILITY_UNAVAILABLE");
125
+ await bridge.openModelSettings();
126
+ setNotice("已打开模型设置。");
127
+ } catch (reason) { setNotice(publicError(reason, "模型设置暂不可用") ?? "当前游戏暂不支持模型设置。"); }
128
+ finally { pending.current = false; setBusy(false); }
129
+ };
130
+ return <div className="chat-choice-list"><p>在设置中选择对话模型。</p><button className="chat-button" type="button" disabled={busy} onClick={() => { void open(); }}>{busy ? "正在打开…" : "打开模型设置"}</button>{notice && <p role="status">{notice}</p>}</div>;
131
+ }
132
+
133
+ function ConfiguredActions({ bridge }: { bridge: ChatPlayConversationBridge }) {
134
+ const [pending, setPending] = useState<{ eventType: string; interactionId: string; nonce: string; status: "pending" | "uncertain" } | null>(null);
135
+ const [notice, setNotice] = useState<string | null>(null);
136
+ const active = useRef<{ eventType: string; interactionId: string; nonce: string | null } | null>(null);
137
+ const buffered = useRef(new Map<string, TmwInteractionResult>());
138
+ const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
139
+ const live = useRef(true);
140
+ const clearTimer = () => {
141
+ if (timeout.current !== null) clearTimeout(timeout.current);
142
+ timeout.current = null;
143
+ };
144
+ const clearActive = () => {
145
+ clearTimer(); active.current = null; buffered.current.clear();
146
+ };
147
+ const settle = (result: TmwInteractionResult) => {
148
+ const current = active.current;
149
+ if (!current || current.interactionId !== result.interactionId) return;
150
+ if (current.nonce === null) { buffered.current.set(result.nonce, result); return; }
151
+ if (current.nonce !== result.nonce) return;
152
+ clearTimer();
153
+ if (!live.current) return;
154
+ if (result.status === "uncertain") {
155
+ setPending({ ...current, nonce: result.nonce, status: "uncertain" });
156
+ setNotice("暂未确认结果,请先查看故事变化。");
157
+ return;
158
+ }
159
+ clearActive(); setPending(null);
160
+ if (result.status === "committed") setNotice("互动已提交。");
161
+ else setNotice("互动未能完成,请重试。");
162
+ };
163
+ useEffect(() => {
164
+ live.current = true;
165
+ const stop = bridge.on("interactionResult", settle);
166
+ return () => { live.current = false; stop(); clearActive(); };
167
+ }, [bridge]);
168
+ if (!viewConfig.actions.length) return <>当前没有可选互动。</>;
169
+ const run = (action: (typeof viewConfig.actions)[number], retryUncertain = false) => {
170
+ if (active.current && !retryUncertain) return;
171
+ if (retryUncertain) { clearActive(); setPending(null); }
172
+ const interactionId = crypto.randomUUID();
173
+ active.current = { eventType: action.eventType, interactionId, nonce: null };
174
+ setPending({ eventType: action.eventType, interactionId, nonce: "", status: "pending" }); setNotice(null);
175
+ try {
176
+ const receipt = emitConfiguredAction(bridge, action.eventType, action.payload, interactionId);
177
+ const current = active.current;
178
+ if (!current || current.interactionId !== receipt.interactionId) return;
179
+ current.nonce = receipt.nonce;
180
+ setPending({ eventType: action.eventType, interactionId: receipt.interactionId, nonce: receipt.nonce, status: "pending" });
181
+ setNotice("互动请求已发送。");
182
+ const early = buffered.current.get(receipt.nonce);
183
+ if (early) settle(early);
184
+ else timeout.current = setTimeout(() => settle({ interactionId: receipt.interactionId, nonce: receipt.nonce, status: "uncertain" }), 15_000);
185
+ } catch (reason) { clearActive(); setPending(null); setNotice(publicError(reason, "互动暂不可用") ?? "当前游戏暂不支持此互动。"); }
186
+ };
187
+ return <div className="chat-choice-list">{viewConfig.actions.map((action) => <button className="chat-button" key={action.eventType} type="button" disabled={pending !== null} onClick={() => run(action)}>{pending?.eventType === action.eventType && pending.status === "pending" ? "正在确认…" : action.label}</button>)}{pending?.status === "uncertain" && <button className="chat-button" type="button" onClick={() => { const action = viewConfig.actions.find((item) => item.eventType === pending.eventType); if (action) void run(action, true); }}>再次发送</button>}{notice && <p role="status">{notice}</p>}</div>;
188
+ }
189
+
190
+
191
+ function History({ bridge, initialMessages }: { bridge: ChatPlayConversationBridge; initialMessages: readonly ChatMessage[] }) {
192
+ const [query, setQuery] = useState("");
193
+ const [messages, setMessages] = useState(initialMessages);
194
+ const [loading, setLoading] = useState(false);
195
+ const [error, setError] = useState<string | null>(null);
196
+ useEffect(() => setMessages(initialMessages), [initialMessages]);
197
+ const found = useMemo(() => messages.filter((message) => message.text.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase())), [messages, query]);
198
+ const readEarlier = async () => {
199
+ const before = messages[0]?.id;
200
+ if (!before || loading) return;
201
+ setLoading(true); setError(null);
202
+ try {
203
+ const earlier = await bridge.listMessages({ before, limit: 80 });
204
+ setMessages((current) => Array.from(new Map([...earlier, ...current].map((message) => [message.id, message])).values()).sort((left, right) => left.ts - right.ts || left.id.localeCompare(right.id)));
205
+ } catch (reason) { setError(publicError(reason, "更早记录暂不可用") ?? "当前游戏未开放更早记录。"); }
206
+ finally { setLoading(false); }
207
+ };
208
+ return <div className="chat-choice-list"><label>搜索聊天记录<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="输入关键词" /></label>{found.length ? <ul className="chat-search-results">{found.map((message) => <li key={message.id}>{message.text}</li>)}</ul> : <p>没有匹配的记录。</p>}{messages.length > 0 && <button className="chat-button" type="button" disabled={loading} onClick={() => { void readEarlier(); }}>{loading ? "正在读取…" : "读取更早记录"}</button>}{error && <p className="chat-error" role="alert">{error}</p>}</div>;
209
+ }
210
+
211
+ function Worldlines({ bridge, initialCatalog, messages, onChanged }: { bridge: ChatPlayConversationBridge; initialCatalog: Awaited<ReturnType<ChatPlayConversationBridge["worldline"]["list"]>>; messages: readonly ChatMessage[]; onChanged: () => void }) {
212
+ const [catalog, setCatalog] = useState(initialCatalog);
213
+ const anchors = messages.filter((message) => message.role === "assistant");
214
+ const [anchor, setAnchor] = useState(anchors.at(-1)?.id ?? "");
215
+ const [title, setTitle] = useState("");
216
+ const [busy, setBusy] = useState(false);
217
+ const [error, setError] = useState<string | null>(null);
218
+ const pending = useRef(false);
219
+ useEffect(() => { setCatalog(initialCatalog); }, [initialCatalog]);
220
+ useEffect(() => { if (!anchors.some((message) => message.id === anchor)) setAnchor(anchors.at(-1)?.id ?? ""); }, [anchor, anchors]);
221
+ const refreshCatalog = async () => setCatalog(await bridge.worldline.list());
222
+ const run = async (work: () => Promise<unknown>) => {
223
+ if (pending.current) return;
224
+ pending.current = true;
225
+ setBusy(true); setError(null);
226
+ try {
227
+ await work();
228
+ onChanged();
229
+ try { await refreshCatalog(); }
230
+ catch (reason) { setError(publicError(reason, "世界线目录刷新失败") ?? "世界线已切换,目录暂时无法刷新。"); }
231
+ }
232
+ catch (reason) { setError(publicError(reason, "世界线操作失败") ?? "当前游戏暂不支持世界线。"); }
233
+ finally { pending.current = false; setBusy(false); }
234
+ };
235
+ const depths = worldlineDepths(catalog);
236
+ return <div className="chat-choice-list">{error && <p className="chat-error" role="alert">{error}</p>}<ul className="chat-worldline-list">{catalog.worldlines.map((line) => <li key={line.worldlineId} style={{ paddingInlineStart: depths.get(line.worldlineId)! * 16 }}><button className="chat-button" type="button" disabled={busy || line.worldlineId === catalog.activeWorldlineId} onClick={() => { void run(() => bridge.worldline.activate(line.worldlineId)); }}><span>{line.title || "未命名世界线"}</span>{line.worldlineId === catalog.activeWorldlineId && <span className="chat-worldline-active">当前</span>}</button></li>)}</ul>{anchors.length > 0 ? <div className="chat-choice-list"><label>从这条记录分支<select value={anchor} onChange={(event) => setAnchor(event.target.value)}>{anchors.map((message) => <option key={message.id} value={message.id}>{message.text.slice(0, 48) || "空白记录"}</option>)}</select></label><label>分支名称<input value={title} onInput={(event) => setTitle(event.currentTarget.value)} onChange={(event) => setTitle(event.target.value)} placeholder="可选" /></label><button className="chat-button" type="button" disabled={busy || !anchor} onClick={() => { void run(() => bridge.worldline.fork(anchor, title.trim() || undefined)); }}>新建分支</button></div> : <p>收到故事回复后可创建分支。</p>}</div>;
237
+ }
238
+
239
+ function Asset({ bridge, asset, label }: { bridge: ChatPlayConversationBridge; asset: Awaited<ReturnType<ChatPlayConversationBridge["listAssets"]>>[number]; label: string }) {
240
+ const [url, setUrl] = useState<string | null>(null);
241
+ const [error, setError] = useState<string | null>(null);
242
+ const [attempt, setAttempt] = useState(0);
243
+ useEffect(() => {
244
+ let active = true;
245
+ setUrl(null); setError(null);
246
+ void bridge.getAssetUrl(asset.id).then((next) => {
247
+ if (!active) return;
248
+ if (isSafeAssetUrl(next)) setUrl(next); else setError("资源地址不可用。");
249
+ }).catch((reason) => { if (active) setError(publicError(reason, "资源预览不可用") ?? "当前游戏未开放资源预览。"); });
250
+ return () => { active = false; };
251
+ }, [asset.id, attempt, bridge]);
252
+ const playable = asset.mime.startsWith("image/") || asset.mime.startsWith("audio/") || asset.mime.startsWith("video/");
253
+ const mediaFailed = () => { setUrl(null); setError("资源加载失败。"); };
254
+ const mediaLoaded = () => setError(null);
255
+ return <figure className="chat-gallery-item"><figcaption>{label}</figcaption>{error && <div><p className="chat-error" role="alert">{error}</p><button className="chat-button" type="button" onClick={() => setAttempt((value) => value + 1)}>重试预览</button></div>}{url && asset.mime.startsWith("image/") && <img src={url} alt={label} onLoad={mediaLoaded} onError={mediaFailed} />}{url && asset.mime.startsWith("audio/") && <audio controls src={url} onLoadedData={mediaLoaded} onError={mediaFailed} />}{url && asset.mime.startsWith("video/") && <video controls src={url} onLoadedData={mediaLoaded} onError={mediaFailed} />}{url && !playable && <p>此资源类型暂不支持预览。</p>}</figure>;
256
+ }
257
+
258
+ function assetLabels(assets: readonly Awaited<ReturnType<ChatPlayConversationBridge["listAssets"]>>[number][]): string[] {
259
+ const counts = new Map<string, number>();
260
+ return assets.map((asset) => {
261
+ const filename = visibleFilename(asset.filename);
262
+ if (filename && !isOpaqueAssetFilename(filename)) return filename;
263
+ const kind = resourceKind(asset.mime);
264
+ const index = (counts.get(kind) ?? 0) + 1;
265
+ counts.set(kind, index);
266
+ return kind + " " + index;
267
+ });
268
+ }
269
+
270
+ function ownedResourceRows(groups: readonly Awaited<ReturnType<ChatPlayConversationBridge["getOwnedResources"]>>[number][]) {
271
+ const counts = new Map<string, number>();
272
+ return groups.flatMap((group, groupIndex) => group.resources.map((item, resourceIndex) => {
273
+ const kind = resourceKind(item.type);
274
+ const filename = visibleFilename(item.uri);
275
+ if (filename && !isOpaqueAssetFilename(filename)) return { key: groupIndex + ":" + resourceIndex, kind, label: filename };
276
+ const index = (counts.get(kind) ?? 0) + 1;
277
+ counts.set(kind, index);
278
+ return { key: groupIndex + ":" + resourceIndex, kind, label: kind + " " + index };
279
+ }));
280
+ }
281
+
282
+ function visibleFilename(filename: string): string {
283
+ const raw = filename.split(/[?#]/u)[0] ?? "";
284
+ const leaf = raw.split(/[\\/]/u).filter(Boolean).at(-1) ?? "";
285
+ let decoded = leaf;
286
+ try { decoded = decodeURIComponent(leaf); } catch { /* Keep the raw leaf, then sanitize it below. */ }
287
+ return decoded.split(/[\\/]/u).filter(Boolean).at(-1)?.split(/[?#]/u)[0]?.replace(/[\u0000-\u001f\u007f]/gu, "").trim() ?? "";
288
+ }
289
+ function isOpaqueAssetFilename(filename: string): boolean {
290
+ const stem = filename.replace(/\.[^.]+$/u, "");
291
+ return /^(?:sha256[-_])?[a-f0-9]{32,128}$/iu.test(stem) || /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/iu.test(stem);
292
+ }
293
+ function resourceKind(value: string): string { const kind = value.toLowerCase(); if (kind.startsWith("image/") || kind.includes("image") || kind.includes("img")) return "图片"; if (kind.startsWith("audio/") || kind.includes("audio")) return "音频"; if (kind.startsWith("video/") || kind.includes("video")) return "视频"; return "资源"; }
294
+
295
+ function worldlineDepths(catalog: Awaited<ReturnType<ChatPlayConversationBridge["worldline"]["list"]>>) {
296
+ const parents = new Map(catalog.worldlines.map((line) => [line.worldlineId, line.parentWorldlineId]));
297
+ const depths = new Map<string, number>();
298
+ for (const line of catalog.worldlines) {
299
+ let depth = 0; let current = line.parentWorldlineId; const seen = new Set<string>([line.worldlineId]);
300
+ while (current && !seen.has(current)) { seen.add(current); depth += 1; current = parents.get(current) ?? null; }
301
+ depths.set(line.worldlineId, Math.min(depth, 8));
302
+ }
303
+ return depths;
304
+ }
305
+
306
+ function customStateText(entry: unknown): string { return entry && typeof entry === "object" && "value" in entry ? displayValue((entry as { value: unknown }).value) : "暂不可用"; }
307
+ function unlockText(entry: unknown): string { return entry && typeof entry === "object" && "unlocked" in entry ? (entry as { unlocked: boolean }).unlocked ? "已解锁" : "未解锁" : "暂不可用"; }
308
+ function variableValue(variable: unknown): string { return variable && typeof variable === "object" && "value" in variable ? displayValue((variable as { value: unknown }).value) : displayValue(variable); }
309
+ function displayValue(value: unknown): string { if (value === undefined) return "暂不可用"; if (value === null) return "无"; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value); return "已设置"; }
310
+ function isSafeAssetUrl(value: string): boolean { try { return ["blob:", "https:", "http:"].includes(new URL(value, window.location.href).protocol); } catch { return false; } }
311
+ `;
@@ -0,0 +1,2 @@
1
+ /** sandbox iframe 内独立使用的玩家聊天样式,主题由宿主的 data-theme 同步。 */
2
+ export declare const DEFAULT_CHAT_STYLES: string;
@@ -0,0 +1,125 @@
1
+ /** sandbox iframe 内独立使用的玩家聊天样式,主题由宿主的 data-theme 同步。 */
2
+ export const DEFAULT_CHAT_STYLES = String.raw `@layer reset, chat;
3
+ @layer reset {
4
+ *, *::before, *::after { box-sizing: border-box; }
5
+ html, body, #root { width: 100%; min-width: 0; height: 100%; min-height: 0; margin: 0; }
6
+ body { overflow: hidden; }
7
+ button, textarea { font: inherit; }
8
+ button { color: inherit; }
9
+ }
10
+ @layer chat {
11
+ :root { font-family: "Segoe UI Variable Text", "Segoe UI", -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Microsoft YaHei", sans-serif; color-scheme: light; }
12
+ .chat-app {
13
+ --chat-accent: #0f6cbd; --chat-accent-hover: #005a9e; --chat-accent-soft: #e5f1fb;
14
+ --chat-canvas: #f7f8fa; --chat-surface: #fff; --chat-surface-raised: #fff; --chat-input: #fff;
15
+ --chat-hover: #edf1f6; --chat-border: #d9dee7; --chat-border-soft: #e8ebf0;
16
+ --chat-text: #1b1c1f; --chat-text-secondary: #4b5260; --chat-text-tertiary: #707784;
17
+ --chat-on-accent: #fff; --chat-danger: #b42318; --chat-danger-surface: #fff1f0;
18
+ --chat-shadow: 0 10px 28px rgb(29 41 57 / 12%), 0 2px 8px rgb(29 41 57 / 7%); --chat-shadow-soft: 0 2px 6px rgb(29 41 57 / 8%);
19
+ --chat-radius-window: 12px; --chat-radius-control: 8px; --chat-radius-bubble: 16px;
20
+ --chat-safe-top: env(safe-area-inset-top, 0px); --chat-safe-right: env(safe-area-inset-right, 0px); --chat-safe-bottom: env(safe-area-inset-bottom, 0px); --chat-safe-left: env(safe-area-inset-left, 0px);
21
+ display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; width: 100%; height: 100dvh; min-height: 0; overflow: hidden; color: var(--chat-text); background: var(--chat-canvas);
22
+ }
23
+ .chat-app[data-theme="dark"] {
24
+ --chat-accent: #60a5fa; --chat-accent-hover: #93c5fd; --chat-accent-soft: #1e3a5f;
25
+ --chat-canvas: #171a20; --chat-surface: #1b1f27; --chat-surface-raised: #232833; --chat-input: #202530;
26
+ --chat-hover: #292f3b; --chat-border: #3a4150; --chat-border-soft: #2c323d;
27
+ --chat-text: #f4f6f9; --chat-text-secondary: #c7ced9; --chat-text-tertiary: #929baa;
28
+ --chat-on-accent: #101827; --chat-danger: #ffb4ab; --chat-danger-surface: #54221f;
29
+ --chat-shadow: 0 14px 32px rgb(0 0 0 / 38%), 0 3px 9px rgb(0 0 0 / 22%); --chat-shadow-soft: 0 2px 6px rgb(0 0 0 / 24%);
30
+ color-scheme: dark;
31
+ }
32
+ .chat-main { display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; width: 100%; height: 100%; min-height: 0; background: var(--chat-surface); }
33
+ .chat-header { display: flex; grid-row: 1; min-width: 0; min-height: 64px; align-items: center; justify-content: center; padding: 12px 24px; border-bottom: 1px solid var(--chat-border-soft); background: var(--chat-surface); }
34
+ .chat-scene-title { max-width: 900px; overflow: hidden; color: var(--chat-text); font-size: 16px; font-weight: 650; letter-spacing: -.01em; text-align: center; text-overflow: ellipsis; white-space: nowrap; }
35
+ .chat-progress { width: min(160px, 24vw); height: 3px; margin-left: 12px; overflow: hidden; border-radius: 999px; background: var(--chat-border); }
36
+ .chat-progress > span { display: block; height: 100%; border-radius: inherit; background: var(--chat-accent); }
37
+ .chat-transcript { display: flex; grid-row: 3; width: min(100%, 900px); min-height: 0; margin: 0 auto; padding: 30px clamp(20px, 5vw, 56px); flex-direction: column; gap: 16px; overflow: auto; overscroll-behavior: contain; scroll-behavior: smooth; }
38
+ .chat-message { width: fit-content; max-width: min(78%, 720px); padding: 11px 14px; border: 1px solid var(--chat-border-soft); border-radius: var(--chat-radius-bubble) var(--chat-radius-bubble) var(--chat-radius-bubble) 4px; color: var(--chat-text); background: var(--chat-surface-raised); box-shadow: var(--chat-shadow-soft); }
39
+ .chat-message[data-role="user"] { align-self: flex-end; border-color: transparent; border-radius: var(--chat-radius-bubble) var(--chat-radius-bubble) 4px var(--chat-radius-bubble); color: var(--chat-on-accent); background: var(--chat-accent); }
40
+ .chat-message[data-role="assistant"] { align-self: flex-start; }
41
+ .chat-message[data-role="system"] { align-self: center; max-width: min(100%, 640px); padding: 8px 11px; border-radius: 8px; color: var(--chat-text-secondary); font-size: 13px; }
42
+ .chat-message-body { margin: 0; overflow-wrap: anywhere; font-size: 15px; line-height: 1.62; white-space: pre-wrap; }
43
+ .chat-message-body :where(pre, code) { max-width: 100%; overflow: auto; font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace; }
44
+ .chat-message-body pre { margin: 10px 0 0; padding: 12px; border: 1px solid var(--chat-border); border-radius: 8px; background: var(--chat-surface); white-space: pre; }
45
+ .chat-message[data-role="user"] .chat-message-body pre { border-color: rgb(255 255 255 / 26%); color: var(--chat-on-accent); background: rgb(0 0 0 / 14%); }
46
+ .chat-empty { display: grid; min-height: 100%; place-content: center; gap: 9px; padding: 24px; color: var(--chat-text-tertiary); text-align: center; }
47
+ .chat-empty-illustration { width: 44px; height: 44px; margin: 0 auto 6px; fill: none; stroke: var(--chat-accent); stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.5; }
48
+ .chat-empty h2 { margin: 0; color: var(--chat-text); font-size: 18px; font-weight: 650; letter-spacing: -.02em; }
49
+ .chat-empty p { max-width: 310px; margin: 0; line-height: 1.55; }
50
+ .chat-app > .chat-error, .chat-app > .chat-status[role="status"], .chat-main > .chat-error, .chat-main > .chat-status[role="status"] { grid-row: 2; width: min(900px, calc(100% - 40px)); min-height: 0; margin: 12px auto 0; padding: 10px 12px; border: 1px solid var(--chat-border-soft); border-radius: 8px; color: var(--chat-text-secondary); background: var(--chat-surface-raised); font-size: 13px; line-height: 1.45; }
51
+ .chat-error { border-color: var(--chat-danger); color: var(--chat-danger); background: var(--chat-danger-surface); }
52
+ .chat-composer { position: sticky; bottom: 0; display: flex; grid-row: 4; width: 100%; min-width: 0; align-items: flex-end; justify-content: center; gap: 10px; padding: 14px max(24px, calc((100% - 900px) / 2)) 24px; border-top: 1px solid var(--chat-border-soft); background: var(--chat-surface); }
53
+ .chat-input { width: min(100%, 760px); max-height: 160px; resize: none; padding: 11px 13px; border: 1px solid var(--chat-border); border-radius: var(--chat-radius-control); outline: 0; color: var(--chat-text); background: var(--chat-input); font-size: 15px; line-height: 1.45; }
54
+ .chat-input::placeholder { color: var(--chat-text-tertiary); opacity: 1; }
55
+ .chat-input:hover { border-color: var(--chat-text-tertiary); }
56
+ .chat-input:focus { border-color: var(--chat-accent); box-shadow: 0 0 0 3px var(--chat-accent-soft); }
57
+ .chat-fab-row { position: relative; display: flex; flex: 0 0 auto; align-items: flex-end; gap: 8px; }
58
+ .chat-fab, .chat-panel-close, .chat-retry, .chat-fab-option { border: 0; cursor: pointer; touch-action: manipulation; transition: transform 140ms ease, background-color 140ms ease, color 140ms ease, border-color 140ms ease, opacity 140ms ease; }
59
+ .chat-fab { display: inline-grid; width: 44px; min-width: 44px; height: 44px; flex: 0 0 44px; place-items: center; padding: 0; border: 1px solid var(--chat-border); border-radius: 50%; color: var(--chat-text-secondary); background: var(--chat-surface); }
60
+ .chat-fab-add { background: transparent; }
61
+ .chat-fab-send { border-color: var(--chat-accent); color: var(--chat-on-accent); background: var(--chat-accent); }
62
+ .chat-fab:hover:not(:disabled) { background: var(--chat-hover); }
63
+ .chat-fab-send:hover:not(:disabled) { background: var(--chat-accent-hover); }
64
+ .chat-fab:disabled { cursor: not-allowed; opacity: .48; }
65
+ .chat-fab svg, .chat-fab-option svg, .chat-panel-close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
66
+ .chat-fab:active:not(:disabled), .chat-fab-option:active, .chat-panel-close:active { transform: scale(.96); }
67
+ .chat-fab-menu { position: absolute; left: 0; bottom: calc(100% + 10px); display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px 8px; width: min(480px, calc(100vw - 48px)); max-height: min(360px, calc(100dvh - 176px)); padding: 14px 12px; overflow: auto; border: 1px solid var(--chat-border); border-radius: 10px; background: var(--chat-surface-raised); box-shadow: var(--chat-shadow); }
68
+ .chat-fab-menu[aria-expanded="false"] { display: none; }
69
+ .chat-fab-option { display: grid; min-width: 0; min-height: 78px; place-items: center; gap: 5px; padding: 2px; border-radius: 8px; color: var(--chat-text-secondary); background: transparent; text-align: center; }
70
+ .chat-fab-option:hover { color: var(--chat-text); background: var(--chat-hover); }
71
+ .chat-fab-option-icon { display: inline-grid; width: 44px; height: 44px; place-items: center; border-radius: 50%; color: var(--chat-on-accent); background: var(--chat-accent); }
72
+ .chat-fab-option:hover .chat-fab-option-icon { background: var(--chat-accent-hover); }
73
+ .chat-fab-option svg { width: 20px; height: 20px; color: inherit; }
74
+ .chat-fab-option-label { display: block; width: 100%; overflow: hidden; font-size: 12px; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; }
75
+ .chat-panel-overlay { position: fixed; z-index: 4; inset: 0; background: rgb(0 0 0 / 18%); }
76
+ .chat-app[data-theme="dark"] .chat-panel-overlay { background: rgb(0 0 0 / 35%); }
77
+ .chat-player-panel { position: fixed; z-index: 5; top: 76px; right: max(20px, calc((100vw - 980px) / 2)); width: min(380px, calc(100vw - 40px)); max-height: min(620px, calc(100dvh - 100px)); overflow: auto; border: 1px solid var(--chat-border); border-radius: var(--chat-radius-window); background: var(--chat-surface-raised); box-shadow: var(--chat-shadow); }
78
+ .chat-panel-header { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px 10px 16px; border-bottom: 1px solid var(--chat-border-soft); color: var(--chat-text); font-size: 14px; font-weight: 650; }
79
+ .chat-panel-close { display: inline-grid; width: 36px; height: 36px; place-items: center; padding: 0; border-radius: 8px; color: var(--chat-text-secondary); background: transparent; }
80
+ .chat-panel-close:hover { color: var(--chat-text); background: var(--chat-hover); }
81
+ .chat-panel-content { padding: 14px 16px 18px; color: var(--chat-text-secondary); font-size: 14px; line-height: 1.5; }
82
+ .chat-panel-content :where(h2, h3) { margin: 0 0 10px; color: var(--chat-text); font-size: 14px; }
83
+ .chat-panel-content p { margin: 7px 0 0; }
84
+ .chat-panel-content :where(input, select) { width: 100%; min-height: 40px; padding: 8px 10px; border: 1px solid var(--chat-border); border-radius: 8px; color: var(--chat-text); background: var(--chat-input); font: inherit; }
85
+ .chat-panel-content :where(input, select):focus { border-color: var(--chat-accent); outline: 0; box-shadow: 0 0 0 3px var(--chat-accent-soft); }
86
+ .chat-detail-list, .chat-search-results, .chat-worldline-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
87
+ .chat-detail-list li, .chat-search-results li, .chat-worldline-list li { min-width: 0; padding: 9px 10px; border: 1px solid var(--chat-border-soft); border-radius: 8px; color: var(--chat-text-secondary); overflow-wrap: anywhere; }
88
+ .chat-search-results li { background: var(--chat-surface); }
89
+ .chat-worldline-list li { background: var(--chat-surface); }
90
+ .chat-worldline-active { border-color: var(--chat-accent) !important; color: var(--chat-accent) !important; background: var(--chat-accent-soft) !important; font-weight: 650; }
91
+ .chat-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; margin: 0; padding: 0; list-style: none; }
92
+ .chat-gallery-item { min-width: 0; margin: 0; overflow: hidden; border: 1px solid var(--chat-border-soft); border-radius: 8px; background: var(--chat-surface); }
93
+ .chat-gallery-item :where(img, video, audio) { display: block; width: 100%; max-width: 100%; max-height: 150px; object-fit: cover; }
94
+ .chat-gallery-item figcaption { padding: 7px 8px; overflow: hidden; color: var(--chat-text-secondary); font-size: 12px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
95
+ .chat-choice-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
96
+ .chat-choice-list label { display: grid; gap: 5px; color: var(--chat-text-secondary); font-size: 13px; }
97
+ .chat-choice-list button, .chat-panel-content .chat-button { width: 100%; min-height: 40px; padding: 9px 10px; border: 1px solid var(--chat-border); border-radius: 8px; color: var(--chat-text); background: var(--chat-surface); text-align: left; cursor: pointer; }
98
+ .chat-choice-list button:hover, .chat-panel-content .chat-button:hover { border-color: var(--chat-accent); background: var(--chat-accent-soft); }
99
+ :where(.chat-app) :where(button, textarea):focus-visible { outline: 3px solid var(--chat-accent-soft); outline-offset: 2px; }
100
+ @media (max-width: 720px) {
101
+ .chat-app { --chat-radius-control: 16px; --chat-radius-bubble: 18px; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Helvetica Neue", sans-serif; }
102
+ .chat-header { min-height: calc(52px + var(--chat-safe-top)); padding: calc(8px + var(--chat-safe-top)) calc(16px + var(--chat-safe-right)) 8px calc(16px + var(--chat-safe-left)); }
103
+ .chat-scene-title { font-size: 17px; }
104
+ .chat-progress { display: none; }
105
+ .chat-transcript { width: 100%; gap: 14px; padding: 22px 16px 12px; }
106
+ .chat-message { max-width: 90%; padding: 11px 13px; }
107
+ .chat-message-body { font-size: 16px; line-height: 1.58; }
108
+ .chat-app > .chat-error, .chat-app > .chat-status[role="status"], .chat-main > .chat-error, .chat-main > .chat-status[role="status"] { grid-row: 2; width: calc(100% - 24px); margin-top: 8px; }
109
+ .chat-composer { gap: 8px; padding: 10px calc(12px + var(--chat-safe-right)) calc(10px + var(--chat-safe-bottom)) calc(12px + var(--chat-safe-left)); }
110
+ .chat-composer { background: rgb(255 255 255 / 82%); -webkit-backdrop-filter: blur(18px) saturate(140%); backdrop-filter: blur(18px) saturate(140%); }
111
+ .chat-app[data-theme="dark"] .chat-composer { background: rgb(27 31 39 / 84%); }
112
+ .chat-input, .chat-panel-content :where(input, select) { font-size: 16px; }
113
+ .chat-fab { width: 46px; min-width: 46px; height: 46px; flex-basis: 46px; }
114
+ .chat-fab-menu { left: 0; width: min(480px, calc(100vw - 24px)); max-height: min(360px, calc(100dvh - 164px)); padding: 12px 8px; background: rgb(255 255 255 / 86%); -webkit-backdrop-filter: blur(18px) saturate(140%); backdrop-filter: blur(18px) saturate(140%); }
115
+ .chat-app[data-theme="dark"] .chat-fab-menu { background: rgb(35 40 51 / 86%); }
116
+ .chat-player-panel { top: calc(8px + var(--chat-safe-top)); right: calc(12px + var(--chat-safe-right)); width: calc(100vw - 24px - var(--chat-safe-left) - var(--chat-safe-right)); max-height: calc(100dvh - 16px - var(--chat-safe-top) - var(--chat-safe-bottom)); border-radius: 18px; background: rgb(255 255 255 / 88%); -webkit-backdrop-filter: blur(22px) saturate(140%); backdrop-filter: blur(22px) saturate(140%); }
117
+ .chat-app[data-theme="dark"] .chat-player-panel { background: rgb(35 40 51 / 89%); }
118
+ .chat-panel-close { width: 44px; height: 44px; border-radius: 50%; }
119
+ }
120
+ @media (max-width: 720px) and (orientation: landscape) { .chat-header { min-height: calc(44px + var(--chat-safe-top)); padding-top: calc(4px + var(--chat-safe-top)); padding-bottom: 4px; } .chat-transcript { padding-top: 14px; } .chat-player-panel { top: calc(4px + var(--chat-safe-top)); max-height: calc(100dvh - 8px - var(--chat-safe-top) - var(--chat-safe-bottom)); } }
121
+ @media (max-width: 720px) and (prefers-reduced-transparency: reduce) { .chat-composer, .chat-fab-menu, .chat-player-panel { background: var(--chat-surface-raised); -webkit-backdrop-filter: none; backdrop-filter: none; } }
122
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: 1ms !important; } }
123
+ @media (forced-colors: active) { .chat-app { --chat-accent: Highlight; --chat-accent-hover: Highlight; --chat-accent-soft: Canvas; --chat-canvas: Canvas; --chat-surface: Canvas; --chat-surface-raised: Canvas; --chat-input: Field; --chat-border: CanvasText; --chat-border-soft: CanvasText; --chat-text: CanvasText; --chat-text-secondary: CanvasText; --chat-text-tertiary: GrayText; --chat-on-accent: HighlightText; --chat-danger: CanvasText; --chat-danger-surface: Canvas; } .chat-composer, .chat-fab-menu, .chat-player-panel { background: Canvas; -webkit-backdrop-filter: none; backdrop-filter: none; } .chat-fab-send, .chat-message[data-role="user"] { forced-color-adjust: none; color: HighlightText; background: Highlight; } }
124
+ }
125
+ `;
@@ -0,0 +1,5 @@
1
+ export type ProjectInitializationErrorCode = "E_TARGET_NOT_EMPTY" | "E_TARGET_INVALID" | "E_PACKAGE_SOURCE_INVALID" | "E_DEPENDENCY_INSTALL_FAILED" | "E_INITIALIZATION_VERIFY_FAILED";
2
+ export declare class ProjectInitializationError extends Error {
3
+ readonly code: ProjectInitializationErrorCode;
4
+ constructor(code: ProjectInitializationErrorCode, message: string);
5
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,8 @@
1
+ export class ProjectInitializationError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = "ProjectInitializationError";
6
+ this.code = code;
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./errors.js";
2
+ export * from "./initialize.js";
3
+ export { AUTHOR_PROJECT_VITE_VERSION, AUTHOR_PROJECT_WORKER_FORMAT } from "./template.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./errors.js";
2
+ export * from "./initialize.js";
3
+ export { AUTHOR_PROJECT_VITE_VERSION, AUTHOR_PROJECT_WORKER_FORMAT } from "./template.js";