@spzxg/cli-ui 0.0.0-stage → 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/README.md +101 -2
- package/dist/cli-ui-embed-BeBZKDZO.js +36487 -0
- package/dist/cli-ui-index-CuwqVeUk.js +13516 -0
- package/dist/cli-ui.css +1 -0
- package/dist/cli-ui.js +11 -0
- package/package.json +49 -4
- package/src/chat/CliChat.tsx +516 -0
- package/src/chat/Composer.tsx +194 -0
- package/src/chat/export.ts +34 -0
- package/src/core/blocks.ts +132 -0
- package/src/core/client.ts +230 -0
- package/src/core/config.ts +220 -0
- package/src/core/conversation.ts +371 -0
- package/src/core/provider.tsx +89 -0
- package/src/core/sse.ts +51 -0
- package/src/core/store.ts +491 -0
- package/src/core/types.ts +278 -0
- package/src/elements.tsx +122 -0
- package/src/embed.ts +47 -0
- package/src/index.ts +34 -0
- package/src/msglist/CliMsgList.tsx +248 -0
- package/src/operator.css +1628 -0
- package/src/styles.css +50 -0
- package/src/ui/Markdown.tsx +26 -0
- package/src/ui/TextBlock.tsx +49 -0
- package/src/ui/Transcript.tsx +408 -0
- package/src/ui/format.ts +101 -0
- package/src/ui/icons.tsx +89 -0
- package/src/ui/shared.tsx +38 -0
- package/src/ui/transcriptModel.ts +76 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两套数据形状:
|
|
3
|
+
* · center 服务(cli-task-center)对话任务接口的返回(snake_case,原样保留,便于对照 API 文档);
|
|
4
|
+
* · Operator 节点转录的消息与事件(center 的实时流原样转发,界面按 Operator 的规则渲染)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ── center:对话任务 ────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type JobStatus = "queued" | "bound" | "idle" | "running" | "paused" | "succeeded" | "failed" | "cancelled" | string;
|
|
10
|
+
export type TurnStatus = "queued" | "dispatching" | "running" | "succeeded" | "failed" | "cancelled" | "merged" | string;
|
|
11
|
+
|
|
12
|
+
export interface CenterTurn {
|
|
13
|
+
turn_id: string;
|
|
14
|
+
job_id: string;
|
|
15
|
+
task_id: string;
|
|
16
|
+
seq: number;
|
|
17
|
+
business_id: string | null;
|
|
18
|
+
status: TurnStatus;
|
|
19
|
+
input: string;
|
|
20
|
+
/** 合并了后面排队的消息时,实际发给 Agent 的全文;否则为 null(= input)。 */
|
|
21
|
+
sent_input: string | null;
|
|
22
|
+
merged_into_turn_id: string | null;
|
|
23
|
+
reply: string | null;
|
|
24
|
+
/** true = 这一轮以 Agent 提问结束:reply 是问题,下一条消息就是回答。 */
|
|
25
|
+
asked: boolean;
|
|
26
|
+
last_error: string | null;
|
|
27
|
+
attempt: number;
|
|
28
|
+
agent: string | null;
|
|
29
|
+
resolved_model: string | null;
|
|
30
|
+
total_tokens: number;
|
|
31
|
+
input_tokens: number;
|
|
32
|
+
output_tokens: number;
|
|
33
|
+
cache_read_tokens: number;
|
|
34
|
+
cache_creation_tokens: number;
|
|
35
|
+
/** true = 节点上的会话重建了,这一轮起之前的对话没带过去(工作区文件还在)。 */
|
|
36
|
+
context_reset: boolean;
|
|
37
|
+
/** 这一轮的改动是否已合并回项目基线。 */
|
|
38
|
+
merged?: boolean;
|
|
39
|
+
merge_error?: string | null;
|
|
40
|
+
created_at: string;
|
|
41
|
+
started_at: string | null;
|
|
42
|
+
ended_at: string | null;
|
|
43
|
+
queue_position?: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CenterTask {
|
|
47
|
+
id: string;
|
|
48
|
+
job_id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
status: string;
|
|
51
|
+
agent: string | null;
|
|
52
|
+
resolved_model: string | null;
|
|
53
|
+
total_tokens: number;
|
|
54
|
+
input_tokens: number;
|
|
55
|
+
output_tokens: number;
|
|
56
|
+
cache_read_tokens: number;
|
|
57
|
+
cache_creation_tokens: number;
|
|
58
|
+
browser?: boolean;
|
|
59
|
+
last_error: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ConversationSummary {
|
|
63
|
+
turns: number;
|
|
64
|
+
queued_turns: number;
|
|
65
|
+
running_turn: boolean;
|
|
66
|
+
last_turn: CenterTurn | null;
|
|
67
|
+
last_activity_at: string | null;
|
|
68
|
+
expires_at: string | null;
|
|
69
|
+
closed_reason: string | null;
|
|
70
|
+
continued_from_job_id: string | null;
|
|
71
|
+
continued_by_job_id: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** GET /api/jobs/:id */
|
|
75
|
+
export interface CenterJob {
|
|
76
|
+
id: string;
|
|
77
|
+
title: string;
|
|
78
|
+
status: JobStatus;
|
|
79
|
+
mode?: "conversation" | string;
|
|
80
|
+
conversation?: ConversationSummary;
|
|
81
|
+
tasks: CenterTask[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** POST /api/jobs/:id/messages */
|
|
85
|
+
export interface SendResult {
|
|
86
|
+
job_id: string;
|
|
87
|
+
status: JobStatus;
|
|
88
|
+
duplicate: boolean;
|
|
89
|
+
/** true = 原对话已结束,这条消息开了一个新对话(job_id 是新的)。 */
|
|
90
|
+
new_conversation: boolean;
|
|
91
|
+
previous_job_id: string | null;
|
|
92
|
+
queue_position: number | null;
|
|
93
|
+
turn: CenterTurn;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 新建对话时可带的参数(POST /api/jobs,mode=conversation)。 */
|
|
97
|
+
export interface NewConversationOptions {
|
|
98
|
+
title?: string;
|
|
99
|
+
/** 业务自己的 ID(tasks[0].business_id):第一轮的回调里原样带回,用来和业务数据对应。 */
|
|
100
|
+
businessId?: string;
|
|
101
|
+
workspace?: { name?: string; context?: string; branch?: string };
|
|
102
|
+
callback?: { url: string; code: string };
|
|
103
|
+
agent?: string;
|
|
104
|
+
timeoutSeconds?: number;
|
|
105
|
+
browser?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Operator:节点转录 ─────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export type MsgRole = "user" | "assistant" | "tool" | "system" | "session_break";
|
|
111
|
+
|
|
112
|
+
export interface Message {
|
|
113
|
+
id: string;
|
|
114
|
+
task_id?: string;
|
|
115
|
+
generation: number;
|
|
116
|
+
role: MsgRole;
|
|
117
|
+
content: string;
|
|
118
|
+
created_at: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface PendingMessage {
|
|
122
|
+
id: string;
|
|
123
|
+
generation: number;
|
|
124
|
+
content: string;
|
|
125
|
+
created_at: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface Msg {
|
|
129
|
+
id: string;
|
|
130
|
+
/** queued:中枢里还在排队、没送到节点的消息(界面上是可撤回的「排队中」气泡)。 */
|
|
131
|
+
role: MsgRole | "queued";
|
|
132
|
+
content: string;
|
|
133
|
+
generation: number;
|
|
134
|
+
toolId?: string;
|
|
135
|
+
ts?: number;
|
|
136
|
+
/** queued 气泡对应的中枢轮次,撤回时用。 */
|
|
137
|
+
turnId?: string;
|
|
138
|
+
/** queued 气泡:中枢给出的等待原因(节点连不上、没有可用节点……),正常排队时为空。 */
|
|
139
|
+
note?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface AskOption { label: string; description?: string }
|
|
143
|
+
export interface AskQuestion { question: string; header: string; multiSelect?: boolean; options: AskOption[] }
|
|
144
|
+
export type AskAnswers = string[][];
|
|
145
|
+
|
|
146
|
+
export interface InputField { label: string; secret?: boolean; options?: string[]; placeholder?: string }
|
|
147
|
+
export interface InputRequest {
|
|
148
|
+
id: string;
|
|
149
|
+
message: string;
|
|
150
|
+
fields: InputField[];
|
|
151
|
+
status: "pending" | "answered" | "cancelled";
|
|
152
|
+
values?: string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type DiffLine = { sign: "+" | "-" | " "; text: string };
|
|
156
|
+
|
|
157
|
+
export type ToolPeek =
|
|
158
|
+
| { kind: "count"; text: string }
|
|
159
|
+
| { kind: "diff"; added: number; removed: number; label?: string; lines: DiffLine[]; truncated?: number }
|
|
160
|
+
| { kind: "lines"; label?: string; lines: string[]; truncated?: number }
|
|
161
|
+
| { kind: "todos"; items: { text: string; status: string }[] };
|
|
162
|
+
|
|
163
|
+
export interface ToolData {
|
|
164
|
+
title: string;
|
|
165
|
+
detail?: string;
|
|
166
|
+
result?: string;
|
|
167
|
+
isError?: boolean;
|
|
168
|
+
peek?: ToolPeek;
|
|
169
|
+
diff?: DiffLine[];
|
|
170
|
+
ask?: { id: string; questions: AskQuestion[]; answers?: AskAnswers };
|
|
171
|
+
input?: InputRequest;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface TurnUsage {
|
|
175
|
+
cost_usd: number;
|
|
176
|
+
input_tokens: number;
|
|
177
|
+
output_tokens: number;
|
|
178
|
+
cache_read_tokens: number;
|
|
179
|
+
cache_creation_tokens: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 节点回合事件(center 流里 `{type:"node", event}` 的 event)。 */
|
|
183
|
+
export type NodeEvent =
|
|
184
|
+
| { type: "user"; content: string; msgId: string; generation: number; ts?: number }
|
|
185
|
+
| { type: "queued"; msgId: string; content: string; generation: number; ts?: number }
|
|
186
|
+
| { type: "dequeued"; msgId: string }
|
|
187
|
+
| { type: "model"; model: string }
|
|
188
|
+
| { type: "assistant"; content: string; msgId?: string; generation?: number; ts?: number }
|
|
189
|
+
| { type: "tool"; id: string; title: string; detail: string; peek?: ToolPeek; diff?: DiffLine[]; msgId?: string; generation?: number }
|
|
190
|
+
| { type: "tool_result"; id: string; content: string; isError: boolean; peek?: ToolPeek; msgId?: string }
|
|
191
|
+
| { type: "ask"; id: string; questions: AskQuestion[]; msgId?: string; generation?: number }
|
|
192
|
+
| { type: "ask_answered"; id: string; answers: AskAnswers; msgId?: string }
|
|
193
|
+
| { type: "input"; input: InputRequest; msgId?: string; generation?: number }
|
|
194
|
+
| { type: "usage"; usage: TurnUsage }
|
|
195
|
+
| { type: "notice"; content: string; msgId?: string; generation?: number }
|
|
196
|
+
| { type: "error"; content: string; msgId?: string; generation?: number }
|
|
197
|
+
| { type: "turn_end" }
|
|
198
|
+
| { type: string; [key: string]: unknown };
|
|
199
|
+
|
|
200
|
+
/** center 实时流 chunk.events 里的一项。 */
|
|
201
|
+
export type StreamItem =
|
|
202
|
+
| { type: "transcript"; messages: Message[] | null; pending: PendingMessage[] | null; running: boolean | null }
|
|
203
|
+
| { type: "node"; event: NodeEvent }
|
|
204
|
+
| { type: "turn"; turn: CenterTurn }
|
|
205
|
+
| { type: "job"; job: { id: string; status: JobStatus; closed_reason: string | null; node_id: string | null } };
|
|
206
|
+
|
|
207
|
+
export interface StreamSnapshot {
|
|
208
|
+
job: { id: string; status: JobStatus; closed_reason: string | null; node_id: string | null; mode: string; title: string; conversation: ConversationSummary };
|
|
209
|
+
turns: CenterTurn[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface StreamEnd {
|
|
213
|
+
reason: string;
|
|
214
|
+
continued_by_job_id?: string | null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── center 回调体(与中枢 conversation.ts turnCallbackPayload / job-callback.ts 的字段一一对应) ──
|
|
218
|
+
//
|
|
219
|
+
// 组件在前端发出的 conversation_turn / conversation_closed 事件,载荷与中枢投递给 callback.url 的请求体同名同义,
|
|
220
|
+
// 业务方可以用同一套代码处理「后端回调」和「前端事件」。前端拿不到的字段(event_id、callback_seq、cumulative、
|
|
221
|
+
// carried)不出现;以后端回调为准落库,前端事件只用来刷新界面。
|
|
222
|
+
|
|
223
|
+
/** 每轮一次:type = "conversation_turn"。 */
|
|
224
|
+
export interface ConversationTurnCallback {
|
|
225
|
+
type: "conversation_turn";
|
|
226
|
+
/** 投递顺序号(= seq)。同一对话里按它从小到大处理;结束事件是最后一轮 + 1。 */
|
|
227
|
+
callback_seq: number;
|
|
228
|
+
job_id: string;
|
|
229
|
+
task_id: string;
|
|
230
|
+
turn_id: string;
|
|
231
|
+
seq: number;
|
|
232
|
+
business_id: string | null;
|
|
233
|
+
/** succeeded / failed / cancelled(被合并的消息不单独回调,见 messages)。 */
|
|
234
|
+
status: TurnStatus;
|
|
235
|
+
occurred_at: string;
|
|
236
|
+
/** 实际发给 Agent 的正文(合并了排队消息时是拼起来的全文)。 */
|
|
237
|
+
input: string;
|
|
238
|
+
/** 这一轮包含的全部消息,按发送顺序(排队的消息会合并进来)。 */
|
|
239
|
+
messages: Array<{ turn_id: string; seq: number; business_id: string | null; input: string }>;
|
|
240
|
+
reply: string | null;
|
|
241
|
+
/** true = 这一轮以 Agent 提问结束(reply 是问题),status 仍是 succeeded;下一条消息就是回答。 */
|
|
242
|
+
asked: boolean;
|
|
243
|
+
/** 失败 / 取消原因(中枢原文)。 */
|
|
244
|
+
message: string | null;
|
|
245
|
+
continued_from_job_id: string | null;
|
|
246
|
+
context_reset: boolean;
|
|
247
|
+
merged: boolean;
|
|
248
|
+
merge_error: string | null;
|
|
249
|
+
agent: string | null;
|
|
250
|
+
resolved_model: string | null;
|
|
251
|
+
total_tokens: number;
|
|
252
|
+
input_tokens: number;
|
|
253
|
+
output_tokens: number;
|
|
254
|
+
cache_read_tokens: number;
|
|
255
|
+
cache_creation_tokens: number;
|
|
256
|
+
started_at: string | null;
|
|
257
|
+
ended_at: string | null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 对话结束一次:type = "conversation_closed"。 */
|
|
261
|
+
export interface ConversationClosedCallback {
|
|
262
|
+
type: "conversation_closed";
|
|
263
|
+
mode: "conversation";
|
|
264
|
+
/** 投递顺序号:最后一轮的 seq + 1,永远排在这个对话所有 conversation_turn 之后。 */
|
|
265
|
+
callback_seq: number;
|
|
266
|
+
job_id: string;
|
|
267
|
+
/** succeeded(关闭 / 过期 / 换线)/ cancelled / failed。 */
|
|
268
|
+
status: JobStatus;
|
|
269
|
+
/** closed / cancelled / expired / node_reassigned / node_lost。 */
|
|
270
|
+
closed_reason: string | null;
|
|
271
|
+
/** 一共几轮(= 最大的 seq,被合并的消息也算)。 */
|
|
272
|
+
turns: number;
|
|
273
|
+
/** 接续出来的新对话(过期、换线、节点丢失后再发消息时才有)。 */
|
|
274
|
+
continued_by_job_id: string | null;
|
|
275
|
+
occurred_at: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export type ConversationCallback = ConversationTurnCallback | ConversationClosedCallback;
|
package/src/elements.tsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Component 版:任何页面(不用 React)都能直接放 <cli-msglist> / <cli-chat>。
|
|
3
|
+
*
|
|
4
|
+
* import "@spzxg/cli-ui/styles.css";
|
|
5
|
+
* import { defineCliElements } from "@spzxg/cli-ui/elements";
|
|
6
|
+
* defineCliElements();
|
|
7
|
+
*
|
|
8
|
+
* <cli-msglist group="a" base-url="/api/center" title="战略"></cli-msglist>
|
|
9
|
+
* <cli-chat group="a"></cli-chat>
|
|
10
|
+
*
|
|
11
|
+
* 同一个 group 的元素共享一个仓库:列表点哪个,对话就显示哪个;不写 group 各自独立。
|
|
12
|
+
* 属性:base-url、token、line、namespace、group、job-id、title、color、theme、context、read-only、
|
|
13
|
+
* config(界面配置 JSON)、new-conversation(新建对话参数 JSON:title / businessId / workspace / callback / agent …)。
|
|
14
|
+
* 界面配置也可以直接赋对象:document.querySelector("cli-chat").config = { header: { export: false } }。
|
|
15
|
+
* 事件:cli-event(detail 是 CliEvent:select / conversation_turn / conversation_closed / job_change / error);
|
|
16
|
+
* cli-callback(detail 与 center 回调 conversation_turn / conversation_closed 的请求体同名同义)。
|
|
17
|
+
* 渲染在 light DOM 里(不用 Shadow DOM),样式靠页面引入 styles.css,作用域都在 .cli-ui 内。
|
|
18
|
+
*/
|
|
19
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
20
|
+
import { CliMsgList } from "./msglist/CliMsgList.js";
|
|
21
|
+
import { CliChat } from "./chat/CliChat.js";
|
|
22
|
+
import { createCliStore, type CliStore } from "./core/store.js";
|
|
23
|
+
|
|
24
|
+
const groups = new Map<string, CliStore>();
|
|
25
|
+
|
|
26
|
+
function storeFor(el: HTMLElement): CliStore | undefined {
|
|
27
|
+
const group = el.getAttribute("group");
|
|
28
|
+
if (!group) return undefined;
|
|
29
|
+
const baseUrl = el.getAttribute("base-url") ?? undefined;
|
|
30
|
+
const key = `${group}|${baseUrl ?? ""}`;
|
|
31
|
+
let store = groups.get(key);
|
|
32
|
+
if (!store) {
|
|
33
|
+
store = createCliStore(storeOptions(el));
|
|
34
|
+
groups.set(key, store);
|
|
35
|
+
}
|
|
36
|
+
return store;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ATTRS = ["base-url", "token", "line", "namespace", "group", "job-id", "title", "color", "theme", "context", "read-only", "config", "new-conversation"];
|
|
40
|
+
|
|
41
|
+
function jsonAttr(el: HTMLElement, name: string): object | undefined {
|
|
42
|
+
const raw = el.getAttribute(name);
|
|
43
|
+
if (!raw) return undefined;
|
|
44
|
+
try { return JSON.parse(raw) as object; } catch { console.warn(`<${el.localName}> 的 ${name} 不是合法 JSON,已忽略`); return undefined; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function storeOptions(el: HTMLElement) {
|
|
48
|
+
return {
|
|
49
|
+
baseUrl: el.getAttribute("base-url") ?? undefined,
|
|
50
|
+
token: el.getAttribute("token") ?? undefined,
|
|
51
|
+
line: el.getAttribute("line") ?? undefined,
|
|
52
|
+
namespace: el.getAttribute("namespace") ?? el.getAttribute("group") ?? undefined,
|
|
53
|
+
newConversation: jsonAttr(el, "new-conversation"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
abstract class CliElement extends HTMLElement {
|
|
58
|
+
static observedAttributes = ATTRS;
|
|
59
|
+
private root: Root | null = null;
|
|
60
|
+
private ownStore: CliStore | undefined;
|
|
61
|
+
private configValue: object | undefined;
|
|
62
|
+
|
|
63
|
+
/** 界面配置(对象)。优先于 config 属性里的 JSON。 */
|
|
64
|
+
get config(): object | undefined {
|
|
65
|
+
return this.configValue ?? jsonAttr(this, "config");
|
|
66
|
+
}
|
|
67
|
+
set config(value: object | undefined) {
|
|
68
|
+
this.configValue = value;
|
|
69
|
+
if (this.root) this.render();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
connectedCallback() {
|
|
73
|
+
// 缺省撑满父容器;组件自己内部滚动(宿主给定高度即可)。
|
|
74
|
+
this.style.display ||= "block";
|
|
75
|
+
this.style.height ||= "100%";
|
|
76
|
+
this.style.minHeight ||= "0";
|
|
77
|
+
this.root ??= createRoot(this);
|
|
78
|
+
this.render();
|
|
79
|
+
}
|
|
80
|
+
disconnectedCallback() {
|
|
81
|
+
// 移动节点时会先断开再连上:推到下一轮再卸载。
|
|
82
|
+
queueMicrotask(() => { if (!this.isConnected) { this.root?.unmount(); this.root = null; } });
|
|
83
|
+
}
|
|
84
|
+
attributeChangedCallback() { if (this.root) this.render(); }
|
|
85
|
+
|
|
86
|
+
protected common() {
|
|
87
|
+
const store = storeFor(this) ?? (this.ownStore ??= createCliStore(storeOptions(this)));
|
|
88
|
+
return {
|
|
89
|
+
store,
|
|
90
|
+
title: this.getAttribute("title") ?? undefined,
|
|
91
|
+
color: this.getAttribute("color") ?? undefined,
|
|
92
|
+
theme: (this.getAttribute("theme") as "light" | "dark" | null) ?? undefined,
|
|
93
|
+
onEvent: (event: unknown) => this.dispatchEvent(new CustomEvent("cli-event", { detail: event, bubbles: true })),
|
|
94
|
+
onCallback: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-callback", { detail: payload, bubbles: true })),
|
|
95
|
+
style: { height: "100%" },
|
|
96
|
+
config: this.config as never,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
protected abstract element(): React.ReactNode;
|
|
100
|
+
private render() { this.root?.render(this.element()); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class CliMsgListElement extends CliElement {
|
|
104
|
+
protected element() {
|
|
105
|
+
const context = this.getAttribute("context");
|
|
106
|
+
// 回调类事件只由 <cli-chat> 发:同一个 group 里列表和对话共用一个仓库,两边都发的话 document 上会收到两遍。
|
|
107
|
+
const { onCallback: _chatOnly, ...common } = this.common();
|
|
108
|
+
return <CliMsgList {...common} {...(context !== null ? { context } : {})} />;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class CliChatElement extends CliElement {
|
|
113
|
+
protected element() {
|
|
114
|
+
const jobId = this.getAttribute("job-id");
|
|
115
|
+
return <CliChat {...this.common()} {...(jobId !== null ? { jobId: jobId || null } : {})} readOnly={this.hasAttribute("read-only")} />;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function defineCliElements() {
|
|
120
|
+
if (!customElements.get("cli-msglist")) customElements.define("cli-msglist", CliMsgListElement);
|
|
121
|
+
if (!customElements.get("cli-chat")) customElements.define("cli-chat", CliChatElement);
|
|
122
|
+
}
|
package/src/embed.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 给第三方页面用的一体化入口:引入一个 JS 和一个 CSS 就能用,不需要 React、打包工具或 TypeScript。
|
|
3
|
+
*
|
|
4
|
+
* <link rel="stylesheet" href="/static/cli-ui/cli-ui.css">
|
|
5
|
+
* <script type="module" src="/static/cli-ui/cli-ui.js"></script>
|
|
6
|
+
* <cli-chat base-url="/api/center" job-id="job_xxx"></cli-chat>
|
|
7
|
+
*
|
|
8
|
+
* 引入后自动注册 <cli-chat> / <cli-msglist>;想用 JS 控制(传回调函数、随时改参数)就用 window.CliUI.mountChat / mountMsgList。
|
|
9
|
+
* 产物由 `npm run build:embed -w @spzxg/cli-ui` 生成在 dist/。
|
|
10
|
+
*/
|
|
11
|
+
import "./styles.css";
|
|
12
|
+
import { createElement } from "react";
|
|
13
|
+
import { createRoot } from "react-dom/client";
|
|
14
|
+
import { defineCliElements } from "./elements.js";
|
|
15
|
+
import { CliChat, type CliChatProps } from "./chat/CliChat.js";
|
|
16
|
+
import { CliMsgList, type CliMsgListProps } from "./msglist/CliMsgList.js";
|
|
17
|
+
import { CliCenterProvider } from "./core/provider.js";
|
|
18
|
+
import { createCliStore } from "./core/store.js";
|
|
19
|
+
import { createCenterClient } from "./core/client.js";
|
|
20
|
+
|
|
21
|
+
export interface Mounted<P> {
|
|
22
|
+
/** 合并更新参数(只传要改的)。 */
|
|
23
|
+
update(next: Partial<P>): void;
|
|
24
|
+
unmount(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mount<P extends object>(Component: (props: P) => unknown, el: Element, props: P): Mounted<P> {
|
|
28
|
+
const root = createRoot(el);
|
|
29
|
+
let current = props;
|
|
30
|
+
const render = () => root.render(createElement(Component as never, { style: { height: "100%" }, ...current }));
|
|
31
|
+
render();
|
|
32
|
+
return {
|
|
33
|
+
update(next) { current = { ...current, ...next }; render(); },
|
|
34
|
+
unmount() { root.unmount(); },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 在一个元素里挂对话组件。参数与 React 的 <CliChat> 相同(jobId、newConversation、onCallback、config……)。 */
|
|
39
|
+
export const mountChat = (el: Element, props: CliChatProps = {}) => mount(CliChat, el, props);
|
|
40
|
+
/** 在一个元素里挂列表组件。参数与 React 的 <CliMsgList> 相同。 */
|
|
41
|
+
export const mountMsgList = (el: Element, props: CliMsgListProps = {}) => mount(CliMsgList, el, props);
|
|
42
|
+
|
|
43
|
+
export { defineCliElements, CliChat, CliMsgList, CliCenterProvider, createCliStore, createCenterClient };
|
|
44
|
+
|
|
45
|
+
defineCliElements();
|
|
46
|
+
const api = { mountChat, mountMsgList, createCliStore, createCenterClient, defineCliElements };
|
|
47
|
+
(globalThis as { CliUI?: typeof api }).CliUI = api;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @spzxg/cli-ui —— 任务中心(cli-task-center)对话任务的前端积木,数据全部来自 center 服务。
|
|
3
|
+
*
|
|
4
|
+
* cli-msglist 消息对话列表(CliMsgList)
|
|
5
|
+
* cli-chat 对话(CliChat)
|
|
6
|
+
* cli-board 右侧看板 —— 规划中,见 PLAN.md
|
|
7
|
+
*
|
|
8
|
+
* 单独用:
|
|
9
|
+
* <CliChat baseUrl="/api/center" jobId="job_xxx" />
|
|
10
|
+
* 组合用(同一个 Provider 里共享选中和实时流):
|
|
11
|
+
* <CliCenterProvider baseUrl="/api/center" namespace="strategy">
|
|
12
|
+
* <CliMsgList title="战略" />
|
|
13
|
+
* <CliChat />
|
|
14
|
+
* </CliCenterProvider>
|
|
15
|
+
*
|
|
16
|
+
* import "@spzxg/cli-ui/styles.css";
|
|
17
|
+
* 非 React 页面用 Web Component:import { defineCliElements } from "@spzxg/cli-ui/elements"。
|
|
18
|
+
*/
|
|
19
|
+
export { CliMsgList, type CliMsgListProps } from "./msglist/CliMsgList.js";
|
|
20
|
+
export { CliChat, type CliChatProps } from "./chat/CliChat.js";
|
|
21
|
+
export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps } from "./core/provider.js";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
resolveChatConfig, resolveMsgListConfig, DEFAULT_CHAT_CONFIG, DEFAULT_MSGLIST_CONFIG,
|
|
25
|
+
type CliChatConfig, type CliMsgListConfig, type ResolvedChatConfig, type ResolvedMsgListConfig,
|
|
26
|
+
} from "./core/config.js";
|
|
27
|
+
export { createCliStore, type CliStore, type CliStoreOptions, type CliState, type CliEvent, type JobEntry } from "./core/store.js";
|
|
28
|
+
export { createCenterClient, CenterError, type CenterClient, type CenterClientOptions, type StreamHandlers } from "./core/client.js";
|
|
29
|
+
export { viewMessages, isRunning, runningTurn, type ConvState } from "./core/conversation.js";
|
|
30
|
+
export {
|
|
31
|
+
composeMessage, splitMessage, serializeBlock, readFileText, isLongText, ACCEPTED_EXTENSIONS, MAX_MESSAGE_CHARS,
|
|
32
|
+
type TextBlock, type MessagePart,
|
|
33
|
+
} from "./core/blocks.js";
|
|
34
|
+
export type * from "./core/types.js";
|