@pactor-app/chat 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/dist/index.js ADDED
@@ -0,0 +1,3353 @@
1
+ // src/model.ts
2
+ var idCounter = 0;
3
+ function createChatId(prefix = "chat") {
4
+ idCounter += 1;
5
+ return `${prefix}-${Date.now().toString(36)}-${idCounter}`;
6
+ }
7
+
8
+ // src/store.ts
9
+ var ConversationStore = class {
10
+ constructor() {
11
+ this.messages = [];
12
+ this.listeners = /* @__PURE__ */ new Set();
13
+ }
14
+ /** 当前消息数组(只读快照;引用在不变时保持稳定) */
15
+ getMessages() {
16
+ return this.messages;
17
+ }
18
+ getMessage(id) {
19
+ return this.messages.find((message) => message.id === id);
20
+ }
21
+ /** 追加消息(id 重复时告警并忽略) */
22
+ appendMessage(message) {
23
+ if (this.getMessage(message.id)) {
24
+ console.warn(
25
+ `[@pactor-app/chat] duplicate message id "${message.id}", ignored`
26
+ );
27
+ return;
28
+ }
29
+ this.commit([...this.messages, message]);
30
+ }
31
+ /** 更新消息级字段(status / role / parts 等,id 不可改) */
32
+ updateMessage(id, patch) {
33
+ const index = this.messages.findIndex((message) => message.id === id);
34
+ if (index < 0) {
35
+ console.warn(`[@pactor-app/chat] message not found: "${id}"`);
36
+ return;
37
+ }
38
+ const next = [...this.messages];
39
+ next[index] = { ...next[index], ...patch, id };
40
+ this.commit(next);
41
+ }
42
+ /** 追加 part 到目标消息 */
43
+ appendPart(messageId, part) {
44
+ this.mutateMessage(messageId, (message) => ({
45
+ ...message,
46
+ parts: [...message.parts, part]
47
+ }));
48
+ }
49
+ /**
50
+ * 更新目标 part:partRef 为数组下标或 part id。
51
+ * 未命中时告警并忽略(宽容约定同 updateMessage)。
52
+ */
53
+ updatePart(messageId, partRef, patch) {
54
+ this.mutateMessage(messageId, (message) => {
55
+ const index = typeof partRef === "number" ? partRef : message.parts.findIndex((part) => part.id === partRef);
56
+ if (index < 0 || index >= message.parts.length) {
57
+ console.warn(
58
+ `[@pactor-app/chat] part not found: message "${messageId}", ref "${String(partRef)}"`
59
+ );
60
+ return message;
61
+ }
62
+ const parts = [...message.parts];
63
+ parts[index] = { ...parts[index], ...patch };
64
+ return { ...message, parts };
65
+ });
66
+ }
67
+ /** 设置消息状态 */
68
+ setStatus(id, status) {
69
+ this.updateMessage(id, { status });
70
+ }
71
+ /**
72
+ * 不可变地移除目标消息及其后全部消息(编辑重发 / 重新生成的截断基础)。
73
+ * 未知 id 时告警并忽略。
74
+ */
75
+ removeMessagesFrom(messageId) {
76
+ const index = this.messages.findIndex((message) => message.id === messageId);
77
+ if (index < 0) {
78
+ console.warn(`[@pactor-app/chat] message not found: "${messageId}"`);
79
+ return;
80
+ }
81
+ this.commit(this.messages.slice(0, index));
82
+ }
83
+ /** 清空会话 */
84
+ clear() {
85
+ if (this.messages.length === 0) {
86
+ return;
87
+ }
88
+ this.commit([]);
89
+ }
90
+ /** 订阅变更;返回退订函数 */
91
+ subscribe(listener) {
92
+ this.listeners.add(listener);
93
+ return () => {
94
+ this.listeners.delete(listener);
95
+ };
96
+ }
97
+ /** 以 mutator 更新单条消息(找不到时告警并忽略) */
98
+ mutateMessage(id, mutate) {
99
+ const index = this.messages.findIndex((message) => message.id === id);
100
+ if (index < 0) {
101
+ console.warn(`[@pactor-app/chat] message not found: "${id}"`);
102
+ return;
103
+ }
104
+ const nextMessage = mutate(this.messages[index]);
105
+ if (nextMessage === this.messages[index]) {
106
+ return;
107
+ }
108
+ const next = [...this.messages];
109
+ next[index] = nextMessage;
110
+ this.commit(next);
111
+ }
112
+ commit(next) {
113
+ this.messages = next;
114
+ for (const listener of this.listeners) {
115
+ listener();
116
+ }
117
+ }
118
+ };
119
+
120
+ // src/artifact.ts
121
+ var ArtifactStore = class {
122
+ constructor() {
123
+ this.artifact = null;
124
+ this.listeners = /* @__PURE__ */ new Set();
125
+ }
126
+ /** 当前产出物(未打开时为 null;引用在不变时保持稳定) */
127
+ get() {
128
+ return this.artifact;
129
+ }
130
+ /** 打开(或替换)当前产出物 */
131
+ open(artifact) {
132
+ this.artifact = artifact;
133
+ this.notify();
134
+ }
135
+ /** 部分更新当前产出物(id 不可改);未 open 时告警并忽略 */
136
+ update(patch) {
137
+ if (!this.artifact) {
138
+ console.warn("[@pactor-app/chat] artifact not open, update ignored");
139
+ return;
140
+ }
141
+ this.artifact = { ...this.artifact, ...patch, id: this.artifact.id };
142
+ this.notify();
143
+ }
144
+ /** 关闭面板(未打开时幂等,不重复通知) */
145
+ close() {
146
+ if (this.artifact === null) {
147
+ return;
148
+ }
149
+ this.artifact = null;
150
+ this.notify();
151
+ }
152
+ /** 订阅变更;返回退订函数 */
153
+ subscribe(listener) {
154
+ this.listeners.add(listener);
155
+ return () => {
156
+ this.listeners.delete(listener);
157
+ };
158
+ }
159
+ notify() {
160
+ for (const listener of this.listeners) {
161
+ listener();
162
+ }
163
+ }
164
+ };
165
+
166
+ // src/events.ts
167
+ function applyChatEvent(store, event) {
168
+ switch (event.type) {
169
+ case "message.start": {
170
+ const existing = store.getMessage(event.messageId);
171
+ if (existing) {
172
+ store.updateMessage(event.messageId, {
173
+ role: event.role ?? existing.role,
174
+ status: "streaming"
175
+ });
176
+ } else {
177
+ store.appendMessage({
178
+ id: event.messageId,
179
+ role: event.role ?? "assistant",
180
+ status: "streaming",
181
+ parts: [],
182
+ createdAt: Date.now()
183
+ });
184
+ }
185
+ return;
186
+ }
187
+ case "text.delta": {
188
+ ensureMessage(store, event.messageId);
189
+ const message = store.getMessage(event.messageId);
190
+ const part = locatePart(
191
+ message,
192
+ event.partId,
193
+ (p) => event.format === "markdown" ? p.type === "markdown" : p.type === "text" || p.type === "markdown"
194
+ );
195
+ if (part) {
196
+ store.updatePart(event.messageId, part.index, {
197
+ content: part.part.content + event.delta,
198
+ status: "streaming"
199
+ });
200
+ } else {
201
+ store.appendPart(event.messageId, {
202
+ id: event.partId ?? createChatId("part"),
203
+ type: event.format === "markdown" ? "markdown" : "text",
204
+ content: event.delta,
205
+ status: "streaming"
206
+ });
207
+ }
208
+ return;
209
+ }
210
+ case "thinking.delta": {
211
+ ensureMessage(store, event.messageId);
212
+ const message = store.getMessage(event.messageId);
213
+ const part = locatePart(message, event.partId, (p) => p.type === "thinking");
214
+ if (part) {
215
+ store.updatePart(event.messageId, part.index, {
216
+ content: part.part.content + event.delta,
217
+ status: "streaming"
218
+ });
219
+ } else {
220
+ store.appendPart(event.messageId, {
221
+ id: event.partId ?? createChatId("part"),
222
+ type: "thinking",
223
+ content: event.delta,
224
+ status: "streaming"
225
+ });
226
+ }
227
+ return;
228
+ }
229
+ case "tool.start": {
230
+ ensureMessage(store, event.messageId);
231
+ const message = store.getMessage(event.messageId);
232
+ const part = locatePart(
233
+ message,
234
+ event.partId,
235
+ (p) => p.type === "tool-call" && p.name === event.name
236
+ );
237
+ if (part) {
238
+ store.updatePart(event.messageId, part.index, {
239
+ status: "running",
240
+ title: event.title,
241
+ args: event.args
242
+ });
243
+ } else {
244
+ store.appendPart(event.messageId, {
245
+ id: event.partId ?? createChatId("part"),
246
+ type: "tool-call",
247
+ name: event.name,
248
+ title: event.title,
249
+ args: event.args,
250
+ status: "running"
251
+ });
252
+ }
253
+ return;
254
+ }
255
+ case "tool.result": {
256
+ ensureMessage(store, event.messageId);
257
+ const message = store.getMessage(event.messageId);
258
+ const call = locatePart(
259
+ message,
260
+ event.partId,
261
+ (p) => p.type === "tool-call" && p.name === event.name
262
+ );
263
+ if (call) {
264
+ store.updatePart(event.messageId, call.index, {
265
+ status: event.status
266
+ });
267
+ }
268
+ store.appendPart(event.messageId, {
269
+ id: createChatId("part"),
270
+ type: "tool-result",
271
+ name: event.name,
272
+ status: event.status,
273
+ result: event.result,
274
+ error: event.error
275
+ });
276
+ return;
277
+ }
278
+ case "dsl.start": {
279
+ ensureMessage(store, event.messageId);
280
+ store.appendPart(event.messageId, {
281
+ id: event.partId ?? createChatId("part"),
282
+ type: "dsl",
283
+ status: "pending"
284
+ });
285
+ return;
286
+ }
287
+ case "dsl.complete": {
288
+ ensureMessage(store, event.messageId);
289
+ const message = store.getMessage(event.messageId);
290
+ const part = locatePart(message, event.partId, (p) => p.type === "dsl");
291
+ if (part) {
292
+ store.updatePart(event.messageId, part.index, {
293
+ schema: event.schema,
294
+ status: "ready"
295
+ });
296
+ } else {
297
+ store.appendPart(event.messageId, {
298
+ id: event.partId ?? createChatId("part"),
299
+ type: "dsl",
300
+ schema: event.schema,
301
+ status: "ready"
302
+ });
303
+ }
304
+ return;
305
+ }
306
+ case "artifact": {
307
+ ensureMessage(store, event.messageId);
308
+ const message = store.getMessage(event.messageId);
309
+ const part = locatePart(message, event.partId, (p) => p.type === "artifact");
310
+ if (part) {
311
+ store.updatePart(event.messageId, part.index, {
312
+ artifact: event.artifact,
313
+ status: "ready"
314
+ });
315
+ } else {
316
+ store.appendPart(event.messageId, {
317
+ id: event.partId ?? createChatId("part"),
318
+ type: "artifact",
319
+ artifact: event.artifact,
320
+ status: "ready"
321
+ });
322
+ }
323
+ return;
324
+ }
325
+ case "message.end": {
326
+ const message = store.getMessage(event.messageId);
327
+ if (!message) {
328
+ console.warn(
329
+ `[@pactor-app/chat] message.end for unknown message "${event.messageId}", ignored`
330
+ );
331
+ return;
332
+ }
333
+ const parts = message.parts.map(
334
+ (part) => "status" in part && (part.status === "streaming" || part.status === "pending") ? { ...part, status: "ready" } : part
335
+ );
336
+ store.updateMessage(event.messageId, { status: "completed", parts });
337
+ return;
338
+ }
339
+ case "message.error": {
340
+ ensureMessage(store, event.messageId);
341
+ store.appendPart(event.messageId, {
342
+ id: createChatId("part"),
343
+ type: "error",
344
+ message: event.error
345
+ });
346
+ store.setStatus(event.messageId, "error");
347
+ return;
348
+ }
349
+ default: {
350
+ console.warn(
351
+ `[@pactor-app/chat] unknown chat event, ignored:`,
352
+ event.type ?? event
353
+ );
354
+ }
355
+ }
356
+ }
357
+ function ensureMessage(store, messageId) {
358
+ const existing = store.getMessage(messageId);
359
+ if (!existing) {
360
+ store.appendMessage({
361
+ id: messageId,
362
+ role: "assistant",
363
+ status: "streaming",
364
+ parts: [],
365
+ createdAt: Date.now()
366
+ });
367
+ } else if (existing.status === "pending") {
368
+ store.setStatus(messageId, "streaming");
369
+ }
370
+ }
371
+ function locatePart(message, partId, match) {
372
+ if (partId !== void 0) {
373
+ const index = message.parts.findIndex((part) => part.id === partId);
374
+ return index >= 0 ? { index, part: message.parts[index] } : void 0;
375
+ }
376
+ for (let index = message.parts.length - 1; index >= 0; index -= 1) {
377
+ const part = message.parts[index];
378
+ if (match(part)) {
379
+ return { index, part };
380
+ }
381
+ }
382
+ return void 0;
383
+ }
384
+
385
+ // src/activity.ts
386
+ function detectStreamingMode(message) {
387
+ if (message.status !== "streaming" && message.status !== "pending") {
388
+ return null;
389
+ }
390
+ for (const part of message.parts) {
391
+ if (part.type === "tool-call" && (part.status === "waiting" || part.status === "running")) {
392
+ return "tool";
393
+ }
394
+ }
395
+ for (let index = message.parts.length - 1; index >= 0; index -= 1) {
396
+ const part = message.parts[index];
397
+ if (part.type === "thinking" && part.status === "streaming") {
398
+ return "reasoning";
399
+ }
400
+ if ((part.type === "text" || part.type === "markdown" || part.type === "dsl") && part.status === "streaming") {
401
+ return "responding";
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+ function pairToolTraces(parts) {
407
+ const entries = [];
408
+ for (let index = 0; index < parts.length; index += 1) {
409
+ const part = parts[index];
410
+ if (part.type === "tool-call") {
411
+ entries.push({
412
+ name: part.name,
413
+ title: part.title,
414
+ args: part.args,
415
+ status: part.status,
416
+ callIndex: index
417
+ });
418
+ } else if (part.type === "tool-result") {
419
+ const entry = entries.find(
420
+ (candidate) => candidate.name === part.name && candidate.resultIndex === void 0
421
+ );
422
+ if (entry) {
423
+ entry.status = part.status;
424
+ entry.result = part.result;
425
+ entry.error = part.error;
426
+ entry.resultIndex = index;
427
+ }
428
+ }
429
+ }
430
+ return entries;
431
+ }
432
+
433
+ // src/transport.ts
434
+ async function runChatTurn(store, transport, input, options = {}) {
435
+ const userMessageId = options.userMessageId ?? createChatId("msg");
436
+ const assistantMessageId = options.assistantMessageId ?? createChatId("msg");
437
+ if (!options.skipUserMessage) {
438
+ store.appendMessage({
439
+ id: userMessageId,
440
+ role: "user",
441
+ status: "completed",
442
+ parts: [
443
+ { id: createChatId("part"), type: "text", content: input.text, status: "ready" }
444
+ ],
445
+ createdAt: Date.now()
446
+ });
447
+ }
448
+ store.appendMessage({
449
+ id: assistantMessageId,
450
+ role: "assistant",
451
+ status: "pending",
452
+ parts: [],
453
+ createdAt: Date.now()
454
+ });
455
+ try {
456
+ for await (const event of transport.send(input)) {
457
+ if (options.signal?.aborted) {
458
+ store.updateMessage(assistantMessageId, { status: "cancelled" });
459
+ return { userMessageId, assistantMessageId };
460
+ }
461
+ applyChatEvent(store, { ...event, messageId: assistantMessageId });
462
+ }
463
+ if (options.signal?.aborted) {
464
+ store.updateMessage(assistantMessageId, { status: "cancelled" });
465
+ return { userMessageId, assistantMessageId };
466
+ }
467
+ const message = store.getMessage(assistantMessageId);
468
+ if (message && (message.status === "pending" || message.status === "streaming")) {
469
+ applyChatEvent(store, { type: "message.end", messageId: assistantMessageId });
470
+ }
471
+ } catch (cause) {
472
+ applyChatEvent(store, {
473
+ type: "message.error",
474
+ messageId: assistantMessageId,
475
+ error: cause instanceof Error ? cause.message : String(cause)
476
+ });
477
+ }
478
+ return { userMessageId, assistantMessageId };
479
+ }
480
+ async function runRegenerateTurn(store, transport, options = {}) {
481
+ const messages = store.getMessages();
482
+ let lastUserIndex = -1;
483
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
484
+ if (messages[index].role === "user") {
485
+ lastUserIndex = index;
486
+ break;
487
+ }
488
+ }
489
+ if (lastUserIndex < 0) {
490
+ console.warn("[@pactor-app/chat] runRegenerateTurn: no user message, ignored");
491
+ return void 0;
492
+ }
493
+ const lastUser = messages[lastUserIndex];
494
+ const next = messages[lastUserIndex + 1];
495
+ if (next) {
496
+ store.removeMessagesFrom(next.id);
497
+ }
498
+ const text = lastUser.parts.filter(
499
+ (part) => part.type === "text" || part.type === "markdown"
500
+ ).map((part) => part.content).join("");
501
+ return runChatTurn(store, transport, { text }, {
502
+ ...options,
503
+ userMessageId: lastUser.id,
504
+ skipUserMessage: true
505
+ });
506
+ }
507
+ function createManualDriver() {
508
+ const queue = [];
509
+ let waiting = null;
510
+ let done = false;
511
+ let failure2;
512
+ let started = false;
513
+ const notify = () => {
514
+ if (waiting) {
515
+ const resolve = waiting;
516
+ waiting = null;
517
+ resolve();
518
+ }
519
+ };
520
+ return {
521
+ transport: {
522
+ async *send() {
523
+ if (started) {
524
+ throw new Error("[@pactor-app/chat] manual driver transport can only be sent once");
525
+ }
526
+ started = true;
527
+ for (; ; ) {
528
+ while (queue.length > 0) {
529
+ yield queue.shift();
530
+ }
531
+ if (failure2 !== void 0) {
532
+ throw failure2;
533
+ }
534
+ if (done) {
535
+ return;
536
+ }
537
+ await new Promise((resolve) => {
538
+ waiting = resolve;
539
+ });
540
+ }
541
+ }
542
+ },
543
+ push(event) {
544
+ queue.push(event);
545
+ notify();
546
+ },
547
+ complete() {
548
+ done = true;
549
+ notify();
550
+ },
551
+ fail(cause) {
552
+ failure2 = cause;
553
+ notify();
554
+ }
555
+ };
556
+ }
557
+
558
+ // src/safe-registry.ts
559
+ import { Registry } from "@pactor-app/core";
560
+ var SafeComponentRegistry = class extends Registry {
561
+ constructor(source, allowed) {
562
+ super("SafeComponentRegistry");
563
+ this.source = source;
564
+ this.allowed = allowed;
565
+ }
566
+ get(name) {
567
+ if (!this.allowed.has(name)) {
568
+ return void 0;
569
+ }
570
+ return this.source.get(name);
571
+ }
572
+ has(name) {
573
+ return this.allowed.has(name) && this.source.has(name);
574
+ }
575
+ list() {
576
+ return this.source.list().filter((name) => this.allowed.has(name));
577
+ }
578
+ };
579
+ function createSafeRegistry(source, allowed) {
580
+ return new SafeComponentRegistry(source, new Set(allowed));
581
+ }
582
+
583
+ // src/capability-action.ts
584
+ function failure(message) {
585
+ return { ok: false, error: new Error(message) };
586
+ }
587
+ function registerCapabilityAction(actions, deps) {
588
+ if (actions.has("capability")) {
589
+ return;
590
+ }
591
+ actions.register("capability", async (action, ctx) => {
592
+ const name = action.params?.["name"];
593
+ if (typeof name !== "string" || !name) {
594
+ return failure("[@pactor-app/chat] capability \u52A8\u4F5C\u7F3A\u5C11 name \u53C2\u6570");
595
+ }
596
+ const capability = deps.capabilities.get(name);
597
+ if (!capability) {
598
+ return failure(`[@pactor-app/chat] capability not registered: "${name}"`);
599
+ }
600
+ if (capability.permission !== void 0 && deps.permission !== void 0 && !deps.permission.check(capability.permission)) {
601
+ return failure(
602
+ `[@pactor-app/chat] permission denied for capability "${name}"`
603
+ );
604
+ }
605
+ const params = action.params?.["params"] ?? {};
606
+ if (capability.risk === "high") {
607
+ if (!deps.confirm) {
608
+ return failure(
609
+ `[@pactor-app/chat] capability "${name}" \u4E3A\u9AD8\u98CE\u9669\u64CD\u4F5C\uFF0C\u4F46\u672A\u6CE8\u5165 confirm`
610
+ );
611
+ }
612
+ const confirmed = await deps.confirm({ name, params });
613
+ if (!confirmed) {
614
+ return { ok: true, skipped: true };
615
+ }
616
+ }
617
+ const data = await capability.execute(params, ctx);
618
+ return { ok: true, data };
619
+ });
620
+ }
621
+
622
+ // src/scope.ts
623
+ import { createPermissionAdapter } from "@pactor-app/permission";
624
+ import {
625
+ ActionEngine,
626
+ createRuntimeContext,
627
+ registerBuiltinActions
628
+ } from "@pactor-app/runtime";
629
+ function buildChatScope(scope) {
630
+ return {
631
+ conversation: scope.conversation,
632
+ message: scope.message,
633
+ params: scope.params ?? {}
634
+ };
635
+ }
636
+ function createChatRuntime(options = {}) {
637
+ const permission = options.permission ?? (options.permissionStore ? createPermissionAdapter(options.permissionStore) : void 0);
638
+ const context = createRuntimeContext({
639
+ components: options.safeRegistry,
640
+ user: options.user,
641
+ permission
642
+ });
643
+ registerBuiltinActions(context.actions);
644
+ if (options.capabilities) {
645
+ const deps = {
646
+ capabilities: options.capabilities,
647
+ permission,
648
+ confirm: options.confirm
649
+ };
650
+ registerCapabilityAction(context.actions, deps);
651
+ }
652
+ return { context, engine: new ActionEngine(context) };
653
+ }
654
+
655
+ // src/dsl-guard.ts
656
+ var CHAT_DENIED_TYPE = "ChatDenied";
657
+ function guardChatDsl(node, registry) {
658
+ if (node.type !== CHAT_DENIED_TYPE && !registry.has(node.type)) {
659
+ return {
660
+ type: CHAT_DENIED_TYPE,
661
+ props: { deniedType: node.type }
662
+ };
663
+ }
664
+ if (!node.children || node.children.length === 0) {
665
+ return node;
666
+ }
667
+ return {
668
+ ...node,
669
+ children: node.children.map((child) => guardChatDsl(child, registry))
670
+ };
671
+ }
672
+
673
+ // src/hooks/use-smooth-stream-text.ts
674
+ import { useEffect, useRef, useState } from "react";
675
+ function useSmoothStreamText(content, isStreaming, options = {}) {
676
+ const {
677
+ maxCharsPerFrame = 120,
678
+ minCharsPerFrame = 2,
679
+ catchUpDivisor = 5,
680
+ enabled = true
681
+ } = options;
682
+ const [shown, setShown] = useState(content);
683
+ const shownLengthRef = useRef(content.length);
684
+ const rafRef = useRef(0);
685
+ useEffect(() => {
686
+ if (!enabled || !isStreaming || shownLengthRef.current > content.length) {
687
+ if (rafRef.current) {
688
+ cancelAnimationFrame(rafRef.current);
689
+ rafRef.current = 0;
690
+ }
691
+ if (shownLengthRef.current !== content.length) {
692
+ shownLengthRef.current = content.length;
693
+ setShown(content);
694
+ }
695
+ return;
696
+ }
697
+ if (shownLengthRef.current >= content.length) {
698
+ return;
699
+ }
700
+ const step = () => {
701
+ rafRef.current = 0;
702
+ const target = content.length;
703
+ const current = shownLengthRef.current;
704
+ if (current >= target) {
705
+ return;
706
+ }
707
+ const backlog = target - current;
708
+ const advance = Math.min(
709
+ maxCharsPerFrame,
710
+ Math.max(minCharsPerFrame, Math.ceil(backlog / catchUpDivisor))
711
+ );
712
+ const next = Math.min(target, current + advance);
713
+ shownLengthRef.current = next;
714
+ setShown(content.slice(0, next));
715
+ if (next < target) {
716
+ rafRef.current = requestAnimationFrame(step);
717
+ }
718
+ };
719
+ if (!rafRef.current) {
720
+ rafRef.current = requestAnimationFrame(step);
721
+ }
722
+ return () => {
723
+ if (rafRef.current) {
724
+ cancelAnimationFrame(rafRef.current);
725
+ rafRef.current = 0;
726
+ }
727
+ };
728
+ }, [content, isStreaming, enabled, maxCharsPerFrame, minCharsPerFrame, catchUpDivisor]);
729
+ return shown;
730
+ }
731
+
732
+ // src/hooks/use-chat-auto-scroll.ts
733
+ import { useCallback, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
734
+ var RELEASE_THRESHOLD = 80;
735
+ var REENGAGE_THRESHOLD = 40;
736
+ function useChatAutoScroll({
737
+ messageCount,
738
+ lastContentLength: lastContentLength2,
739
+ isStreaming
740
+ }) {
741
+ const containerRef = useRef2(null);
742
+ const followRef = useRef2(true);
743
+ const [isFollowing, setIsFollowing] = useState2(true);
744
+ const setFollow = (follow) => {
745
+ followRef.current = follow;
746
+ setIsFollowing(follow);
747
+ };
748
+ useLayoutEffect(() => {
749
+ const container = containerRef.current;
750
+ if (container && followRef.current) {
751
+ container.scrollTop = container.scrollHeight;
752
+ }
753
+ }, [messageCount, lastContentLength2, isStreaming]);
754
+ useLayoutEffect(() => {
755
+ const container = containerRef.current;
756
+ if (!container) {
757
+ return;
758
+ }
759
+ const onScroll = () => {
760
+ const distance = container.scrollHeight - container.scrollTop - container.clientHeight;
761
+ if (followRef.current && distance > RELEASE_THRESHOLD) {
762
+ setFollow(false);
763
+ } else if (!followRef.current && distance < REENGAGE_THRESHOLD) {
764
+ setFollow(true);
765
+ }
766
+ };
767
+ container.addEventListener("scroll", onScroll, { passive: true });
768
+ return () => container.removeEventListener("scroll", onScroll);
769
+ }, []);
770
+ const scrollToBottom = useCallback(() => {
771
+ const container = containerRef.current;
772
+ if (container) {
773
+ container.scrollTop = container.scrollHeight;
774
+ }
775
+ setFollow(true);
776
+ }, []);
777
+ return { containerRef, isFollowing, scrollToBottom };
778
+ }
779
+
780
+ // src/chat-composition.tsx
781
+ import { useRenderer as useRenderer3 } from "@pactor-app/runtime";
782
+ import {
783
+ Component as Component2,
784
+ isValidElement,
785
+ useRef as useRef11,
786
+ useState as useState15,
787
+ useSyncExternalStore as useSyncExternalStore2
788
+ } from "react";
789
+
790
+ // src/hooks/use-chat-model.ts
791
+ import { useState as useState3 } from "react";
792
+ function useChatModel({
793
+ models,
794
+ model,
795
+ onModelChange
796
+ }) {
797
+ const modelsEnabled = models !== void 0 && models.length > 0;
798
+ const modelControlled = model !== void 0 && onModelChange !== void 0;
799
+ const [innerModel, setInnerModel] = useState3(
800
+ () => models?.[0]?.value
801
+ );
802
+ const currentModel = modelsEnabled ? modelControlled ? model : innerModel ?? models[0].value : void 0;
803
+ const handleModelChange = (value) => {
804
+ if (!modelControlled) {
805
+ setInnerModel(value);
806
+ }
807
+ onModelChange?.(value);
808
+ };
809
+ return { modelsEnabled, currentModel, handleModelChange };
810
+ }
811
+
812
+ // src/view/AppShell.tsx
813
+ import { Menu, PanelLeft } from "lucide-react";
814
+ import {
815
+ createContext,
816
+ useCallback as useCallback2,
817
+ useContext,
818
+ useEffect as useEffect2,
819
+ useState as useState4
820
+ } from "react";
821
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
822
+ var SidebarDrawerContext = createContext(null);
823
+ function useSidebarDrawer() {
824
+ return useContext(SidebarDrawerContext);
825
+ }
826
+ var MOBILE_MEDIA_QUERY = "(max-width: 767px)";
827
+ function useIsMobileViewport() {
828
+ const [isMobile, setIsMobile] = useState4(
829
+ () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(MOBILE_MEDIA_QUERY).matches
830
+ );
831
+ useEffect2(() => {
832
+ if (typeof window.matchMedia !== "function") {
833
+ return;
834
+ }
835
+ const media = window.matchMedia(MOBILE_MEDIA_QUERY);
836
+ const onChange = (event) => {
837
+ setIsMobile(event.matches);
838
+ };
839
+ setIsMobile(media.matches);
840
+ media.addEventListener("change", onChange);
841
+ return () => media.removeEventListener("change", onChange);
842
+ }, []);
843
+ return isMobile;
844
+ }
845
+ function AppShell({ sidebar, brand, locationKey, sidebarFooter, collapsible = false, children, className }) {
846
+ const [drawerOpen, setDrawerOpen] = useState4(false);
847
+ const isMobile = useIsMobileViewport();
848
+ const close = useCallback2(() => setDrawerOpen(false), []);
849
+ const [collapsed, setCollapsed] = useState4(false);
850
+ const collapsedEffective = collapsible && collapsed && !isMobile;
851
+ const framed = brand !== void 0 || sidebarFooter !== void 0;
852
+ const [trackedLocationKey, setTrackedLocationKey] = useState4(locationKey);
853
+ if (trackedLocationKey !== locationKey) {
854
+ setTrackedLocationKey(locationKey);
855
+ setDrawerOpen(false);
856
+ }
857
+ useEffect2(() => {
858
+ if (!drawerOpen) {
859
+ return;
860
+ }
861
+ const onKeyDown = (event) => {
862
+ if (event.key === "Escape") {
863
+ setDrawerOpen(false);
864
+ }
865
+ };
866
+ window.addEventListener("keydown", onKeyDown);
867
+ return () => window.removeEventListener("keydown", onKeyDown);
868
+ }, [drawerOpen]);
869
+ return /* @__PURE__ */ jsx(SidebarDrawerContext.Provider, { value: { close, collapsed: collapsedEffective }, children: /* @__PURE__ */ jsxs("div", { className: `pactor-chat-shell${className ? ` ${className}` : ""}`, children: [
870
+ drawerOpen ? /* @__PURE__ */ jsx("div", { className: "pactor-chat-shell__scrim", "aria-hidden": true, onClick: close }) : null,
871
+ /* @__PURE__ */ jsx(
872
+ "div",
873
+ {
874
+ className: `pactor-chat-shell__sidebar${framed ? " pactor-chat-shell__sidebar--framed" : ""}${collapsedEffective ? " pactor-chat-shell__sidebar--collapsed" : ""}`,
875
+ "data-open": drawerOpen ? "true" : "false",
876
+ inert: isMobile && !drawerOpen ? true : void 0,
877
+ children: framed ? /* @__PURE__ */ jsxs(Fragment, { children: [
878
+ brand !== void 0 && // 桌面品牌行(决策 D3):brand 左、折叠钮右;移动端由
879
+ // CSS 隐藏(品牌维持顶栏现状)
880
+ /* @__PURE__ */ jsxs("div", { className: "pactor-chat-shell__sidebar-header", children: [
881
+ /* @__PURE__ */ jsx("div", { className: "pactor-chat-shell__brand", children: brand }),
882
+ collapsible && !isMobile && /* @__PURE__ */ jsx(
883
+ "button",
884
+ {
885
+ type: "button",
886
+ className: "pactor-chat-icon-button pactor-chat-shell__collapse",
887
+ "aria-label": collapsedEffective ? "\u5C55\u5F00\u4FA7\u680F" : "\u6298\u53E0\u4FA7\u680F",
888
+ "aria-expanded": !collapsedEffective,
889
+ title: collapsedEffective ? "\u5C55\u5F00\u4FA7\u680F" : "\u6298\u53E0\u4FA7\u680F",
890
+ onClick: () => setCollapsed((value) => !value),
891
+ children: /* @__PURE__ */ jsx(PanelLeft, { size: 16, strokeWidth: 1.7 })
892
+ }
893
+ )
894
+ ] }),
895
+ /* @__PURE__ */ jsx("div", { className: "pactor-chat-shell__sidebar-body", children: sidebar }),
896
+ sidebarFooter !== void 0 && /* @__PURE__ */ jsx("div", { className: "pactor-chat-shell__sidebar-footer", children: sidebarFooter })
897
+ ] }) : sidebar
898
+ }
899
+ ),
900
+ /* @__PURE__ */ jsxs("main", { className: "pactor-chat-shell__main", children: [
901
+ /* @__PURE__ */ jsxs("div", { className: "pactor-chat-shell__topbar", children: [
902
+ /* @__PURE__ */ jsx(
903
+ "button",
904
+ {
905
+ type: "button",
906
+ className: "pactor-chat-icon-button pactor-chat-shell__menu",
907
+ "aria-label": "\u6253\u5F00\u5BFC\u822A",
908
+ "aria-expanded": drawerOpen,
909
+ onClick: () => setDrawerOpen(true),
910
+ children: /* @__PURE__ */ jsx(Menu, { size: 18, strokeWidth: 1.7 })
911
+ }
912
+ ),
913
+ brand
914
+ ] }),
915
+ /* @__PURE__ */ jsx("div", { className: "pactor-chat-shell__content", children })
916
+ ] })
917
+ ] }) });
918
+ }
919
+
920
+ // src/view/ArtifactPanel.tsx
921
+ import { X } from "lucide-react";
922
+ import { Registry as Registry2 } from "@pactor-app/core";
923
+ import { MarkdownRenderer as MarkdownRenderer2 } from "@pactor-app/markdown";
924
+ import { DslRenderer as DslRenderer2, useRenderer as useRenderer2 } from "@pactor-app/runtime";
925
+ import {
926
+ Component,
927
+ useMemo,
928
+ useRef as useRef3,
929
+ useState as useState5
930
+ } from "react";
931
+
932
+ // src/view/PartRenderer.tsx
933
+ import { CircleAlert, PanelRightOpen } from "lucide-react";
934
+ import { MarkdownRenderer } from "@pactor-app/markdown";
935
+ import { DslRenderer, useRenderer } from "@pactor-app/runtime";
936
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
937
+ function ChatDenied({ deniedType }) {
938
+ return /* @__PURE__ */ jsxs2("div", { className: "pactor-chat-denied", "data-pactor-chat-denied": deniedType, children: [
939
+ "\u8BE5\u7EC4\u4EF6\u4E0D\u5141\u8BB8\u5728 Chat \u4E2D\u4F7F\u7528",
940
+ deniedType ? `\uFF1A${deniedType}` : ""
941
+ ] });
942
+ }
943
+ function ChatDslNode({
944
+ node,
945
+ scope
946
+ }) {
947
+ const renderer = useRenderer();
948
+ const extraScope = scope ? buildChatScope(scope) : void 0;
949
+ return /* @__PURE__ */ jsx2(Fragment2, { children: renderer.renderChildren([node], extraScope) });
950
+ }
951
+ function SmoothText({ content, streaming }) {
952
+ const shown = useSmoothStreamText(content, streaming);
953
+ return /* @__PURE__ */ jsx2("div", { className: "pactor-chat-text", children: shown });
954
+ }
955
+ function SmoothMarkdown({ content, streaming }) {
956
+ const shown = useSmoothStreamText(content, streaming);
957
+ return /* @__PURE__ */ jsx2(MarkdownRenderer, { content: shown });
958
+ }
959
+ function PartRenderer({ part, runtime, scope, streaming = false, onOpenArtifact }) {
960
+ switch (part.type) {
961
+ case "text":
962
+ return /* @__PURE__ */ jsx2(SmoothText, { content: part.content, streaming });
963
+ case "markdown":
964
+ return /* @__PURE__ */ jsx2(SmoothMarkdown, { content: part.content, streaming });
965
+ case "thinking":
966
+ case "tool-call":
967
+ case "tool-result":
968
+ return null;
969
+ case "dsl": {
970
+ if (!part.schema || !runtime) {
971
+ return /* @__PURE__ */ jsx2("div", { className: "pactor-chat-dsl-pending", children: part.schema ? "DSL \u6E32\u67D3\u4E0D\u53EF\u7528\uFF08\u672A\u6CE8\u5165 Safe Registry\uFF09" : "\u5185\u5BB9\u751F\u6210\u4E2D\u2026" });
972
+ }
973
+ const guarded = guardChatDsl(part.schema, runtime.context.components);
974
+ return /* @__PURE__ */ jsx2("div", { className: "pactor-chat-dsl", children: /* @__PURE__ */ jsx2(DslRenderer, { context: runtime.context, engine: runtime.engine, children: /* @__PURE__ */ jsx2(ChatDslNode, { node: guarded, scope }) }) });
975
+ }
976
+ case "error":
977
+ return /* @__PURE__ */ jsxs2("div", { className: "pactor-chat-error", role: "alert", children: [
978
+ /* @__PURE__ */ jsx2(CircleAlert, { size: 14 }),
979
+ /* @__PURE__ */ jsx2("span", { children: part.message })
980
+ ] });
981
+ case "status":
982
+ return /* @__PURE__ */ jsx2("div", { className: "pactor-chat-status", children: part.text });
983
+ case "artifact":
984
+ return /* @__PURE__ */ jsxs2(
985
+ "div",
986
+ {
987
+ className: "pactor-chat-artifact-card",
988
+ "data-artifact-kind": part.artifact.kind,
989
+ children: [
990
+ /* @__PURE__ */ jsx2("span", { className: "pactor-chat-artifact-card__title", children: part.artifact.title }),
991
+ /* @__PURE__ */ jsx2("span", { className: "pactor-chat-artifact-card__kind", children: part.artifact.kind }),
992
+ /* @__PURE__ */ jsxs2(
993
+ "button",
994
+ {
995
+ type: "button",
996
+ className: "pactor-chat-artifact-card__open",
997
+ onClick: () => onOpenArtifact?.(part.artifact),
998
+ children: [
999
+ /* @__PURE__ */ jsx2(PanelRightOpen, { size: 13, strokeWidth: 1.7 }),
1000
+ "\u5728\u9762\u677F\u4E2D\u6253\u5F00"
1001
+ ]
1002
+ }
1003
+ )
1004
+ ]
1005
+ }
1006
+ );
1007
+ default:
1008
+ return null;
1009
+ }
1010
+ }
1011
+
1012
+ // src/view/ArtifactPanel.tsx
1013
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1014
+ function useArtifactRuntime(safeRegistry, user) {
1015
+ return useMemo(() => {
1016
+ if (!safeRegistry) {
1017
+ return void 0;
1018
+ }
1019
+ const components = new Registry2("ChatArtifactRegistry");
1020
+ components.register(CHAT_DENIED_TYPE, ChatDenied);
1021
+ for (const name of safeRegistry.list()) {
1022
+ components.register(name, safeRegistry.get(name));
1023
+ }
1024
+ return createChatRuntime({ safeRegistry: components, user });
1025
+ }, [safeRegistry, user]);
1026
+ }
1027
+ var ArtifactDslGuard = class extends Component {
1028
+ constructor() {
1029
+ super(...arguments);
1030
+ this.state = { failed: false };
1031
+ }
1032
+ static getDerivedStateFromError() {
1033
+ return { failed: true };
1034
+ }
1035
+ componentDidCatch(error) {
1036
+ console.warn("[@pactor-app/chat] DSL \u4EA7\u51FA\u7269\u6E32\u67D3\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u5C55\u793A", error);
1037
+ }
1038
+ render() {
1039
+ return this.state.failed ? /* @__PURE__ */ jsx3("div", { className: "pactor-chat-denied", children: "DSL \u4EA7\u51FA\u7269\u6E32\u67D3\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u5C55\u793A" }) : this.props.children;
1040
+ }
1041
+ };
1042
+ function ArtifactDslNode({ node }) {
1043
+ const { renderChildren } = useRenderer2();
1044
+ return /* @__PURE__ */ jsx3(Fragment3, { children: renderChildren([node]) });
1045
+ }
1046
+ function ArtifactDslContent({
1047
+ content,
1048
+ runtime
1049
+ }) {
1050
+ const node = typeof content === "object" && content !== null && !Array.isArray(content) && typeof content.type === "string" ? content : void 0;
1051
+ if (!node || !runtime) {
1052
+ return /* @__PURE__ */ jsx3("div", { className: "pactor-chat-denied", children: node ? "DSL \u9884\u89C8\u4E0D\u53EF\u7528\uFF08\u672A\u6CE8\u5165 Safe Registry\uFF09" : "DSL \u4EA7\u51FA\u7269\u5185\u5BB9\u65E0\u6548" });
1053
+ }
1054
+ const guarded = guardChatDsl(node, runtime.context.components);
1055
+ return /* @__PURE__ */ jsx3(DslRenderer2, { context: runtime.context, engine: runtime.engine, children: /* @__PURE__ */ jsx3(ArtifactDslGuard, { children: /* @__PURE__ */ jsx3(ArtifactDslNode, { node: guarded }) }) });
1056
+ }
1057
+ function ArtifactContent({
1058
+ artifact,
1059
+ runtime
1060
+ }) {
1061
+ switch (artifact.kind) {
1062
+ case "markdown":
1063
+ return /* @__PURE__ */ jsx3(MarkdownRenderer2, { content: String(artifact.content) });
1064
+ case "code":
1065
+ return /* @__PURE__ */ jsxs3("div", { className: "pactor-chat-inspector__code", children: [
1066
+ artifact.language ? /* @__PURE__ */ jsx3("span", { className: "pactor-chat-inspector__code-lang", children: artifact.language }) : null,
1067
+ /* @__PURE__ */ jsx3("pre", { children: /* @__PURE__ */ jsx3("code", { children: String(artifact.content) }) })
1068
+ ] });
1069
+ case "text":
1070
+ return /* @__PURE__ */ jsx3("pre", { className: "pactor-chat-inspector__text", children: String(artifact.content) });
1071
+ case "dsl":
1072
+ return /* @__PURE__ */ jsx3(ArtifactDslContent, { content: artifact.content, runtime });
1073
+ default:
1074
+ return null;
1075
+ }
1076
+ }
1077
+ function ArtifactPanel({
1078
+ artifact,
1079
+ inspector,
1080
+ onClose,
1081
+ safeRegistry,
1082
+ user,
1083
+ className
1084
+ }) {
1085
+ const runtime = useArtifactRuntime(safeRegistry, user);
1086
+ const panelRef = useRef3(null);
1087
+ const [width, setWidth] = useState5(null);
1088
+ const handleResizeStart = (event) => {
1089
+ event.preventDefault();
1090
+ const panel = panelRef.current;
1091
+ if (!panel) {
1092
+ return;
1093
+ }
1094
+ const hostRect = panel.parentElement?.getBoundingClientRect();
1095
+ const hostWidth = hostRect !== void 0 && hostRect.width > 0 ? hostRect.width : window.innerWidth;
1096
+ const maxWidth = hostWidth * 0.7;
1097
+ const startWidth = panel.getBoundingClientRect().width;
1098
+ const startX = event.clientX;
1099
+ const prevUserSelect = document.body.style.userSelect;
1100
+ document.body.style.userSelect = "none";
1101
+ const onMove = (moveEvent) => {
1102
+ const next = startWidth + (startX - moveEvent.clientX);
1103
+ setWidth(Math.round(Math.min(Math.max(next, 240), maxWidth)));
1104
+ };
1105
+ const onUp = () => {
1106
+ document.body.style.userSelect = prevUserSelect;
1107
+ document.removeEventListener("pointermove", onMove);
1108
+ document.removeEventListener("pointerup", onUp);
1109
+ };
1110
+ document.addEventListener("pointermove", onMove);
1111
+ document.addEventListener("pointerup", onUp);
1112
+ };
1113
+ return /* @__PURE__ */ jsxs3(
1114
+ "aside",
1115
+ {
1116
+ ref: panelRef,
1117
+ className: `pactor-chat-inspector${className ? ` ${className}` : ""}`,
1118
+ "data-kind": artifact?.kind,
1119
+ style: width !== null ? { width: `${width}px` } : void 0,
1120
+ children: [
1121
+ /* @__PURE__ */ jsx3(
1122
+ "div",
1123
+ {
1124
+ className: "pactor-chat-inspector__resize",
1125
+ role: "separator",
1126
+ "aria-orientation": "vertical",
1127
+ "aria-label": "\u62D6\u52A8\u8C03\u6574\u9762\u677F\u5BBD\u5EA6",
1128
+ onPointerDown: handleResizeStart
1129
+ }
1130
+ ),
1131
+ /* @__PURE__ */ jsxs3("div", { className: "pactor-chat-inspector__header", children: [
1132
+ /* @__PURE__ */ jsx3("span", { className: "pactor-chat-inspector__title", children: artifact?.title ?? "\u4EA7\u51FA\u7269" }),
1133
+ artifact ? /* @__PURE__ */ jsx3("span", { className: "pactor-chat-inspector__kind", children: artifact.kind }) : null,
1134
+ /* @__PURE__ */ jsx3(
1135
+ "button",
1136
+ {
1137
+ type: "button",
1138
+ className: "pactor-chat-icon-button pactor-chat-inspector__close",
1139
+ "aria-label": "\u5173\u95ED\u9762\u677F",
1140
+ onClick: onClose,
1141
+ children: /* @__PURE__ */ jsx3(X, { size: 16, strokeWidth: 1.7 })
1142
+ }
1143
+ )
1144
+ ] }),
1145
+ /* @__PURE__ */ jsx3("div", { className: "pactor-chat-inspector__body", children: inspector !== void 0 && inspector !== null ? inspector : artifact ? /* @__PURE__ */ jsx3(ArtifactContent, { artifact, runtime }) : null })
1146
+ ]
1147
+ }
1148
+ );
1149
+ }
1150
+
1151
+ // src/view/ChatView.tsx
1152
+ import { ArrowDown } from "lucide-react";
1153
+ import { Registry as Registry3 } from "@pactor-app/core";
1154
+ import { useEffect as useEffect6, useMemo as useMemo3, useRef as useRef9, useState as useState13, useSyncExternalStore } from "react";
1155
+
1156
+ // src/view/Composer.tsx
1157
+ import { ArrowUp, FileText, Paperclip, Plus, Square, X as X2 } from "lucide-react";
1158
+ import { useLayoutEffect as useLayoutEffect3, useMemo as useMemo2, useRef as useRef6, useState as useState9 } from "react";
1159
+
1160
+ // src/view/AnchoredPopover.tsx
1161
+ import {
1162
+ useEffect as useEffect3,
1163
+ useLayoutEffect as useLayoutEffect2,
1164
+ useRef as useRef4,
1165
+ useState as useState6
1166
+ } from "react";
1167
+ import { jsx as jsx4 } from "react/jsx-runtime";
1168
+ function AnchoredPopover({
1169
+ anchorRect,
1170
+ onClose,
1171
+ children,
1172
+ className,
1173
+ role = "dialog",
1174
+ sideOffset = 8
1175
+ }) {
1176
+ const ref = useRef4(null);
1177
+ const [position, setPosition] = useState6(null);
1178
+ useLayoutEffect2(() => {
1179
+ const element = ref.current;
1180
+ if (!element) return;
1181
+ const height = element.offsetHeight;
1182
+ const width = element.offsetWidth;
1183
+ const below = height + sideOffset > anchorRect.top;
1184
+ const left = Math.max(
1185
+ 0,
1186
+ Math.min(anchorRect.left, window.innerWidth - width - sideOffset)
1187
+ );
1188
+ setPosition(
1189
+ below ? { below, left, top: anchorRect.bottom + sideOffset } : {
1190
+ below,
1191
+ left,
1192
+ bottom: window.innerHeight - anchorRect.top + sideOffset
1193
+ }
1194
+ );
1195
+ }, [anchorRect, sideOffset]);
1196
+ useEffect3(() => {
1197
+ const onKeyDown = (event) => {
1198
+ if (event.key !== "Escape") return;
1199
+ const element = ref.current;
1200
+ const active = document.activeElement;
1201
+ if (element && active instanceof HTMLElement && element.contains(active)) {
1202
+ active.blur();
1203
+ }
1204
+ onClose();
1205
+ };
1206
+ const onPointerDown = (event) => {
1207
+ const element = ref.current;
1208
+ if (element && event.target instanceof Node && !element.contains(event.target)) {
1209
+ onClose();
1210
+ }
1211
+ };
1212
+ document.addEventListener("keydown", onKeyDown);
1213
+ document.addEventListener("pointerdown", onPointerDown, true);
1214
+ return () => {
1215
+ document.removeEventListener("keydown", onKeyDown);
1216
+ document.removeEventListener("pointerdown", onPointerDown, true);
1217
+ };
1218
+ }, [onClose]);
1219
+ const classNames = [
1220
+ "pactor-chat-popover",
1221
+ position?.below ? "pactor-chat-popover--below" : "",
1222
+ className ?? ""
1223
+ ].filter(Boolean).join(" ");
1224
+ return /* @__PURE__ */ jsx4(
1225
+ "div",
1226
+ {
1227
+ ref,
1228
+ className: classNames,
1229
+ role,
1230
+ tabIndex: -1,
1231
+ style: {
1232
+ left: position?.left ?? anchorRect.left,
1233
+ top: position?.top,
1234
+ bottom: position?.bottom,
1235
+ visibility: position ? "visible" : "hidden"
1236
+ },
1237
+ children
1238
+ }
1239
+ );
1240
+ }
1241
+
1242
+ // src/view/ModelChip.tsx
1243
+ import { Check, ChevronsUpDown } from "lucide-react";
1244
+ import { useRef as useRef5, useState as useState7 } from "react";
1245
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1246
+ function ModelChip({
1247
+ models,
1248
+ model,
1249
+ onModelChange
1250
+ }) {
1251
+ const chipRef = useRef5(null);
1252
+ const [anchor, setAnchor] = useState7(null);
1253
+ const currentOption = models.find((option) => option.value === model) ?? models[0];
1254
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
1255
+ /* @__PURE__ */ jsxs4(
1256
+ "button",
1257
+ {
1258
+ ref: chipRef,
1259
+ type: "button",
1260
+ className: "pactor-chat-model-chip",
1261
+ title: "\u9009\u62E9\u6A21\u578B",
1262
+ "aria-label": "\u9009\u62E9\u6A21\u578B",
1263
+ onClick: () => setAnchor(chipRef.current.getBoundingClientRect()),
1264
+ children: [
1265
+ /* @__PURE__ */ jsx5("span", { className: "pactor-chat-model-chip__label", children: currentOption.label }),
1266
+ /* @__PURE__ */ jsx5(ChevronsUpDown, { size: 12 })
1267
+ ]
1268
+ }
1269
+ ),
1270
+ anchor && /* @__PURE__ */ jsx5(
1271
+ AnchoredPopover,
1272
+ {
1273
+ anchorRect: anchor,
1274
+ onClose: () => setAnchor(null),
1275
+ role: "listbox",
1276
+ children: models.map((option) => /* @__PURE__ */ jsxs4(
1277
+ "button",
1278
+ {
1279
+ type: "button",
1280
+ className: "pactor-chat-popover__item",
1281
+ role: "option",
1282
+ "aria-selected": option.value === currentOption.value,
1283
+ onClick: () => {
1284
+ onModelChange?.(option.value);
1285
+ setAnchor(null);
1286
+ },
1287
+ children: [
1288
+ /* @__PURE__ */ jsx5("span", { className: "pactor-chat-popover__item-label", children: option.label }),
1289
+ option.value === currentOption.value && /* @__PURE__ */ jsx5(Check, { size: 13, className: "pactor-chat-popover__item-check" })
1290
+ ]
1291
+ },
1292
+ option.value
1293
+ ))
1294
+ }
1295
+ )
1296
+ ] });
1297
+ }
1298
+
1299
+ // src/view/context-sources.tsx
1300
+ import { useEffect as useEffect4, useState as useState8 } from "react";
1301
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1302
+ function sourceCacheKey(source) {
1303
+ return `${source.service ?? ""}:${JSON.stringify(source.params ?? {})}`;
1304
+ }
1305
+ function resolveSource(source, callService, cache) {
1306
+ if (source.items) {
1307
+ return Promise.resolve(source.items);
1308
+ }
1309
+ if (!source.service) {
1310
+ return Promise.resolve([]);
1311
+ }
1312
+ const key = sourceCacheKey(source);
1313
+ let cached = cache.get(key);
1314
+ if (!cached) {
1315
+ cached = callService ? callService(source.service, source.params).then(
1316
+ (value) => Array.isArray(value) ? value : []
1317
+ ) : Promise.reject(
1318
+ new Error(
1319
+ `\u672A\u6CE8\u5165 callService\uFF0C\u65E0\u6CD5\u89E3\u6790\u670D\u52A1 "${source.service}"`
1320
+ )
1321
+ );
1322
+ cache.set(key, cached);
1323
+ }
1324
+ return cached;
1325
+ }
1326
+ function matchesQuery(item, query) {
1327
+ return item.label.toLowerCase().includes(query.toLowerCase());
1328
+ }
1329
+ function ContextSourceList({
1330
+ sources,
1331
+ query,
1332
+ callService,
1333
+ cache,
1334
+ onSelect
1335
+ }) {
1336
+ const [resolutions, setResolutions] = useState8([]);
1337
+ useEffect4(() => {
1338
+ let cancelled = false;
1339
+ const initial = sources.map(
1340
+ (source) => source.items ? { status: "ready", items: source.items } : { status: "loading" }
1341
+ );
1342
+ setResolutions(initial);
1343
+ sources.forEach((source, index) => {
1344
+ if (source.items) {
1345
+ return;
1346
+ }
1347
+ resolveSource(source, callService, cache).then(
1348
+ (items) => {
1349
+ if (!cancelled) {
1350
+ setResolutions((prev) => {
1351
+ const next = [...prev];
1352
+ next[index] = { status: "ready", items };
1353
+ return next;
1354
+ });
1355
+ }
1356
+ },
1357
+ (cause) => {
1358
+ if (!cancelled) {
1359
+ setResolutions((prev) => {
1360
+ const next = [...prev];
1361
+ next[index] = {
1362
+ status: "error",
1363
+ message: cause instanceof Error ? cause.message : String(cause)
1364
+ };
1365
+ return next;
1366
+ });
1367
+ }
1368
+ }
1369
+ );
1370
+ });
1371
+ return () => {
1372
+ cancelled = true;
1373
+ };
1374
+ }, [sources, callService, cache]);
1375
+ let totalMatched = 0;
1376
+ const groups = sources.map((source, index) => {
1377
+ const resolution = resolutions[index] ?? { status: "loading" };
1378
+ const items = resolution.status === "ready" ? resolution.items.filter((item) => matchesQuery(item, query)) : [];
1379
+ totalMatched += items.length;
1380
+ return { source, resolution, items };
1381
+ });
1382
+ return /* @__PURE__ */ jsxs5("div", { className: "pactor-chat-source-list", children: [
1383
+ groups.map(({ source, resolution, items }) => /* @__PURE__ */ jsxs5("div", { className: "pactor-chat-source-list__group", children: [
1384
+ sources.length > 1 && /* @__PURE__ */ jsx6("div", { className: "pactor-chat-source-list__group-label", children: source.label }),
1385
+ resolution.status === "loading" && /* @__PURE__ */ jsx6("div", { className: "pactor-chat-source-list__hint", children: "\u52A0\u8F7D\u4E2D\u2026" }),
1386
+ resolution.status === "error" && /* @__PURE__ */ jsx6("div", { className: "pactor-chat-source-list__error", role: "alert", children: resolution.message }),
1387
+ items.map((item) => /* @__PURE__ */ jsx6(
1388
+ "button",
1389
+ {
1390
+ type: "button",
1391
+ className: "pactor-chat-popover__item",
1392
+ onClick: () => onSelect(item, source),
1393
+ children: item.label
1394
+ },
1395
+ item.id
1396
+ ))
1397
+ ] }, `${source.trigger}:${source.label}`)),
1398
+ totalMatched === 0 && groups.every(({ resolution }) => resolution.status === "ready") && /* @__PURE__ */ jsx6("div", { className: "pactor-chat-source-list__hint", children: "\u65E0\u5339\u914D\u9879" })
1399
+ ] });
1400
+ }
1401
+
1402
+ // src/view/Composer.tsx
1403
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1404
+ var MAX_INPUT_HEIGHT = 200;
1405
+ function formatSize(bytes) {
1406
+ if (bytes < 1024) {
1407
+ return `${bytes} B`;
1408
+ }
1409
+ if (bytes < 1024 * 1024) {
1410
+ return `${(bytes / 1024).toFixed(1)} KB`;
1411
+ }
1412
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1413
+ }
1414
+ function detectMention(text, caret) {
1415
+ const beforeCaret = text.slice(0, caret);
1416
+ const match = /(?:^|\s)@([^\s@]*)$/.exec(beforeCaret);
1417
+ return match ? match[1] : null;
1418
+ }
1419
+ function collectDropFiles(event) {
1420
+ return event.dataTransfer ? Array.from(event.dataTransfer.files) : [];
1421
+ }
1422
+ function Composer({
1423
+ onSend,
1424
+ streaming = false,
1425
+ onStop,
1426
+ placeholder = "\u8F93\u5165\u6D88\u606F\uFF0CEnter \u53D1\u9001\uFF0CShift+Enter \u6362\u884C",
1427
+ disabled = false,
1428
+ attachments,
1429
+ onAttachmentsChange,
1430
+ onAttachFiles,
1431
+ onAttachmentClick,
1432
+ toolbarStart,
1433
+ toolbarEnd,
1434
+ onMentionTrigger,
1435
+ onPlusMenu,
1436
+ models,
1437
+ model,
1438
+ onModelChange,
1439
+ contextSources,
1440
+ contextChips,
1441
+ onContextChipsChange,
1442
+ callService
1443
+ }) {
1444
+ const [text, setText] = useState9("");
1445
+ const [dragOver, setDragOver] = useState9(false);
1446
+ const inputRef = useRef6(null);
1447
+ const fileInputRef = useRef6(null);
1448
+ const plusButtonRef = useRef6(null);
1449
+ const composingRef = useRef6(false);
1450
+ const modelChipEnabled = models !== void 0 && models.length > 0;
1451
+ const mentionSources = useMemo2(
1452
+ () => contextSources?.filter((source) => source.trigger === "@") ?? [],
1453
+ [contextSources]
1454
+ );
1455
+ const plusSources = useMemo2(
1456
+ () => contextSources?.filter((source) => source.trigger === "+") ?? [],
1457
+ [contextSources]
1458
+ );
1459
+ const builtinMentionEnabled = onMentionTrigger === void 0 && mentionSources.length > 0;
1460
+ const builtinPlusEnabled = onPlusMenu === void 0 && plusSources.length > 0;
1461
+ const [mentionState, setMentionState] = useState9(null);
1462
+ const [plusAnchor, setPlusAnchor] = useState9(null);
1463
+ const sourceCacheRef = useRef6(null);
1464
+ if (sourceCacheRef.current === null) {
1465
+ sourceCacheRef.current = /* @__PURE__ */ new Map();
1466
+ }
1467
+ const handleSelectContext = (item, source) => {
1468
+ const chip = {
1469
+ ...item,
1470
+ trigger: item.trigger ?? source.trigger
1471
+ };
1472
+ const chips = contextChips ?? [];
1473
+ const duplicated = chips.some(
1474
+ (existing) => existing.id === chip.id && existing.trigger === chip.trigger
1475
+ );
1476
+ if (!duplicated) {
1477
+ onContextChipsChange?.([...chips, chip]);
1478
+ }
1479
+ setMentionState(null);
1480
+ setPlusAnchor(null);
1481
+ };
1482
+ useLayoutEffect3(() => {
1483
+ const input = inputRef.current;
1484
+ if (input) {
1485
+ input.style.height = "auto";
1486
+ input.style.height = `${Math.min(input.scrollHeight, MAX_INPUT_HEIGHT)}px`;
1487
+ }
1488
+ }, [text]);
1489
+ const send = () => {
1490
+ const value = text.trim();
1491
+ if (!value || disabled) {
1492
+ return;
1493
+ }
1494
+ onSend?.(value);
1495
+ setText("");
1496
+ };
1497
+ const handleChange = (event) => {
1498
+ const value = event.target.value;
1499
+ setText(value);
1500
+ if (onMentionTrigger || builtinMentionEnabled) {
1501
+ const caret = event.target.selectionStart ?? value.length;
1502
+ const query = detectMention(value, caret);
1503
+ if (onMentionTrigger) {
1504
+ if (query !== null) {
1505
+ onMentionTrigger(query, event.target.getBoundingClientRect());
1506
+ }
1507
+ } else {
1508
+ setMentionState(
1509
+ query !== null ? { query, anchorRect: event.target.getBoundingClientRect() } : null
1510
+ );
1511
+ }
1512
+ }
1513
+ };
1514
+ const showToolbar = toolbarStart !== void 0 || toolbarEnd !== void 0 || onPlusMenu !== void 0 || builtinPlusEnabled || onAttachFiles !== void 0 || modelChipEnabled;
1515
+ return /* @__PURE__ */ jsxs6(
1516
+ "div",
1517
+ {
1518
+ className: `pactor-chat-composer${dragOver ? " pactor-chat-composer--dragover" : ""}`,
1519
+ onDragOver: (event) => {
1520
+ event.preventDefault();
1521
+ setDragOver(true);
1522
+ },
1523
+ onDragLeave: () => setDragOver(false),
1524
+ onDrop: (event) => {
1525
+ event.preventDefault();
1526
+ setDragOver(false);
1527
+ const files = collectDropFiles(event);
1528
+ if (files.length > 0 && onAttachFiles) {
1529
+ onAttachFiles(files);
1530
+ }
1531
+ },
1532
+ children: [
1533
+ attachments !== void 0 && attachments.length > 0 && /* @__PURE__ */ jsx7("div", { className: "pactor-chat-composer__attachments", children: attachments.map((attachment) => {
1534
+ const isImage = attachment.mimeType.startsWith("image/");
1535
+ return /* @__PURE__ */ jsxs6(
1536
+ "div",
1537
+ {
1538
+ className: "pactor-chat-composer__attachment",
1539
+ "data-attachment-id": attachment.id,
1540
+ onClick: () => onAttachmentClick?.(attachment),
1541
+ children: [
1542
+ isImage && attachment.url ? /* @__PURE__ */ jsx7("span", { className: "pactor-chat-composer__attachment-thumb", children: /* @__PURE__ */ jsx7("img", { src: attachment.url, alt: attachment.name }) }) : /* @__PURE__ */ jsxs6("span", { className: "pactor-chat-composer__attachment-info", children: [
1543
+ /* @__PURE__ */ jsx7(FileText, { size: 14 }),
1544
+ /* @__PURE__ */ jsx7("span", { className: "pactor-chat-composer__attachment-name", children: attachment.name }),
1545
+ attachment.size !== void 0 && /* @__PURE__ */ jsx7("span", { className: "pactor-chat-composer__attachment-size", children: formatSize(attachment.size) })
1546
+ ] }),
1547
+ /* @__PURE__ */ jsx7(
1548
+ "button",
1549
+ {
1550
+ type: "button",
1551
+ className: "pactor-chat-icon-button pactor-chat-composer__attachment-remove",
1552
+ title: "\u79FB\u9664\u9644\u4EF6",
1553
+ "aria-label": "\u79FB\u9664\u9644\u4EF6",
1554
+ onClick: (event) => {
1555
+ event.stopPropagation();
1556
+ onAttachmentsChange?.(
1557
+ attachments.filter((item) => item.id !== attachment.id)
1558
+ );
1559
+ },
1560
+ children: /* @__PURE__ */ jsx7(X2, { size: 12 })
1561
+ }
1562
+ )
1563
+ ]
1564
+ },
1565
+ attachment.id
1566
+ );
1567
+ }) }),
1568
+ contextChips !== void 0 && contextChips.length > 0 && /* @__PURE__ */ jsx7("div", { className: "pactor-chat-composer__contexts", children: contextChips.map((chip) => /* @__PURE__ */ jsxs6(
1569
+ "span",
1570
+ {
1571
+ className: "pactor-chat-context-chip",
1572
+ "data-context-id": chip.id,
1573
+ children: [
1574
+ /* @__PURE__ */ jsx7("span", { className: "pactor-chat-context-chip__label", children: chip.label }),
1575
+ /* @__PURE__ */ jsx7(
1576
+ "button",
1577
+ {
1578
+ type: "button",
1579
+ className: "pactor-chat-icon-button pactor-chat-context-chip__remove",
1580
+ title: `\u79FB\u9664\u4E0A\u4E0B\u6587 ${chip.label}`,
1581
+ "aria-label": `\u79FB\u9664\u4E0A\u4E0B\u6587 ${chip.label}`,
1582
+ onClick: () => onContextChipsChange?.(
1583
+ contextChips.filter(
1584
+ (existing) => !(existing.id === chip.id && existing.trigger === chip.trigger)
1585
+ )
1586
+ ),
1587
+ children: /* @__PURE__ */ jsx7(X2, { size: 12 })
1588
+ }
1589
+ )
1590
+ ]
1591
+ },
1592
+ `${chip.trigger ?? ""}:${chip.id}`
1593
+ )) }),
1594
+ /* @__PURE__ */ jsxs6("div", { className: "pactor-chat-composer__row", children: [
1595
+ /* @__PURE__ */ jsx7(
1596
+ "textarea",
1597
+ {
1598
+ ref: inputRef,
1599
+ className: "pactor-chat-composer__input",
1600
+ rows: 1,
1601
+ value: text,
1602
+ placeholder,
1603
+ disabled,
1604
+ onChange: handleChange,
1605
+ onCompositionStart: () => {
1606
+ composingRef.current = true;
1607
+ },
1608
+ onCompositionEnd: () => {
1609
+ composingRef.current = false;
1610
+ },
1611
+ onPaste: (event) => {
1612
+ const files = Array.from(event.clipboardData.files);
1613
+ if (files.length > 0 && onAttachFiles) {
1614
+ event.preventDefault();
1615
+ onAttachFiles(files);
1616
+ }
1617
+ },
1618
+ onKeyDown: (event) => {
1619
+ if (event.nativeEvent.isComposing || composingRef.current) {
1620
+ return;
1621
+ }
1622
+ if (event.key === "Enter" && !event.shiftKey) {
1623
+ event.preventDefault();
1624
+ send();
1625
+ }
1626
+ }
1627
+ }
1628
+ ),
1629
+ /* @__PURE__ */ jsx7(
1630
+ "button",
1631
+ {
1632
+ type: "button",
1633
+ className: `pactor-chat-composer__button${streaming ? " pactor-chat-composer__button--stop" : ""}`,
1634
+ title: streaming ? "\u505C\u6B62\u751F\u6210" : "\u53D1\u9001",
1635
+ "aria-label": streaming ? "\u505C\u6B62\u751F\u6210" : "\u53D1\u9001",
1636
+ disabled: streaming ? false : disabled || !text.trim(),
1637
+ onClick: streaming ? onStop : send,
1638
+ children: streaming ? /* @__PURE__ */ jsx7(Square, { size: 14, fill: "currentColor" }) : /* @__PURE__ */ jsx7(ArrowUp, { size: 16 })
1639
+ }
1640
+ )
1641
+ ] }),
1642
+ showToolbar && /* @__PURE__ */ jsxs6("div", { className: "pactor-chat-composer__toolbar", children: [
1643
+ toolbarStart,
1644
+ /* @__PURE__ */ jsx7("span", { className: "pactor-chat-composer__toolbar-spacer" }),
1645
+ (onPlusMenu || builtinPlusEnabled) && /* @__PURE__ */ jsx7(
1646
+ "button",
1647
+ {
1648
+ ref: plusButtonRef,
1649
+ type: "button",
1650
+ className: "pactor-chat-icon-button",
1651
+ title: "\u66F4\u591A\u529F\u80FD",
1652
+ "aria-label": "\u66F4\u591A\u529F\u80FD",
1653
+ onClick: () => {
1654
+ const rect = plusButtonRef.current.getBoundingClientRect();
1655
+ if (onPlusMenu) {
1656
+ onPlusMenu(rect);
1657
+ } else {
1658
+ setPlusAnchor(rect);
1659
+ }
1660
+ },
1661
+ children: /* @__PURE__ */ jsx7(Plus, { size: 14 })
1662
+ }
1663
+ ),
1664
+ onAttachFiles && /* @__PURE__ */ jsx7(
1665
+ "button",
1666
+ {
1667
+ type: "button",
1668
+ className: "pactor-chat-icon-button",
1669
+ title: "\u6DFB\u52A0\u9644\u4EF6",
1670
+ "aria-label": "\u6DFB\u52A0\u9644\u4EF6",
1671
+ onClick: () => fileInputRef.current?.click(),
1672
+ children: /* @__PURE__ */ jsx7(Paperclip, { size: 14 })
1673
+ }
1674
+ ),
1675
+ modelChipEnabled && /* @__PURE__ */ jsx7(
1676
+ ModelChip,
1677
+ {
1678
+ models,
1679
+ model,
1680
+ onModelChange
1681
+ }
1682
+ ),
1683
+ toolbarEnd
1684
+ ] }),
1685
+ onAttachFiles && /* @__PURE__ */ jsx7(
1686
+ "input",
1687
+ {
1688
+ ref: fileInputRef,
1689
+ type: "file",
1690
+ multiple: true,
1691
+ hidden: true,
1692
+ onChange: (event) => {
1693
+ const files = event.target.files ? Array.from(event.target.files) : [];
1694
+ if (files.length > 0) {
1695
+ onAttachFiles(files);
1696
+ }
1697
+ event.target.value = "";
1698
+ }
1699
+ }
1700
+ ),
1701
+ builtinMentionEnabled && mentionState && /* @__PURE__ */ jsx7(
1702
+ AnchoredPopover,
1703
+ {
1704
+ anchorRect: mentionState.anchorRect,
1705
+ onClose: () => setMentionState(null),
1706
+ role: "listbox",
1707
+ children: /* @__PURE__ */ jsx7(
1708
+ ContextSourceList,
1709
+ {
1710
+ sources: mentionSources,
1711
+ query: mentionState.query,
1712
+ callService,
1713
+ cache: sourceCacheRef.current,
1714
+ onSelect: handleSelectContext
1715
+ }
1716
+ )
1717
+ }
1718
+ ),
1719
+ builtinPlusEnabled && plusAnchor && /* @__PURE__ */ jsx7(
1720
+ AnchoredPopover,
1721
+ {
1722
+ anchorRect: plusAnchor,
1723
+ onClose: () => setPlusAnchor(null),
1724
+ role: "listbox",
1725
+ children: /* @__PURE__ */ jsx7(
1726
+ ContextSourceList,
1727
+ {
1728
+ sources: plusSources,
1729
+ query: "",
1730
+ callService,
1731
+ cache: sourceCacheRef.current,
1732
+ onSelect: handleSelectContext
1733
+ }
1734
+ )
1735
+ }
1736
+ )
1737
+ ]
1738
+ }
1739
+ );
1740
+ }
1741
+
1742
+ // src/view/EmptyState.tsx
1743
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1744
+ var DEFAULT_CHAT_GREETING = "\u4F60\u597D\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u4F60\uFF1F";
1745
+ function suggestionLabel(suggestion) {
1746
+ return typeof suggestion === "string" ? suggestion : suggestion.label;
1747
+ }
1748
+ function suggestionPrompt(suggestion) {
1749
+ return typeof suggestion === "string" ? suggestion : suggestion.prompt ?? suggestion.label;
1750
+ }
1751
+ function EmptyState({
1752
+ greeting = DEFAULT_CHAT_GREETING,
1753
+ subtitle,
1754
+ suggestions,
1755
+ onSuggestionClick
1756
+ }) {
1757
+ return /* @__PURE__ */ jsxs7("div", { className: "pactor-chat-empty", children: [
1758
+ /* @__PURE__ */ jsx8("div", { className: "pactor-chat-empty__greeting", children: greeting }),
1759
+ subtitle && /* @__PURE__ */ jsx8("div", { className: "pactor-chat-empty__subtitle", children: subtitle }),
1760
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx8("div", { className: "pactor-chat-empty__suggestions", children: suggestions.map((suggestion) => /* @__PURE__ */ jsx8(
1761
+ "button",
1762
+ {
1763
+ type: "button",
1764
+ className: "pactor-chat-empty__suggestion",
1765
+ onClick: () => onSuggestionClick?.(suggestionPrompt(suggestion)),
1766
+ children: suggestionLabel(suggestion)
1767
+ },
1768
+ suggestionLabel(suggestion)
1769
+ )) })
1770
+ ] });
1771
+ }
1772
+
1773
+ // src/view/MessageBubble.tsx
1774
+ import { useRef as useRef8, useState as useState12 } from "react";
1775
+
1776
+ // src/view/ActivityTrace.tsx
1777
+ import {
1778
+ Brain,
1779
+ CheckCircle2,
1780
+ ChevronRight,
1781
+ Clock,
1782
+ Loader2,
1783
+ XCircle
1784
+ } from "lucide-react";
1785
+ import { MarkdownRenderer as MarkdownRenderer3 } from "@pactor-app/markdown";
1786
+ import { useEffect as useEffect5, useRef as useRef7, useState as useState10 } from "react";
1787
+ import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1788
+ function usePinnedCollapse(autoOpen) {
1789
+ const [state, setState] = useState10("auto");
1790
+ const open = state === "auto" ? autoOpen : state === "pinned-open";
1791
+ const toggle = () => {
1792
+ setState(open ? "pinned-closed" : "pinned-open");
1793
+ };
1794
+ return { open, toggle };
1795
+ }
1796
+ var STREAMING_STATUS_TEXT = {
1797
+ reasoning: "\u6B63\u5728\u601D\u8003\u2026",
1798
+ tool: "\u6B63\u5728\u8C03\u7528\u5DE5\u5177\u2026",
1799
+ responding: "\u6B63\u5728\u751F\u6210\u56DE\u590D\u2026"
1800
+ };
1801
+ function statusText(message, mode, toolCount) {
1802
+ if (message.status === "pending" || message.status === "streaming") {
1803
+ return mode ? STREAMING_STATUS_TEXT[mode] : "\u6B63\u5728\u54CD\u5E94\u2026";
1804
+ }
1805
+ if (message.status === "error") {
1806
+ return "\u51FA\u9519\u4E86";
1807
+ }
1808
+ if (message.status === "cancelled") {
1809
+ return "\u5DF2\u505C\u6B62";
1810
+ }
1811
+ return toolCount > 0 ? `\u5DF2\u5B8C\u6210 \xB7 ${toolCount} \u4E2A\u5DE5\u5177\u8C03\u7528` : "\u5DF2\u5B8C\u6210";
1812
+ }
1813
+ function useElapsedSeconds(startedAt, active) {
1814
+ const compute = () => Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
1815
+ const [elapsed, setElapsed] = useState10(compute);
1816
+ useEffect5(() => {
1817
+ if (!active) {
1818
+ setElapsed(compute());
1819
+ return;
1820
+ }
1821
+ const tick = () => setElapsed(compute());
1822
+ tick();
1823
+ const id = setInterval(tick, 1e3);
1824
+ return () => clearInterval(id);
1825
+ }, [active, startedAt]);
1826
+ return elapsed;
1827
+ }
1828
+ function TraceStatusIcon({ status }) {
1829
+ if (status === "running") {
1830
+ return /* @__PURE__ */ jsx9(Loader2, { size: 13, className: "pactor-chat-spin" });
1831
+ }
1832
+ if (status === "success") {
1833
+ return /* @__PURE__ */ jsx9(CheckCircle2, { size: 13 });
1834
+ }
1835
+ if (status === "failed") {
1836
+ return /* @__PURE__ */ jsx9(XCircle, { size: 13 });
1837
+ }
1838
+ return /* @__PURE__ */ jsx9(Clock, { size: 13 });
1839
+ }
1840
+ function summarizeArgs(args) {
1841
+ if (args === void 0) {
1842
+ return "";
1843
+ }
1844
+ if (typeof args === "string") {
1845
+ return args;
1846
+ }
1847
+ try {
1848
+ return JSON.stringify(args) ?? "";
1849
+ } catch {
1850
+ return String(args);
1851
+ }
1852
+ }
1853
+ function formatDetail(value) {
1854
+ if (value === void 0) {
1855
+ return "\uFF08\u65E0\u8FD4\u56DE\uFF09";
1856
+ }
1857
+ if (typeof value === "string") {
1858
+ return value;
1859
+ }
1860
+ try {
1861
+ return JSON.stringify(value, null, 2);
1862
+ } catch {
1863
+ return String(value);
1864
+ }
1865
+ }
1866
+ function TraceRow({ entry }) {
1867
+ const [open, setOpen] = useState10(false);
1868
+ const summary = summarizeArgs(entry.args);
1869
+ return /* @__PURE__ */ jsxs8(
1870
+ "div",
1871
+ {
1872
+ className: "pactor-chat-trace__entry",
1873
+ "data-status": entry.status,
1874
+ "data-open": open,
1875
+ children: [
1876
+ /* @__PURE__ */ jsxs8(
1877
+ "button",
1878
+ {
1879
+ type: "button",
1880
+ className: "pactor-chat-trace__entry-header",
1881
+ "aria-expanded": open,
1882
+ onClick: () => setOpen((value) => !value),
1883
+ children: [
1884
+ /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ jsx9(ChevronRight, { size: 12 }) }),
1885
+ /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__entry-icon", children: /* @__PURE__ */ jsx9(TraceStatusIcon, { status: entry.status }) }),
1886
+ /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__entry-title", children: entry.title ?? entry.name }),
1887
+ summary && /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__entry-args", children: summary })
1888
+ ]
1889
+ }
1890
+ ),
1891
+ open && /* @__PURE__ */ jsxs8("div", { className: "pactor-chat-trace__entry-body", children: [
1892
+ entry.args !== void 0 && /* @__PURE__ */ jsx9("pre", { className: "pactor-chat-trace__entry-detail", children: formatDetail(entry.args) }),
1893
+ /* @__PURE__ */ jsx9("pre", { className: "pactor-chat-trace__entry-detail", children: entry.status === "failed" ? entry.error ?? "\u6267\u884C\u5931\u8D25" : formatDetail(entry.result) })
1894
+ ] })
1895
+ ]
1896
+ }
1897
+ );
1898
+ }
1899
+ function ThinkingCard({
1900
+ part,
1901
+ answerPhase
1902
+ }) {
1903
+ const partStreaming = part.status === "streaming";
1904
+ const { open, toggle } = usePinnedCollapse(partStreaming && !answerPhase);
1905
+ return /* @__PURE__ */ jsxs8(
1906
+ "div",
1907
+ {
1908
+ className: "pactor-chat-thinking",
1909
+ "data-open": open,
1910
+ "data-streaming": partStreaming,
1911
+ children: [
1912
+ /* @__PURE__ */ jsxs8(
1913
+ "button",
1914
+ {
1915
+ type: "button",
1916
+ className: "pactor-chat-thinking__header",
1917
+ "aria-expanded": open,
1918
+ onClick: toggle,
1919
+ children: [
1920
+ /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ jsx9(ChevronRight, { size: 12 }) }),
1921
+ /* @__PURE__ */ jsx9("span", { className: "pactor-chat-thinking__icon", children: partStreaming ? /* @__PURE__ */ jsx9(Loader2, { size: 13, className: "pactor-chat-spin" }) : /* @__PURE__ */ jsx9(Brain, { size: 13 }) }),
1922
+ /* @__PURE__ */ jsx9("span", { children: "\u601D\u8003\u8FC7\u7A0B" })
1923
+ ]
1924
+ }
1925
+ ),
1926
+ open && /* @__PURE__ */ jsx9("div", { className: "pactor-chat-thinking__body", children: /* @__PURE__ */ jsx9(MarkdownRenderer3, { content: part.content }) })
1927
+ ]
1928
+ }
1929
+ );
1930
+ }
1931
+ function ActivityTrace({ message }) {
1932
+ const mode = detectStreamingMode(message);
1933
+ const streaming = message.status === "pending" || message.status === "streaming";
1934
+ const entries = pairToolTraces(message.parts);
1935
+ const thinkingParts = message.parts.filter(
1936
+ (part) => part.type === "thinking"
1937
+ );
1938
+ const answerPhase = mode === "responding" || !streaming;
1939
+ const { open, toggle } = usePinnedCollapse(!answerPhase);
1940
+ const mountTimeRef = useRef7(0);
1941
+ if (mountTimeRef.current === 0) {
1942
+ mountTimeRef.current = Date.now();
1943
+ }
1944
+ const elapsed = useElapsedSeconds(
1945
+ message.createdAt ?? mountTimeRef.current,
1946
+ streaming
1947
+ );
1948
+ const header = /* @__PURE__ */ jsxs8(Fragment5, { children: [
1949
+ /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__status-icon", children: streaming ? /* @__PURE__ */ jsx9(Loader2, { size: 13, className: "pactor-chat-spin" }) : /* @__PURE__ */ jsx9(CheckCircle2, { size: 13 }) }),
1950
+ /* @__PURE__ */ jsx9(
1951
+ "span",
1952
+ {
1953
+ className: `pactor-chat-trace__status-text${streaming ? " pactor-chat-trace__status-text--breathing" : ""}`,
1954
+ children: statusText(message, mode, entries.length)
1955
+ }
1956
+ ),
1957
+ /* @__PURE__ */ jsxs8("span", { className: "pactor-chat-trace__elapsed", children: [
1958
+ elapsed,
1959
+ "s"
1960
+ ] }),
1961
+ entries.length > 0 && /* @__PURE__ */ jsx9("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ jsx9(ChevronRight, { size: 12 }) })
1962
+ ] });
1963
+ return /* @__PURE__ */ jsxs8("div", { className: "pactor-chat-trace", "data-open": open, children: [
1964
+ entries.length > 0 ? /* @__PURE__ */ jsx9(
1965
+ "button",
1966
+ {
1967
+ type: "button",
1968
+ className: "pactor-chat-trace__header",
1969
+ "aria-expanded": open,
1970
+ onClick: toggle,
1971
+ children: header
1972
+ }
1973
+ ) : /* @__PURE__ */ jsx9("div", { className: "pactor-chat-trace__header", children: header }),
1974
+ thinkingParts.map((part, index) => /* @__PURE__ */ jsx9(
1975
+ ThinkingCard,
1976
+ {
1977
+ part,
1978
+ answerPhase
1979
+ },
1980
+ part.id ?? index
1981
+ )),
1982
+ open && entries.length > 0 && /* @__PURE__ */ jsx9("div", { className: "pactor-chat-trace__entries", children: entries.map((entry) => /* @__PURE__ */ jsx9(TraceRow, { entry }, entry.callIndex)) })
1983
+ ] });
1984
+ }
1985
+
1986
+ // src/view/MessageActions.tsx
1987
+ import {
1988
+ Bookmark,
1989
+ Check as Check2,
1990
+ Copy,
1991
+ HelpCircle,
1992
+ Pencil,
1993
+ Quote,
1994
+ RotateCcw,
1995
+ Share2,
1996
+ ThumbsDown,
1997
+ ThumbsUp
1998
+ } from "lucide-react";
1999
+ import { useState as useState11 } from "react";
2000
+ import { jsx as jsx10 } from "react/jsx-runtime";
2001
+ var MESSAGE_ACTION_ICONS = {
2002
+ copy: Copy,
2003
+ check: Check2,
2004
+ "rotate-ccw": RotateCcw,
2005
+ pencil: Pencil,
2006
+ quote: Quote,
2007
+ "thumbs-up": ThumbsUp,
2008
+ "thumbs-down": ThumbsDown,
2009
+ share: Share2,
2010
+ bookmark: Bookmark
2011
+ };
2012
+ var BUILTIN_ACTION_DEFAULTS = {
2013
+ copy: {
2014
+ label: "\u590D\u5236",
2015
+ icon: "copy",
2016
+ roles: ["assistant"],
2017
+ visibleWhen: "completed"
2018
+ },
2019
+ regenerate: {
2020
+ label: "\u91CD\u65B0\u751F\u6210",
2021
+ icon: "rotate-ccw",
2022
+ roles: ["assistant"],
2023
+ visibleWhen: "completed"
2024
+ },
2025
+ retry: {
2026
+ label: "\u91CD\u8BD5",
2027
+ icon: "rotate-ccw",
2028
+ roles: ["assistant"],
2029
+ visibleWhen: "error"
2030
+ },
2031
+ edit: {
2032
+ label: "\u7F16\u8F91",
2033
+ icon: "pencil",
2034
+ roles: ["user"],
2035
+ visibleWhen: "completed"
2036
+ }
2037
+ };
2038
+ var DEFAULT_ACTIONS = [
2039
+ "copy",
2040
+ "regenerate",
2041
+ "retry",
2042
+ "edit"
2043
+ ];
2044
+ function resolveMessageActions(actions) {
2045
+ return (actions ?? DEFAULT_ACTIONS).map((item) => {
2046
+ const config = typeof item === "string" ? { id: item, builtin: item } : item;
2047
+ const defaults = config.builtin ? BUILTIN_ACTION_DEFAULTS[config.builtin] : void 0;
2048
+ return {
2049
+ id: config.id,
2050
+ builtin: config.builtin,
2051
+ label: config.label ?? defaults?.label ?? config.id,
2052
+ icon: config.icon ?? defaults?.icon,
2053
+ roles: config.roles ?? defaults?.roles ?? ["assistant"],
2054
+ visibleWhen: config.visibleWhen ?? defaults?.visibleWhen ?? "completed",
2055
+ service: config.service
2056
+ };
2057
+ });
2058
+ }
2059
+ function matchesStatus(visibleWhen, status) {
2060
+ if (visibleWhen === "always") {
2061
+ return true;
2062
+ }
2063
+ if (visibleWhen === "error") {
2064
+ return status === "error";
2065
+ }
2066
+ return status === "completed" || status === "cancelled";
2067
+ }
2068
+ function collectMessageText(message) {
2069
+ return message.parts.filter(
2070
+ (part) => part.type === "text" || part.type === "markdown"
2071
+ ).map((part) => part.content).join("\n");
2072
+ }
2073
+ function MessageActions({
2074
+ message,
2075
+ actions,
2076
+ showRegenerate = false,
2077
+ onRegenerate,
2078
+ onEdit,
2079
+ onRetry,
2080
+ forceVisible = false,
2081
+ callService,
2082
+ onMessageAction
2083
+ }) {
2084
+ const [copied, setCopied] = useState11(false);
2085
+ const copy = async () => {
2086
+ try {
2087
+ await navigator.clipboard.writeText(collectMessageText(message));
2088
+ setCopied(true);
2089
+ setTimeout(() => setCopied(false), 1500);
2090
+ } catch {
2091
+ }
2092
+ };
2093
+ const visibleActions = resolveMessageActions(actions).filter((action) => {
2094
+ if (!action.roles.includes(message.role)) {
2095
+ return false;
2096
+ }
2097
+ if (!matchesStatus(action.visibleWhen, message.status)) {
2098
+ return false;
2099
+ }
2100
+ switch (action.builtin) {
2101
+ case "regenerate":
2102
+ return showRegenerate && onRegenerate !== void 0;
2103
+ case "retry":
2104
+ return onRetry !== void 0;
2105
+ case "edit":
2106
+ return onEdit !== void 0;
2107
+ default:
2108
+ return true;
2109
+ }
2110
+ });
2111
+ if (visibleActions.length === 0) {
2112
+ return null;
2113
+ }
2114
+ const dispatch = (action) => {
2115
+ switch (action.builtin) {
2116
+ case "copy":
2117
+ void copy();
2118
+ break;
2119
+ case "regenerate":
2120
+ onRegenerate?.();
2121
+ break;
2122
+ case "retry":
2123
+ onRetry?.();
2124
+ break;
2125
+ case "edit":
2126
+ onEdit?.();
2127
+ break;
2128
+ default:
2129
+ if (action.service) {
2130
+ if (callService) {
2131
+ void callService(action.service, {
2132
+ actionId: action.id,
2133
+ message
2134
+ });
2135
+ } else {
2136
+ console.warn(
2137
+ `[@pactor-app/chat] \u6D88\u606F\u52A8\u4F5C\u300C${action.id}\u300D\u914D\u7F6E\u4E86 service\u300C${action.service}\u300D\u4F46\u672A\u6CE8\u5165 callService\uFF0C\u5DF2\u8DF3\u8FC7\u6D3E\u53D1`
2138
+ );
2139
+ }
2140
+ }
2141
+ }
2142
+ onMessageAction?.(action.id, message);
2143
+ };
2144
+ return /* @__PURE__ */ jsx10(
2145
+ "div",
2146
+ {
2147
+ className: `pactor-chat-message__actions${forceVisible ? " pactor-chat-message__actions--visible" : ""}`,
2148
+ children: visibleActions.map((action) => {
2149
+ const copiedFeedback = action.builtin === "copy" && copied;
2150
+ const label = copiedFeedback ? "\u5DF2\u590D\u5236" : action.label;
2151
+ const iconName = copiedFeedback ? "check" : action.icon;
2152
+ const Icon = iconName !== void 0 ? MESSAGE_ACTION_ICONS[iconName] : void 0;
2153
+ return /* @__PURE__ */ jsx10(
2154
+ "button",
2155
+ {
2156
+ type: "button",
2157
+ className: "pactor-chat-icon-button",
2158
+ title: label,
2159
+ "aria-label": label,
2160
+ "data-message-action": action.id,
2161
+ onClick: () => dispatch(action),
2162
+ children: Icon ? /* @__PURE__ */ jsx10(Icon, { size: 14 }) : /* @__PURE__ */ jsx10(HelpCircle, { size: 14, "data-icon-placeholder": "true" })
2163
+ },
2164
+ action.id
2165
+ );
2166
+ })
2167
+ }
2168
+ );
2169
+ }
2170
+
2171
+ // src/view/MessageBubble.tsx
2172
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2173
+ function MessageEditForm({
2174
+ initialText,
2175
+ onConfirm,
2176
+ onCancel
2177
+ }) {
2178
+ const [text, setText] = useState12(initialText);
2179
+ const confirm = () => {
2180
+ const value = text.trim();
2181
+ if (!value) {
2182
+ return;
2183
+ }
2184
+ onConfirm(value);
2185
+ };
2186
+ return /* @__PURE__ */ jsxs9("div", { className: "pactor-chat-message__edit", children: [
2187
+ /* @__PURE__ */ jsx11(
2188
+ "textarea",
2189
+ {
2190
+ className: "pactor-chat-message__edit-input",
2191
+ "aria-label": "\u7F16\u8F91\u6D88\u606F",
2192
+ rows: 3,
2193
+ value: text,
2194
+ onChange: (event) => setText(event.target.value),
2195
+ onKeyDown: (event) => {
2196
+ if (event.key === "Enter" && !event.shiftKey) {
2197
+ event.preventDefault();
2198
+ confirm();
2199
+ } else if (event.key === "Escape") {
2200
+ onCancel();
2201
+ }
2202
+ }
2203
+ }
2204
+ ),
2205
+ /* @__PURE__ */ jsxs9("div", { className: "pactor-chat-message__edit-actions", children: [
2206
+ /* @__PURE__ */ jsx11(
2207
+ "button",
2208
+ {
2209
+ type: "button",
2210
+ className: "pactor-chat-message__edit-button pactor-chat-message__edit-button--confirm",
2211
+ "aria-label": "\u786E\u8BA4\u53D1\u9001",
2212
+ onClick: confirm,
2213
+ children: "\u53D1\u9001"
2214
+ }
2215
+ ),
2216
+ /* @__PURE__ */ jsx11(
2217
+ "button",
2218
+ {
2219
+ type: "button",
2220
+ className: "pactor-chat-message__edit-button",
2221
+ "aria-label": "\u53D6\u6D88",
2222
+ onClick: onCancel,
2223
+ children: "\u53D6\u6D88"
2224
+ }
2225
+ )
2226
+ ] })
2227
+ ] });
2228
+ }
2229
+ function MessageBubble({
2230
+ message,
2231
+ runtime,
2232
+ scope,
2233
+ showRegenerate = false,
2234
+ onRegenerate,
2235
+ onRetry,
2236
+ onEditResend,
2237
+ messageActions,
2238
+ callService,
2239
+ onMessageAction,
2240
+ onOpenArtifact
2241
+ }) {
2242
+ const [editing, setEditing] = useState12(false);
2243
+ const bubbleScope = {
2244
+ conversation: scope?.conversation ?? {},
2245
+ message: { id: message.id },
2246
+ params: scope?.params
2247
+ };
2248
+ const streaming = message.status === "streaming";
2249
+ const live = message.status === "pending" || message.status === "streaming";
2250
+ const wasLiveRef = useRef8(live);
2251
+ if (live) {
2252
+ wasLiveRef.current = true;
2253
+ }
2254
+ const showTrace = message.role === "assistant" && (wasLiveRef.current || message.parts.some(
2255
+ (part) => part.type === "thinking" || part.type === "tool-call" || part.type === "tool-result"
2256
+ ));
2257
+ return /* @__PURE__ */ jsxs9(
2258
+ "div",
2259
+ {
2260
+ className: `pactor-chat-message pactor-chat-message--${message.role}`,
2261
+ "data-message-id": message.id,
2262
+ "data-message-status": message.status,
2263
+ children: [
2264
+ editing ? /* @__PURE__ */ jsx11(
2265
+ MessageEditForm,
2266
+ {
2267
+ initialText: collectMessageText(message),
2268
+ onConfirm: (text) => {
2269
+ setEditing(false);
2270
+ onEditResend?.(text);
2271
+ },
2272
+ onCancel: () => setEditing(false)
2273
+ }
2274
+ ) : /* @__PURE__ */ jsxs9("div", { className: "pactor-chat-message__body", children: [
2275
+ showTrace && /* @__PURE__ */ jsx11(ActivityTrace, { message }),
2276
+ message.parts.length === 0 && message.status === "pending" && message.role !== "assistant" ? /* @__PURE__ */ jsx11("span", { className: "pactor-chat-message__pending", children: "\u2026" }) : message.parts.map((part, index) => /* @__PURE__ */ jsx11(
2277
+ PartRenderer,
2278
+ {
2279
+ part,
2280
+ runtime,
2281
+ scope: bubbleScope,
2282
+ streaming,
2283
+ onOpenArtifact
2284
+ },
2285
+ part.id ?? index
2286
+ ))
2287
+ ] }),
2288
+ !editing && /* @__PURE__ */ jsx11(
2289
+ MessageActions,
2290
+ {
2291
+ message,
2292
+ actions: messageActions,
2293
+ showRegenerate,
2294
+ onRegenerate,
2295
+ onRetry,
2296
+ onEdit: onEditResend ? () => setEditing(true) : void 0,
2297
+ forceVisible: message.status === "error",
2298
+ callService,
2299
+ onMessageAction
2300
+ }
2301
+ )
2302
+ ]
2303
+ }
2304
+ );
2305
+ }
2306
+
2307
+ // src/view/ChatView.tsx
2308
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2309
+ function lastContentLength(messages) {
2310
+ const last = messages[messages.length - 1];
2311
+ if (!last) {
2312
+ return 0;
2313
+ }
2314
+ return last.parts.reduce(
2315
+ (total, part) => total + ("content" in part ? String(part.content).length : 0),
2316
+ 0
2317
+ );
2318
+ }
2319
+ function ChatView({
2320
+ store,
2321
+ transport,
2322
+ onSend,
2323
+ safeRegistry,
2324
+ capabilities,
2325
+ permission,
2326
+ confirm,
2327
+ conversationId,
2328
+ user,
2329
+ params,
2330
+ greeting,
2331
+ subtitle,
2332
+ suggestions,
2333
+ className,
2334
+ onArtifact,
2335
+ onOpenArtifact,
2336
+ attachments,
2337
+ onAttachmentsChange,
2338
+ onAttachFiles,
2339
+ onAttachmentClick,
2340
+ toolbarStart,
2341
+ toolbarEnd,
2342
+ onMentionTrigger,
2343
+ onPlusMenu,
2344
+ models,
2345
+ model,
2346
+ onModelChange,
2347
+ modelPlacement = "composer",
2348
+ contexts,
2349
+ onContextsChange,
2350
+ callService,
2351
+ messageActions,
2352
+ onMessageAction
2353
+ }) {
2354
+ const messages = useSyncExternalStore(
2355
+ (listener) => store.subscribe(listener),
2356
+ () => store.getMessages()
2357
+ );
2358
+ const isStreaming = messages.some(
2359
+ (message) => message.status === "pending" || message.status === "streaming"
2360
+ );
2361
+ const runtime = useMemo3(() => {
2362
+ if (!safeRegistry) {
2363
+ return void 0;
2364
+ }
2365
+ const components = new Registry3("ChatComponentRegistry");
2366
+ components.register(CHAT_DENIED_TYPE, ChatDenied);
2367
+ for (const name of safeRegistry.list()) {
2368
+ components.register(name, safeRegistry.get(name));
2369
+ }
2370
+ return createChatRuntime({
2371
+ safeRegistry: components,
2372
+ capabilities,
2373
+ permission,
2374
+ confirm,
2375
+ user
2376
+ });
2377
+ }, [safeRegistry, capabilities, permission, confirm, user]);
2378
+ const scope = useMemo3(
2379
+ () => ({ conversation: { id: conversationId }, params }),
2380
+ [conversationId, params]
2381
+ );
2382
+ const notifiedArtifactsRef = useRef9(/* @__PURE__ */ new Set());
2383
+ useEffect6(() => {
2384
+ if (!onArtifact) {
2385
+ return;
2386
+ }
2387
+ for (const message of messages) {
2388
+ if (message.role !== "assistant") {
2389
+ continue;
2390
+ }
2391
+ message.parts.forEach((part, index) => {
2392
+ if (part.type === "artifact" && (part.status ?? "ready") === "ready") {
2393
+ const key = `${message.id}:${part.id ?? index}`;
2394
+ if (!notifiedArtifactsRef.current.has(key)) {
2395
+ notifiedArtifactsRef.current.add(key);
2396
+ onArtifact(part.artifact);
2397
+ }
2398
+ }
2399
+ });
2400
+ }
2401
+ }, [messages, onArtifact]);
2402
+ const abortRef = useRef9(null);
2403
+ const { containerRef, isFollowing, scrollToBottom } = useChatAutoScroll({
2404
+ messageCount: messages.length,
2405
+ lastContentLength: lastContentLength(messages),
2406
+ isStreaming
2407
+ });
2408
+ const { currentModel, handleModelChange } = useChatModel({
2409
+ models,
2410
+ model,
2411
+ onModelChange
2412
+ });
2413
+ const [contextChips, setContextChips] = useState13([]);
2414
+ const handleContextChipsChange = (chips) => {
2415
+ setContextChips(chips);
2416
+ onContextsChange?.(chips);
2417
+ };
2418
+ const handleSend = (text) => {
2419
+ onSend?.(text);
2420
+ if (transport) {
2421
+ const controller = new AbortController();
2422
+ abortRef.current = controller;
2423
+ void runChatTurn(
2424
+ store,
2425
+ transport,
2426
+ {
2427
+ text,
2428
+ conversationId,
2429
+ model: currentModel,
2430
+ contexts: contextChips.length > 0 ? contextChips : void 0
2431
+ },
2432
+ {
2433
+ signal: controller.signal
2434
+ }
2435
+ );
2436
+ }
2437
+ if (contextChips.length > 0) {
2438
+ handleContextChipsChange([]);
2439
+ }
2440
+ };
2441
+ const handleStop = () => {
2442
+ abortRef.current?.abort();
2443
+ abortRef.current = null;
2444
+ };
2445
+ const handleRegenerate = () => {
2446
+ if (!transport) {
2447
+ return;
2448
+ }
2449
+ handleStop();
2450
+ const controller = new AbortController();
2451
+ abortRef.current = controller;
2452
+ void runRegenerateTurn(store, transport, { signal: controller.signal });
2453
+ };
2454
+ const handleEditResend = (messageId, text) => {
2455
+ handleStop();
2456
+ store.removeMessagesFrom(messageId);
2457
+ handleSend(text);
2458
+ };
2459
+ const lastMessage = messages[messages.length - 1];
2460
+ const regenerableMessageId = lastMessage?.role === "assistant" && lastMessage.status === "completed" ? lastMessage.id : void 0;
2461
+ const isEmpty = messages.length === 0;
2462
+ const composerNode = /* @__PURE__ */ jsx12(
2463
+ Composer,
2464
+ {
2465
+ onSend: handleSend,
2466
+ streaming: isStreaming,
2467
+ onStop: handleStop,
2468
+ attachments,
2469
+ onAttachmentsChange,
2470
+ onAttachFiles,
2471
+ onAttachmentClick,
2472
+ toolbarStart,
2473
+ toolbarEnd,
2474
+ onMentionTrigger,
2475
+ onPlusMenu,
2476
+ models: modelPlacement === "header" ? void 0 : models,
2477
+ model: currentModel,
2478
+ onModelChange: handleModelChange,
2479
+ contextSources: contexts,
2480
+ contextChips,
2481
+ onContextChipsChange: handleContextChipsChange,
2482
+ callService
2483
+ }
2484
+ );
2485
+ return /* @__PURE__ */ jsxs10(
2486
+ "div",
2487
+ {
2488
+ className: `pactor-chat-view${isEmpty ? " pactor-chat-view--empty" : ""}${className ? ` ${className}` : ""}`,
2489
+ children: [
2490
+ /* @__PURE__ */ jsxs10("div", { className: "pactor-chat-messages", ref: containerRef, children: [
2491
+ isEmpty ? /* @__PURE__ */ jsxs10("div", { className: "pactor-chat-view__empty-stack", children: [
2492
+ /* @__PURE__ */ jsx12(
2493
+ EmptyState,
2494
+ {
2495
+ greeting,
2496
+ subtitle,
2497
+ suggestions,
2498
+ onSuggestionClick: handleSend
2499
+ }
2500
+ ),
2501
+ composerNode
2502
+ ] }) : messages.map((message) => /* @__PURE__ */ jsx12(
2503
+ MessageBubble,
2504
+ {
2505
+ message,
2506
+ runtime,
2507
+ scope,
2508
+ showRegenerate: message.id === regenerableMessageId,
2509
+ onRegenerate: transport ? handleRegenerate : void 0,
2510
+ onRetry: transport ? handleRegenerate : void 0,
2511
+ onEditResend: (text) => handleEditResend(message.id, text),
2512
+ messageActions,
2513
+ callService,
2514
+ onMessageAction,
2515
+ onOpenArtifact
2516
+ },
2517
+ message.id
2518
+ )),
2519
+ !isFollowing && /* @__PURE__ */ jsxs10(
2520
+ "button",
2521
+ {
2522
+ type: "button",
2523
+ className: "pactor-chat-scroll-bottom",
2524
+ onClick: scrollToBottom,
2525
+ children: [
2526
+ /* @__PURE__ */ jsx12(ArrowDown, { size: 13 }),
2527
+ "\u56DE\u5230\u5E95\u90E8"
2528
+ ]
2529
+ }
2530
+ )
2531
+ ] }),
2532
+ !isEmpty && composerNode
2533
+ ]
2534
+ }
2535
+ );
2536
+ }
2537
+
2538
+ // src/view/WorkspaceHeader.tsx
2539
+ import { Download, Moon, Sun, Trash2 } from "lucide-react";
2540
+ import { useRef as useRef10, useState as useState14 } from "react";
2541
+
2542
+ // src/export-markdown.ts
2543
+ var ROLE_LABELS = {
2544
+ user: "\u7528\u6237",
2545
+ assistant: "\u52A9\u624B",
2546
+ system: "\u7CFB\u7EDF"
2547
+ };
2548
+ function serializeMessages(title, messages) {
2549
+ const lines = [`# ${title}`, ""];
2550
+ for (const message of messages) {
2551
+ const content = message.parts.filter(
2552
+ (part) => (part.type === "text" || part.type === "markdown") && "content" in part
2553
+ ).map((part) => part.content).join("\n\n").trim();
2554
+ if (!content) {
2555
+ continue;
2556
+ }
2557
+ lines.push(`## ${ROLE_LABELS[message.role]}`, "", content, "");
2558
+ }
2559
+ return lines.join("\n");
2560
+ }
2561
+ function downloadMessagesAsMarkdown(title, messages) {
2562
+ const markdown = serializeMessages(title, messages);
2563
+ const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
2564
+ const url = URL.createObjectURL(blob);
2565
+ const anchor = document.createElement("a");
2566
+ anchor.href = url;
2567
+ anchor.download = `${title || "chat"}.md`;
2568
+ anchor.click();
2569
+ URL.revokeObjectURL(url);
2570
+ }
2571
+
2572
+ // src/view/WorkspaceHeader.tsx
2573
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2574
+ function readCurrentTheme() {
2575
+ if (typeof document === "undefined") {
2576
+ return "light";
2577
+ }
2578
+ return document.documentElement.getAttribute("data-pactor-theme") === "dark" ? "dark" : "light";
2579
+ }
2580
+ function WorkspaceHeader({
2581
+ title,
2582
+ headerExtra,
2583
+ headerActions,
2584
+ editableTitle = false,
2585
+ headerStart,
2586
+ store,
2587
+ onExport,
2588
+ onThemeChange,
2589
+ onClear,
2590
+ onTitleChange
2591
+ }) {
2592
+ const hasActions = headerActions !== void 0 && headerActions.length > 0;
2593
+ const [overrideTitle, setOverrideTitle] = useState14(null);
2594
+ const [editing, setEditing] = useState14(false);
2595
+ const [draft, setDraft] = useState14("");
2596
+ const prevTitleRef = useRef10(title);
2597
+ if (prevTitleRef.current !== title) {
2598
+ prevTitleRef.current = title;
2599
+ if (overrideTitle !== null) {
2600
+ setOverrideTitle(null);
2601
+ }
2602
+ }
2603
+ const displayTitle = overrideTitle ?? title;
2604
+ const [theme, setTheme] = useState14(readCurrentTheme);
2605
+ const commitTitle = () => {
2606
+ setEditing(false);
2607
+ const next = draft.trim();
2608
+ if (next && next !== displayTitle) {
2609
+ setOverrideTitle(next);
2610
+ onTitleChange?.(next);
2611
+ }
2612
+ };
2613
+ const handleExport = () => {
2614
+ downloadMessagesAsMarkdown(displayTitle ?? "chat", store.getMessages());
2615
+ onExport?.();
2616
+ };
2617
+ const handleTheme = () => {
2618
+ const next = readCurrentTheme() === "dark" ? "light" : "dark";
2619
+ if (next === "dark") {
2620
+ document.documentElement.setAttribute("data-pactor-theme", "dark");
2621
+ } else {
2622
+ document.documentElement.removeAttribute("data-pactor-theme");
2623
+ }
2624
+ setTheme(next);
2625
+ onThemeChange?.(next);
2626
+ };
2627
+ const handleClear = () => {
2628
+ store.clear();
2629
+ onClear?.();
2630
+ };
2631
+ const actionButtons = {
2632
+ export: /* @__PURE__ */ jsx13(
2633
+ "button",
2634
+ {
2635
+ type: "button",
2636
+ className: "pactor-chat-icon-button",
2637
+ title: "\u5BFC\u51FA Markdown",
2638
+ "aria-label": "\u5BFC\u51FA Markdown",
2639
+ onClick: handleExport,
2640
+ children: /* @__PURE__ */ jsx13(Download, { size: 15 })
2641
+ },
2642
+ "export"
2643
+ ),
2644
+ theme: /* @__PURE__ */ jsx13(
2645
+ "button",
2646
+ {
2647
+ type: "button",
2648
+ className: "pactor-chat-icon-button",
2649
+ title: theme === "dark" ? "\u5207\u6362\u4E3A\u6D45\u8272" : "\u5207\u6362\u4E3A\u6DF1\u8272",
2650
+ "aria-label": "\u5207\u6362\u660E\u6697\u4E3B\u9898",
2651
+ onClick: handleTheme,
2652
+ children: theme === "dark" ? /* @__PURE__ */ jsx13(Sun, { size: 15 }) : /* @__PURE__ */ jsx13(Moon, { size: 15 })
2653
+ },
2654
+ "theme"
2655
+ ),
2656
+ clear: /* @__PURE__ */ jsx13(
2657
+ "button",
2658
+ {
2659
+ type: "button",
2660
+ className: "pactor-chat-icon-button",
2661
+ title: "\u6E05\u7A7A\u4F1A\u8BDD",
2662
+ "aria-label": "\u6E05\u7A7A\u4F1A\u8BDD",
2663
+ onClick: handleClear,
2664
+ children: /* @__PURE__ */ jsx13(Trash2, { size: 15 })
2665
+ },
2666
+ "clear"
2667
+ )
2668
+ };
2669
+ const titleNode = editableTitle ? editing ? /* @__PURE__ */ jsx13(
2670
+ "input",
2671
+ {
2672
+ className: "pactor-chat-workspace__title-input",
2673
+ value: draft,
2674
+ autoFocus: true,
2675
+ "aria-label": "\u7F16\u8F91\u6807\u9898",
2676
+ onChange: (event) => setDraft(event.target.value),
2677
+ onBlur: commitTitle,
2678
+ onKeyDown: (event) => {
2679
+ if (event.key === "Enter") {
2680
+ commitTitle();
2681
+ } else if (event.key === "Escape") {
2682
+ setEditing(false);
2683
+ }
2684
+ }
2685
+ }
2686
+ ) : /* @__PURE__ */ jsx13(
2687
+ "button",
2688
+ {
2689
+ type: "button",
2690
+ className: "pactor-chat-workspace__title pactor-chat-workspace__title--editable",
2691
+ title: "\u70B9\u51FB\u7F16\u8F91\u6807\u9898",
2692
+ onClick: () => {
2693
+ setDraft(displayTitle ?? "");
2694
+ setEditing(true);
2695
+ },
2696
+ children: displayTitle
2697
+ }
2698
+ ) : /* @__PURE__ */ jsx13("div", { className: "pactor-chat-workspace__title", children: title });
2699
+ return /* @__PURE__ */ jsxs11("header", { className: "pactor-chat-workspace__header", children: [
2700
+ headerStart !== void 0 ? (
2701
+ // header 位模型 chip(决策 D2):与 title 组成左端组,chip 在前
2702
+ /* @__PURE__ */ jsxs11("div", { className: "pactor-chat-workspace__header-left", children: [
2703
+ headerStart,
2704
+ titleNode
2705
+ ] })
2706
+ ) : titleNode,
2707
+ hasActions ? /* @__PURE__ */ jsxs11("div", { className: "pactor-chat-workspace__header-right", children: [
2708
+ /* @__PURE__ */ jsx13("div", { className: "pactor-chat-workspace__header-actions", children: headerActions.map((action) => actionButtons[action]) }),
2709
+ headerExtra !== void 0 ? /* @__PURE__ */ jsx13("div", { className: "pactor-chat-workspace__header-extra", children: headerExtra }) : null
2710
+ ] }) : headerExtra !== void 0 ? /* @__PURE__ */ jsx13("div", { className: "pactor-chat-workspace__header-extra", children: headerExtra }) : null
2711
+ ] });
2712
+ }
2713
+
2714
+ // src/chat-composition.tsx
2715
+ import { Fragment as Fragment6, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2716
+ function DslNodeSlotInner({ node }) {
2717
+ const { renderChildren } = useRenderer3();
2718
+ return renderChildren([node]);
2719
+ }
2720
+ var DslSlotGuard = class extends Component2 {
2721
+ constructor() {
2722
+ super(...arguments);
2723
+ this.state = { failed: false };
2724
+ }
2725
+ static getDerivedStateFromError() {
2726
+ return { failed: true };
2727
+ }
2728
+ componentDidCatch() {
2729
+ console.warn(
2730
+ "[@pactor-app/chat] DslNode \u69FD\u4EC5\u5728\u9875\u9762\u8FD0\u884C\u65F6\uFF08DslRenderer\uFF09\u4E0A\u4E0B\u6587\u5185\u53EF\u6E32\u67D3\uFF0C\u5DF2\u8DF3\u8FC7"
2731
+ );
2732
+ }
2733
+ render() {
2734
+ return this.state.failed ? null : this.props.children;
2735
+ }
2736
+ };
2737
+ function DslNodeSlot({ node }) {
2738
+ return /* @__PURE__ */ jsx14(DslSlotGuard, { children: /* @__PURE__ */ jsx14(DslNodeSlotInner, { node }) });
2739
+ }
2740
+ function isDslNodeValue(value) {
2741
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !isValidElement(value) && typeof value.type === "string";
2742
+ }
2743
+ function resolveSlot(slot) {
2744
+ if (slot === void 0 || slot === null) {
2745
+ return void 0;
2746
+ }
2747
+ if (isDslNodeValue(slot)) {
2748
+ return /* @__PURE__ */ jsx14(DslNodeSlot, { node: slot });
2749
+ }
2750
+ return slot;
2751
+ }
2752
+ function ChatComposition({
2753
+ services,
2754
+ title,
2755
+ greeting,
2756
+ subtitle,
2757
+ suggestions,
2758
+ conversationId,
2759
+ params,
2760
+ showHeader,
2761
+ header,
2762
+ headerExtra,
2763
+ headerActions,
2764
+ editableTitle,
2765
+ onExport,
2766
+ onThemeChange,
2767
+ onClear,
2768
+ onTitleChange,
2769
+ models,
2770
+ model,
2771
+ onModelChange,
2772
+ modelPlacement = "composer",
2773
+ contexts,
2774
+ onContextsChange,
2775
+ messageActions,
2776
+ onMessageAction,
2777
+ sidebar,
2778
+ toolbarEnd,
2779
+ className,
2780
+ onSend,
2781
+ artifact,
2782
+ panelOpen,
2783
+ onPanelClose,
2784
+ inspector,
2785
+ onArtifact,
2786
+ onOpenArtifact
2787
+ }) {
2788
+ const storeRef = useRef11(null);
2789
+ if (storeRef.current === null) {
2790
+ storeRef.current = services.store ?? services.storeFactory?.() ?? new ConversationStore();
2791
+ }
2792
+ const store = storeRef.current;
2793
+ const artifactStoreRef = useRef11(null);
2794
+ if (artifactStoreRef.current === null) {
2795
+ artifactStoreRef.current = services.artifactStore ?? new ArtifactStore();
2796
+ }
2797
+ const artifactStore = artifactStoreRef.current;
2798
+ const panelControlled = panelOpen !== void 0 || artifact !== void 0;
2799
+ const storeArtifact = useSyncExternalStore2(
2800
+ (listener) => artifactStore.subscribe(listener),
2801
+ () => artifactStore.get()
2802
+ );
2803
+ const effectiveArtifact = panelControlled ? artifact ?? null : storeArtifact;
2804
+ const effectivePanelOpen = panelControlled ? panelOpen ?? artifact != null : storeArtifact !== null;
2805
+ const handlePanelClose = () => {
2806
+ onPanelClose?.();
2807
+ if (!panelControlled) {
2808
+ artifactStore.close();
2809
+ }
2810
+ };
2811
+ const handleOpenArtifact = (next) => {
2812
+ onOpenArtifact?.(next);
2813
+ if (!panelControlled) {
2814
+ artifactStore.open(next);
2815
+ }
2816
+ };
2817
+ const [attachments, setAttachments] = useState15([]);
2818
+ const handleAttachFiles = services.onAttachFiles ? (files) => {
2819
+ void Promise.resolve(services.onAttachFiles(files)).then(
2820
+ (created) => {
2821
+ if (created && created.length > 0) {
2822
+ setAttachments((prev) => [...prev, ...created]);
2823
+ }
2824
+ }
2825
+ );
2826
+ } : void 0;
2827
+ const handleSend = (text) => {
2828
+ services.onSend?.(text);
2829
+ onSend?.(text);
2830
+ setAttachments([]);
2831
+ };
2832
+ const { modelsEnabled, currentModel, handleModelChange } = useChatModel({
2833
+ models,
2834
+ model,
2835
+ onModelChange
2836
+ });
2837
+ const headerModelChip = modelPlacement === "header" && modelsEnabled ? /* @__PURE__ */ jsx14(
2838
+ ModelChip,
2839
+ {
2840
+ models,
2841
+ model: currentModel,
2842
+ onModelChange: handleModelChange
2843
+ }
2844
+ ) : void 0;
2845
+ const mergedToolbarEnd = services.toolbarEnd !== void 0 || toolbarEnd !== void 0 ? /* @__PURE__ */ jsxs12(Fragment6, { children: [
2846
+ services.toolbarEnd,
2847
+ toolbarEnd
2848
+ ] }) : void 0;
2849
+ const workspace = /* @__PURE__ */ jsxs12(
2850
+ "div",
2851
+ {
2852
+ className: `pactor-chat-workspace${className ? ` ${className}` : ""}`,
2853
+ children: [
2854
+ showHeader ? header ?? /* @__PURE__ */ jsx14(
2855
+ WorkspaceHeader,
2856
+ {
2857
+ title,
2858
+ headerExtra,
2859
+ headerActions,
2860
+ editableTitle,
2861
+ headerStart: headerModelChip,
2862
+ store,
2863
+ onExport,
2864
+ onThemeChange,
2865
+ onClear,
2866
+ onTitleChange
2867
+ }
2868
+ ) : null,
2869
+ /* @__PURE__ */ jsx14(
2870
+ ChatView,
2871
+ {
2872
+ store,
2873
+ transport: services.transport,
2874
+ onSend: handleSend,
2875
+ safeRegistry: services.safeRegistry,
2876
+ capabilities: services.capabilities,
2877
+ permission: services.permission,
2878
+ confirm: services.confirm,
2879
+ conversationId,
2880
+ user: services.user,
2881
+ params,
2882
+ greeting,
2883
+ subtitle,
2884
+ suggestions,
2885
+ attachments,
2886
+ onAttachmentsChange: setAttachments,
2887
+ onAttachFiles: handleAttachFiles,
2888
+ onAttachmentClick: services.onAttachmentClick,
2889
+ toolbarStart: services.toolbarStart,
2890
+ toolbarEnd: mergedToolbarEnd,
2891
+ onMentionTrigger: services.onMentionTrigger,
2892
+ onPlusMenu: services.onPlusMenu,
2893
+ onArtifact,
2894
+ onOpenArtifact: handleOpenArtifact,
2895
+ models,
2896
+ model: currentModel,
2897
+ onModelChange: handleModelChange,
2898
+ modelPlacement,
2899
+ contexts,
2900
+ onContextsChange,
2901
+ callService: services.callService,
2902
+ messageActions,
2903
+ onMessageAction
2904
+ }
2905
+ )
2906
+ ]
2907
+ }
2908
+ );
2909
+ const main = sidebar ? /* @__PURE__ */ jsx14(AppShell, { sidebar, children: workspace }) : workspace;
2910
+ const panelFeatureUsed = artifact !== void 0 || panelOpen !== void 0 || inspector !== void 0 || onPanelClose !== void 0 || onArtifact !== void 0 || onOpenArtifact !== void 0 || services.artifactStore !== void 0;
2911
+ if (!panelFeatureUsed && !effectivePanelOpen) {
2912
+ return main;
2913
+ }
2914
+ return /* @__PURE__ */ jsxs12("div", { className: "pactor-chat-inspector-host", children: [
2915
+ main,
2916
+ effectivePanelOpen ? /* @__PURE__ */ jsxs12(Fragment6, { children: [
2917
+ /* @__PURE__ */ jsx14(
2918
+ "div",
2919
+ {
2920
+ className: "pactor-chat-inspector__scrim",
2921
+ "aria-hidden": true,
2922
+ onClick: handlePanelClose
2923
+ }
2924
+ ),
2925
+ /* @__PURE__ */ jsx14(
2926
+ ArtifactPanel,
2927
+ {
2928
+ artifact: effectiveArtifact,
2929
+ inspector,
2930
+ onClose: handlePanelClose,
2931
+ safeRegistry: services.safeRegistry,
2932
+ user: services.user
2933
+ }
2934
+ )
2935
+ ] }) : null
2936
+ ] });
2937
+ }
2938
+
2939
+ // src/chat-services.tsx
2940
+ import { createContext as createContext2, useContext as useContext2 } from "react";
2941
+ import { jsx as jsx15 } from "react/jsx-runtime";
2942
+ var ChatServicesContext = createContext2(null);
2943
+ function ChatServicesProvider({
2944
+ value,
2945
+ children
2946
+ }) {
2947
+ return /* @__PURE__ */ jsx15(ChatServicesContext.Provider, { value, children });
2948
+ }
2949
+ function useChatServices() {
2950
+ return useContext2(ChatServicesContext);
2951
+ }
2952
+
2953
+ // src/chat-workspace.tsx
2954
+ import { jsx as jsx16 } from "react/jsx-runtime";
2955
+ function createChatWorkspace(services) {
2956
+ function ChatWorkspace({
2957
+ title,
2958
+ greeting,
2959
+ subtitle,
2960
+ suggestions,
2961
+ conversationId,
2962
+ params,
2963
+ sidebar,
2964
+ toolbarEnd,
2965
+ artifact,
2966
+ panelOpen,
2967
+ onPanelClose,
2968
+ inspector,
2969
+ onArtifact,
2970
+ onOpenArtifact,
2971
+ headerActions,
2972
+ editableTitle,
2973
+ onExport,
2974
+ onThemeChange,
2975
+ onClear,
2976
+ onTitleChange,
2977
+ models,
2978
+ model,
2979
+ onModelChange,
2980
+ modelPlacement,
2981
+ contexts,
2982
+ onContextsChange,
2983
+ messageActions,
2984
+ onMessageAction,
2985
+ className
2986
+ }) {
2987
+ return /* @__PURE__ */ jsx16(ChatServicesProvider, { value: services, children: /* @__PURE__ */ jsx16(
2988
+ ChatComposition,
2989
+ {
2990
+ services,
2991
+ title,
2992
+ greeting,
2993
+ subtitle,
2994
+ suggestions,
2995
+ conversationId,
2996
+ params,
2997
+ className,
2998
+ showHeader: title !== void 0 || services.headerExtra !== void 0 || // header 位模型 chip(决策 D2):仅配置 models 也渲染极简头部
2999
+ modelPlacement === "header" && models !== void 0 && models.length > 0,
3000
+ headerExtra: services.headerExtra,
3001
+ sidebar: sidebar ? /* @__PURE__ */ jsx16(DslNodeSlot, { node: sidebar }) : void 0,
3002
+ toolbarEnd: toolbarEnd ? /* @__PURE__ */ jsx16(DslNodeSlot, { node: toolbarEnd }) : void 0,
3003
+ artifact,
3004
+ panelOpen,
3005
+ onPanelClose,
3006
+ inspector: inspector ? /* @__PURE__ */ jsx16(DslNodeSlot, { node: inspector }) : void 0,
3007
+ onArtifact,
3008
+ onOpenArtifact,
3009
+ headerActions,
3010
+ editableTitle,
3011
+ onExport,
3012
+ onThemeChange,
3013
+ onClear,
3014
+ onTitleChange,
3015
+ models,
3016
+ model,
3017
+ onModelChange,
3018
+ modelPlacement,
3019
+ contexts,
3020
+ onContextsChange,
3021
+ messageActions,
3022
+ onMessageAction
3023
+ }
3024
+ ) });
3025
+ }
3026
+ return ChatWorkspace;
3027
+ }
3028
+
3029
+ // src/view/Chat.tsx
3030
+ import {
3031
+ Folder,
3032
+ History,
3033
+ MessageSquare,
3034
+ Plus as Plus2,
3035
+ Search,
3036
+ Settings,
3037
+ Star,
3038
+ User
3039
+ } from "lucide-react";
3040
+ import {
3041
+ Fragment as Fragment7,
3042
+ useEffect as useEffect7
3043
+ } from "react";
3044
+
3045
+ // src/view/ChatShell.tsx
3046
+ import { jsx as jsx17 } from "react/jsx-runtime";
3047
+ function ChatShell({
3048
+ menu,
3049
+ activeMenuKey,
3050
+ brand,
3051
+ collapsible,
3052
+ sidebar,
3053
+ sidebarFooter,
3054
+ content,
3055
+ className,
3056
+ onMenuSelect
3057
+ }) {
3058
+ const sidebarContent = resolveSlot(sidebar) ?? (menu && menu.length > 0 ? /* @__PURE__ */ jsx17(
3059
+ ChatSidebarMenu,
3060
+ {
3061
+ menu,
3062
+ activeMenuKey,
3063
+ onMenuSelect
3064
+ }
3065
+ ) : null);
3066
+ return /* @__PURE__ */ jsx17(
3067
+ AppShell,
3068
+ {
3069
+ sidebar: sidebarContent,
3070
+ brand,
3071
+ collapsible,
3072
+ sidebarFooter: resolveSlot(sidebarFooter),
3073
+ className,
3074
+ children: resolveSlot(content)
3075
+ }
3076
+ );
3077
+ }
3078
+ var CHAT_SHELL_COMPONENT_TYPE = "ChatShell";
3079
+ function ChatShellDslAdapter({
3080
+ onMenuSelect,
3081
+ ...rest
3082
+ }) {
3083
+ return /* @__PURE__ */ jsx17(
3084
+ ChatShell,
3085
+ {
3086
+ ...rest,
3087
+ onMenuSelect: onMenuSelect ? (key, item) => onMenuSelect({ key, item }) : void 0
3088
+ }
3089
+ );
3090
+ }
3091
+ function registerChatShellComponent(registry) {
3092
+ if (!registry.has(CHAT_SHELL_COMPONENT_TYPE)) {
3093
+ registry.register(CHAT_SHELL_COMPONENT_TYPE, ChatShellDslAdapter);
3094
+ }
3095
+ }
3096
+
3097
+ // src/view/Chat.tsx
3098
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3099
+ var MENU_ICONS = {
3100
+ plus: Plus2,
3101
+ chat: MessageSquare,
3102
+ history: History,
3103
+ settings: Settings,
3104
+ star: Star,
3105
+ search: Search,
3106
+ user: User,
3107
+ folder: Folder
3108
+ };
3109
+ function ChatSidebarMenu({
3110
+ menu,
3111
+ activeMenuKey,
3112
+ onMenuSelect
3113
+ }) {
3114
+ const drawer = useSidebarDrawer();
3115
+ let lastGroup;
3116
+ return /* @__PURE__ */ jsx18("nav", { className: "pactor-chat-menu", "aria-label": "\u4F1A\u8BDD\u5BFC\u822A", children: menu.map((item) => {
3117
+ const groupLabel = item.group !== void 0 && item.group !== lastGroup ? item.group : void 0;
3118
+ lastGroup = item.group;
3119
+ const Icon = item.icon !== void 0 ? MENU_ICONS[item.icon] : void 0;
3120
+ const active = item.key === activeMenuKey;
3121
+ return /* @__PURE__ */ jsxs13(Fragment7, { children: [
3122
+ groupLabel !== void 0 ? /* @__PURE__ */ jsx18("div", { className: "pactor-chat-menu__group", children: groupLabel }) : null,
3123
+ /* @__PURE__ */ jsxs13(
3124
+ "button",
3125
+ {
3126
+ type: "button",
3127
+ className: `pactor-chat-menu__item${active ? " pactor-chat-menu__item--active" : ""}`,
3128
+ disabled: item.disabled === true,
3129
+ "aria-current": active ? "true" : void 0,
3130
+ title: drawer?.collapsed ? item.label : void 0,
3131
+ onClick: () => {
3132
+ if (item.disabled) {
3133
+ return;
3134
+ }
3135
+ onMenuSelect?.(item.key, item);
3136
+ drawer?.close();
3137
+ },
3138
+ children: [
3139
+ item.icon !== void 0 ? Icon ? /* @__PURE__ */ jsx18("span", { className: "pactor-chat-menu__icon", children: /* @__PURE__ */ jsx18(Icon, { size: 15, strokeWidth: 1.7 }) }) : /* @__PURE__ */ jsx18(
3140
+ "span",
3141
+ {
3142
+ className: "pactor-chat-menu__icon-placeholder",
3143
+ "aria-hidden": true
3144
+ }
3145
+ ) : null,
3146
+ /* @__PURE__ */ jsx18("span", { className: "pactor-chat-menu__label", children: item.label }),
3147
+ item.dot === true ? /* @__PURE__ */ jsx18("span", { className: "pactor-chat-menu__dot", "aria-hidden": true }) : null,
3148
+ item.badge !== void 0 ? /* @__PURE__ */ jsx18("span", { className: "pactor-chat-menu__badge", children: item.badge }) : null
3149
+ ]
3150
+ }
3151
+ )
3152
+ ] }, item.key);
3153
+ }) });
3154
+ }
3155
+ function Chat({
3156
+ title,
3157
+ greeting,
3158
+ subtitle,
3159
+ suggestions,
3160
+ conversationId,
3161
+ params,
3162
+ showHeader,
3163
+ menu,
3164
+ activeMenuKey,
3165
+ sidebar,
3166
+ headerExtra,
3167
+ header,
3168
+ headerActions,
3169
+ editableTitle,
3170
+ onExport,
3171
+ onThemeChange,
3172
+ onClear,
3173
+ onTitleChange,
3174
+ models,
3175
+ model,
3176
+ onModelChange,
3177
+ modelPlacement,
3178
+ contexts,
3179
+ onContextsChange,
3180
+ messageActions,
3181
+ onMessageAction,
3182
+ artifact,
3183
+ panelOpen,
3184
+ onPanelClose,
3185
+ inspector,
3186
+ onArtifact,
3187
+ onOpenArtifact,
3188
+ className,
3189
+ onSend,
3190
+ onMenuSelect
3191
+ }) {
3192
+ const services = useChatServices();
3193
+ useEffect7(() => {
3194
+ if (!services) {
3195
+ console.warn(
3196
+ "[@pactor-app/chat] Chat \u672A\u68C0\u6D4B\u5230 ChatServicesProvider\uFF1A\u8BF7\u5728\u5E94\u7528\u6839\u90E8\u4EE5 <ChatServicesProvider> \u6CE8\u5165 transport \u7B49\u670D\u52A1"
3197
+ );
3198
+ }
3199
+ }, [services]);
3200
+ if (!services) {
3201
+ return /* @__PURE__ */ jsx18("div", { className: "pactor-chat-notice", role: "note", children: "\u672A\u914D\u7F6E\u804A\u5929\u670D\u52A1\uFF1A\u8BF7\u5728 Chat \u5916\u5C42\u4F7F\u7528 ChatServicesProvider \u6CE8\u5165 transport \u7B49\u670D\u52A1\u540E\u91CD\u8BD5\u3002" });
3202
+ }
3203
+ const sidebarSlot = resolveSlot(sidebar);
3204
+ const sidebarContent = sidebarSlot ?? (menu && menu.length > 0 ? /* @__PURE__ */ jsx18(
3205
+ ChatSidebarMenu,
3206
+ {
3207
+ menu,
3208
+ activeMenuKey,
3209
+ onMenuSelect
3210
+ }
3211
+ ) : void 0);
3212
+ return /* @__PURE__ */ jsx18(
3213
+ ChatComposition,
3214
+ {
3215
+ services,
3216
+ title,
3217
+ greeting,
3218
+ subtitle,
3219
+ suggestions,
3220
+ conversationId,
3221
+ params,
3222
+ className,
3223
+ showHeader: showHeader !== false,
3224
+ header: resolveSlot(header),
3225
+ headerExtra: resolveSlot(headerExtra) ?? services.headerExtra,
3226
+ headerActions,
3227
+ editableTitle,
3228
+ onExport,
3229
+ onThemeChange,
3230
+ onClear,
3231
+ onTitleChange,
3232
+ models,
3233
+ model,
3234
+ onModelChange,
3235
+ modelPlacement,
3236
+ contexts,
3237
+ onContextsChange,
3238
+ messageActions,
3239
+ onMessageAction,
3240
+ sidebar: sidebarContent,
3241
+ onSend,
3242
+ artifact,
3243
+ panelOpen,
3244
+ onPanelClose,
3245
+ inspector: resolveSlot(inspector),
3246
+ onArtifact,
3247
+ onOpenArtifact
3248
+ }
3249
+ );
3250
+ }
3251
+ var CHAT_COMPONENT_TYPE = "Chat";
3252
+ function ChatDslAdapter({
3253
+ onSend,
3254
+ onMenuSelect,
3255
+ onPanelClose,
3256
+ onArtifact,
3257
+ onOpenArtifact,
3258
+ onModelChange,
3259
+ onContextsChange,
3260
+ onMessageAction,
3261
+ onExport,
3262
+ onThemeChange,
3263
+ onClear,
3264
+ onTitleChange,
3265
+ ...rest
3266
+ }) {
3267
+ return /* @__PURE__ */ jsx18(
3268
+ Chat,
3269
+ {
3270
+ ...rest,
3271
+ onSend: onSend ? (text) => onSend({ text }) : void 0,
3272
+ onMenuSelect: onMenuSelect ? (key, item) => onMenuSelect({ key, item }) : void 0,
3273
+ onPanelClose: onPanelClose ? () => onPanelClose({}) : void 0,
3274
+ onArtifact: onArtifact ? (artifact) => onArtifact({ artifact }) : void 0,
3275
+ onOpenArtifact: onOpenArtifact ? (artifact) => onOpenArtifact({ artifact }) : void 0,
3276
+ onModelChange: onModelChange ? (model) => onModelChange({ model }) : void 0,
3277
+ onContextsChange: onContextsChange ? (contexts) => onContextsChange({ contexts }) : void 0,
3278
+ onMessageAction: onMessageAction ? (action, message) => onMessageAction({ action, message }) : void 0,
3279
+ onExport: onExport ? () => onExport({}) : void 0,
3280
+ onThemeChange: onThemeChange ? (theme) => onThemeChange({ theme }) : void 0,
3281
+ onClear: onClear ? () => onClear({}) : void 0,
3282
+ onTitleChange: onTitleChange ? (title) => onTitleChange({ title }) : void 0
3283
+ }
3284
+ );
3285
+ }
3286
+ function registerChatComponent(registry) {
3287
+ if (!registry.has(CHAT_COMPONENT_TYPE)) {
3288
+ registry.register(CHAT_COMPONENT_TYPE, ChatDslAdapter);
3289
+ }
3290
+ registerChatShellComponent(registry);
3291
+ }
3292
+
3293
+ // src/view/ChatLayout.tsx
3294
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
3295
+ function ChatLayout({ title, children }) {
3296
+ return /* @__PURE__ */ jsxs14("div", { className: "pactor-chat-layout", children: [
3297
+ /* @__PURE__ */ jsx19("header", { className: "pactor-chat-layout__header", children: title ?? "Chat" }),
3298
+ /* @__PURE__ */ jsx19("main", { className: "pactor-chat-layout__main", children })
3299
+ ] });
3300
+ }
3301
+ var CHAT_LAYOUT_TYPE = "chat";
3302
+ function registerChatLayout(layoutRegistry) {
3303
+ if (!layoutRegistry.has(CHAT_LAYOUT_TYPE)) {
3304
+ layoutRegistry.register(CHAT_LAYOUT_TYPE, ChatLayout);
3305
+ }
3306
+ }
3307
+ export {
3308
+ ActivityTrace,
3309
+ AppShell,
3310
+ ArtifactPanel,
3311
+ ArtifactStore,
3312
+ CHAT_COMPONENT_TYPE,
3313
+ CHAT_DENIED_TYPE,
3314
+ CHAT_LAYOUT_TYPE,
3315
+ CHAT_SHELL_COMPONENT_TYPE,
3316
+ Chat,
3317
+ ChatDenied,
3318
+ ChatLayout,
3319
+ ChatServicesProvider,
3320
+ ChatShell,
3321
+ ChatSidebarMenu,
3322
+ ChatView,
3323
+ Composer,
3324
+ ConversationStore,
3325
+ DEFAULT_CHAT_GREETING,
3326
+ EmptyState,
3327
+ MENU_ICONS,
3328
+ MESSAGE_ACTION_ICONS,
3329
+ MessageActions,
3330
+ MessageBubble,
3331
+ PartRenderer,
3332
+ applyChatEvent,
3333
+ buildChatScope,
3334
+ collectMessageText,
3335
+ createChatId,
3336
+ createChatRuntime,
3337
+ createChatWorkspace,
3338
+ createManualDriver,
3339
+ createSafeRegistry,
3340
+ detectStreamingMode,
3341
+ guardChatDsl,
3342
+ pairToolTraces,
3343
+ registerCapabilityAction,
3344
+ registerChatComponent,
3345
+ registerChatLayout,
3346
+ registerChatShellComponent,
3347
+ runChatTurn,
3348
+ runRegenerateTurn,
3349
+ useChatAutoScroll,
3350
+ useChatServices,
3351
+ useSidebarDrawer,
3352
+ useSmoothStreamText
3353
+ };