@contractspec/module.ai-chat 4.0.3 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -10
- package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/browser/core/index.js +1143 -21
- package/dist/browser/index.js +2813 -631
- package/dist/browser/presentation/components/index.js +3160 -358
- package/dist/browser/presentation/hooks/index.js +978 -43
- package/dist/browser/presentation/index.js +2801 -666
- package/dist/core/agent-adapter.d.ts +53 -0
- package/dist/core/agent-tools-adapter.d.ts +12 -0
- package/dist/core/chat-service.d.ts +49 -1
- package/dist/core/contracts-context.d.ts +46 -0
- package/dist/core/contracts-context.test.d.ts +1 -0
- package/dist/core/conversation-store.d.ts +16 -2
- package/dist/core/create-chat-route.d.ts +3 -0
- package/dist/core/export-formatters.d.ts +29 -0
- package/dist/core/export-formatters.test.d.ts +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +1143 -21
- package/dist/core/local-storage-conversation-store.d.ts +33 -0
- package/dist/core/message-types.d.ts +6 -0
- package/dist/core/surface-planner-tools.d.ts +23 -0
- package/dist/core/surface-planner-tools.test.d.ts +1 -0
- package/dist/core/thinking-levels.d.ts +38 -0
- package/dist/core/thinking-levels.test.d.ts +1 -0
- package/dist/core/workflow-tools.d.ts +18 -0
- package/dist/core/workflow-tools.test.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2813 -631
- package/dist/node/core/index.js +1143 -21
- package/dist/node/index.js +2813 -631
- package/dist/node/presentation/components/index.js +3160 -358
- package/dist/node/presentation/hooks/index.js +978 -43
- package/dist/node/presentation/index.js +2804 -669
- package/dist/presentation/components/ChatContainer.d.ts +3 -1
- package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
- package/dist/presentation/components/ChatMessage.d.ts +16 -1
- package/dist/presentation/components/ChatSidebar.d.ts +26 -0
- package/dist/presentation/components/ChatWithExport.d.ts +34 -0
- package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
- package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
- package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
- package/dist/presentation/components/index.d.ts +6 -0
- package/dist/presentation/components/index.js +3160 -358
- package/dist/presentation/hooks/index.d.ts +2 -0
- package/dist/presentation/hooks/index.js +978 -43
- package/dist/presentation/hooks/useChat.d.ts +44 -2
- package/dist/presentation/hooks/useConversations.d.ts +18 -0
- package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
- package/dist/presentation/index.js +2804 -669
- package/package.json +14 -18
package/dist/index.js
CHANGED
|
@@ -404,12 +404,13 @@ function createNodeFileOperations(workspacePath, allowWrites = false) {
|
|
|
404
404
|
import * as React from "react";
|
|
405
405
|
import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
|
|
406
406
|
import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
407
|
-
import {
|
|
407
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
408
408
|
"use client";
|
|
409
409
|
function ChatContainer({
|
|
410
410
|
children,
|
|
411
411
|
className,
|
|
412
|
-
showScrollButton = true
|
|
412
|
+
showScrollButton = true,
|
|
413
|
+
headerContent
|
|
413
414
|
}) {
|
|
414
415
|
const scrollRef = React.useRef(null);
|
|
415
416
|
const [showScrollDown, setShowScrollDown] = React.useState(false);
|
|
@@ -436,24 +437,28 @@ function ChatContainer({
|
|
|
436
437
|
});
|
|
437
438
|
}
|
|
438
439
|
}, []);
|
|
439
|
-
return /* @__PURE__ */
|
|
440
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
440
441
|
className: cn("relative flex flex-1 flex-col", className),
|
|
441
442
|
children: [
|
|
442
|
-
/* @__PURE__ */
|
|
443
|
+
headerContent && /* @__PURE__ */ jsx("div", {
|
|
444
|
+
className: "border-border flex shrink-0 items-center justify-end gap-2 border-b px-4 py-2",
|
|
445
|
+
children: headerContent
|
|
446
|
+
}),
|
|
447
|
+
/* @__PURE__ */ jsx(ScrollArea, {
|
|
443
448
|
ref: scrollRef,
|
|
444
449
|
className: "flex-1",
|
|
445
450
|
onScroll: handleScroll,
|
|
446
|
-
children: /* @__PURE__ */
|
|
451
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
447
452
|
className: "flex flex-col gap-4 p-4",
|
|
448
453
|
children
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
showScrollButton && showScrollDown && /* @__PURE__ */
|
|
454
|
+
})
|
|
455
|
+
}),
|
|
456
|
+
showScrollButton && showScrollDown && /* @__PURE__ */ jsxs("button", {
|
|
452
457
|
onClick: scrollToBottom,
|
|
453
458
|
className: cn("absolute bottom-4 left-1/2 -translate-x-1/2", "bg-primary text-primary-foreground", "rounded-full px-3 py-1.5 text-sm font-medium shadow-lg", "hover:bg-primary/90 transition-colors", "flex items-center gap-1.5"),
|
|
454
459
|
"aria-label": "Scroll to bottom",
|
|
455
460
|
children: [
|
|
456
|
-
/* @__PURE__ */
|
|
461
|
+
/* @__PURE__ */ jsx("svg", {
|
|
457
462
|
xmlns: "http://www.w3.org/2000/svg",
|
|
458
463
|
width: "16",
|
|
459
464
|
height: "16",
|
|
@@ -463,15 +468,15 @@ function ChatContainer({
|
|
|
463
468
|
strokeWidth: "2",
|
|
464
469
|
strokeLinecap: "round",
|
|
465
470
|
strokeLinejoin: "round",
|
|
466
|
-
children: /* @__PURE__ */
|
|
471
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
467
472
|
d: "m6 9 6 6 6-6"
|
|
468
|
-
}
|
|
469
|
-
}
|
|
473
|
+
})
|
|
474
|
+
}),
|
|
470
475
|
"New messages"
|
|
471
476
|
]
|
|
472
|
-
}
|
|
477
|
+
})
|
|
473
478
|
]
|
|
474
|
-
}
|
|
479
|
+
});
|
|
475
480
|
}
|
|
476
481
|
// src/presentation/components/ChatMessage.tsx
|
|
477
482
|
import * as React3 from "react";
|
|
@@ -485,16 +490,19 @@ import {
|
|
|
485
490
|
Copy as Copy2,
|
|
486
491
|
Check as Check2,
|
|
487
492
|
ExternalLink,
|
|
488
|
-
Wrench
|
|
493
|
+
Wrench,
|
|
494
|
+
Pencil,
|
|
495
|
+
X
|
|
489
496
|
} from "lucide-react";
|
|
490
497
|
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
498
|
+
import { Checkbox } from "@contractspec/lib.ui-kit-web/ui/checkbox";
|
|
491
499
|
|
|
492
500
|
// src/presentation/components/CodePreview.tsx
|
|
493
501
|
import * as React2 from "react";
|
|
494
502
|
import { cn as cn2 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
495
503
|
import { Button } from "@contractspec/lib.design-system";
|
|
496
504
|
import { Copy, Check, Play, Download } from "lucide-react";
|
|
497
|
-
import {
|
|
505
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
498
506
|
"use client";
|
|
499
507
|
var LANGUAGE_NAMES = {
|
|
500
508
|
ts: "TypeScript",
|
|
@@ -547,93 +555,147 @@ function CodePreview({
|
|
|
547
555
|
document.body.removeChild(a);
|
|
548
556
|
URL.revokeObjectURL(url);
|
|
549
557
|
}, [code, filename, language]);
|
|
550
|
-
return /* @__PURE__ */
|
|
558
|
+
return /* @__PURE__ */ jsxs2("div", {
|
|
551
559
|
className: cn2("overflow-hidden rounded-lg border", "bg-muted/50", className),
|
|
552
560
|
children: [
|
|
553
|
-
/* @__PURE__ */
|
|
561
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
554
562
|
className: cn2("flex items-center justify-between px-3 py-1.5", "bg-muted/80 border-b"),
|
|
555
563
|
children: [
|
|
556
|
-
/* @__PURE__ */
|
|
564
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
557
565
|
className: "flex items-center gap-2 text-sm",
|
|
558
566
|
children: [
|
|
559
|
-
filename && /* @__PURE__ */
|
|
567
|
+
filename && /* @__PURE__ */ jsx2("span", {
|
|
560
568
|
className: "text-foreground font-mono",
|
|
561
569
|
children: filename
|
|
562
|
-
}
|
|
563
|
-
/* @__PURE__ */
|
|
570
|
+
}),
|
|
571
|
+
/* @__PURE__ */ jsx2("span", {
|
|
564
572
|
className: "text-muted-foreground",
|
|
565
573
|
children: displayLanguage
|
|
566
|
-
}
|
|
574
|
+
})
|
|
567
575
|
]
|
|
568
|
-
}
|
|
569
|
-
/* @__PURE__ */
|
|
576
|
+
}),
|
|
577
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
570
578
|
className: "flex items-center gap-1",
|
|
571
579
|
children: [
|
|
572
|
-
showExecute && onExecute && /* @__PURE__ */
|
|
580
|
+
showExecute && onExecute && /* @__PURE__ */ jsx2(Button, {
|
|
573
581
|
variant: "ghost",
|
|
574
582
|
size: "sm",
|
|
575
583
|
onPress: () => onExecute(code),
|
|
576
584
|
className: "h-7 w-7 p-0",
|
|
577
585
|
"aria-label": "Execute code",
|
|
578
|
-
children: /* @__PURE__ */
|
|
586
|
+
children: /* @__PURE__ */ jsx2(Play, {
|
|
579
587
|
className: "h-3.5 w-3.5"
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
showDownload && /* @__PURE__ */
|
|
588
|
+
})
|
|
589
|
+
}),
|
|
590
|
+
showDownload && /* @__PURE__ */ jsx2(Button, {
|
|
583
591
|
variant: "ghost",
|
|
584
592
|
size: "sm",
|
|
585
593
|
onPress: handleDownload,
|
|
586
594
|
className: "h-7 w-7 p-0",
|
|
587
595
|
"aria-label": "Download code",
|
|
588
|
-
children: /* @__PURE__ */
|
|
596
|
+
children: /* @__PURE__ */ jsx2(Download, {
|
|
589
597
|
className: "h-3.5 w-3.5"
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
showCopy && /* @__PURE__ */
|
|
598
|
+
})
|
|
599
|
+
}),
|
|
600
|
+
showCopy && /* @__PURE__ */ jsx2(Button, {
|
|
593
601
|
variant: "ghost",
|
|
594
602
|
size: "sm",
|
|
595
603
|
onPress: handleCopy,
|
|
596
604
|
className: "h-7 w-7 p-0",
|
|
597
605
|
"aria-label": copied ? "Copied" : "Copy code",
|
|
598
|
-
children: copied ? /* @__PURE__ */
|
|
606
|
+
children: copied ? /* @__PURE__ */ jsx2(Check, {
|
|
599
607
|
className: "h-3.5 w-3.5 text-green-500"
|
|
600
|
-
}
|
|
608
|
+
}) : /* @__PURE__ */ jsx2(Copy, {
|
|
601
609
|
className: "h-3.5 w-3.5"
|
|
602
|
-
}
|
|
603
|
-
}
|
|
610
|
+
})
|
|
611
|
+
})
|
|
604
612
|
]
|
|
605
|
-
}
|
|
613
|
+
})
|
|
606
614
|
]
|
|
607
|
-
}
|
|
608
|
-
/* @__PURE__ */
|
|
615
|
+
}),
|
|
616
|
+
/* @__PURE__ */ jsx2("div", {
|
|
609
617
|
className: "overflow-auto",
|
|
610
618
|
style: { maxHeight },
|
|
611
|
-
children: /* @__PURE__ */
|
|
619
|
+
children: /* @__PURE__ */ jsx2("pre", {
|
|
612
620
|
className: "p-3",
|
|
613
|
-
children: /* @__PURE__ */
|
|
621
|
+
children: /* @__PURE__ */ jsx2("code", {
|
|
614
622
|
className: "text-sm",
|
|
615
|
-
children: lines.map((line, i) => /* @__PURE__ */
|
|
623
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsxs2("div", {
|
|
616
624
|
className: "flex",
|
|
617
625
|
children: [
|
|
618
|
-
/* @__PURE__ */
|
|
626
|
+
/* @__PURE__ */ jsx2("span", {
|
|
619
627
|
className: "text-muted-foreground mr-4 w-8 text-right select-none",
|
|
620
628
|
children: i + 1
|
|
621
|
-
}
|
|
622
|
-
/* @__PURE__ */
|
|
629
|
+
}),
|
|
630
|
+
/* @__PURE__ */ jsx2("span", {
|
|
623
631
|
className: "flex-1",
|
|
624
632
|
children: line || " "
|
|
625
|
-
}
|
|
633
|
+
})
|
|
626
634
|
]
|
|
627
|
-
}, i
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
635
|
+
}, i))
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
})
|
|
639
|
+
]
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/presentation/components/ToolResultRenderer.tsx
|
|
644
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
645
|
+
"use client";
|
|
646
|
+
function isPresentationToolResult(result) {
|
|
647
|
+
return typeof result === "object" && result !== null && "presentationKey" in result && typeof result.presentationKey === "string";
|
|
648
|
+
}
|
|
649
|
+
function isFormToolResult(result) {
|
|
650
|
+
return typeof result === "object" && result !== null && "formKey" in result && typeof result.formKey === "string";
|
|
651
|
+
}
|
|
652
|
+
function ToolResultRenderer({
|
|
653
|
+
toolName,
|
|
654
|
+
result,
|
|
655
|
+
presentationRenderer,
|
|
656
|
+
formRenderer,
|
|
657
|
+
showRawFallback = true
|
|
658
|
+
}) {
|
|
659
|
+
if (result === undefined || result === null) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
if (isPresentationToolResult(result) && presentationRenderer) {
|
|
663
|
+
const rendered = presentationRenderer(result.presentationKey, result.data);
|
|
664
|
+
if (rendered != null) {
|
|
665
|
+
return /* @__PURE__ */ jsx3("div", {
|
|
666
|
+
className: "border-border bg-background/50 mt-2 rounded-md border p-3",
|
|
667
|
+
children: rendered
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (isFormToolResult(result) && formRenderer) {
|
|
672
|
+
const rendered = formRenderer(result.formKey, result.defaultValues);
|
|
673
|
+
if (rendered != null) {
|
|
674
|
+
return /* @__PURE__ */ jsx3("div", {
|
|
675
|
+
className: "border-border bg-background/50 mt-2 rounded-md border p-3",
|
|
676
|
+
children: rendered
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!showRawFallback) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
return /* @__PURE__ */ jsxs3("div", {
|
|
684
|
+
children: [
|
|
685
|
+
/* @__PURE__ */ jsx3("span", {
|
|
686
|
+
className: "text-muted-foreground font-medium",
|
|
687
|
+
children: "Output:"
|
|
688
|
+
}),
|
|
689
|
+
/* @__PURE__ */ jsx3("pre", {
|
|
690
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2 text-xs",
|
|
691
|
+
children: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result)
|
|
692
|
+
})
|
|
631
693
|
]
|
|
632
|
-
}
|
|
694
|
+
});
|
|
633
695
|
}
|
|
634
696
|
|
|
635
697
|
// src/presentation/components/ChatMessage.tsx
|
|
636
|
-
import {
|
|
698
|
+
import { jsx as jsx4, jsxs as jsxs4, Fragment } from "react/jsx-runtime";
|
|
637
699
|
"use client";
|
|
638
700
|
function extractCodeBlocks(content) {
|
|
639
701
|
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
@@ -656,33 +718,33 @@ function renderInlineMarkdown(text) {
|
|
|
656
718
|
let key = 0;
|
|
657
719
|
while ((match = linkRegex.exec(text)) !== null) {
|
|
658
720
|
if (match.index > lastIndex) {
|
|
659
|
-
parts.push(/* @__PURE__ */
|
|
721
|
+
parts.push(/* @__PURE__ */ jsx4("span", {
|
|
660
722
|
children: text.slice(lastIndex, match.index)
|
|
661
|
-
}, key
|
|
723
|
+
}, key++));
|
|
662
724
|
}
|
|
663
|
-
parts.push(/* @__PURE__ */
|
|
725
|
+
parts.push(/* @__PURE__ */ jsx4("a", {
|
|
664
726
|
href: match[2],
|
|
665
727
|
target: "_blank",
|
|
666
728
|
rel: "noopener noreferrer",
|
|
667
729
|
className: "text-primary underline hover:no-underline",
|
|
668
730
|
children: match[1]
|
|
669
|
-
}, key
|
|
731
|
+
}, key++));
|
|
670
732
|
lastIndex = match.index + match[0].length;
|
|
671
733
|
}
|
|
672
734
|
if (lastIndex < text.length) {
|
|
673
|
-
parts.push(/* @__PURE__ */
|
|
735
|
+
parts.push(/* @__PURE__ */ jsx4("span", {
|
|
674
736
|
children: text.slice(lastIndex)
|
|
675
|
-
}, key
|
|
737
|
+
}, key++));
|
|
676
738
|
}
|
|
677
739
|
return parts.length > 0 ? parts : [text];
|
|
678
740
|
}
|
|
679
741
|
function MessageContent({ content }) {
|
|
680
742
|
const codeBlocks = extractCodeBlocks(content);
|
|
681
743
|
if (codeBlocks.length === 0) {
|
|
682
|
-
return /* @__PURE__ */
|
|
744
|
+
return /* @__PURE__ */ jsx4("p", {
|
|
683
745
|
className: "whitespace-pre-wrap",
|
|
684
746
|
children: renderInlineMarkdown(content)
|
|
685
|
-
}
|
|
747
|
+
});
|
|
686
748
|
}
|
|
687
749
|
let remaining = content;
|
|
688
750
|
const parts = [];
|
|
@@ -690,33 +752,40 @@ function MessageContent({ content }) {
|
|
|
690
752
|
for (const block of codeBlocks) {
|
|
691
753
|
const [before, after] = remaining.split(block.raw);
|
|
692
754
|
if (before) {
|
|
693
|
-
parts.push(/* @__PURE__ */
|
|
755
|
+
parts.push(/* @__PURE__ */ jsx4("p", {
|
|
694
756
|
className: "whitespace-pre-wrap",
|
|
695
757
|
children: renderInlineMarkdown(before.trim())
|
|
696
|
-
}, key
|
|
758
|
+
}, key++));
|
|
697
759
|
}
|
|
698
|
-
parts.push(/* @__PURE__ */
|
|
760
|
+
parts.push(/* @__PURE__ */ jsx4(CodePreview, {
|
|
699
761
|
code: block.code,
|
|
700
762
|
language: block.language,
|
|
701
763
|
className: "my-2"
|
|
702
|
-
}, key
|
|
764
|
+
}, key++));
|
|
703
765
|
remaining = after ?? "";
|
|
704
766
|
}
|
|
705
767
|
if (remaining.trim()) {
|
|
706
|
-
parts.push(/* @__PURE__ */
|
|
768
|
+
parts.push(/* @__PURE__ */ jsx4("p", {
|
|
707
769
|
className: "whitespace-pre-wrap",
|
|
708
770
|
children: renderInlineMarkdown(remaining.trim())
|
|
709
|
-
}, key
|
|
771
|
+
}, key++));
|
|
710
772
|
}
|
|
711
|
-
return /* @__PURE__ */
|
|
773
|
+
return /* @__PURE__ */ jsx4(Fragment, {
|
|
712
774
|
children: parts
|
|
713
|
-
}
|
|
775
|
+
});
|
|
714
776
|
}
|
|
715
777
|
function ChatMessage({
|
|
716
778
|
message,
|
|
717
779
|
className,
|
|
718
780
|
showCopy = true,
|
|
719
|
-
showAvatar = true
|
|
781
|
+
showAvatar = true,
|
|
782
|
+
selectable = false,
|
|
783
|
+
selected = false,
|
|
784
|
+
onSelect,
|
|
785
|
+
editable = false,
|
|
786
|
+
onEdit,
|
|
787
|
+
presentationRenderer,
|
|
788
|
+
formRenderer
|
|
720
789
|
}) {
|
|
721
790
|
const [copied, setCopied] = React3.useState(false);
|
|
722
791
|
const isUser = message.role === "user";
|
|
@@ -727,185 +796,262 @@ function ChatMessage({
|
|
|
727
796
|
setCopied(true);
|
|
728
797
|
setTimeout(() => setCopied(false), 2000);
|
|
729
798
|
}, [message.content]);
|
|
730
|
-
|
|
799
|
+
const handleSelectChange = React3.useCallback((checked) => {
|
|
800
|
+
if (checked !== "indeterminate")
|
|
801
|
+
onSelect?.(message.id);
|
|
802
|
+
}, [message.id, onSelect]);
|
|
803
|
+
const [isEditing, setIsEditing] = React3.useState(false);
|
|
804
|
+
const [editContent, setEditContent] = React3.useState(message.content);
|
|
805
|
+
React3.useEffect(() => {
|
|
806
|
+
setEditContent(message.content);
|
|
807
|
+
}, [message.content]);
|
|
808
|
+
const handleStartEdit = React3.useCallback(() => {
|
|
809
|
+
setEditContent(message.content);
|
|
810
|
+
setIsEditing(true);
|
|
811
|
+
}, [message.content]);
|
|
812
|
+
const handleSaveEdit = React3.useCallback(async () => {
|
|
813
|
+
const trimmed = editContent.trim();
|
|
814
|
+
if (trimmed !== message.content) {
|
|
815
|
+
await onEdit?.(message.id, trimmed);
|
|
816
|
+
}
|
|
817
|
+
setIsEditing(false);
|
|
818
|
+
}, [editContent, message.id, message.content, onEdit]);
|
|
819
|
+
const handleCancelEdit = React3.useCallback(() => {
|
|
820
|
+
setEditContent(message.content);
|
|
821
|
+
setIsEditing(false);
|
|
822
|
+
}, [message.content]);
|
|
823
|
+
return /* @__PURE__ */ jsxs4("div", {
|
|
731
824
|
className: cn3("group flex gap-3", isUser && "flex-row-reverse", className),
|
|
732
825
|
children: [
|
|
733
|
-
|
|
826
|
+
selectable && /* @__PURE__ */ jsx4("div", {
|
|
827
|
+
className: cn3("flex shrink-0 items-start pt-1", "opacity-0 transition-opacity group-hover:opacity-100"),
|
|
828
|
+
children: /* @__PURE__ */ jsx4(Checkbox, {
|
|
829
|
+
checked: selected,
|
|
830
|
+
onCheckedChange: handleSelectChange,
|
|
831
|
+
"aria-label": selected ? "Deselect message" : "Select message"
|
|
832
|
+
})
|
|
833
|
+
}),
|
|
834
|
+
showAvatar && /* @__PURE__ */ jsx4(Avatar, {
|
|
734
835
|
className: "h-8 w-8 shrink-0",
|
|
735
|
-
children: /* @__PURE__ */
|
|
836
|
+
children: /* @__PURE__ */ jsx4(AvatarFallback, {
|
|
736
837
|
className: cn3(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
|
|
737
|
-
children: isUser ? /* @__PURE__ */
|
|
838
|
+
children: isUser ? /* @__PURE__ */ jsx4(User, {
|
|
738
839
|
className: "h-4 w-4"
|
|
739
|
-
}
|
|
840
|
+
}) : /* @__PURE__ */ jsx4(Bot, {
|
|
740
841
|
className: "h-4 w-4"
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
/* @__PURE__ */
|
|
842
|
+
})
|
|
843
|
+
})
|
|
844
|
+
}),
|
|
845
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
745
846
|
className: cn3("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
|
|
746
847
|
children: [
|
|
747
|
-
/* @__PURE__ */
|
|
848
|
+
/* @__PURE__ */ jsx4("div", {
|
|
748
849
|
className: cn3("rounded-2xl px-4 py-2", isUser ? "bg-primary text-primary-foreground" : "bg-muted text-foreground", isError && "border-destructive bg-destructive/10 border"),
|
|
749
|
-
children: isError && message.error ? /* @__PURE__ */
|
|
850
|
+
children: isError && message.error ? /* @__PURE__ */ jsxs4("div", {
|
|
750
851
|
className: "flex items-start gap-2",
|
|
751
852
|
children: [
|
|
752
|
-
/* @__PURE__ */
|
|
853
|
+
/* @__PURE__ */ jsx4(AlertCircle, {
|
|
753
854
|
className: "text-destructive mt-0.5 h-4 w-4 shrink-0"
|
|
754
|
-
}
|
|
755
|
-
/* @__PURE__ */
|
|
855
|
+
}),
|
|
856
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
756
857
|
children: [
|
|
757
|
-
/* @__PURE__ */
|
|
858
|
+
/* @__PURE__ */ jsx4("p", {
|
|
758
859
|
className: "text-destructive font-medium",
|
|
759
860
|
children: message.error.code
|
|
760
|
-
}
|
|
761
|
-
/* @__PURE__ */
|
|
861
|
+
}),
|
|
862
|
+
/* @__PURE__ */ jsx4("p", {
|
|
762
863
|
className: "text-muted-foreground text-sm",
|
|
763
864
|
children: message.error.message
|
|
764
|
-
}
|
|
865
|
+
})
|
|
866
|
+
]
|
|
867
|
+
})
|
|
868
|
+
]
|
|
869
|
+
}) : isEditing ? /* @__PURE__ */ jsxs4("div", {
|
|
870
|
+
className: "flex flex-col gap-2",
|
|
871
|
+
children: [
|
|
872
|
+
/* @__PURE__ */ jsx4("textarea", {
|
|
873
|
+
value: editContent,
|
|
874
|
+
onChange: (e) => setEditContent(e.target.value),
|
|
875
|
+
className: "bg-background/50 min-h-[80px] w-full resize-y rounded-md border px-3 py-2 text-sm",
|
|
876
|
+
rows: 4,
|
|
877
|
+
autoFocus: true
|
|
878
|
+
}),
|
|
879
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
880
|
+
className: "flex gap-2",
|
|
881
|
+
children: [
|
|
882
|
+
/* @__PURE__ */ jsxs4(Button2, {
|
|
883
|
+
variant: "default",
|
|
884
|
+
size: "sm",
|
|
885
|
+
onPress: handleSaveEdit,
|
|
886
|
+
"aria-label": "Save edit",
|
|
887
|
+
children: [
|
|
888
|
+
/* @__PURE__ */ jsx4(Check2, {
|
|
889
|
+
className: "h-3 w-3"
|
|
890
|
+
}),
|
|
891
|
+
"Save"
|
|
892
|
+
]
|
|
893
|
+
}),
|
|
894
|
+
/* @__PURE__ */ jsxs4(Button2, {
|
|
895
|
+
variant: "ghost",
|
|
896
|
+
size: "sm",
|
|
897
|
+
onPress: handleCancelEdit,
|
|
898
|
+
"aria-label": "Cancel edit",
|
|
899
|
+
children: [
|
|
900
|
+
/* @__PURE__ */ jsx4(X, {
|
|
901
|
+
className: "h-3 w-3"
|
|
902
|
+
}),
|
|
903
|
+
"Cancel"
|
|
904
|
+
]
|
|
905
|
+
})
|
|
765
906
|
]
|
|
766
|
-
}
|
|
907
|
+
})
|
|
767
908
|
]
|
|
768
|
-
}
|
|
909
|
+
}) : isStreaming && !message.content ? /* @__PURE__ */ jsxs4("div", {
|
|
769
910
|
className: "flex flex-col gap-2",
|
|
770
911
|
children: [
|
|
771
|
-
/* @__PURE__ */
|
|
912
|
+
/* @__PURE__ */ jsx4(Skeleton, {
|
|
772
913
|
className: "h-4 w-48"
|
|
773
|
-
}
|
|
774
|
-
/* @__PURE__ */
|
|
914
|
+
}),
|
|
915
|
+
/* @__PURE__ */ jsx4(Skeleton, {
|
|
775
916
|
className: "h-4 w-32"
|
|
776
|
-
}
|
|
917
|
+
})
|
|
777
918
|
]
|
|
778
|
-
}
|
|
919
|
+
}) : /* @__PURE__ */ jsx4(MessageContent, {
|
|
779
920
|
content: message.content
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
/* @__PURE__ */
|
|
921
|
+
})
|
|
922
|
+
}),
|
|
923
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
783
924
|
className: cn3("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
|
|
784
925
|
children: [
|
|
785
|
-
/* @__PURE__ */
|
|
926
|
+
/* @__PURE__ */ jsx4("span", {
|
|
786
927
|
children: new Date(message.createdAt).toLocaleTimeString([], {
|
|
787
928
|
hour: "2-digit",
|
|
788
929
|
minute: "2-digit"
|
|
789
930
|
})
|
|
790
|
-
}
|
|
791
|
-
message.usage && /* @__PURE__ */
|
|
931
|
+
}),
|
|
932
|
+
message.usage && /* @__PURE__ */ jsxs4("span", {
|
|
792
933
|
children: [
|
|
793
934
|
message.usage.inputTokens + message.usage.outputTokens,
|
|
794
935
|
" tokens"
|
|
795
936
|
]
|
|
796
|
-
}
|
|
797
|
-
showCopy && !isUser && message.content && /* @__PURE__ */
|
|
937
|
+
}),
|
|
938
|
+
showCopy && !isUser && message.content && /* @__PURE__ */ jsx4(Button2, {
|
|
798
939
|
variant: "ghost",
|
|
799
940
|
size: "sm",
|
|
800
941
|
className: "h-6 w-6 p-0",
|
|
801
942
|
onPress: handleCopy,
|
|
802
943
|
"aria-label": copied ? "Copied" : "Copy message",
|
|
803
|
-
children: copied ? /* @__PURE__ */
|
|
944
|
+
children: copied ? /* @__PURE__ */ jsx4(Check2, {
|
|
945
|
+
className: "h-3 w-3"
|
|
946
|
+
}) : /* @__PURE__ */ jsx4(Copy2, {
|
|
804
947
|
className: "h-3 w-3"
|
|
805
|
-
}
|
|
948
|
+
})
|
|
949
|
+
}),
|
|
950
|
+
editable && isUser && !isEditing && /* @__PURE__ */ jsx4(Button2, {
|
|
951
|
+
variant: "ghost",
|
|
952
|
+
size: "sm",
|
|
953
|
+
className: "h-6 w-6 p-0",
|
|
954
|
+
onPress: handleStartEdit,
|
|
955
|
+
"aria-label": "Edit message",
|
|
956
|
+
children: /* @__PURE__ */ jsx4(Pencil, {
|
|
806
957
|
className: "h-3 w-3"
|
|
807
|
-
}
|
|
808
|
-
}
|
|
958
|
+
})
|
|
959
|
+
})
|
|
809
960
|
]
|
|
810
|
-
}
|
|
811
|
-
message.reasoning && /* @__PURE__ */
|
|
961
|
+
}),
|
|
962
|
+
message.reasoning && /* @__PURE__ */ jsxs4("details", {
|
|
812
963
|
className: "text-muted-foreground mt-2 text-sm",
|
|
813
964
|
children: [
|
|
814
|
-
/* @__PURE__ */
|
|
965
|
+
/* @__PURE__ */ jsx4("summary", {
|
|
815
966
|
className: "cursor-pointer hover:underline",
|
|
816
967
|
children: "View reasoning"
|
|
817
|
-
}
|
|
818
|
-
/* @__PURE__ */
|
|
968
|
+
}),
|
|
969
|
+
/* @__PURE__ */ jsx4("div", {
|
|
819
970
|
className: "bg-muted mt-1 rounded-md p-2",
|
|
820
|
-
children: /* @__PURE__ */
|
|
971
|
+
children: /* @__PURE__ */ jsx4("p", {
|
|
821
972
|
className: "whitespace-pre-wrap",
|
|
822
973
|
children: message.reasoning
|
|
823
|
-
}
|
|
824
|
-
}
|
|
974
|
+
})
|
|
975
|
+
})
|
|
825
976
|
]
|
|
826
|
-
}
|
|
827
|
-
message.sources && message.sources.length > 0 && /* @__PURE__ */
|
|
977
|
+
}),
|
|
978
|
+
message.sources && message.sources.length > 0 && /* @__PURE__ */ jsx4("div", {
|
|
828
979
|
className: "mt-2 flex flex-wrap gap-2",
|
|
829
|
-
children: message.sources.map((source) => /* @__PURE__ */
|
|
980
|
+
children: message.sources.map((source) => /* @__PURE__ */ jsxs4("a", {
|
|
830
981
|
href: source.url ?? "#",
|
|
831
982
|
target: "_blank",
|
|
832
983
|
rel: "noopener noreferrer",
|
|
833
984
|
className: "text-muted-foreground hover:text-foreground bg-muted inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
|
|
834
985
|
children: [
|
|
835
|
-
/* @__PURE__ */
|
|
986
|
+
/* @__PURE__ */ jsx4(ExternalLink, {
|
|
836
987
|
className: "h-3 w-3"
|
|
837
|
-
}
|
|
988
|
+
}),
|
|
838
989
|
source.title || source.url || source.id
|
|
839
990
|
]
|
|
840
|
-
}, source.id
|
|
841
|
-
}
|
|
842
|
-
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */
|
|
991
|
+
}, source.id))
|
|
992
|
+
}),
|
|
993
|
+
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsx4("div", {
|
|
843
994
|
className: "mt-2 space-y-2",
|
|
844
|
-
children: message.toolCalls.map((tc) => /* @__PURE__ */
|
|
995
|
+
children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxs4("details", {
|
|
845
996
|
className: "bg-muted border-border rounded-md border",
|
|
846
997
|
children: [
|
|
847
|
-
/* @__PURE__ */
|
|
998
|
+
/* @__PURE__ */ jsxs4("summary", {
|
|
848
999
|
className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
|
|
849
1000
|
children: [
|
|
850
|
-
/* @__PURE__ */
|
|
1001
|
+
/* @__PURE__ */ jsx4(Wrench, {
|
|
851
1002
|
className: "text-muted-foreground h-4 w-4"
|
|
852
|
-
}
|
|
1003
|
+
}),
|
|
853
1004
|
tc.name,
|
|
854
|
-
/* @__PURE__ */
|
|
1005
|
+
/* @__PURE__ */ jsx4("span", {
|
|
855
1006
|
className: cn3("ml-auto rounded px-1.5 py-0.5 text-xs", tc.status === "completed" && "bg-green-500/20 text-green-700 dark:text-green-400", tc.status === "error" && "bg-destructive/20 text-destructive", tc.status === "running" && "bg-blue-500/20 text-blue-700 dark:text-blue-400"),
|
|
856
1007
|
children: tc.status
|
|
857
|
-
}
|
|
1008
|
+
})
|
|
858
1009
|
]
|
|
859
|
-
}
|
|
860
|
-
/* @__PURE__ */
|
|
1010
|
+
}),
|
|
1011
|
+
/* @__PURE__ */ jsxs4("div", {
|
|
861
1012
|
className: "border-border border-t px-3 py-2 text-xs",
|
|
862
1013
|
children: [
|
|
863
|
-
Object.keys(tc.args).length > 0 && /* @__PURE__ */
|
|
1014
|
+
Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxs4("div", {
|
|
864
1015
|
className: "mb-2",
|
|
865
1016
|
children: [
|
|
866
|
-
/* @__PURE__ */
|
|
1017
|
+
/* @__PURE__ */ jsx4("span", {
|
|
867
1018
|
className: "text-muted-foreground font-medium",
|
|
868
1019
|
children: "Input:"
|
|
869
|
-
}
|
|
870
|
-
/* @__PURE__ */
|
|
1020
|
+
}),
|
|
1021
|
+
/* @__PURE__ */ jsx4("pre", {
|
|
871
1022
|
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
872
1023
|
children: JSON.stringify(tc.args, null, 2)
|
|
873
|
-
}
|
|
1024
|
+
})
|
|
874
1025
|
]
|
|
875
|
-
}
|
|
876
|
-
tc.result !== undefined && /* @__PURE__ */
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
|
|
885
|
-
}, undefined, false, undefined, this)
|
|
886
|
-
]
|
|
887
|
-
}, undefined, true, undefined, this),
|
|
888
|
-
tc.error && /* @__PURE__ */ jsxDEV3("p", {
|
|
1026
|
+
}),
|
|
1027
|
+
tc.result !== undefined && /* @__PURE__ */ jsx4(ToolResultRenderer, {
|
|
1028
|
+
toolName: tc.name,
|
|
1029
|
+
result: tc.result,
|
|
1030
|
+
presentationRenderer,
|
|
1031
|
+
formRenderer,
|
|
1032
|
+
showRawFallback: true
|
|
1033
|
+
}),
|
|
1034
|
+
tc.error && /* @__PURE__ */ jsx4("p", {
|
|
889
1035
|
className: "text-destructive mt-1",
|
|
890
1036
|
children: tc.error
|
|
891
|
-
}
|
|
1037
|
+
})
|
|
892
1038
|
]
|
|
893
|
-
}
|
|
1039
|
+
})
|
|
894
1040
|
]
|
|
895
|
-
}, tc.id
|
|
896
|
-
}
|
|
1041
|
+
}, tc.id))
|
|
1042
|
+
})
|
|
897
1043
|
]
|
|
898
|
-
}
|
|
1044
|
+
})
|
|
899
1045
|
]
|
|
900
|
-
}
|
|
1046
|
+
});
|
|
901
1047
|
}
|
|
902
1048
|
// src/presentation/components/ChatInput.tsx
|
|
903
1049
|
import * as React4 from "react";
|
|
904
1050
|
import { cn as cn4 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
905
1051
|
import { Textarea } from "@contractspec/lib.design-system";
|
|
906
1052
|
import { Button as Button3 } from "@contractspec/lib.design-system";
|
|
907
|
-
import { Send, Paperclip, X, Loader2, FileText, Code } from "lucide-react";
|
|
908
|
-
import {
|
|
1053
|
+
import { Send, Paperclip, X as X2, Loader2, FileText, Code } from "lucide-react";
|
|
1054
|
+
import { jsx as jsx5, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
909
1055
|
"use client";
|
|
910
1056
|
function ChatInput({
|
|
911
1057
|
onSend,
|
|
@@ -971,42 +1117,42 @@ function ChatInput({
|
|
|
971
1117
|
const removeAttachment = React4.useCallback((id) => {
|
|
972
1118
|
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
973
1119
|
}, []);
|
|
974
|
-
return /* @__PURE__ */
|
|
1120
|
+
return /* @__PURE__ */ jsxs5("div", {
|
|
975
1121
|
className: cn4("flex flex-col gap-2", className),
|
|
976
1122
|
children: [
|
|
977
|
-
attachments.length > 0 && /* @__PURE__ */
|
|
1123
|
+
attachments.length > 0 && /* @__PURE__ */ jsx5("div", {
|
|
978
1124
|
className: "flex flex-wrap gap-2",
|
|
979
|
-
children: attachments.map((attachment) => /* @__PURE__ */
|
|
1125
|
+
children: attachments.map((attachment) => /* @__PURE__ */ jsxs5("div", {
|
|
980
1126
|
className: cn4("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
|
|
981
1127
|
children: [
|
|
982
|
-
attachment.type === "code" ? /* @__PURE__ */
|
|
1128
|
+
attachment.type === "code" ? /* @__PURE__ */ jsx5(Code, {
|
|
983
1129
|
className: "h-3.5 w-3.5"
|
|
984
|
-
}
|
|
1130
|
+
}) : /* @__PURE__ */ jsx5(FileText, {
|
|
985
1131
|
className: "h-3.5 w-3.5"
|
|
986
|
-
}
|
|
987
|
-
/* @__PURE__ */
|
|
1132
|
+
}),
|
|
1133
|
+
/* @__PURE__ */ jsx5("span", {
|
|
988
1134
|
className: "max-w-[150px] truncate",
|
|
989
1135
|
children: attachment.name
|
|
990
|
-
}
|
|
991
|
-
/* @__PURE__ */
|
|
1136
|
+
}),
|
|
1137
|
+
/* @__PURE__ */ jsx5("button", {
|
|
992
1138
|
type: "button",
|
|
993
1139
|
onClick: () => removeAttachment(attachment.id),
|
|
994
1140
|
className: "hover:text-foreground",
|
|
995
1141
|
"aria-label": `Remove ${attachment.name}`,
|
|
996
|
-
children: /* @__PURE__ */
|
|
1142
|
+
children: /* @__PURE__ */ jsx5(X2, {
|
|
997
1143
|
className: "h-3.5 w-3.5"
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1144
|
+
})
|
|
1145
|
+
})
|
|
1000
1146
|
]
|
|
1001
|
-
}, attachment.id
|
|
1002
|
-
}
|
|
1003
|
-
/* @__PURE__ */
|
|
1147
|
+
}, attachment.id))
|
|
1148
|
+
}),
|
|
1149
|
+
/* @__PURE__ */ jsxs5("form", {
|
|
1004
1150
|
onSubmit: handleSubmit,
|
|
1005
1151
|
className: "flex items-end gap-2",
|
|
1006
1152
|
children: [
|
|
1007
|
-
showAttachments && /* @__PURE__ */
|
|
1153
|
+
showAttachments && /* @__PURE__ */ jsxs5(Fragment2, {
|
|
1008
1154
|
children: [
|
|
1009
|
-
/* @__PURE__ */
|
|
1155
|
+
/* @__PURE__ */ jsx5("input", {
|
|
1010
1156
|
ref: fileInputRef,
|
|
1011
1157
|
type: "file",
|
|
1012
1158
|
multiple: true,
|
|
@@ -1014,23 +1160,23 @@ function ChatInput({
|
|
|
1014
1160
|
onChange: handleFileSelect,
|
|
1015
1161
|
className: "hidden",
|
|
1016
1162
|
"aria-label": "Attach files"
|
|
1017
|
-
}
|
|
1018
|
-
/* @__PURE__ */
|
|
1163
|
+
}),
|
|
1164
|
+
/* @__PURE__ */ jsx5(Button3, {
|
|
1019
1165
|
type: "button",
|
|
1020
1166
|
variant: "ghost",
|
|
1021
1167
|
size: "sm",
|
|
1022
1168
|
onPress: () => fileInputRef.current?.click(),
|
|
1023
1169
|
disabled: disabled || attachments.length >= maxAttachments,
|
|
1024
1170
|
"aria-label": "Attach files",
|
|
1025
|
-
children: /* @__PURE__ */
|
|
1171
|
+
children: /* @__PURE__ */ jsx5(Paperclip, {
|
|
1026
1172
|
className: "h-4 w-4"
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1173
|
+
})
|
|
1174
|
+
})
|
|
1029
1175
|
]
|
|
1030
|
-
}
|
|
1031
|
-
/* @__PURE__ */
|
|
1176
|
+
}),
|
|
1177
|
+
/* @__PURE__ */ jsx5("div", {
|
|
1032
1178
|
className: "relative flex-1",
|
|
1033
|
-
children: /* @__PURE__ */
|
|
1179
|
+
children: /* @__PURE__ */ jsx5(Textarea, {
|
|
1034
1180
|
value: content,
|
|
1035
1181
|
onChange: (e) => setContent(e.target.value),
|
|
1036
1182
|
onKeyDown: handleKeyDown,
|
|
@@ -1039,32 +1185,418 @@ function ChatInput({
|
|
|
1039
1185
|
className: cn4("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
|
|
1040
1186
|
rows: 1,
|
|
1041
1187
|
"aria-label": "Chat message"
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
/* @__PURE__ */
|
|
1188
|
+
})
|
|
1189
|
+
}),
|
|
1190
|
+
/* @__PURE__ */ jsx5(Button3, {
|
|
1045
1191
|
type: "submit",
|
|
1046
1192
|
disabled: !canSend || disabled || isLoading,
|
|
1047
1193
|
size: "sm",
|
|
1048
1194
|
"aria-label": isLoading ? "Sending..." : "Send message",
|
|
1049
|
-
children: isLoading ? /* @__PURE__ */
|
|
1195
|
+
children: isLoading ? /* @__PURE__ */ jsx5(Loader2, {
|
|
1050
1196
|
className: "h-4 w-4 animate-spin"
|
|
1051
|
-
}
|
|
1197
|
+
}) : /* @__PURE__ */ jsx5(Send, {
|
|
1052
1198
|
className: "h-4 w-4"
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1199
|
+
})
|
|
1200
|
+
})
|
|
1055
1201
|
]
|
|
1056
|
-
}
|
|
1057
|
-
/* @__PURE__ */
|
|
1202
|
+
}),
|
|
1203
|
+
/* @__PURE__ */ jsx5("p", {
|
|
1058
1204
|
className: "text-muted-foreground text-xs",
|
|
1059
1205
|
children: "Press Enter to send, Shift+Enter for new line"
|
|
1060
|
-
}
|
|
1206
|
+
})
|
|
1061
1207
|
]
|
|
1062
|
-
}
|
|
1208
|
+
});
|
|
1063
1209
|
}
|
|
1064
|
-
// src/presentation/components/
|
|
1210
|
+
// src/presentation/components/ChatExportToolbar.tsx
|
|
1065
1211
|
import * as React5 from "react";
|
|
1066
|
-
import {
|
|
1212
|
+
import { Download as Download2, FileText as FileText2, Copy as Copy3, Check as Check3, Plus, GitFork } from "lucide-react";
|
|
1067
1213
|
import { Button as Button4 } from "@contractspec/lib.design-system";
|
|
1214
|
+
import {
|
|
1215
|
+
DropdownMenu,
|
|
1216
|
+
DropdownMenuContent,
|
|
1217
|
+
DropdownMenuItem,
|
|
1218
|
+
DropdownMenuSeparator,
|
|
1219
|
+
DropdownMenuTrigger
|
|
1220
|
+
} from "@contractspec/lib.ui-kit-web/ui/dropdown-menu";
|
|
1221
|
+
|
|
1222
|
+
// src/core/export-formatters.ts
|
|
1223
|
+
function formatTimestamp(date) {
|
|
1224
|
+
return date.toLocaleTimeString([], {
|
|
1225
|
+
hour: "2-digit",
|
|
1226
|
+
minute: "2-digit"
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function toIsoString(date) {
|
|
1230
|
+
return date.toISOString();
|
|
1231
|
+
}
|
|
1232
|
+
function messageToJsonSerializable(msg) {
|
|
1233
|
+
return {
|
|
1234
|
+
id: msg.id,
|
|
1235
|
+
conversationId: msg.conversationId,
|
|
1236
|
+
role: msg.role,
|
|
1237
|
+
content: msg.content,
|
|
1238
|
+
status: msg.status,
|
|
1239
|
+
createdAt: toIsoString(msg.createdAt),
|
|
1240
|
+
updatedAt: toIsoString(msg.updatedAt),
|
|
1241
|
+
...msg.attachments && { attachments: msg.attachments },
|
|
1242
|
+
...msg.codeBlocks && { codeBlocks: msg.codeBlocks },
|
|
1243
|
+
...msg.toolCalls && { toolCalls: msg.toolCalls },
|
|
1244
|
+
...msg.sources && { sources: msg.sources },
|
|
1245
|
+
...msg.reasoning && { reasoning: msg.reasoning },
|
|
1246
|
+
...msg.usage && { usage: msg.usage },
|
|
1247
|
+
...msg.error && { error: msg.error },
|
|
1248
|
+
...msg.metadata && { metadata: msg.metadata }
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function formatSourcesMarkdown(sources) {
|
|
1252
|
+
if (sources.length === 0)
|
|
1253
|
+
return "";
|
|
1254
|
+
return `
|
|
1255
|
+
|
|
1256
|
+
**Sources:**
|
|
1257
|
+
` + sources.map((s) => `- [${s.title}](${s.url ?? "#"})`).join(`
|
|
1258
|
+
`);
|
|
1259
|
+
}
|
|
1260
|
+
function formatSourcesTxt(sources) {
|
|
1261
|
+
if (sources.length === 0)
|
|
1262
|
+
return "";
|
|
1263
|
+
return `
|
|
1264
|
+
|
|
1265
|
+
Sources:
|
|
1266
|
+
` + sources.map((s) => `- ${s.title}${s.url ? ` - ${s.url}` : ""}`).join(`
|
|
1267
|
+
`);
|
|
1268
|
+
}
|
|
1269
|
+
function formatToolCallsMarkdown(toolCalls) {
|
|
1270
|
+
if (toolCalls.length === 0)
|
|
1271
|
+
return "";
|
|
1272
|
+
return `
|
|
1273
|
+
|
|
1274
|
+
**Tool calls:**
|
|
1275
|
+
` + toolCalls.map((tc) => `**${tc.name}** (${tc.status})
|
|
1276
|
+
\`\`\`json
|
|
1277
|
+
${JSON.stringify(tc.args, null, 2)}
|
|
1278
|
+
\`\`\`` + (tc.result !== undefined ? `
|
|
1279
|
+
Output:
|
|
1280
|
+
\`\`\`json
|
|
1281
|
+
${typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)}
|
|
1282
|
+
\`\`\`` : "") + (tc.error ? `
|
|
1283
|
+
Error: ${tc.error}` : "")).join(`
|
|
1284
|
+
|
|
1285
|
+
`);
|
|
1286
|
+
}
|
|
1287
|
+
function formatToolCallsTxt(toolCalls) {
|
|
1288
|
+
if (toolCalls.length === 0)
|
|
1289
|
+
return "";
|
|
1290
|
+
return `
|
|
1291
|
+
|
|
1292
|
+
Tool calls:
|
|
1293
|
+
` + toolCalls.map((tc) => `- ${tc.name} (${tc.status}): ${JSON.stringify(tc.args)}` + (tc.result !== undefined ? ` -> ${typeof tc.result === "object" ? JSON.stringify(tc.result) : String(tc.result)}` : "") + (tc.error ? ` [Error: ${tc.error}]` : "")).join(`
|
|
1294
|
+
`);
|
|
1295
|
+
}
|
|
1296
|
+
function formatUsage(usage) {
|
|
1297
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
1298
|
+
return ` (${total} tokens)`;
|
|
1299
|
+
}
|
|
1300
|
+
function formatMessagesAsMarkdown(messages) {
|
|
1301
|
+
const parts = [];
|
|
1302
|
+
for (const msg of messages) {
|
|
1303
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
|
|
1304
|
+
const header = `## ${roleLabel}`;
|
|
1305
|
+
const timestamp = `*${formatTimestamp(msg.createdAt)}*`;
|
|
1306
|
+
const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
|
|
1307
|
+
const meta = `${timestamp}${usageSuffix}
|
|
1308
|
+
|
|
1309
|
+
`;
|
|
1310
|
+
let body = msg.content;
|
|
1311
|
+
if (msg.error) {
|
|
1312
|
+
body += `
|
|
1313
|
+
|
|
1314
|
+
**Error:** ${msg.error.code} - ${msg.error.message}`;
|
|
1315
|
+
}
|
|
1316
|
+
if (msg.reasoning) {
|
|
1317
|
+
body += `
|
|
1318
|
+
|
|
1319
|
+
> **Reasoning:**
|
|
1320
|
+
> ${msg.reasoning.replace(/\n/g, `
|
|
1321
|
+
> `)}`;
|
|
1322
|
+
}
|
|
1323
|
+
body += formatSourcesMarkdown(msg.sources ?? []);
|
|
1324
|
+
body += formatToolCallsMarkdown(msg.toolCalls ?? []);
|
|
1325
|
+
parts.push(`${header}
|
|
1326
|
+
|
|
1327
|
+
${meta}${body}`);
|
|
1328
|
+
}
|
|
1329
|
+
return parts.join(`
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
`);
|
|
1334
|
+
}
|
|
1335
|
+
function formatMessagesAsTxt(messages) {
|
|
1336
|
+
const parts = [];
|
|
1337
|
+
for (const msg of messages) {
|
|
1338
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
|
|
1339
|
+
const timestamp = `(${formatTimestamp(msg.createdAt)})`;
|
|
1340
|
+
const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
|
|
1341
|
+
const header = `[${roleLabel}] ${timestamp}${usageSuffix}
|
|
1342
|
+
|
|
1343
|
+
`;
|
|
1344
|
+
let body = msg.content;
|
|
1345
|
+
if (msg.error) {
|
|
1346
|
+
body += `
|
|
1347
|
+
|
|
1348
|
+
Error: ${msg.error.code} - ${msg.error.message}`;
|
|
1349
|
+
}
|
|
1350
|
+
if (msg.reasoning) {
|
|
1351
|
+
body += `
|
|
1352
|
+
|
|
1353
|
+
Reasoning: ${msg.reasoning}`;
|
|
1354
|
+
}
|
|
1355
|
+
body += formatSourcesTxt(msg.sources ?? []);
|
|
1356
|
+
body += formatToolCallsTxt(msg.toolCalls ?? []);
|
|
1357
|
+
parts.push(`${header}${body}`);
|
|
1358
|
+
}
|
|
1359
|
+
return parts.join(`
|
|
1360
|
+
|
|
1361
|
+
---
|
|
1362
|
+
|
|
1363
|
+
`);
|
|
1364
|
+
}
|
|
1365
|
+
function formatMessagesAsJson(messages, conversation) {
|
|
1366
|
+
const payload = {
|
|
1367
|
+
messages: messages.map(messageToJsonSerializable)
|
|
1368
|
+
};
|
|
1369
|
+
if (conversation) {
|
|
1370
|
+
payload.conversation = {
|
|
1371
|
+
id: conversation.id,
|
|
1372
|
+
title: conversation.title,
|
|
1373
|
+
status: conversation.status,
|
|
1374
|
+
createdAt: toIsoString(conversation.createdAt),
|
|
1375
|
+
updatedAt: toIsoString(conversation.updatedAt),
|
|
1376
|
+
provider: conversation.provider,
|
|
1377
|
+
model: conversation.model,
|
|
1378
|
+
workspacePath: conversation.workspacePath,
|
|
1379
|
+
contextFiles: conversation.contextFiles,
|
|
1380
|
+
summary: conversation.summary,
|
|
1381
|
+
metadata: conversation.metadata
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
return JSON.stringify(payload, null, 2);
|
|
1385
|
+
}
|
|
1386
|
+
function getExportFilename(format, conversation) {
|
|
1387
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1388
|
+
const base = conversation?.title ? conversation.title.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 40) : "chat-export";
|
|
1389
|
+
const ext = format === "markdown" ? "md" : format === "txt" ? "txt" : "json";
|
|
1390
|
+
return `${base}-${timestamp}.${ext}`;
|
|
1391
|
+
}
|
|
1392
|
+
var MIME_TYPES = {
|
|
1393
|
+
markdown: "text/markdown",
|
|
1394
|
+
txt: "text/plain",
|
|
1395
|
+
json: "application/json"
|
|
1396
|
+
};
|
|
1397
|
+
function downloadAsFile(content, filename, mimeType) {
|
|
1398
|
+
const blob = new Blob([content], { type: mimeType });
|
|
1399
|
+
const url = URL.createObjectURL(blob);
|
|
1400
|
+
const a = document.createElement("a");
|
|
1401
|
+
a.href = url;
|
|
1402
|
+
a.download = filename;
|
|
1403
|
+
document.body.appendChild(a);
|
|
1404
|
+
a.click();
|
|
1405
|
+
document.body.removeChild(a);
|
|
1406
|
+
URL.revokeObjectURL(url);
|
|
1407
|
+
}
|
|
1408
|
+
function exportToFile(messages, format, conversation) {
|
|
1409
|
+
let content;
|
|
1410
|
+
if (format === "markdown") {
|
|
1411
|
+
content = formatMessagesAsMarkdown(messages);
|
|
1412
|
+
} else if (format === "txt") {
|
|
1413
|
+
content = formatMessagesAsTxt(messages);
|
|
1414
|
+
} else {
|
|
1415
|
+
content = formatMessagesAsJson(messages, conversation);
|
|
1416
|
+
}
|
|
1417
|
+
const filename = getExportFilename(format, conversation);
|
|
1418
|
+
const mimeType = MIME_TYPES[format];
|
|
1419
|
+
downloadAsFile(content, filename, mimeType);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/presentation/components/ChatExportToolbar.tsx
|
|
1423
|
+
import { jsx as jsx6, jsxs as jsxs6, Fragment as Fragment3 } from "react/jsx-runtime";
|
|
1424
|
+
"use client";
|
|
1425
|
+
function ChatExportToolbar({
|
|
1426
|
+
messages,
|
|
1427
|
+
conversation,
|
|
1428
|
+
selectedIds,
|
|
1429
|
+
onExported,
|
|
1430
|
+
showSelectionSummary = true,
|
|
1431
|
+
onSelectAll,
|
|
1432
|
+
onClearSelection,
|
|
1433
|
+
selectedCount = selectedIds.size,
|
|
1434
|
+
totalCount = messages.length,
|
|
1435
|
+
onCreateNew,
|
|
1436
|
+
onFork
|
|
1437
|
+
}) {
|
|
1438
|
+
const [copied, setCopied] = React5.useState(false);
|
|
1439
|
+
const toExport = React5.useMemo(() => {
|
|
1440
|
+
if (selectedIds.size > 0) {
|
|
1441
|
+
const idSet = selectedIds;
|
|
1442
|
+
return messages.filter((m) => idSet.has(m.id));
|
|
1443
|
+
}
|
|
1444
|
+
return messages;
|
|
1445
|
+
}, [messages, selectedIds]);
|
|
1446
|
+
const handleExport = React5.useCallback((format) => {
|
|
1447
|
+
exportToFile(toExport, format, conversation);
|
|
1448
|
+
onExported?.(format, toExport.length);
|
|
1449
|
+
}, [toExport, conversation, onExported]);
|
|
1450
|
+
const handleCopy = React5.useCallback(async () => {
|
|
1451
|
+
const content = formatMessagesAsMarkdown(toExport);
|
|
1452
|
+
await navigator.clipboard.writeText(content);
|
|
1453
|
+
setCopied(true);
|
|
1454
|
+
setTimeout(() => setCopied(false), 2000);
|
|
1455
|
+
onExported?.("markdown", toExport.length);
|
|
1456
|
+
}, [toExport, onExported]);
|
|
1457
|
+
const disabled = messages.length === 0;
|
|
1458
|
+
const [forking, setForking] = React5.useState(false);
|
|
1459
|
+
const handleFork = React5.useCallback(async (upToMessageId) => {
|
|
1460
|
+
if (!onFork)
|
|
1461
|
+
return;
|
|
1462
|
+
setForking(true);
|
|
1463
|
+
try {
|
|
1464
|
+
await onFork(upToMessageId);
|
|
1465
|
+
} finally {
|
|
1466
|
+
setForking(false);
|
|
1467
|
+
}
|
|
1468
|
+
}, [onFork]);
|
|
1469
|
+
return /* @__PURE__ */ jsxs6("div", {
|
|
1470
|
+
className: "flex items-center gap-2",
|
|
1471
|
+
children: [
|
|
1472
|
+
onCreateNew && /* @__PURE__ */ jsxs6(Button4, {
|
|
1473
|
+
variant: "outline",
|
|
1474
|
+
size: "sm",
|
|
1475
|
+
onPress: onCreateNew,
|
|
1476
|
+
"aria-label": "New conversation",
|
|
1477
|
+
children: [
|
|
1478
|
+
/* @__PURE__ */ jsx6(Plus, {
|
|
1479
|
+
className: "h-4 w-4"
|
|
1480
|
+
}),
|
|
1481
|
+
"New"
|
|
1482
|
+
]
|
|
1483
|
+
}),
|
|
1484
|
+
onFork && messages.length > 0 && /* @__PURE__ */ jsxs6(Button4, {
|
|
1485
|
+
variant: "outline",
|
|
1486
|
+
size: "sm",
|
|
1487
|
+
disabled: forking,
|
|
1488
|
+
onPress: () => handleFork(),
|
|
1489
|
+
"aria-label": "Fork conversation",
|
|
1490
|
+
children: [
|
|
1491
|
+
/* @__PURE__ */ jsx6(GitFork, {
|
|
1492
|
+
className: "h-4 w-4"
|
|
1493
|
+
}),
|
|
1494
|
+
"Fork"
|
|
1495
|
+
]
|
|
1496
|
+
}),
|
|
1497
|
+
showSelectionSummary && selectedCount > 0 && /* @__PURE__ */ jsxs6("span", {
|
|
1498
|
+
className: "text-muted-foreground text-sm",
|
|
1499
|
+
children: [
|
|
1500
|
+
selectedCount,
|
|
1501
|
+
" message",
|
|
1502
|
+
selectedCount !== 1 ? "s" : "",
|
|
1503
|
+
" selected"
|
|
1504
|
+
]
|
|
1505
|
+
}),
|
|
1506
|
+
onSelectAll && onClearSelection && totalCount > 0 && /* @__PURE__ */ jsxs6(Fragment3, {
|
|
1507
|
+
children: [
|
|
1508
|
+
/* @__PURE__ */ jsx6(Button4, {
|
|
1509
|
+
variant: "ghost",
|
|
1510
|
+
size: "sm",
|
|
1511
|
+
onPress: onSelectAll,
|
|
1512
|
+
className: "text-xs",
|
|
1513
|
+
children: "Select all"
|
|
1514
|
+
}),
|
|
1515
|
+
selectedCount > 0 && /* @__PURE__ */ jsx6(Button4, {
|
|
1516
|
+
variant: "ghost",
|
|
1517
|
+
size: "sm",
|
|
1518
|
+
onPress: onClearSelection,
|
|
1519
|
+
className: "text-xs",
|
|
1520
|
+
children: "Clear"
|
|
1521
|
+
})
|
|
1522
|
+
]
|
|
1523
|
+
}),
|
|
1524
|
+
/* @__PURE__ */ jsxs6(DropdownMenu, {
|
|
1525
|
+
children: [
|
|
1526
|
+
/* @__PURE__ */ jsx6(DropdownMenuTrigger, {
|
|
1527
|
+
asChild: true,
|
|
1528
|
+
children: /* @__PURE__ */ jsxs6(Button4, {
|
|
1529
|
+
variant: "outline",
|
|
1530
|
+
size: "sm",
|
|
1531
|
+
disabled,
|
|
1532
|
+
"aria-label": selectedCount > 0 ? "Export selected messages" : "Export conversation",
|
|
1533
|
+
children: [
|
|
1534
|
+
/* @__PURE__ */ jsx6(Download2, {
|
|
1535
|
+
className: "h-4 w-4"
|
|
1536
|
+
}),
|
|
1537
|
+
"Export"
|
|
1538
|
+
]
|
|
1539
|
+
})
|
|
1540
|
+
}),
|
|
1541
|
+
/* @__PURE__ */ jsxs6(DropdownMenuContent, {
|
|
1542
|
+
align: "end",
|
|
1543
|
+
children: [
|
|
1544
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1545
|
+
onSelect: () => handleExport("markdown"),
|
|
1546
|
+
disabled,
|
|
1547
|
+
children: [
|
|
1548
|
+
/* @__PURE__ */ jsx6(FileText2, {
|
|
1549
|
+
className: "h-4 w-4"
|
|
1550
|
+
}),
|
|
1551
|
+
"Export as Markdown (.md)"
|
|
1552
|
+
]
|
|
1553
|
+
}),
|
|
1554
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1555
|
+
onSelect: () => handleExport("txt"),
|
|
1556
|
+
disabled,
|
|
1557
|
+
children: [
|
|
1558
|
+
/* @__PURE__ */ jsx6(FileText2, {
|
|
1559
|
+
className: "h-4 w-4"
|
|
1560
|
+
}),
|
|
1561
|
+
"Export as Plain Text (.txt)"
|
|
1562
|
+
]
|
|
1563
|
+
}),
|
|
1564
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1565
|
+
onSelect: () => handleExport("json"),
|
|
1566
|
+
disabled,
|
|
1567
|
+
children: [
|
|
1568
|
+
/* @__PURE__ */ jsx6(FileText2, {
|
|
1569
|
+
className: "h-4 w-4"
|
|
1570
|
+
}),
|
|
1571
|
+
"Export as JSON (.json)"
|
|
1572
|
+
]
|
|
1573
|
+
}),
|
|
1574
|
+
/* @__PURE__ */ jsx6(DropdownMenuSeparator, {}),
|
|
1575
|
+
/* @__PURE__ */ jsxs6(DropdownMenuItem, {
|
|
1576
|
+
onSelect: () => handleCopy(),
|
|
1577
|
+
disabled,
|
|
1578
|
+
children: [
|
|
1579
|
+
copied ? /* @__PURE__ */ jsx6(Check3, {
|
|
1580
|
+
className: "h-4 w-4 text-green-500"
|
|
1581
|
+
}) : /* @__PURE__ */ jsx6(Copy3, {
|
|
1582
|
+
className: "h-4 w-4"
|
|
1583
|
+
}),
|
|
1584
|
+
copied ? "Copied to clipboard" : "Copy to clipboard"
|
|
1585
|
+
]
|
|
1586
|
+
})
|
|
1587
|
+
]
|
|
1588
|
+
})
|
|
1589
|
+
]
|
|
1590
|
+
})
|
|
1591
|
+
]
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
// src/presentation/components/ChatWithExport.tsx
|
|
1595
|
+
import * as React8 from "react";
|
|
1596
|
+
|
|
1597
|
+
// src/presentation/components/ThinkingLevelPicker.tsx
|
|
1598
|
+
import * as React6 from "react";
|
|
1599
|
+
import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
1068
1600
|
import {
|
|
1069
1601
|
Select,
|
|
1070
1602
|
SelectContent,
|
|
@@ -1072,402 +1604,487 @@ import {
|
|
|
1072
1604
|
SelectTrigger,
|
|
1073
1605
|
SelectValue
|
|
1074
1606
|
} from "@contractspec/lib.ui-kit-web/ui/select";
|
|
1075
|
-
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
1076
1607
|
import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
"
|
|
1083
|
-
|
|
1084
|
-
ollama: /* @__PURE__ */ jsxDEV5(Cpu, {
|
|
1085
|
-
className: "h-4 w-4"
|
|
1086
|
-
}, undefined, false, undefined, this),
|
|
1087
|
-
openai: /* @__PURE__ */ jsxDEV5(Bot2, {
|
|
1088
|
-
className: "h-4 w-4"
|
|
1089
|
-
}, undefined, false, undefined, this),
|
|
1090
|
-
anthropic: /* @__PURE__ */ jsxDEV5(Sparkles, {
|
|
1091
|
-
className: "h-4 w-4"
|
|
1092
|
-
}, undefined, false, undefined, this),
|
|
1093
|
-
mistral: /* @__PURE__ */ jsxDEV5(Cloud, {
|
|
1094
|
-
className: "h-4 w-4"
|
|
1095
|
-
}, undefined, false, undefined, this),
|
|
1096
|
-
gemini: /* @__PURE__ */ jsxDEV5(Sparkles, {
|
|
1097
|
-
className: "h-4 w-4"
|
|
1098
|
-
}, undefined, false, undefined, this)
|
|
1099
|
-
};
|
|
1100
|
-
var PROVIDER_NAMES = {
|
|
1101
|
-
ollama: "Ollama (Local)",
|
|
1102
|
-
openai: "OpenAI",
|
|
1103
|
-
anthropic: "Anthropic",
|
|
1104
|
-
mistral: "Mistral",
|
|
1105
|
-
gemini: "Google Gemini"
|
|
1608
|
+
|
|
1609
|
+
// src/core/thinking-levels.ts
|
|
1610
|
+
var THINKING_LEVEL_LABELS = {
|
|
1611
|
+
instant: "Instant",
|
|
1612
|
+
thinking: "Thinking",
|
|
1613
|
+
extra_thinking: "Extra Thinking",
|
|
1614
|
+
max: "Max"
|
|
1106
1615
|
};
|
|
1107
|
-
var
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1616
|
+
var THINKING_LEVEL_DESCRIPTIONS = {
|
|
1617
|
+
instant: "Fast responses, minimal reasoning",
|
|
1618
|
+
thinking: "Standard reasoning depth",
|
|
1619
|
+
extra_thinking: "More thorough reasoning",
|
|
1620
|
+
max: "Maximum reasoning depth"
|
|
1111
1621
|
};
|
|
1112
|
-
function
|
|
1622
|
+
function getProviderOptions(level, providerName) {
|
|
1623
|
+
if (!level || level === "instant") {
|
|
1624
|
+
return {};
|
|
1625
|
+
}
|
|
1626
|
+
switch (providerName) {
|
|
1627
|
+
case "anthropic": {
|
|
1628
|
+
const budgetMap = {
|
|
1629
|
+
thinking: 8000,
|
|
1630
|
+
extra_thinking: 16000,
|
|
1631
|
+
max: 32000
|
|
1632
|
+
};
|
|
1633
|
+
return {
|
|
1634
|
+
anthropic: {
|
|
1635
|
+
thinking: { type: "enabled", budgetTokens: budgetMap[level] }
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
case "openai": {
|
|
1640
|
+
const effortMap = {
|
|
1641
|
+
thinking: "low",
|
|
1642
|
+
extra_thinking: "medium",
|
|
1643
|
+
max: "high"
|
|
1644
|
+
};
|
|
1645
|
+
return {
|
|
1646
|
+
openai: {
|
|
1647
|
+
reasoningEffort: effortMap[level]
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
case "ollama":
|
|
1652
|
+
case "mistral":
|
|
1653
|
+
case "gemini":
|
|
1654
|
+
return {};
|
|
1655
|
+
default:
|
|
1656
|
+
return {};
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/presentation/components/ThinkingLevelPicker.tsx
|
|
1661
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1662
|
+
"use client";
|
|
1663
|
+
var THINKING_LEVELS = [
|
|
1664
|
+
"instant",
|
|
1665
|
+
"thinking",
|
|
1666
|
+
"extra_thinking",
|
|
1667
|
+
"max"
|
|
1668
|
+
];
|
|
1669
|
+
function ThinkingLevelPicker({
|
|
1113
1670
|
value,
|
|
1114
1671
|
onChange,
|
|
1115
|
-
availableProviders,
|
|
1116
1672
|
className,
|
|
1117
1673
|
compact = false
|
|
1118
1674
|
}) {
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
{ provider: "anthropic", available: true, mode: "byok" },
|
|
1123
|
-
{ provider: "mistral", available: true, mode: "byok" },
|
|
1124
|
-
{ provider: "gemini", available: true, mode: "byok" }
|
|
1125
|
-
];
|
|
1126
|
-
const models = getModelsForProvider(value.provider);
|
|
1127
|
-
const selectedModel = models.find((m) => m.id === value.model);
|
|
1128
|
-
const handleProviderChange = React5.useCallback((providerName) => {
|
|
1129
|
-
const provider = providerName;
|
|
1130
|
-
const providerInfo = providers.find((p) => p.provider === provider);
|
|
1131
|
-
const providerModels = getModelsForProvider(provider);
|
|
1132
|
-
const defaultModel = providerModels[0]?.id ?? "";
|
|
1133
|
-
onChange({
|
|
1134
|
-
provider,
|
|
1135
|
-
model: defaultModel,
|
|
1136
|
-
mode: providerInfo?.mode ?? "byok"
|
|
1137
|
-
});
|
|
1138
|
-
}, [onChange, providers]);
|
|
1139
|
-
const handleModelChange = React5.useCallback((modelId) => {
|
|
1140
|
-
onChange({
|
|
1141
|
-
...value,
|
|
1142
|
-
model: modelId
|
|
1143
|
-
});
|
|
1144
|
-
}, [onChange, value]);
|
|
1675
|
+
const handleChange = React6.useCallback((v) => {
|
|
1676
|
+
onChange(v);
|
|
1677
|
+
}, [onChange]);
|
|
1145
1678
|
if (compact) {
|
|
1146
|
-
return /* @__PURE__ */
|
|
1147
|
-
|
|
1679
|
+
return /* @__PURE__ */ jsxs7(Select, {
|
|
1680
|
+
value,
|
|
1681
|
+
onValueChange: handleChange,
|
|
1148
1682
|
children: [
|
|
1149
|
-
/* @__PURE__ */
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
value: p.provider,
|
|
1160
|
-
disabled: !p.available,
|
|
1161
|
-
children: /* @__PURE__ */ jsxDEV5("div", {
|
|
1162
|
-
className: "flex items-center gap-2",
|
|
1163
|
-
children: [
|
|
1164
|
-
PROVIDER_ICONS[p.provider],
|
|
1165
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
1166
|
-
children: PROVIDER_NAMES[p.provider]
|
|
1167
|
-
}, undefined, false, undefined, this)
|
|
1168
|
-
]
|
|
1169
|
-
}, undefined, true, undefined, this)
|
|
1170
|
-
}, p.provider, false, undefined, this))
|
|
1171
|
-
}, undefined, false, undefined, this)
|
|
1172
|
-
]
|
|
1173
|
-
}, undefined, true, undefined, this),
|
|
1174
|
-
/* @__PURE__ */ jsxDEV5(Select, {
|
|
1175
|
-
value: value.model,
|
|
1176
|
-
onValueChange: handleModelChange,
|
|
1177
|
-
children: [
|
|
1178
|
-
/* @__PURE__ */ jsxDEV5(SelectTrigger, {
|
|
1179
|
-
className: "w-[160px]",
|
|
1180
|
-
children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
|
|
1181
|
-
}, undefined, false, undefined, this),
|
|
1182
|
-
/* @__PURE__ */ jsxDEV5(SelectContent, {
|
|
1183
|
-
children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
|
|
1184
|
-
value: m.id,
|
|
1185
|
-
children: m.name
|
|
1186
|
-
}, m.id, false, undefined, this))
|
|
1187
|
-
}, undefined, false, undefined, this)
|
|
1188
|
-
]
|
|
1189
|
-
}, undefined, true, undefined, this)
|
|
1683
|
+
/* @__PURE__ */ jsx7(SelectTrigger, {
|
|
1684
|
+
className: cn5("w-[140px]", className),
|
|
1685
|
+
children: /* @__PURE__ */ jsx7(SelectValue, {})
|
|
1686
|
+
}),
|
|
1687
|
+
/* @__PURE__ */ jsx7(SelectContent, {
|
|
1688
|
+
children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
|
|
1689
|
+
value: level,
|
|
1690
|
+
children: THINKING_LEVEL_LABELS[level]
|
|
1691
|
+
}, level))
|
|
1692
|
+
})
|
|
1190
1693
|
]
|
|
1191
|
-
}
|
|
1694
|
+
});
|
|
1192
1695
|
}
|
|
1193
|
-
return /* @__PURE__ */
|
|
1194
|
-
className: cn5("flex flex-col gap-
|
|
1696
|
+
return /* @__PURE__ */ jsxs7("div", {
|
|
1697
|
+
className: cn5("flex flex-col gap-1.5", className),
|
|
1195
1698
|
children: [
|
|
1196
|
-
/* @__PURE__ */
|
|
1197
|
-
|
|
1699
|
+
/* @__PURE__ */ jsx7(Label, {
|
|
1700
|
+
htmlFor: "thinking-level-picker",
|
|
1701
|
+
className: "text-sm font-medium",
|
|
1702
|
+
children: "Thinking Level"
|
|
1703
|
+
}),
|
|
1704
|
+
/* @__PURE__ */ jsxs7(Select, {
|
|
1705
|
+
name: "thinking-level-picker",
|
|
1706
|
+
value,
|
|
1707
|
+
onValueChange: handleChange,
|
|
1198
1708
|
children: [
|
|
1199
|
-
/* @__PURE__ */
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1204
|
-
/* @__PURE__ */
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
disabled: !p.available,
|
|
1212
|
-
className: cn5(!p.available && "opacity-50"),
|
|
1213
|
-
children: [
|
|
1214
|
-
PROVIDER_ICONS[p.provider],
|
|
1215
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
1216
|
-
children: PROVIDER_NAMES[p.provider]
|
|
1217
|
-
}, undefined, false, undefined, this),
|
|
1218
|
-
/* @__PURE__ */ jsxDEV5(Badge, {
|
|
1219
|
-
variant: MODE_BADGES[p.mode].variant,
|
|
1220
|
-
className: "ml-1",
|
|
1221
|
-
children: MODE_BADGES[p.mode].label
|
|
1222
|
-
}, undefined, false, undefined, this)
|
|
1223
|
-
]
|
|
1224
|
-
}, p.provider, true, undefined, this))
|
|
1225
|
-
}, undefined, false, undefined, this)
|
|
1226
|
-
]
|
|
1227
|
-
}, undefined, true, undefined, this),
|
|
1228
|
-
/* @__PURE__ */ jsxDEV5("div", {
|
|
1229
|
-
className: "flex flex-col gap-1.5",
|
|
1230
|
-
children: [
|
|
1231
|
-
/* @__PURE__ */ jsxDEV5(Label, {
|
|
1232
|
-
htmlFor: "model-picker",
|
|
1233
|
-
className: "text-sm font-medium",
|
|
1234
|
-
children: "Model"
|
|
1235
|
-
}, undefined, false, undefined, this),
|
|
1236
|
-
/* @__PURE__ */ jsxDEV5(Select, {
|
|
1237
|
-
name: "model-picker",
|
|
1238
|
-
value: value.model,
|
|
1239
|
-
onValueChange: handleModelChange,
|
|
1240
|
-
children: [
|
|
1241
|
-
/* @__PURE__ */ jsxDEV5(SelectTrigger, {
|
|
1242
|
-
children: /* @__PURE__ */ jsxDEV5(SelectValue, {
|
|
1243
|
-
placeholder: "Select a model"
|
|
1244
|
-
}, undefined, false, undefined, this)
|
|
1245
|
-
}, undefined, false, undefined, this),
|
|
1246
|
-
/* @__PURE__ */ jsxDEV5(SelectContent, {
|
|
1247
|
-
children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
|
|
1248
|
-
value: m.id,
|
|
1249
|
-
children: /* @__PURE__ */ jsxDEV5("div", {
|
|
1250
|
-
className: "flex items-center gap-2",
|
|
1251
|
-
children: [
|
|
1252
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
1253
|
-
children: m.name
|
|
1254
|
-
}, undefined, false, undefined, this),
|
|
1255
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
1256
|
-
className: "text-muted-foreground text-xs",
|
|
1257
|
-
children: [
|
|
1258
|
-
Math.round(m.contextWindow / 1000),
|
|
1259
|
-
"K"
|
|
1260
|
-
]
|
|
1261
|
-
}, undefined, true, undefined, this),
|
|
1262
|
-
m.capabilities.vision && /* @__PURE__ */ jsxDEV5(Badge, {
|
|
1263
|
-
variant: "outline",
|
|
1264
|
-
className: "text-xs",
|
|
1265
|
-
children: "Vision"
|
|
1266
|
-
}, undefined, false, undefined, this),
|
|
1267
|
-
m.capabilities.reasoning && /* @__PURE__ */ jsxDEV5(Badge, {
|
|
1268
|
-
variant: "outline",
|
|
1269
|
-
className: "text-xs",
|
|
1270
|
-
children: "Reasoning"
|
|
1271
|
-
}, undefined, false, undefined, this)
|
|
1272
|
-
]
|
|
1273
|
-
}, undefined, true, undefined, this)
|
|
1274
|
-
}, m.id, false, undefined, this))
|
|
1275
|
-
}, undefined, false, undefined, this)
|
|
1276
|
-
]
|
|
1277
|
-
}, undefined, true, undefined, this)
|
|
1278
|
-
]
|
|
1279
|
-
}, undefined, true, undefined, this),
|
|
1280
|
-
selectedModel && /* @__PURE__ */ jsxDEV5("div", {
|
|
1281
|
-
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
1282
|
-
children: [
|
|
1283
|
-
/* @__PURE__ */ jsxDEV5("span", {
|
|
1284
|
-
children: [
|
|
1285
|
-
"Context: ",
|
|
1286
|
-
Math.round(selectedModel.contextWindow / 1000),
|
|
1287
|
-
"K tokens"
|
|
1288
|
-
]
|
|
1289
|
-
}, undefined, true, undefined, this),
|
|
1290
|
-
selectedModel.capabilities.vision && /* @__PURE__ */ jsxDEV5("span", {
|
|
1291
|
-
children: "\u2022 Vision"
|
|
1292
|
-
}, undefined, false, undefined, this),
|
|
1293
|
-
selectedModel.capabilities.tools && /* @__PURE__ */ jsxDEV5("span", {
|
|
1294
|
-
children: "\u2022 Tools"
|
|
1295
|
-
}, undefined, false, undefined, this),
|
|
1296
|
-
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsxDEV5("span", {
|
|
1297
|
-
children: "\u2022 Reasoning"
|
|
1298
|
-
}, undefined, false, undefined, this)
|
|
1709
|
+
/* @__PURE__ */ jsx7(SelectTrigger, {
|
|
1710
|
+
children: /* @__PURE__ */ jsx7(SelectValue, {
|
|
1711
|
+
placeholder: "Select thinking level"
|
|
1712
|
+
})
|
|
1713
|
+
}),
|
|
1714
|
+
/* @__PURE__ */ jsx7(SelectContent, {
|
|
1715
|
+
children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
|
|
1716
|
+
value: level,
|
|
1717
|
+
title: THINKING_LEVEL_DESCRIPTIONS[level],
|
|
1718
|
+
children: THINKING_LEVEL_LABELS[level]
|
|
1719
|
+
}, level))
|
|
1720
|
+
})
|
|
1299
1721
|
]
|
|
1300
|
-
}
|
|
1722
|
+
})
|
|
1301
1723
|
]
|
|
1302
|
-
}
|
|
1724
|
+
});
|
|
1303
1725
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
import
|
|
1307
|
-
import {
|
|
1308
|
-
Tooltip,
|
|
1309
|
-
TooltipContent,
|
|
1310
|
-
TooltipProvider,
|
|
1311
|
-
TooltipTrigger
|
|
1312
|
-
} from "@contractspec/lib.ui-kit-web/ui/tooltip";
|
|
1313
|
-
import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
|
|
1314
|
-
import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
|
|
1726
|
+
|
|
1727
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1728
|
+
import * as React7 from "react";
|
|
1315
1729
|
"use client";
|
|
1316
|
-
function
|
|
1317
|
-
|
|
1318
|
-
|
|
1730
|
+
function useMessageSelection(messageIds) {
|
|
1731
|
+
const [selectedIds, setSelectedIds] = React7.useState(() => new Set);
|
|
1732
|
+
const idSet = React7.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1733
|
+
React7.useEffect(() => {
|
|
1734
|
+
setSelectedIds((prev) => {
|
|
1735
|
+
const next = new Set;
|
|
1736
|
+
for (const id of prev) {
|
|
1737
|
+
if (idSet.has(id))
|
|
1738
|
+
next.add(id);
|
|
1739
|
+
}
|
|
1740
|
+
return next.size === prev.size ? prev : next;
|
|
1741
|
+
});
|
|
1742
|
+
}, [idSet]);
|
|
1743
|
+
const toggle = React7.useCallback((id) => {
|
|
1744
|
+
setSelectedIds((prev) => {
|
|
1745
|
+
const next = new Set(prev);
|
|
1746
|
+
if (next.has(id))
|
|
1747
|
+
next.delete(id);
|
|
1748
|
+
else
|
|
1749
|
+
next.add(id);
|
|
1750
|
+
return next;
|
|
1751
|
+
});
|
|
1752
|
+
}, []);
|
|
1753
|
+
const selectAll = React7.useCallback(() => {
|
|
1754
|
+
setSelectedIds(new Set(messageIds));
|
|
1755
|
+
}, [messageIds.join(",")]);
|
|
1756
|
+
const clearSelection = React7.useCallback(() => {
|
|
1757
|
+
setSelectedIds(new Set);
|
|
1758
|
+
}, []);
|
|
1759
|
+
const isSelected = React7.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1760
|
+
const selectedCount = selectedIds.size;
|
|
1761
|
+
return {
|
|
1762
|
+
selectedIds,
|
|
1763
|
+
toggle,
|
|
1764
|
+
selectAll,
|
|
1765
|
+
clearSelection,
|
|
1766
|
+
isSelected,
|
|
1767
|
+
selectedCount
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// src/presentation/components/ChatWithExport.tsx
|
|
1772
|
+
import { jsx as jsx8, jsxs as jsxs8, Fragment as Fragment4 } from "react/jsx-runtime";
|
|
1773
|
+
"use client";
|
|
1774
|
+
function ChatWithExport({
|
|
1775
|
+
messages,
|
|
1776
|
+
conversation,
|
|
1777
|
+
children,
|
|
1319
1778
|
className,
|
|
1320
|
-
|
|
1779
|
+
showExport = true,
|
|
1780
|
+
showMessageSelection = true,
|
|
1781
|
+
showScrollButton = true,
|
|
1782
|
+
onCreateNew,
|
|
1783
|
+
onFork,
|
|
1784
|
+
onEditMessage,
|
|
1785
|
+
thinkingLevel = "thinking",
|
|
1786
|
+
onThinkingLevelChange,
|
|
1787
|
+
presentationRenderer,
|
|
1788
|
+
formRenderer
|
|
1321
1789
|
}) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1790
|
+
const messageIds = React8.useMemo(() => messages.map((m) => m.id), [messages]);
|
|
1791
|
+
const selection = useMessageSelection(messageIds);
|
|
1792
|
+
const hasToolbar = showExport || showMessageSelection;
|
|
1793
|
+
const hasPicker = Boolean(onThinkingLevelChange);
|
|
1794
|
+
const headerContent = hasPicker || hasToolbar ? /* @__PURE__ */ jsxs8(Fragment4, {
|
|
1795
|
+
children: [
|
|
1796
|
+
hasPicker && /* @__PURE__ */ jsx8(ThinkingLevelPicker, {
|
|
1797
|
+
value: thinkingLevel,
|
|
1798
|
+
onChange: onThinkingLevelChange,
|
|
1799
|
+
compact: true
|
|
1800
|
+
}),
|
|
1801
|
+
hasToolbar && /* @__PURE__ */ jsx8(ChatExportToolbar, {
|
|
1802
|
+
messages,
|
|
1803
|
+
conversation,
|
|
1804
|
+
selectedIds: selection.selectedIds,
|
|
1805
|
+
showSelectionSummary: showMessageSelection,
|
|
1806
|
+
onSelectAll: showMessageSelection ? selection.selectAll : undefined,
|
|
1807
|
+
onClearSelection: showMessageSelection ? selection.clearSelection : undefined,
|
|
1808
|
+
selectedCount: selection.selectedCount,
|
|
1809
|
+
totalCount: messages.length,
|
|
1810
|
+
onCreateNew,
|
|
1811
|
+
onFork
|
|
1812
|
+
})
|
|
1813
|
+
]
|
|
1814
|
+
}) : null;
|
|
1815
|
+
return /* @__PURE__ */ jsxs8(ChatContainer, {
|
|
1816
|
+
className,
|
|
1817
|
+
headerContent,
|
|
1818
|
+
showScrollButton,
|
|
1819
|
+
children: [
|
|
1820
|
+
messages.map((msg) => /* @__PURE__ */ jsx8(ChatMessage, {
|
|
1821
|
+
message: msg,
|
|
1822
|
+
selectable: showMessageSelection,
|
|
1823
|
+
selected: selection.isSelected(msg.id),
|
|
1824
|
+
onSelect: showMessageSelection ? selection.toggle : undefined,
|
|
1825
|
+
editable: msg.role === "user" && !!onEditMessage,
|
|
1826
|
+
onEdit: onEditMessage,
|
|
1827
|
+
presentationRenderer,
|
|
1828
|
+
formRenderer
|
|
1829
|
+
}, msg.id)),
|
|
1830
|
+
children
|
|
1831
|
+
]
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
// src/presentation/components/ChatSidebar.tsx
|
|
1835
|
+
import * as React10 from "react";
|
|
1836
|
+
import { Plus as Plus2, Trash2, MessageSquare } from "lucide-react";
|
|
1837
|
+
import { Button as Button5 } from "@contractspec/lib.design-system";
|
|
1838
|
+
import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
1839
|
+
|
|
1840
|
+
// src/presentation/hooks/useConversations.ts
|
|
1841
|
+
import * as React9 from "react";
|
|
1842
|
+
"use client";
|
|
1843
|
+
function useConversations(options) {
|
|
1844
|
+
const { store, projectId, tags, limit = 50 } = options;
|
|
1845
|
+
const [conversations, setConversations] = React9.useState([]);
|
|
1846
|
+
const [isLoading, setIsLoading] = React9.useState(true);
|
|
1847
|
+
const refresh = React9.useCallback(async () => {
|
|
1848
|
+
setIsLoading(true);
|
|
1849
|
+
try {
|
|
1850
|
+
const list = await store.list({
|
|
1851
|
+
status: "active",
|
|
1852
|
+
projectId,
|
|
1853
|
+
tags,
|
|
1854
|
+
limit
|
|
1855
|
+
});
|
|
1856
|
+
setConversations(list);
|
|
1857
|
+
} finally {
|
|
1858
|
+
setIsLoading(false);
|
|
1859
|
+
}
|
|
1860
|
+
}, [store, projectId, tags, limit]);
|
|
1861
|
+
React9.useEffect(() => {
|
|
1862
|
+
refresh();
|
|
1863
|
+
}, [refresh]);
|
|
1864
|
+
const deleteConversation = React9.useCallback(async (id) => {
|
|
1865
|
+
const ok = await store.delete(id);
|
|
1866
|
+
if (ok) {
|
|
1867
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1868
|
+
}
|
|
1869
|
+
return ok;
|
|
1870
|
+
}, [store]);
|
|
1871
|
+
return {
|
|
1872
|
+
conversations,
|
|
1873
|
+
isLoading,
|
|
1874
|
+
refresh,
|
|
1875
|
+
deleteConversation
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/presentation/components/ChatSidebar.tsx
|
|
1880
|
+
import { jsx as jsx9, jsxs as jsxs9, Fragment as Fragment5 } from "react/jsx-runtime";
|
|
1881
|
+
"use client";
|
|
1882
|
+
function formatDate(date) {
|
|
1883
|
+
const d = new Date(date);
|
|
1884
|
+
const now = new Date;
|
|
1885
|
+
const diff = now.getTime() - d.getTime();
|
|
1886
|
+
if (diff < 86400000) {
|
|
1887
|
+
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1334
1888
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1889
|
+
if (diff < 604800000) {
|
|
1890
|
+
return d.toLocaleDateString([], { weekday: "short" });
|
|
1891
|
+
}
|
|
1892
|
+
return d.toLocaleDateString([], { month: "short", day: "numeric" });
|
|
1893
|
+
}
|
|
1894
|
+
function ConversationItem({
|
|
1895
|
+
conversation,
|
|
1896
|
+
selected,
|
|
1897
|
+
onSelect,
|
|
1898
|
+
onDelete
|
|
1899
|
+
}) {
|
|
1900
|
+
const title = conversation.title ?? conversation.messages[0]?.content?.slice(0, 50) ?? "New chat";
|
|
1901
|
+
const displayTitle = title.length > 40 ? `${title.slice(0, 40)}\u2026` : title;
|
|
1902
|
+
return /* @__PURE__ */ jsxs9("div", {
|
|
1903
|
+
role: "button",
|
|
1904
|
+
tabIndex: 0,
|
|
1905
|
+
onClick: onSelect,
|
|
1906
|
+
onKeyDown: (e) => {
|
|
1907
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1908
|
+
e.preventDefault();
|
|
1909
|
+
onSelect();
|
|
1910
|
+
}
|
|
1911
|
+
},
|
|
1912
|
+
className: cn6("group flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors", selected ? "bg-accent text-accent-foreground" : "hover:bg-accent/50"),
|
|
1337
1913
|
children: [
|
|
1338
|
-
/* @__PURE__ */
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
className: "h-3 w-3"
|
|
1344
|
-
}, undefined, false, undefined, this),
|
|
1345
|
-
"Context"
|
|
1346
|
-
]
|
|
1347
|
-
}, undefined, true, undefined, this),
|
|
1348
|
-
summary && showDetails && /* @__PURE__ */ jsxDEV6(Fragment3, {
|
|
1914
|
+
/* @__PURE__ */ jsx9(MessageSquare, {
|
|
1915
|
+
className: "text-muted-foreground h-4 w-4 shrink-0"
|
|
1916
|
+
}),
|
|
1917
|
+
/* @__PURE__ */ jsxs9("div", {
|
|
1918
|
+
className: "min-w-0 flex-1",
|
|
1349
1919
|
children: [
|
|
1350
|
-
/* @__PURE__ */
|
|
1351
|
-
className: "
|
|
1352
|
-
children:
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
/* @__PURE__ */ jsxDEV6("span", {
|
|
1357
|
-
children: summary.name
|
|
1358
|
-
}, undefined, false, undefined, this)
|
|
1359
|
-
]
|
|
1360
|
-
}, undefined, true, undefined, this),
|
|
1361
|
-
/* @__PURE__ */ jsxDEV6("div", {
|
|
1362
|
-
className: "flex items-center gap-1 text-xs",
|
|
1920
|
+
/* @__PURE__ */ jsx9("p", {
|
|
1921
|
+
className: "truncate",
|
|
1922
|
+
children: displayTitle
|
|
1923
|
+
}),
|
|
1924
|
+
/* @__PURE__ */ jsxs9("p", {
|
|
1925
|
+
className: "text-muted-foreground text-xs",
|
|
1363
1926
|
children: [
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
/* @__PURE__ */ jsxDEV6("span", {
|
|
1927
|
+
formatDate(conversation.updatedAt),
|
|
1928
|
+
conversation.projectName && ` \xB7 ${conversation.projectName}`,
|
|
1929
|
+
conversation.tags && conversation.tags.length > 0 && /* @__PURE__ */ jsxs9(Fragment5, {
|
|
1368
1930
|
children: [
|
|
1369
|
-
|
|
1370
|
-
"
|
|
1931
|
+
" \xB7 ",
|
|
1932
|
+
conversation.tags.slice(0, 2).join(", ")
|
|
1371
1933
|
]
|
|
1372
|
-
}
|
|
1934
|
+
})
|
|
1373
1935
|
]
|
|
1374
|
-
}
|
|
1936
|
+
})
|
|
1375
1937
|
]
|
|
1376
|
-
},
|
|
1938
|
+
}),
|
|
1939
|
+
/* @__PURE__ */ jsx9("span", {
|
|
1940
|
+
onClick: (e) => e.stopPropagation(),
|
|
1941
|
+
children: /* @__PURE__ */ jsx9(Button5, {
|
|
1942
|
+
variant: "ghost",
|
|
1943
|
+
size: "sm",
|
|
1944
|
+
className: "h-6 w-6 shrink-0 p-0 opacity-0 group-hover:opacity-100",
|
|
1945
|
+
onPress: onDelete,
|
|
1946
|
+
"aria-label": "Delete conversation",
|
|
1947
|
+
children: /* @__PURE__ */ jsx9(Trash2, {
|
|
1948
|
+
className: "h-3 w-3"
|
|
1949
|
+
})
|
|
1950
|
+
})
|
|
1951
|
+
})
|
|
1377
1952
|
]
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
/* @__PURE__ */ jsxDEV6("span", {
|
|
1449
|
-
className: "mx-1",
|
|
1450
|
-
children: "\u2022"
|
|
1451
|
-
}, undefined, false, undefined, this),
|
|
1452
|
-
/* @__PURE__ */ jsxDEV6("span", {
|
|
1453
|
-
children: [
|
|
1454
|
-
summary.files.specFiles,
|
|
1455
|
-
" spec files"
|
|
1456
|
-
]
|
|
1457
|
-
}, undefined, true, undefined, this)
|
|
1458
|
-
]
|
|
1459
|
-
}, undefined, true, undefined, this)
|
|
1460
|
-
]
|
|
1461
|
-
}, undefined, true, undefined, this)
|
|
1462
|
-
}, undefined, false, undefined, this)
|
|
1463
|
-
]
|
|
1464
|
-
}, undefined, true, undefined, this)
|
|
1465
|
-
}, undefined, false, undefined, this);
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
function ChatSidebar({
|
|
1956
|
+
store,
|
|
1957
|
+
selectedConversationId,
|
|
1958
|
+
onSelectConversation,
|
|
1959
|
+
onCreateNew,
|
|
1960
|
+
projectId,
|
|
1961
|
+
tags,
|
|
1962
|
+
limit = 50,
|
|
1963
|
+
className,
|
|
1964
|
+
collapsed = false,
|
|
1965
|
+
onUpdateConversation,
|
|
1966
|
+
selectedConversation
|
|
1967
|
+
}) {
|
|
1968
|
+
const { conversations, isLoading, refresh, deleteConversation } = useConversations({ store, projectId, tags, limit });
|
|
1969
|
+
const handleDelete = React10.useCallback(async (id) => {
|
|
1970
|
+
const ok = await deleteConversation(id);
|
|
1971
|
+
if (ok && selectedConversationId === id) {
|
|
1972
|
+
onSelectConversation(null);
|
|
1973
|
+
}
|
|
1974
|
+
}, [deleteConversation, selectedConversationId, onSelectConversation]);
|
|
1975
|
+
if (collapsed)
|
|
1976
|
+
return null;
|
|
1977
|
+
return /* @__PURE__ */ jsxs9("div", {
|
|
1978
|
+
className: cn6("border-border flex w-64 shrink-0 flex-col border-r", className),
|
|
1979
|
+
children: [
|
|
1980
|
+
/* @__PURE__ */ jsxs9("div", {
|
|
1981
|
+
className: "border-border flex shrink-0 items-center justify-between border-b p-2",
|
|
1982
|
+
children: [
|
|
1983
|
+
/* @__PURE__ */ jsx9("span", {
|
|
1984
|
+
className: "text-muted-foreground text-sm font-medium",
|
|
1985
|
+
children: "Conversations"
|
|
1986
|
+
}),
|
|
1987
|
+
/* @__PURE__ */ jsx9(Button5, {
|
|
1988
|
+
variant: "ghost",
|
|
1989
|
+
size: "sm",
|
|
1990
|
+
className: "h-8 w-8 p-0",
|
|
1991
|
+
onPress: onCreateNew,
|
|
1992
|
+
"aria-label": "New conversation",
|
|
1993
|
+
children: /* @__PURE__ */ jsx9(Plus2, {
|
|
1994
|
+
className: "h-4 w-4"
|
|
1995
|
+
})
|
|
1996
|
+
})
|
|
1997
|
+
]
|
|
1998
|
+
}),
|
|
1999
|
+
/* @__PURE__ */ jsx9("div", {
|
|
2000
|
+
className: "flex-1 overflow-y-auto p-2",
|
|
2001
|
+
children: isLoading ? /* @__PURE__ */ jsx9("div", {
|
|
2002
|
+
className: "text-muted-foreground py-4 text-center text-sm",
|
|
2003
|
+
children: "Loading\u2026"
|
|
2004
|
+
}) : conversations.length === 0 ? /* @__PURE__ */ jsx9("div", {
|
|
2005
|
+
className: "text-muted-foreground py-4 text-center text-sm",
|
|
2006
|
+
children: "No conversations yet"
|
|
2007
|
+
}) : /* @__PURE__ */ jsx9("div", {
|
|
2008
|
+
className: "flex flex-col gap-1",
|
|
2009
|
+
children: conversations.map((conv) => /* @__PURE__ */ jsx9(ConversationItem, {
|
|
2010
|
+
conversation: conv,
|
|
2011
|
+
selected: conv.id === selectedConversationId,
|
|
2012
|
+
onSelect: () => onSelectConversation(conv.id),
|
|
2013
|
+
onDelete: () => handleDelete(conv.id)
|
|
2014
|
+
}, conv.id))
|
|
2015
|
+
})
|
|
2016
|
+
}),
|
|
2017
|
+
selectedConversation && onUpdateConversation && /* @__PURE__ */ jsx9(ConversationMeta, {
|
|
2018
|
+
conversation: selectedConversation,
|
|
2019
|
+
onUpdate: onUpdateConversation
|
|
2020
|
+
})
|
|
2021
|
+
]
|
|
2022
|
+
});
|
|
1466
2023
|
}
|
|
2024
|
+
function ConversationMeta({
|
|
2025
|
+
conversation,
|
|
2026
|
+
onUpdate
|
|
2027
|
+
}) {
|
|
2028
|
+
const [projectName, setProjectName] = React10.useState(conversation.projectName ?? "");
|
|
2029
|
+
const [tagsStr, setTagsStr] = React10.useState(conversation.tags?.join(", ") ?? "");
|
|
2030
|
+
React10.useEffect(() => {
|
|
2031
|
+
setProjectName(conversation.projectName ?? "");
|
|
2032
|
+
setTagsStr(conversation.tags?.join(", ") ?? "");
|
|
2033
|
+
}, [conversation.id, conversation.projectName, conversation.tags]);
|
|
2034
|
+
const handleBlur = React10.useCallback(() => {
|
|
2035
|
+
const tags = tagsStr.split(",").map((t) => t.trim()).filter(Boolean);
|
|
2036
|
+
if (projectName !== (conversation.projectName ?? "") || JSON.stringify(tags) !== JSON.stringify(conversation.tags ?? [])) {
|
|
2037
|
+
onUpdate(conversation.id, {
|
|
2038
|
+
projectName: projectName || undefined,
|
|
2039
|
+
projectId: projectName ? projectName.replace(/\s+/g, "-") : undefined,
|
|
2040
|
+
tags: tags.length > 0 ? tags : undefined
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
}, [
|
|
2044
|
+
conversation.id,
|
|
2045
|
+
conversation.projectName,
|
|
2046
|
+
conversation.tags,
|
|
2047
|
+
projectName,
|
|
2048
|
+
tagsStr,
|
|
2049
|
+
onUpdate
|
|
2050
|
+
]);
|
|
2051
|
+
return /* @__PURE__ */ jsxs9("div", {
|
|
2052
|
+
className: "border-border shrink-0 border-t p-2",
|
|
2053
|
+
children: [
|
|
2054
|
+
/* @__PURE__ */ jsx9("p", {
|
|
2055
|
+
className: "text-muted-foreground mb-1 text-xs font-medium",
|
|
2056
|
+
children: "Project"
|
|
2057
|
+
}),
|
|
2058
|
+
/* @__PURE__ */ jsx9("input", {
|
|
2059
|
+
type: "text",
|
|
2060
|
+
value: projectName,
|
|
2061
|
+
onChange: (e) => setProjectName(e.target.value),
|
|
2062
|
+
onBlur: handleBlur,
|
|
2063
|
+
placeholder: "Project name",
|
|
2064
|
+
className: "border-input bg-background mb-2 w-full rounded px-2 py-1 text-xs"
|
|
2065
|
+
}),
|
|
2066
|
+
/* @__PURE__ */ jsx9("p", {
|
|
2067
|
+
className: "text-muted-foreground mb-1 text-xs font-medium",
|
|
2068
|
+
children: "Tags"
|
|
2069
|
+
}),
|
|
2070
|
+
/* @__PURE__ */ jsx9("input", {
|
|
2071
|
+
type: "text",
|
|
2072
|
+
value: tagsStr,
|
|
2073
|
+
onChange: (e) => setTagsStr(e.target.value),
|
|
2074
|
+
onBlur: handleBlur,
|
|
2075
|
+
placeholder: "tag1, tag2",
|
|
2076
|
+
className: "border-input bg-background w-full rounded px-2 py-1 text-xs"
|
|
2077
|
+
})
|
|
2078
|
+
]
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
// src/presentation/components/ChatWithSidebar.tsx
|
|
2082
|
+
import * as React12 from "react";
|
|
2083
|
+
|
|
1467
2084
|
// src/presentation/hooks/useChat.tsx
|
|
1468
|
-
import * as
|
|
1469
|
-
import { tool } from "ai";
|
|
1470
|
-
import { z } from "zod";
|
|
2085
|
+
import * as React11 from "react";
|
|
2086
|
+
import { tool as tool4 } from "ai";
|
|
2087
|
+
import { z as z4 } from "zod";
|
|
1471
2088
|
|
|
1472
2089
|
// src/core/chat-service.ts
|
|
1473
2090
|
import { generateText, streamText } from "ai";
|
|
@@ -1549,11 +2166,65 @@ class InMemoryConversationStore {
|
|
|
1549
2166
|
if (options?.status) {
|
|
1550
2167
|
results = results.filter((c) => c.status === options.status);
|
|
1551
2168
|
}
|
|
2169
|
+
if (options?.projectId) {
|
|
2170
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
2171
|
+
}
|
|
2172
|
+
if (options?.tags && options.tags.length > 0) {
|
|
2173
|
+
const tagSet = new Set(options.tags);
|
|
2174
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
2175
|
+
}
|
|
1552
2176
|
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
1553
2177
|
const offset = options?.offset ?? 0;
|
|
1554
2178
|
const limit = options?.limit ?? 100;
|
|
1555
2179
|
return results.slice(offset, offset + limit);
|
|
1556
2180
|
}
|
|
2181
|
+
async fork(conversationId, upToMessageId) {
|
|
2182
|
+
const source = this.conversations.get(conversationId);
|
|
2183
|
+
if (!source) {
|
|
2184
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
2185
|
+
}
|
|
2186
|
+
let messagesToCopy = source.messages;
|
|
2187
|
+
if (upToMessageId) {
|
|
2188
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
2189
|
+
if (idx === -1) {
|
|
2190
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
2191
|
+
}
|
|
2192
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
2193
|
+
}
|
|
2194
|
+
const now = new Date;
|
|
2195
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
2196
|
+
...m,
|
|
2197
|
+
id: generateId("msg"),
|
|
2198
|
+
conversationId: "",
|
|
2199
|
+
createdAt: new Date(m.createdAt),
|
|
2200
|
+
updatedAt: new Date(m.updatedAt)
|
|
2201
|
+
}));
|
|
2202
|
+
const forked = {
|
|
2203
|
+
...source,
|
|
2204
|
+
id: generateId("conv"),
|
|
2205
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
2206
|
+
forkedFromId: source.id,
|
|
2207
|
+
createdAt: now,
|
|
2208
|
+
updatedAt: now,
|
|
2209
|
+
messages: forkedMessages
|
|
2210
|
+
};
|
|
2211
|
+
for (const m of forked.messages) {
|
|
2212
|
+
m.conversationId = forked.id;
|
|
2213
|
+
}
|
|
2214
|
+
this.conversations.set(forked.id, forked);
|
|
2215
|
+
return forked;
|
|
2216
|
+
}
|
|
2217
|
+
async truncateAfter(conversationId, messageId) {
|
|
2218
|
+
const conv = this.conversations.get(conversationId);
|
|
2219
|
+
if (!conv)
|
|
2220
|
+
return null;
|
|
2221
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
2222
|
+
if (idx === -1)
|
|
2223
|
+
return null;
|
|
2224
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
2225
|
+
conv.updatedAt = new Date;
|
|
2226
|
+
return conv;
|
|
2227
|
+
}
|
|
1557
2228
|
async search(query, limit = 20) {
|
|
1558
2229
|
const lowerQuery = query.toLowerCase();
|
|
1559
2230
|
const results = [];
|
|
@@ -1566,20 +2237,471 @@ class InMemoryConversationStore {
|
|
|
1566
2237
|
if (hasMatch) {
|
|
1567
2238
|
results.push(conversation);
|
|
1568
2239
|
}
|
|
1569
|
-
if (results.length >= limit)
|
|
1570
|
-
break;
|
|
2240
|
+
if (results.length >= limit)
|
|
2241
|
+
break;
|
|
2242
|
+
}
|
|
2243
|
+
return results;
|
|
2244
|
+
}
|
|
2245
|
+
clear() {
|
|
2246
|
+
this.conversations.clear();
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
function createInMemoryConversationStore() {
|
|
2250
|
+
return new InMemoryConversationStore;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/core/workflow-tools.ts
|
|
2254
|
+
import { tool } from "ai";
|
|
2255
|
+
import { z } from "zod";
|
|
2256
|
+
import {
|
|
2257
|
+
WorkflowComposer,
|
|
2258
|
+
validateExtension
|
|
2259
|
+
} from "@contractspec/lib.workflow-composer";
|
|
2260
|
+
var StepTypeSchema = z.enum(["human", "automation", "decision"]);
|
|
2261
|
+
var StepActionSchema = z.object({
|
|
2262
|
+
operation: z.object({
|
|
2263
|
+
name: z.string(),
|
|
2264
|
+
version: z.number()
|
|
2265
|
+
}).optional(),
|
|
2266
|
+
form: z.object({
|
|
2267
|
+
key: z.string(),
|
|
2268
|
+
version: z.number()
|
|
2269
|
+
}).optional()
|
|
2270
|
+
}).optional();
|
|
2271
|
+
var StepSchema = z.object({
|
|
2272
|
+
id: z.string(),
|
|
2273
|
+
type: StepTypeSchema,
|
|
2274
|
+
label: z.string(),
|
|
2275
|
+
description: z.string().optional(),
|
|
2276
|
+
action: StepActionSchema
|
|
2277
|
+
});
|
|
2278
|
+
var StepInjectionSchema = z.object({
|
|
2279
|
+
after: z.string().optional(),
|
|
2280
|
+
before: z.string().optional(),
|
|
2281
|
+
inject: StepSchema,
|
|
2282
|
+
transitionTo: z.string().optional(),
|
|
2283
|
+
transitionFrom: z.string().optional(),
|
|
2284
|
+
when: z.string().optional()
|
|
2285
|
+
});
|
|
2286
|
+
var WorkflowExtensionInputSchema = z.object({
|
|
2287
|
+
workflow: z.string(),
|
|
2288
|
+
tenantId: z.string().optional(),
|
|
2289
|
+
role: z.string().optional(),
|
|
2290
|
+
priority: z.number().optional(),
|
|
2291
|
+
customSteps: z.array(StepInjectionSchema).optional(),
|
|
2292
|
+
hiddenSteps: z.array(z.string()).optional()
|
|
2293
|
+
});
|
|
2294
|
+
function createWorkflowTools(config) {
|
|
2295
|
+
const { baseWorkflows, composer } = config;
|
|
2296
|
+
const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
|
|
2297
|
+
const createWorkflowExtensionTool = tool({
|
|
2298
|
+
description: "Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",
|
|
2299
|
+
inputSchema: WorkflowExtensionInputSchema,
|
|
2300
|
+
execute: async (input) => {
|
|
2301
|
+
const extension = {
|
|
2302
|
+
workflow: input.workflow,
|
|
2303
|
+
tenantId: input.tenantId,
|
|
2304
|
+
role: input.role,
|
|
2305
|
+
priority: input.priority,
|
|
2306
|
+
customSteps: input.customSteps,
|
|
2307
|
+
hiddenSteps: input.hiddenSteps
|
|
2308
|
+
};
|
|
2309
|
+
const base = baseByKey.get(input.workflow);
|
|
2310
|
+
if (!base) {
|
|
2311
|
+
return {
|
|
2312
|
+
success: false,
|
|
2313
|
+
error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
2314
|
+
extension
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
try {
|
|
2318
|
+
validateExtension(extension, base);
|
|
2319
|
+
return {
|
|
2320
|
+
success: true,
|
|
2321
|
+
message: "Extension validated successfully",
|
|
2322
|
+
extension
|
|
2323
|
+
};
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
return {
|
|
2326
|
+
success: false,
|
|
2327
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2328
|
+
extension
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
const composeWorkflowInputSchema = z.object({
|
|
2334
|
+
workflowKey: z.string().describe("Base workflow meta.key"),
|
|
2335
|
+
tenantId: z.string().optional(),
|
|
2336
|
+
role: z.string().optional(),
|
|
2337
|
+
extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
|
|
2338
|
+
});
|
|
2339
|
+
const composeWorkflowTool = tool({
|
|
2340
|
+
description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
|
|
2341
|
+
inputSchema: composeWorkflowInputSchema,
|
|
2342
|
+
execute: async (input) => {
|
|
2343
|
+
const base = baseByKey.get(input.workflowKey);
|
|
2344
|
+
if (!base) {
|
|
2345
|
+
return {
|
|
2346
|
+
success: false,
|
|
2347
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
const comp = composer ?? new WorkflowComposer;
|
|
2351
|
+
if (input.extensions?.length) {
|
|
2352
|
+
for (const ext of input.extensions) {
|
|
2353
|
+
comp.register({
|
|
2354
|
+
workflow: ext.workflow,
|
|
2355
|
+
tenantId: ext.tenantId,
|
|
2356
|
+
role: ext.role,
|
|
2357
|
+
priority: ext.priority,
|
|
2358
|
+
customSteps: ext.customSteps,
|
|
2359
|
+
hiddenSteps: ext.hiddenSteps
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
try {
|
|
2364
|
+
const composed = comp.compose({
|
|
2365
|
+
base,
|
|
2366
|
+
tenantId: input.tenantId,
|
|
2367
|
+
role: input.role
|
|
2368
|
+
});
|
|
2369
|
+
return {
|
|
2370
|
+
success: true,
|
|
2371
|
+
workflow: composed,
|
|
2372
|
+
meta: composed.meta,
|
|
2373
|
+
stepIds: composed.definition.steps.map((s) => s.id)
|
|
2374
|
+
};
|
|
2375
|
+
} catch (err) {
|
|
2376
|
+
return {
|
|
2377
|
+
success: false,
|
|
2378
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
const generateWorkflowSpecCodeInputSchema = z.object({
|
|
2384
|
+
workflowKey: z.string().describe("Workflow meta.key"),
|
|
2385
|
+
composedSteps: z.array(z.object({
|
|
2386
|
+
id: z.string(),
|
|
2387
|
+
type: z.enum(["human", "automation", "decision"]),
|
|
2388
|
+
label: z.string(),
|
|
2389
|
+
description: z.string().optional()
|
|
2390
|
+
})).optional().describe("Steps to include; if omitted, uses the base workflow")
|
|
2391
|
+
});
|
|
2392
|
+
const generateWorkflowSpecCodeTool = tool({
|
|
2393
|
+
description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
|
|
2394
|
+
inputSchema: generateWorkflowSpecCodeInputSchema,
|
|
2395
|
+
execute: async (input) => {
|
|
2396
|
+
const base = baseByKey.get(input.workflowKey);
|
|
2397
|
+
if (!base) {
|
|
2398
|
+
return {
|
|
2399
|
+
success: false,
|
|
2400
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
2401
|
+
code: null
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
const steps = input.composedSteps ?? base.definition.steps;
|
|
2405
|
+
const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
|
|
2406
|
+
const stepsCode = steps.map((s) => ` {
|
|
2407
|
+
id: '${s.id}',
|
|
2408
|
+
type: '${s.type}',
|
|
2409
|
+
label: '${escapeString(s.label)}',${s.description ? `
|
|
2410
|
+
description: '${escapeString(s.description)}',` : ""}
|
|
2411
|
+
}`).join(`,
|
|
2412
|
+
`);
|
|
2413
|
+
const meta = base.meta;
|
|
2414
|
+
const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
|
|
2415
|
+
const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* Workflow: ${base.meta.key}
|
|
2419
|
+
* Generated via AI chat workflow tools.
|
|
2420
|
+
*/
|
|
2421
|
+
export const ${specVarName}: WorkflowSpec = {
|
|
2422
|
+
meta: {
|
|
2423
|
+
key: '${base.meta.key}',
|
|
2424
|
+
version: '${String(base.meta.version)}',
|
|
2425
|
+
title: '${escapeString(meta.title ?? base.meta.key)}',
|
|
2426
|
+
description: '${escapeString(meta.description ?? "")}',
|
|
2427
|
+
},
|
|
2428
|
+
definition: {
|
|
2429
|
+
entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
|
|
2430
|
+
steps: [
|
|
2431
|
+
${stepsCode}
|
|
2432
|
+
],
|
|
2433
|
+
transitions: ${transitionsJson},
|
|
2434
|
+
},
|
|
2435
|
+
};
|
|
2436
|
+
`;
|
|
2437
|
+
return {
|
|
2438
|
+
success: true,
|
|
2439
|
+
code,
|
|
2440
|
+
workflowKey: input.workflowKey
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
return {
|
|
2445
|
+
create_workflow_extension: createWorkflowExtensionTool,
|
|
2446
|
+
compose_workflow: composeWorkflowTool,
|
|
2447
|
+
generate_workflow_spec_code: generateWorkflowSpecCodeTool
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
function toPascalCase(value) {
|
|
2451
|
+
return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2452
|
+
}
|
|
2453
|
+
function escapeString(value) {
|
|
2454
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/core/contracts-context.ts
|
|
2458
|
+
function buildContractsContextPrompt(config) {
|
|
2459
|
+
const parts = [];
|
|
2460
|
+
if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
|
|
2461
|
+
return "";
|
|
2462
|
+
}
|
|
2463
|
+
parts.push(`
|
|
2464
|
+
|
|
2465
|
+
## Available resources`);
|
|
2466
|
+
if (config.agentSpecs?.length) {
|
|
2467
|
+
parts.push(`
|
|
2468
|
+
### Agent tools`);
|
|
2469
|
+
for (const agent of config.agentSpecs) {
|
|
2470
|
+
const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
|
|
2471
|
+
parts.push(`- **${agent.key}**: tools: ${toolNames}`);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (config.dataViewSpecs?.length) {
|
|
2475
|
+
parts.push(`
|
|
2476
|
+
### Data views`);
|
|
2477
|
+
for (const dv of config.dataViewSpecs) {
|
|
2478
|
+
parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if (config.formSpecs?.length) {
|
|
2482
|
+
parts.push(`
|
|
2483
|
+
### Forms`);
|
|
2484
|
+
for (const form of config.formSpecs) {
|
|
2485
|
+
parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (config.presentationSpecs?.length) {
|
|
2489
|
+
parts.push(`
|
|
2490
|
+
### Presentations`);
|
|
2491
|
+
for (const pres of config.presentationSpecs) {
|
|
2492
|
+
parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (config.operationRefs?.length) {
|
|
2496
|
+
parts.push(`
|
|
2497
|
+
### Operations`);
|
|
2498
|
+
for (const op of config.operationRefs) {
|
|
2499
|
+
parts.push(`- **${op.key}@${op.version}**`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
parts.push(`
|
|
2503
|
+
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
|
|
2504
|
+
return parts.join(`
|
|
2505
|
+
`);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// src/core/agent-tools-adapter.ts
|
|
2509
|
+
import { tool as tool2 } from "ai";
|
|
2510
|
+
import { z as z2 } from "zod";
|
|
2511
|
+
function getInputSchema(_schema) {
|
|
2512
|
+
return z2.object({}).passthrough();
|
|
2513
|
+
}
|
|
2514
|
+
function agentToolConfigsToToolSet(configs, handlers) {
|
|
2515
|
+
const result = {};
|
|
2516
|
+
for (const config of configs) {
|
|
2517
|
+
const handler = handlers?.[config.name];
|
|
2518
|
+
const inputSchema = getInputSchema(config.schema);
|
|
2519
|
+
result[config.name] = tool2({
|
|
2520
|
+
description: config.description ?? config.name,
|
|
2521
|
+
inputSchema,
|
|
2522
|
+
execute: async (input) => {
|
|
2523
|
+
if (!handler) {
|
|
2524
|
+
return {
|
|
2525
|
+
status: "unimplemented",
|
|
2526
|
+
message: "Wire handler in host",
|
|
2527
|
+
toolName: config.name
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
try {
|
|
2531
|
+
const output = await Promise.resolve(handler(input));
|
|
2532
|
+
return typeof output === "string" ? output : output;
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
return {
|
|
2535
|
+
status: "error",
|
|
2536
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2537
|
+
toolName: config.name
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
return result;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// src/core/surface-planner-tools.ts
|
|
2547
|
+
import { tool as tool3 } from "ai";
|
|
2548
|
+
import { z as z3 } from "zod";
|
|
2549
|
+
import {
|
|
2550
|
+
validatePatchProposal
|
|
2551
|
+
} from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
2552
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
2553
|
+
var VALID_OPS = [
|
|
2554
|
+
"insert-node",
|
|
2555
|
+
"replace-node",
|
|
2556
|
+
"remove-node",
|
|
2557
|
+
"move-node",
|
|
2558
|
+
"resize-panel",
|
|
2559
|
+
"set-layout",
|
|
2560
|
+
"reveal-field",
|
|
2561
|
+
"hide-field",
|
|
2562
|
+
"promote-action",
|
|
2563
|
+
"set-focus"
|
|
2564
|
+
];
|
|
2565
|
+
var DEFAULT_NODE_KINDS = [
|
|
2566
|
+
"entity-section",
|
|
2567
|
+
"entity-card",
|
|
2568
|
+
"data-view",
|
|
2569
|
+
"assistant-panel",
|
|
2570
|
+
"chat-thread",
|
|
2571
|
+
"action-bar",
|
|
2572
|
+
"timeline",
|
|
2573
|
+
"table",
|
|
2574
|
+
"rich-doc",
|
|
2575
|
+
"form",
|
|
2576
|
+
"chart",
|
|
2577
|
+
"custom-widget"
|
|
2578
|
+
];
|
|
2579
|
+
function collectSlotIdsFromRegion(node) {
|
|
2580
|
+
const ids = [];
|
|
2581
|
+
if (node.type === "slot") {
|
|
2582
|
+
ids.push(node.slotId);
|
|
2583
|
+
}
|
|
2584
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
2585
|
+
for (const child of node.children) {
|
|
2586
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
1571
2587
|
}
|
|
1572
|
-
return results;
|
|
1573
2588
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
2589
|
+
if (node.type === "tabs") {
|
|
2590
|
+
for (const tab of node.tabs) {
|
|
2591
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
2592
|
+
}
|
|
1576
2593
|
}
|
|
2594
|
+
if (node.type === "floating") {
|
|
2595
|
+
ids.push(node.anchorSlotId);
|
|
2596
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
2597
|
+
}
|
|
2598
|
+
return ids;
|
|
1577
2599
|
}
|
|
1578
|
-
function
|
|
1579
|
-
|
|
2600
|
+
function deriveConstraints(plan) {
|
|
2601
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
2602
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
2603
|
+
return {
|
|
2604
|
+
allowedOps: VALID_OPS,
|
|
2605
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
2606
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
var ProposePatchInputSchema = z3.object({
|
|
2610
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
2611
|
+
ops: z3.array(z3.object({
|
|
2612
|
+
op: z3.enum([
|
|
2613
|
+
"insert-node",
|
|
2614
|
+
"replace-node",
|
|
2615
|
+
"remove-node",
|
|
2616
|
+
"move-node",
|
|
2617
|
+
"resize-panel",
|
|
2618
|
+
"set-layout",
|
|
2619
|
+
"reveal-field",
|
|
2620
|
+
"hide-field",
|
|
2621
|
+
"promote-action",
|
|
2622
|
+
"set-focus"
|
|
2623
|
+
]),
|
|
2624
|
+
slotId: z3.string().optional(),
|
|
2625
|
+
nodeId: z3.string().optional(),
|
|
2626
|
+
toSlotId: z3.string().optional(),
|
|
2627
|
+
index: z3.number().optional(),
|
|
2628
|
+
node: z3.object({
|
|
2629
|
+
nodeId: z3.string(),
|
|
2630
|
+
kind: z3.string(),
|
|
2631
|
+
title: z3.string().optional(),
|
|
2632
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
2633
|
+
children: z3.array(z3.unknown()).optional()
|
|
2634
|
+
}).optional(),
|
|
2635
|
+
persistKey: z3.string().optional(),
|
|
2636
|
+
sizes: z3.array(z3.number()).optional(),
|
|
2637
|
+
layoutId: z3.string().optional(),
|
|
2638
|
+
fieldId: z3.string().optional(),
|
|
2639
|
+
actionId: z3.string().optional(),
|
|
2640
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
2641
|
+
targetId: z3.string().optional()
|
|
2642
|
+
}))
|
|
2643
|
+
});
|
|
2644
|
+
function createSurfacePlannerTools(config) {
|
|
2645
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
2646
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
2647
|
+
const proposePatchTool = tool3({
|
|
2648
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
2649
|
+
inputSchema: ProposePatchInputSchema,
|
|
2650
|
+
execute: async (input) => {
|
|
2651
|
+
const ops = input.ops;
|
|
2652
|
+
try {
|
|
2653
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
2654
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
2655
|
+
onPatchProposal?.(proposal);
|
|
2656
|
+
return {
|
|
2657
|
+
success: true,
|
|
2658
|
+
proposalId: proposal.proposalId,
|
|
2659
|
+
opsCount: proposal.ops.length,
|
|
2660
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
2661
|
+
};
|
|
2662
|
+
} catch (err) {
|
|
2663
|
+
return {
|
|
2664
|
+
success: false,
|
|
2665
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2666
|
+
proposalId: input.proposalId
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
return {
|
|
2672
|
+
"propose-patch": proposePatchTool
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
function buildPlannerPromptInput(plan) {
|
|
2676
|
+
const constraints = deriveConstraints(plan);
|
|
2677
|
+
return {
|
|
2678
|
+
bundleMeta: {
|
|
2679
|
+
key: plan.bundleKey,
|
|
2680
|
+
version: "0.0.0",
|
|
2681
|
+
title: plan.bundleKey
|
|
2682
|
+
},
|
|
2683
|
+
surfaceId: plan.surfaceId,
|
|
2684
|
+
allowedPatchOps: constraints.allowedOps,
|
|
2685
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
2686
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
2687
|
+
actions: plan.actions.map((a) => ({
|
|
2688
|
+
actionId: a.actionId,
|
|
2689
|
+
title: a.title
|
|
2690
|
+
})),
|
|
2691
|
+
preferences: {
|
|
2692
|
+
guidance: "hints",
|
|
2693
|
+
density: "standard",
|
|
2694
|
+
dataDepth: "detailed",
|
|
2695
|
+
control: "standard",
|
|
2696
|
+
media: "text",
|
|
2697
|
+
pace: "balanced",
|
|
2698
|
+
narrative: "top-down"
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
1580
2701
|
}
|
|
1581
2702
|
|
|
1582
2703
|
// src/core/chat-service.ts
|
|
2704
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
1583
2705
|
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
1584
2706
|
|
|
1585
2707
|
Your capabilities:
|
|
@@ -1594,6 +2716,9 @@ Guidelines:
|
|
|
1594
2716
|
- Reference relevant ContractSpec concepts and patterns
|
|
1595
2717
|
- Ask clarifying questions when the user's intent is unclear
|
|
1596
2718
|
- When suggesting code changes, explain the rationale`;
|
|
2719
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
2720
|
+
|
|
2721
|
+
Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;
|
|
1597
2722
|
|
|
1598
2723
|
class ChatService {
|
|
1599
2724
|
provider;
|
|
@@ -1603,19 +2728,93 @@ class ChatService {
|
|
|
1603
2728
|
maxHistoryMessages;
|
|
1604
2729
|
onUsage;
|
|
1605
2730
|
tools;
|
|
2731
|
+
thinkingLevel;
|
|
1606
2732
|
sendReasoning;
|
|
1607
2733
|
sendSources;
|
|
2734
|
+
modelSelector;
|
|
1608
2735
|
constructor(config) {
|
|
1609
2736
|
this.provider = config.provider;
|
|
1610
2737
|
this.context = config.context;
|
|
1611
2738
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
1612
|
-
this.systemPrompt = config
|
|
2739
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
1613
2740
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
1614
2741
|
this.onUsage = config.onUsage;
|
|
1615
|
-
this.tools = config
|
|
1616
|
-
this.
|
|
2742
|
+
this.tools = this.mergeTools(config);
|
|
2743
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
2744
|
+
this.modelSelector = config.modelSelector;
|
|
2745
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
1617
2746
|
this.sendSources = config.sendSources ?? false;
|
|
1618
2747
|
}
|
|
2748
|
+
buildSystemPrompt(config) {
|
|
2749
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
2750
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
2751
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
2752
|
+
}
|
|
2753
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
2754
|
+
if (contractsPrompt) {
|
|
2755
|
+
base += contractsPrompt;
|
|
2756
|
+
}
|
|
2757
|
+
if (config.surfacePlanConfig?.plan) {
|
|
2758
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
2759
|
+
base += `
|
|
2760
|
+
|
|
2761
|
+
` + compilePlannerPrompt(plannerInput);
|
|
2762
|
+
}
|
|
2763
|
+
return base;
|
|
2764
|
+
}
|
|
2765
|
+
mergeTools(config) {
|
|
2766
|
+
let merged = config.tools ?? {};
|
|
2767
|
+
const wfConfig = config.workflowToolsConfig;
|
|
2768
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
2769
|
+
const workflowTools = createWorkflowTools({
|
|
2770
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
2771
|
+
composer: wfConfig.composer
|
|
2772
|
+
});
|
|
2773
|
+
merged = { ...merged, ...workflowTools };
|
|
2774
|
+
}
|
|
2775
|
+
const contractsCtx = config.contractsContext;
|
|
2776
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
2777
|
+
const allTools = [];
|
|
2778
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
2779
|
+
if (agent.tools?.length)
|
|
2780
|
+
allTools.push(...agent.tools);
|
|
2781
|
+
}
|
|
2782
|
+
if (allTools.length > 0) {
|
|
2783
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
2784
|
+
merged = { ...merged, ...agentTools };
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
2788
|
+
if (surfaceConfig?.plan) {
|
|
2789
|
+
const plannerTools = createSurfacePlannerTools({
|
|
2790
|
+
plan: surfaceConfig.plan,
|
|
2791
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
2792
|
+
});
|
|
2793
|
+
merged = { ...merged, ...plannerTools };
|
|
2794
|
+
}
|
|
2795
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
2796
|
+
merged = { ...merged, ...config.mcpTools };
|
|
2797
|
+
}
|
|
2798
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
2799
|
+
}
|
|
2800
|
+
async resolveModel() {
|
|
2801
|
+
if (this.modelSelector) {
|
|
2802
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
2803
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
2804
|
+
taskDimension: dimension
|
|
2805
|
+
});
|
|
2806
|
+
return { model, providerName: selection.providerKey };
|
|
2807
|
+
}
|
|
2808
|
+
return {
|
|
2809
|
+
model: this.provider.getModel(),
|
|
2810
|
+
providerName: this.provider.name
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
thinkingLevelToDimension(level) {
|
|
2814
|
+
if (!level || level === "instant")
|
|
2815
|
+
return "latency";
|
|
2816
|
+
return "reasoning";
|
|
2817
|
+
}
|
|
1619
2818
|
async send(options) {
|
|
1620
2819
|
let conversation;
|
|
1621
2820
|
if (options.conversationId) {
|
|
@@ -1633,20 +2832,25 @@ class ChatService {
|
|
|
1633
2832
|
workspacePath: this.context?.workspacePath
|
|
1634
2833
|
});
|
|
1635
2834
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2835
|
+
if (!options.skipUserAppend) {
|
|
2836
|
+
await this.store.appendMessage(conversation.id, {
|
|
2837
|
+
role: "user",
|
|
2838
|
+
content: options.content,
|
|
2839
|
+
status: "completed",
|
|
2840
|
+
attachments: options.attachments
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
1642
2844
|
const messages = this.buildMessages(conversation, options);
|
|
1643
|
-
const model = this.
|
|
2845
|
+
const { model, providerName } = await this.resolveModel();
|
|
2846
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
1644
2847
|
try {
|
|
1645
2848
|
const result = await generateText({
|
|
1646
2849
|
model,
|
|
1647
2850
|
messages,
|
|
1648
2851
|
system: this.systemPrompt,
|
|
1649
|
-
tools: this.tools
|
|
2852
|
+
tools: this.tools,
|
|
2853
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
1650
2854
|
});
|
|
1651
2855
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1652
2856
|
role: "assistant",
|
|
@@ -1691,23 +2895,27 @@ class ChatService {
|
|
|
1691
2895
|
workspacePath: this.context?.workspacePath
|
|
1692
2896
|
});
|
|
1693
2897
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2898
|
+
if (!options.skipUserAppend) {
|
|
2899
|
+
await this.store.appendMessage(conversation.id, {
|
|
2900
|
+
role: "user",
|
|
2901
|
+
content: options.content,
|
|
2902
|
+
status: "completed",
|
|
2903
|
+
attachments: options.attachments
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
1700
2907
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1701
2908
|
role: "assistant",
|
|
1702
2909
|
content: "",
|
|
1703
2910
|
status: "streaming"
|
|
1704
2911
|
});
|
|
1705
2912
|
const messages = this.buildMessages(conversation, options);
|
|
1706
|
-
const model = this.
|
|
2913
|
+
const { model, providerName } = await this.resolveModel();
|
|
1707
2914
|
const systemPrompt = this.systemPrompt;
|
|
1708
2915
|
const tools = this.tools;
|
|
1709
2916
|
const store = this.store;
|
|
1710
2917
|
const onUsage = this.onUsage;
|
|
2918
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
1711
2919
|
async function* streamGenerator() {
|
|
1712
2920
|
let fullContent = "";
|
|
1713
2921
|
let fullReasoning = "";
|
|
@@ -1718,7 +2926,8 @@ class ChatService {
|
|
|
1718
2926
|
model,
|
|
1719
2927
|
messages,
|
|
1720
2928
|
system: systemPrompt,
|
|
1721
|
-
tools
|
|
2929
|
+
tools,
|
|
2930
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
1722
2931
|
});
|
|
1723
2932
|
for await (const part of result.fullStream) {
|
|
1724
2933
|
if (part.type === "text-delta") {
|
|
@@ -1833,6 +3042,18 @@ class ChatService {
|
|
|
1833
3042
|
...options
|
|
1834
3043
|
});
|
|
1835
3044
|
}
|
|
3045
|
+
async updateConversation(conversationId, updates) {
|
|
3046
|
+
return this.store.update(conversationId, updates);
|
|
3047
|
+
}
|
|
3048
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
3049
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
3050
|
+
}
|
|
3051
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
3052
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
3053
|
+
}
|
|
3054
|
+
async truncateAfter(conversationId, messageId) {
|
|
3055
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
3056
|
+
}
|
|
1836
3057
|
async deleteConversation(conversationId) {
|
|
1837
3058
|
return this.store.delete(conversationId);
|
|
1838
3059
|
}
|
|
@@ -1903,9 +3124,9 @@ import {
|
|
|
1903
3124
|
function toolsToToolSet(defs) {
|
|
1904
3125
|
const result = {};
|
|
1905
3126
|
for (const def of defs) {
|
|
1906
|
-
result[def.name] =
|
|
3127
|
+
result[def.name] = tool4({
|
|
1907
3128
|
description: def.description ?? def.name,
|
|
1908
|
-
inputSchema:
|
|
3129
|
+
inputSchema: z4.object({}).passthrough(),
|
|
1909
3130
|
execute: async () => ({})
|
|
1910
3131
|
});
|
|
1911
3132
|
}
|
|
@@ -1919,22 +3140,64 @@ function useChat(options = {}) {
|
|
|
1919
3140
|
apiKey,
|
|
1920
3141
|
proxyUrl,
|
|
1921
3142
|
conversationId: initialConversationId,
|
|
3143
|
+
store,
|
|
1922
3144
|
systemPrompt,
|
|
1923
3145
|
streaming = true,
|
|
1924
3146
|
onSend,
|
|
1925
3147
|
onResponse,
|
|
1926
3148
|
onError,
|
|
1927
3149
|
onUsage,
|
|
1928
|
-
tools: toolsDefs
|
|
3150
|
+
tools: toolsDefs,
|
|
3151
|
+
thinkingLevel,
|
|
3152
|
+
workflowToolsConfig,
|
|
3153
|
+
modelSelector,
|
|
3154
|
+
contractsContext,
|
|
3155
|
+
surfacePlanConfig,
|
|
3156
|
+
mcpServers,
|
|
3157
|
+
agentMode
|
|
1929
3158
|
} = options;
|
|
1930
|
-
const [messages, setMessages] =
|
|
1931
|
-
const [
|
|
1932
|
-
const
|
|
1933
|
-
const [
|
|
1934
|
-
const [
|
|
1935
|
-
const
|
|
1936
|
-
const
|
|
1937
|
-
|
|
3159
|
+
const [messages, setMessages] = React11.useState([]);
|
|
3160
|
+
const [mcpTools, setMcpTools] = React11.useState(null);
|
|
3161
|
+
const mcpCleanupRef = React11.useRef(null);
|
|
3162
|
+
const [conversation, setConversation] = React11.useState(null);
|
|
3163
|
+
const [isLoading, setIsLoading] = React11.useState(false);
|
|
3164
|
+
const [error, setError] = React11.useState(null);
|
|
3165
|
+
const [conversationId, setConversationId] = React11.useState(initialConversationId ?? null);
|
|
3166
|
+
const abortControllerRef = React11.useRef(null);
|
|
3167
|
+
const chatServiceRef = React11.useRef(null);
|
|
3168
|
+
React11.useEffect(() => {
|
|
3169
|
+
if (!mcpServers?.length) {
|
|
3170
|
+
setMcpTools(null);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
let cancelled = false;
|
|
3174
|
+
import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
|
|
3175
|
+
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
3176
|
+
if (!cancelled) {
|
|
3177
|
+
setMcpTools(tools);
|
|
3178
|
+
mcpCleanupRef.current = cleanup;
|
|
3179
|
+
} else {
|
|
3180
|
+
cleanup().catch(() => {
|
|
3181
|
+
return;
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
}).catch(() => {
|
|
3185
|
+
if (!cancelled)
|
|
3186
|
+
setMcpTools(null);
|
|
3187
|
+
});
|
|
3188
|
+
});
|
|
3189
|
+
return () => {
|
|
3190
|
+
cancelled = true;
|
|
3191
|
+
const cleanup = mcpCleanupRef.current;
|
|
3192
|
+
mcpCleanupRef.current = null;
|
|
3193
|
+
if (cleanup)
|
|
3194
|
+
cleanup().catch(() => {
|
|
3195
|
+
return;
|
|
3196
|
+
});
|
|
3197
|
+
setMcpTools(null);
|
|
3198
|
+
};
|
|
3199
|
+
}, [mcpServers]);
|
|
3200
|
+
React11.useEffect(() => {
|
|
1938
3201
|
const chatProvider = createProvider({
|
|
1939
3202
|
provider,
|
|
1940
3203
|
model,
|
|
@@ -1943,9 +3206,16 @@ function useChat(options = {}) {
|
|
|
1943
3206
|
});
|
|
1944
3207
|
chatServiceRef.current = new ChatService({
|
|
1945
3208
|
provider: chatProvider,
|
|
3209
|
+
store,
|
|
1946
3210
|
systemPrompt,
|
|
1947
3211
|
onUsage,
|
|
1948
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
3212
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
3213
|
+
thinkingLevel,
|
|
3214
|
+
workflowToolsConfig,
|
|
3215
|
+
modelSelector,
|
|
3216
|
+
contractsContext,
|
|
3217
|
+
surfacePlanConfig,
|
|
3218
|
+
mcpTools
|
|
1949
3219
|
});
|
|
1950
3220
|
}, [
|
|
1951
3221
|
provider,
|
|
@@ -1953,11 +3223,18 @@ function useChat(options = {}) {
|
|
|
1953
3223
|
model,
|
|
1954
3224
|
apiKey,
|
|
1955
3225
|
proxyUrl,
|
|
3226
|
+
store,
|
|
1956
3227
|
systemPrompt,
|
|
1957
3228
|
onUsage,
|
|
1958
|
-
toolsDefs
|
|
3229
|
+
toolsDefs,
|
|
3230
|
+
thinkingLevel,
|
|
3231
|
+
workflowToolsConfig,
|
|
3232
|
+
modelSelector,
|
|
3233
|
+
contractsContext,
|
|
3234
|
+
surfacePlanConfig,
|
|
3235
|
+
mcpTools
|
|
1959
3236
|
]);
|
|
1960
|
-
|
|
3237
|
+
React11.useEffect(() => {
|
|
1961
3238
|
if (!conversationId || !chatServiceRef.current)
|
|
1962
3239
|
return;
|
|
1963
3240
|
const loadConversation = async () => {
|
|
@@ -1971,7 +3248,90 @@ function useChat(options = {}) {
|
|
|
1971
3248
|
};
|
|
1972
3249
|
loadConversation().catch(console.error);
|
|
1973
3250
|
}, [conversationId]);
|
|
1974
|
-
const sendMessage =
|
|
3251
|
+
const sendMessage = React11.useCallback(async (content, attachments, opts) => {
|
|
3252
|
+
if (agentMode?.agent) {
|
|
3253
|
+
setIsLoading(true);
|
|
3254
|
+
setError(null);
|
|
3255
|
+
abortControllerRef.current = new AbortController;
|
|
3256
|
+
try {
|
|
3257
|
+
if (!opts?.skipUserAppend) {
|
|
3258
|
+
const userMessage = {
|
|
3259
|
+
id: `msg_${Date.now()}`,
|
|
3260
|
+
conversationId: conversationId ?? "",
|
|
3261
|
+
role: "user",
|
|
3262
|
+
content,
|
|
3263
|
+
status: "completed",
|
|
3264
|
+
createdAt: new Date,
|
|
3265
|
+
updatedAt: new Date,
|
|
3266
|
+
attachments
|
|
3267
|
+
};
|
|
3268
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
3269
|
+
onSend?.(userMessage);
|
|
3270
|
+
}
|
|
3271
|
+
const result = await agentMode.agent.generate({
|
|
3272
|
+
prompt: content,
|
|
3273
|
+
signal: abortControllerRef.current.signal
|
|
3274
|
+
});
|
|
3275
|
+
const toolCallsMap = new Map;
|
|
3276
|
+
for (const tc of result.toolCalls ?? []) {
|
|
3277
|
+
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
3278
|
+
toolCallsMap.set(tc.toolCallId, {
|
|
3279
|
+
id: tc.toolCallId,
|
|
3280
|
+
name: tc.toolName,
|
|
3281
|
+
args: tc.args ?? {},
|
|
3282
|
+
result: tr?.output,
|
|
3283
|
+
status: "completed"
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
const assistantMessage = {
|
|
3287
|
+
id: `msg_${Date.now()}_a`,
|
|
3288
|
+
conversationId: conversationId ?? "",
|
|
3289
|
+
role: "assistant",
|
|
3290
|
+
content: result.text,
|
|
3291
|
+
status: "completed",
|
|
3292
|
+
createdAt: new Date,
|
|
3293
|
+
updatedAt: new Date,
|
|
3294
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
3295
|
+
usage: result.usage
|
|
3296
|
+
};
|
|
3297
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
3298
|
+
onResponse?.(assistantMessage);
|
|
3299
|
+
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
3300
|
+
if (store && !conversationId) {
|
|
3301
|
+
const conv = await store.create({
|
|
3302
|
+
status: "active",
|
|
3303
|
+
provider: "agent",
|
|
3304
|
+
model: "agent",
|
|
3305
|
+
messages: []
|
|
3306
|
+
});
|
|
3307
|
+
if (!opts?.skipUserAppend) {
|
|
3308
|
+
await store.appendMessage(conv.id, {
|
|
3309
|
+
role: "user",
|
|
3310
|
+
content,
|
|
3311
|
+
status: "completed",
|
|
3312
|
+
attachments
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
await store.appendMessage(conv.id, {
|
|
3316
|
+
role: "assistant",
|
|
3317
|
+
content: result.text,
|
|
3318
|
+
status: "completed",
|
|
3319
|
+
toolCalls: assistantMessage.toolCalls,
|
|
3320
|
+
usage: result.usage
|
|
3321
|
+
});
|
|
3322
|
+
const updated = await store.get(conv.id);
|
|
3323
|
+
if (updated)
|
|
3324
|
+
setConversation(updated);
|
|
3325
|
+
setConversationId(conv.id);
|
|
3326
|
+
}
|
|
3327
|
+
} catch (err) {
|
|
3328
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
3329
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
3330
|
+
} finally {
|
|
3331
|
+
setIsLoading(false);
|
|
3332
|
+
}
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
1975
3335
|
if (!chatServiceRef.current) {
|
|
1976
3336
|
throw new Error("Chat service not initialized");
|
|
1977
3337
|
}
|
|
@@ -1979,25 +3339,28 @@ function useChat(options = {}) {
|
|
|
1979
3339
|
setError(null);
|
|
1980
3340
|
abortControllerRef.current = new AbortController;
|
|
1981
3341
|
try {
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
3342
|
+
if (!opts?.skipUserAppend) {
|
|
3343
|
+
const userMessage = {
|
|
3344
|
+
id: `msg_${Date.now()}`,
|
|
3345
|
+
conversationId: conversationId ?? "",
|
|
3346
|
+
role: "user",
|
|
3347
|
+
content,
|
|
3348
|
+
status: "completed",
|
|
3349
|
+
createdAt: new Date,
|
|
3350
|
+
updatedAt: new Date,
|
|
3351
|
+
attachments
|
|
3352
|
+
};
|
|
3353
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
3354
|
+
onSend?.(userMessage);
|
|
3355
|
+
}
|
|
1994
3356
|
if (streaming) {
|
|
1995
3357
|
const result = await chatServiceRef.current.stream({
|
|
1996
3358
|
conversationId: conversationId ?? undefined,
|
|
1997
3359
|
content,
|
|
1998
|
-
attachments
|
|
3360
|
+
attachments,
|
|
3361
|
+
skipUserAppend: opts?.skipUserAppend
|
|
1999
3362
|
});
|
|
2000
|
-
if (!conversationId) {
|
|
3363
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
2001
3364
|
setConversationId(result.conversationId);
|
|
2002
3365
|
}
|
|
2003
3366
|
const assistantMessage = {
|
|
@@ -2078,7 +3441,8 @@ function useChat(options = {}) {
|
|
|
2078
3441
|
const result = await chatServiceRef.current.send({
|
|
2079
3442
|
conversationId: conversationId ?? undefined,
|
|
2080
3443
|
content,
|
|
2081
|
-
attachments
|
|
3444
|
+
attachments,
|
|
3445
|
+
skipUserAppend: opts?.skipUserAppend
|
|
2082
3446
|
});
|
|
2083
3447
|
setConversation(result.conversation);
|
|
2084
3448
|
setMessages(result.conversation.messages);
|
|
@@ -2095,14 +3459,24 @@ function useChat(options = {}) {
|
|
|
2095
3459
|
setIsLoading(false);
|
|
2096
3460
|
abortControllerRef.current = null;
|
|
2097
3461
|
}
|
|
2098
|
-
}, [
|
|
2099
|
-
|
|
3462
|
+
}, [
|
|
3463
|
+
conversationId,
|
|
3464
|
+
streaming,
|
|
3465
|
+
onSend,
|
|
3466
|
+
onResponse,
|
|
3467
|
+
onError,
|
|
3468
|
+
onUsage,
|
|
3469
|
+
messages,
|
|
3470
|
+
agentMode,
|
|
3471
|
+
store
|
|
3472
|
+
]);
|
|
3473
|
+
const clearConversation = React11.useCallback(() => {
|
|
2100
3474
|
setMessages([]);
|
|
2101
3475
|
setConversation(null);
|
|
2102
3476
|
setConversationId(null);
|
|
2103
3477
|
setError(null);
|
|
2104
3478
|
}, []);
|
|
2105
|
-
const regenerate =
|
|
3479
|
+
const regenerate = React11.useCallback(async () => {
|
|
2106
3480
|
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
2107
3481
|
if (lastUserMessageIndex === -1)
|
|
2108
3482
|
return;
|
|
@@ -2112,11 +3486,51 @@ function useChat(options = {}) {
|
|
|
2112
3486
|
setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
|
|
2113
3487
|
await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
|
|
2114
3488
|
}, [messages, sendMessage]);
|
|
2115
|
-
const stop =
|
|
3489
|
+
const stop = React11.useCallback(() => {
|
|
2116
3490
|
abortControllerRef.current?.abort();
|
|
2117
3491
|
setIsLoading(false);
|
|
2118
3492
|
}, []);
|
|
2119
|
-
const
|
|
3493
|
+
const createNewConversation = clearConversation;
|
|
3494
|
+
const editMessage = React11.useCallback(async (messageId, newContent) => {
|
|
3495
|
+
if (!chatServiceRef.current || !conversationId)
|
|
3496
|
+
return;
|
|
3497
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
3498
|
+
if (!msg || msg.role !== "user")
|
|
3499
|
+
return;
|
|
3500
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, {
|
|
3501
|
+
content: newContent
|
|
3502
|
+
});
|
|
3503
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
3504
|
+
if (truncated) {
|
|
3505
|
+
setMessages(truncated.messages);
|
|
3506
|
+
}
|
|
3507
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
3508
|
+
}, [conversationId, messages, sendMessage]);
|
|
3509
|
+
const forkConversation = React11.useCallback(async (upToMessageId) => {
|
|
3510
|
+
if (!chatServiceRef.current)
|
|
3511
|
+
return null;
|
|
3512
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
3513
|
+
if (!idToFork)
|
|
3514
|
+
return null;
|
|
3515
|
+
try {
|
|
3516
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
3517
|
+
setConversationId(forked.id);
|
|
3518
|
+
setConversation(forked);
|
|
3519
|
+
setMessages(forked.messages);
|
|
3520
|
+
return forked.id;
|
|
3521
|
+
} catch {
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
}, [conversationId, conversation]);
|
|
3525
|
+
const updateConversationFn = React11.useCallback(async (updates) => {
|
|
3526
|
+
if (!chatServiceRef.current || !conversationId)
|
|
3527
|
+
return null;
|
|
3528
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
3529
|
+
if (updated)
|
|
3530
|
+
setConversation(updated);
|
|
3531
|
+
return updated;
|
|
3532
|
+
}, [conversationId]);
|
|
3533
|
+
const addToolApprovalResponse = React11.useCallback((_toolCallId, _result) => {
|
|
2120
3534
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
2121
3535
|
}, []);
|
|
2122
3536
|
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
@@ -2130,20 +3544,731 @@ function useChat(options = {}) {
|
|
|
2130
3544
|
setConversationId,
|
|
2131
3545
|
regenerate,
|
|
2132
3546
|
stop,
|
|
3547
|
+
createNewConversation,
|
|
3548
|
+
editMessage,
|
|
3549
|
+
forkConversation,
|
|
3550
|
+
updateConversation: updateConversationFn,
|
|
2133
3551
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
2134
3552
|
};
|
|
2135
3553
|
}
|
|
3554
|
+
|
|
3555
|
+
// src/core/local-storage-conversation-store.ts
|
|
3556
|
+
var DEFAULT_KEY = "contractspec:ai-chat:conversations";
|
|
3557
|
+
function generateId2(prefix) {
|
|
3558
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
3559
|
+
}
|
|
3560
|
+
function toSerializable(conv) {
|
|
3561
|
+
return {
|
|
3562
|
+
...conv,
|
|
3563
|
+
createdAt: conv.createdAt.toISOString(),
|
|
3564
|
+
updatedAt: conv.updatedAt.toISOString(),
|
|
3565
|
+
messages: conv.messages.map((m) => ({
|
|
3566
|
+
...m,
|
|
3567
|
+
createdAt: m.createdAt.toISOString(),
|
|
3568
|
+
updatedAt: m.updatedAt.toISOString()
|
|
3569
|
+
}))
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
function fromSerializable(raw) {
|
|
3573
|
+
const messages = raw.messages?.map((m) => ({
|
|
3574
|
+
...m,
|
|
3575
|
+
createdAt: new Date(m.createdAt),
|
|
3576
|
+
updatedAt: new Date(m.updatedAt)
|
|
3577
|
+
})) ?? [];
|
|
3578
|
+
return {
|
|
3579
|
+
...raw,
|
|
3580
|
+
createdAt: new Date(raw.createdAt),
|
|
3581
|
+
updatedAt: new Date(raw.updatedAt),
|
|
3582
|
+
messages
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
function loadAll(key) {
|
|
3586
|
+
if (typeof window === "undefined")
|
|
3587
|
+
return new Map;
|
|
3588
|
+
try {
|
|
3589
|
+
const raw = window.localStorage.getItem(key);
|
|
3590
|
+
if (!raw)
|
|
3591
|
+
return new Map;
|
|
3592
|
+
const arr = JSON.parse(raw);
|
|
3593
|
+
const map = new Map;
|
|
3594
|
+
for (const item of arr) {
|
|
3595
|
+
const conv = fromSerializable(item);
|
|
3596
|
+
map.set(conv.id, conv);
|
|
3597
|
+
}
|
|
3598
|
+
return map;
|
|
3599
|
+
} catch {
|
|
3600
|
+
return new Map;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
function saveAll(key, map) {
|
|
3604
|
+
if (typeof window === "undefined")
|
|
3605
|
+
return;
|
|
3606
|
+
try {
|
|
3607
|
+
const arr = Array.from(map.values()).map(toSerializable);
|
|
3608
|
+
window.localStorage.setItem(key, JSON.stringify(arr));
|
|
3609
|
+
} catch {}
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
class LocalStorageConversationStore {
|
|
3613
|
+
key;
|
|
3614
|
+
cache = null;
|
|
3615
|
+
constructor(storageKey = DEFAULT_KEY) {
|
|
3616
|
+
this.key = storageKey;
|
|
3617
|
+
}
|
|
3618
|
+
getMap() {
|
|
3619
|
+
if (!this.cache) {
|
|
3620
|
+
this.cache = loadAll(this.key);
|
|
3621
|
+
}
|
|
3622
|
+
return this.cache;
|
|
3623
|
+
}
|
|
3624
|
+
persist() {
|
|
3625
|
+
saveAll(this.key, this.getMap());
|
|
3626
|
+
}
|
|
3627
|
+
async get(conversationId) {
|
|
3628
|
+
return this.getMap().get(conversationId) ?? null;
|
|
3629
|
+
}
|
|
3630
|
+
async create(conversation) {
|
|
3631
|
+
const now = new Date;
|
|
3632
|
+
const full = {
|
|
3633
|
+
...conversation,
|
|
3634
|
+
id: generateId2("conv"),
|
|
3635
|
+
createdAt: now,
|
|
3636
|
+
updatedAt: now
|
|
3637
|
+
};
|
|
3638
|
+
this.getMap().set(full.id, full);
|
|
3639
|
+
this.persist();
|
|
3640
|
+
return full;
|
|
3641
|
+
}
|
|
3642
|
+
async update(conversationId, updates) {
|
|
3643
|
+
const conv = this.getMap().get(conversationId);
|
|
3644
|
+
if (!conv)
|
|
3645
|
+
return null;
|
|
3646
|
+
const updated = {
|
|
3647
|
+
...conv,
|
|
3648
|
+
...updates,
|
|
3649
|
+
updatedAt: new Date
|
|
3650
|
+
};
|
|
3651
|
+
this.getMap().set(conversationId, updated);
|
|
3652
|
+
this.persist();
|
|
3653
|
+
return updated;
|
|
3654
|
+
}
|
|
3655
|
+
async appendMessage(conversationId, message) {
|
|
3656
|
+
const conv = this.getMap().get(conversationId);
|
|
3657
|
+
if (!conv)
|
|
3658
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
3659
|
+
const now = new Date;
|
|
3660
|
+
const fullMessage = {
|
|
3661
|
+
...message,
|
|
3662
|
+
id: generateId2("msg"),
|
|
3663
|
+
conversationId,
|
|
3664
|
+
createdAt: now,
|
|
3665
|
+
updatedAt: now
|
|
3666
|
+
};
|
|
3667
|
+
conv.messages.push(fullMessage);
|
|
3668
|
+
conv.updatedAt = now;
|
|
3669
|
+
this.persist();
|
|
3670
|
+
return fullMessage;
|
|
3671
|
+
}
|
|
3672
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
3673
|
+
const conv = this.getMap().get(conversationId);
|
|
3674
|
+
if (!conv)
|
|
3675
|
+
return null;
|
|
3676
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
3677
|
+
if (idx === -1)
|
|
3678
|
+
return null;
|
|
3679
|
+
const msg = conv.messages[idx];
|
|
3680
|
+
if (!msg)
|
|
3681
|
+
return null;
|
|
3682
|
+
const updated = {
|
|
3683
|
+
...msg,
|
|
3684
|
+
...updates,
|
|
3685
|
+
updatedAt: new Date
|
|
3686
|
+
};
|
|
3687
|
+
conv.messages[idx] = updated;
|
|
3688
|
+
conv.updatedAt = new Date;
|
|
3689
|
+
this.persist();
|
|
3690
|
+
return updated;
|
|
3691
|
+
}
|
|
3692
|
+
async delete(conversationId) {
|
|
3693
|
+
const deleted = this.getMap().delete(conversationId);
|
|
3694
|
+
if (deleted)
|
|
3695
|
+
this.persist();
|
|
3696
|
+
return deleted;
|
|
3697
|
+
}
|
|
3698
|
+
async list(options) {
|
|
3699
|
+
let results = Array.from(this.getMap().values());
|
|
3700
|
+
if (options?.status) {
|
|
3701
|
+
results = results.filter((c) => c.status === options.status);
|
|
3702
|
+
}
|
|
3703
|
+
if (options?.projectId) {
|
|
3704
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
3705
|
+
}
|
|
3706
|
+
if (options?.tags && options.tags.length > 0) {
|
|
3707
|
+
const tagSet = new Set(options.tags);
|
|
3708
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
3709
|
+
}
|
|
3710
|
+
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
3711
|
+
const offset = options?.offset ?? 0;
|
|
3712
|
+
const limit = options?.limit ?? 100;
|
|
3713
|
+
return results.slice(offset, offset + limit);
|
|
3714
|
+
}
|
|
3715
|
+
async fork(conversationId, upToMessageId) {
|
|
3716
|
+
const source = this.getMap().get(conversationId);
|
|
3717
|
+
if (!source)
|
|
3718
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
3719
|
+
let messagesToCopy = source.messages;
|
|
3720
|
+
if (upToMessageId) {
|
|
3721
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
3722
|
+
if (idx === -1)
|
|
3723
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
3724
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
3725
|
+
}
|
|
3726
|
+
const now = new Date;
|
|
3727
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
3728
|
+
...m,
|
|
3729
|
+
id: generateId2("msg"),
|
|
3730
|
+
conversationId: "",
|
|
3731
|
+
createdAt: new Date(m.createdAt),
|
|
3732
|
+
updatedAt: new Date(m.updatedAt)
|
|
3733
|
+
}));
|
|
3734
|
+
const forked = {
|
|
3735
|
+
...source,
|
|
3736
|
+
id: generateId2("conv"),
|
|
3737
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
3738
|
+
forkedFromId: source.id,
|
|
3739
|
+
createdAt: now,
|
|
3740
|
+
updatedAt: now,
|
|
3741
|
+
messages: forkedMessages
|
|
3742
|
+
};
|
|
3743
|
+
for (const m of forked.messages) {
|
|
3744
|
+
m.conversationId = forked.id;
|
|
3745
|
+
}
|
|
3746
|
+
this.getMap().set(forked.id, forked);
|
|
3747
|
+
this.persist();
|
|
3748
|
+
return forked;
|
|
3749
|
+
}
|
|
3750
|
+
async truncateAfter(conversationId, messageId) {
|
|
3751
|
+
const conv = this.getMap().get(conversationId);
|
|
3752
|
+
if (!conv)
|
|
3753
|
+
return null;
|
|
3754
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
3755
|
+
if (idx === -1)
|
|
3756
|
+
return null;
|
|
3757
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
3758
|
+
conv.updatedAt = new Date;
|
|
3759
|
+
this.persist();
|
|
3760
|
+
return conv;
|
|
3761
|
+
}
|
|
3762
|
+
async search(query, limit = 20) {
|
|
3763
|
+
const lowerQuery = query.toLowerCase();
|
|
3764
|
+
const results = [];
|
|
3765
|
+
for (const conv of this.getMap().values()) {
|
|
3766
|
+
if (conv.title?.toLowerCase().includes(lowerQuery)) {
|
|
3767
|
+
results.push(conv);
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
|
|
3771
|
+
results.push(conv);
|
|
3772
|
+
}
|
|
3773
|
+
if (results.length >= limit)
|
|
3774
|
+
break;
|
|
3775
|
+
}
|
|
3776
|
+
return results;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
function createLocalStorageConversationStore(storageKey) {
|
|
3780
|
+
return new LocalStorageConversationStore(storageKey);
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
// src/presentation/components/ChatWithSidebar.tsx
|
|
3784
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3785
|
+
"use client";
|
|
3786
|
+
var defaultStore = createLocalStorageConversationStore();
|
|
3787
|
+
function ChatWithSidebar({
|
|
3788
|
+
store = defaultStore,
|
|
3789
|
+
projectId,
|
|
3790
|
+
tags,
|
|
3791
|
+
className,
|
|
3792
|
+
thinkingLevel: initialThinkingLevel = "thinking",
|
|
3793
|
+
presentationRenderer,
|
|
3794
|
+
formRenderer,
|
|
3795
|
+
...useChatOptions
|
|
3796
|
+
}) {
|
|
3797
|
+
const effectiveStore = store;
|
|
3798
|
+
const [thinkingLevel, setThinkingLevel] = React12.useState(initialThinkingLevel);
|
|
3799
|
+
const chat = useChat({
|
|
3800
|
+
...useChatOptions,
|
|
3801
|
+
store: effectiveStore,
|
|
3802
|
+
thinkingLevel
|
|
3803
|
+
});
|
|
3804
|
+
const {
|
|
3805
|
+
messages,
|
|
3806
|
+
conversation,
|
|
3807
|
+
sendMessage,
|
|
3808
|
+
isLoading,
|
|
3809
|
+
setConversationId,
|
|
3810
|
+
createNewConversation,
|
|
3811
|
+
editMessage,
|
|
3812
|
+
forkConversation,
|
|
3813
|
+
updateConversation
|
|
3814
|
+
} = chat;
|
|
3815
|
+
const selectedConversationId = conversation?.id ?? null;
|
|
3816
|
+
const handleSelectConversation = React12.useCallback((id) => {
|
|
3817
|
+
setConversationId(id);
|
|
3818
|
+
}, [setConversationId]);
|
|
3819
|
+
return /* @__PURE__ */ jsxs10("div", {
|
|
3820
|
+
className: className ?? "flex h-full w-full",
|
|
3821
|
+
children: [
|
|
3822
|
+
/* @__PURE__ */ jsx10(ChatSidebar, {
|
|
3823
|
+
store: effectiveStore,
|
|
3824
|
+
selectedConversationId,
|
|
3825
|
+
onSelectConversation: handleSelectConversation,
|
|
3826
|
+
onCreateNew: createNewConversation,
|
|
3827
|
+
projectId,
|
|
3828
|
+
tags,
|
|
3829
|
+
selectedConversation: conversation,
|
|
3830
|
+
onUpdateConversation: updateConversation ? async (id, updates) => {
|
|
3831
|
+
if (id === selectedConversationId) {
|
|
3832
|
+
await updateConversation(updates);
|
|
3833
|
+
}
|
|
3834
|
+
} : undefined
|
|
3835
|
+
}),
|
|
3836
|
+
/* @__PURE__ */ jsx10("div", {
|
|
3837
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
3838
|
+
children: /* @__PURE__ */ jsx10(ChatWithExport, {
|
|
3839
|
+
messages,
|
|
3840
|
+
conversation,
|
|
3841
|
+
onCreateNew: createNewConversation,
|
|
3842
|
+
onFork: forkConversation,
|
|
3843
|
+
onEditMessage: editMessage,
|
|
3844
|
+
thinkingLevel,
|
|
3845
|
+
onThinkingLevelChange: setThinkingLevel,
|
|
3846
|
+
presentationRenderer,
|
|
3847
|
+
formRenderer,
|
|
3848
|
+
children: /* @__PURE__ */ jsx10(ChatInput, {
|
|
3849
|
+
onSend: (content, att) => sendMessage(content, att),
|
|
3850
|
+
disabled: isLoading,
|
|
3851
|
+
isLoading
|
|
3852
|
+
})
|
|
3853
|
+
})
|
|
3854
|
+
})
|
|
3855
|
+
]
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
// src/presentation/components/ModelPicker.tsx
|
|
3859
|
+
import * as React13 from "react";
|
|
3860
|
+
import { cn as cn7 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
3861
|
+
import { Button as Button6 } from "@contractspec/lib.design-system";
|
|
3862
|
+
import {
|
|
3863
|
+
Select as Select2,
|
|
3864
|
+
SelectContent as SelectContent2,
|
|
3865
|
+
SelectItem as SelectItem2,
|
|
3866
|
+
SelectTrigger as SelectTrigger2,
|
|
3867
|
+
SelectValue as SelectValue2
|
|
3868
|
+
} from "@contractspec/lib.ui-kit-web/ui/select";
|
|
3869
|
+
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
3870
|
+
import { Label as Label2 } from "@contractspec/lib.ui-kit-web/ui/label";
|
|
3871
|
+
import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
|
|
3872
|
+
import {
|
|
3873
|
+
getModelsForProvider
|
|
3874
|
+
} from "@contractspec/lib.ai-providers";
|
|
3875
|
+
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3876
|
+
"use client";
|
|
3877
|
+
var PROVIDER_ICONS = {
|
|
3878
|
+
ollama: /* @__PURE__ */ jsx11(Cpu, {
|
|
3879
|
+
className: "h-4 w-4"
|
|
3880
|
+
}),
|
|
3881
|
+
openai: /* @__PURE__ */ jsx11(Bot2, {
|
|
3882
|
+
className: "h-4 w-4"
|
|
3883
|
+
}),
|
|
3884
|
+
anthropic: /* @__PURE__ */ jsx11(Sparkles, {
|
|
3885
|
+
className: "h-4 w-4"
|
|
3886
|
+
}),
|
|
3887
|
+
mistral: /* @__PURE__ */ jsx11(Cloud, {
|
|
3888
|
+
className: "h-4 w-4"
|
|
3889
|
+
}),
|
|
3890
|
+
gemini: /* @__PURE__ */ jsx11(Sparkles, {
|
|
3891
|
+
className: "h-4 w-4"
|
|
3892
|
+
})
|
|
3893
|
+
};
|
|
3894
|
+
var PROVIDER_NAMES = {
|
|
3895
|
+
ollama: "Ollama (Local)",
|
|
3896
|
+
openai: "OpenAI",
|
|
3897
|
+
anthropic: "Anthropic",
|
|
3898
|
+
mistral: "Mistral",
|
|
3899
|
+
gemini: "Google Gemini"
|
|
3900
|
+
};
|
|
3901
|
+
var MODE_BADGES = {
|
|
3902
|
+
local: { label: "Local", variant: "secondary" },
|
|
3903
|
+
byok: { label: "BYOK", variant: "outline" },
|
|
3904
|
+
managed: { label: "Managed", variant: "default" }
|
|
3905
|
+
};
|
|
3906
|
+
function ModelPicker({
|
|
3907
|
+
value,
|
|
3908
|
+
onChange,
|
|
3909
|
+
availableProviders,
|
|
3910
|
+
className,
|
|
3911
|
+
compact = false
|
|
3912
|
+
}) {
|
|
3913
|
+
const providers = availableProviders ?? [
|
|
3914
|
+
{ provider: "ollama", available: true, mode: "local" },
|
|
3915
|
+
{ provider: "openai", available: true, mode: "byok" },
|
|
3916
|
+
{ provider: "anthropic", available: true, mode: "byok" },
|
|
3917
|
+
{ provider: "mistral", available: true, mode: "byok" },
|
|
3918
|
+
{ provider: "gemini", available: true, mode: "byok" }
|
|
3919
|
+
];
|
|
3920
|
+
const models = getModelsForProvider(value.provider);
|
|
3921
|
+
const selectedModel = models.find((m) => m.id === value.model);
|
|
3922
|
+
const handleProviderChange = React13.useCallback((providerName) => {
|
|
3923
|
+
const provider = providerName;
|
|
3924
|
+
const providerInfo = providers.find((p) => p.provider === provider);
|
|
3925
|
+
const providerModels = getModelsForProvider(provider);
|
|
3926
|
+
const defaultModel = providerModels[0]?.id ?? "";
|
|
3927
|
+
onChange({
|
|
3928
|
+
provider,
|
|
3929
|
+
model: defaultModel,
|
|
3930
|
+
mode: providerInfo?.mode ?? "byok"
|
|
3931
|
+
});
|
|
3932
|
+
}, [onChange, providers]);
|
|
3933
|
+
const handleModelChange = React13.useCallback((modelId) => {
|
|
3934
|
+
onChange({
|
|
3935
|
+
...value,
|
|
3936
|
+
model: modelId
|
|
3937
|
+
});
|
|
3938
|
+
}, [onChange, value]);
|
|
3939
|
+
if (compact) {
|
|
3940
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3941
|
+
className: cn7("flex items-center gap-2", className),
|
|
3942
|
+
children: [
|
|
3943
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3944
|
+
value: value.provider,
|
|
3945
|
+
onValueChange: handleProviderChange,
|
|
3946
|
+
children: [
|
|
3947
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3948
|
+
className: "w-[140px]",
|
|
3949
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3950
|
+
}),
|
|
3951
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3952
|
+
children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3953
|
+
value: p.provider,
|
|
3954
|
+
disabled: !p.available,
|
|
3955
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
3956
|
+
className: "flex items-center gap-2",
|
|
3957
|
+
children: [
|
|
3958
|
+
PROVIDER_ICONS[p.provider],
|
|
3959
|
+
/* @__PURE__ */ jsx11("span", {
|
|
3960
|
+
children: PROVIDER_NAMES[p.provider]
|
|
3961
|
+
})
|
|
3962
|
+
]
|
|
3963
|
+
})
|
|
3964
|
+
}, p.provider))
|
|
3965
|
+
})
|
|
3966
|
+
]
|
|
3967
|
+
}),
|
|
3968
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
3969
|
+
value: value.model,
|
|
3970
|
+
onValueChange: handleModelChange,
|
|
3971
|
+
children: [
|
|
3972
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
3973
|
+
className: "w-[160px]",
|
|
3974
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {})
|
|
3975
|
+
}),
|
|
3976
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
3977
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
3978
|
+
value: m.id,
|
|
3979
|
+
children: m.name
|
|
3980
|
+
}, m.id))
|
|
3981
|
+
})
|
|
3982
|
+
]
|
|
3983
|
+
})
|
|
3984
|
+
]
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
return /* @__PURE__ */ jsxs11("div", {
|
|
3988
|
+
className: cn7("flex flex-col gap-3", className),
|
|
3989
|
+
children: [
|
|
3990
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
3991
|
+
className: "flex flex-col gap-1.5",
|
|
3992
|
+
children: [
|
|
3993
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
3994
|
+
htmlFor: "provider-selection",
|
|
3995
|
+
className: "text-sm font-medium",
|
|
3996
|
+
children: "Provider"
|
|
3997
|
+
}),
|
|
3998
|
+
/* @__PURE__ */ jsx11("div", {
|
|
3999
|
+
className: "flex flex-wrap gap-2",
|
|
4000
|
+
id: "provider-selection",
|
|
4001
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
|
|
4002
|
+
variant: value.provider === p.provider ? "default" : "outline",
|
|
4003
|
+
size: "sm",
|
|
4004
|
+
onPress: () => p.available && handleProviderChange(p.provider),
|
|
4005
|
+
disabled: !p.available,
|
|
4006
|
+
className: cn7(!p.available && "opacity-50"),
|
|
4007
|
+
children: [
|
|
4008
|
+
PROVIDER_ICONS[p.provider],
|
|
4009
|
+
/* @__PURE__ */ jsx11("span", {
|
|
4010
|
+
children: PROVIDER_NAMES[p.provider]
|
|
4011
|
+
}),
|
|
4012
|
+
/* @__PURE__ */ jsx11(Badge, {
|
|
4013
|
+
variant: MODE_BADGES[p.mode].variant,
|
|
4014
|
+
className: "ml-1",
|
|
4015
|
+
children: MODE_BADGES[p.mode].label
|
|
4016
|
+
})
|
|
4017
|
+
]
|
|
4018
|
+
}, p.provider))
|
|
4019
|
+
})
|
|
4020
|
+
]
|
|
4021
|
+
}),
|
|
4022
|
+
/* @__PURE__ */ jsxs11("div", {
|
|
4023
|
+
className: "flex flex-col gap-1.5",
|
|
4024
|
+
children: [
|
|
4025
|
+
/* @__PURE__ */ jsx11(Label2, {
|
|
4026
|
+
htmlFor: "model-picker",
|
|
4027
|
+
className: "text-sm font-medium",
|
|
4028
|
+
children: "Model"
|
|
4029
|
+
}),
|
|
4030
|
+
/* @__PURE__ */ jsxs11(Select2, {
|
|
4031
|
+
name: "model-picker",
|
|
4032
|
+
value: value.model,
|
|
4033
|
+
onValueChange: handleModelChange,
|
|
4034
|
+
children: [
|
|
4035
|
+
/* @__PURE__ */ jsx11(SelectTrigger2, {
|
|
4036
|
+
children: /* @__PURE__ */ jsx11(SelectValue2, {
|
|
4037
|
+
placeholder: "Select a model"
|
|
4038
|
+
})
|
|
4039
|
+
}),
|
|
4040
|
+
/* @__PURE__ */ jsx11(SelectContent2, {
|
|
4041
|
+
children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
|
|
4042
|
+
value: m.id,
|
|
4043
|
+
children: /* @__PURE__ */ jsxs11("div", {
|
|
4044
|
+
className: "flex items-center gap-2",
|
|
4045
|
+
children: [
|
|
4046
|
+
/* @__PURE__ */ jsx11("span", {
|
|
4047
|
+
children: m.name
|
|
4048
|
+
}),
|
|
4049
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
4050
|
+
className: "text-muted-foreground text-xs",
|
|
4051
|
+
children: [
|
|
4052
|
+
Math.round(m.contextWindow / 1000),
|
|
4053
|
+
"K"
|
|
4054
|
+
]
|
|
4055
|
+
}),
|
|
4056
|
+
m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
|
|
4057
|
+
variant: "outline",
|
|
4058
|
+
className: "text-xs",
|
|
4059
|
+
children: "Vision"
|
|
4060
|
+
}),
|
|
4061
|
+
m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
|
|
4062
|
+
variant: "outline",
|
|
4063
|
+
className: "text-xs",
|
|
4064
|
+
children: "Reasoning"
|
|
4065
|
+
})
|
|
4066
|
+
]
|
|
4067
|
+
})
|
|
4068
|
+
}, m.id))
|
|
4069
|
+
})
|
|
4070
|
+
]
|
|
4071
|
+
})
|
|
4072
|
+
]
|
|
4073
|
+
}),
|
|
4074
|
+
selectedModel && /* @__PURE__ */ jsxs11("div", {
|
|
4075
|
+
className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
|
|
4076
|
+
children: [
|
|
4077
|
+
/* @__PURE__ */ jsxs11("span", {
|
|
4078
|
+
children: [
|
|
4079
|
+
"Context: ",
|
|
4080
|
+
Math.round(selectedModel.contextWindow / 1000),
|
|
4081
|
+
"K tokens"
|
|
4082
|
+
]
|
|
4083
|
+
}),
|
|
4084
|
+
selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
|
|
4085
|
+
children: "\u2022 Vision"
|
|
4086
|
+
}),
|
|
4087
|
+
selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
|
|
4088
|
+
children: "\u2022 Tools"
|
|
4089
|
+
}),
|
|
4090
|
+
selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
|
|
4091
|
+
children: "\u2022 Reasoning"
|
|
4092
|
+
})
|
|
4093
|
+
]
|
|
4094
|
+
})
|
|
4095
|
+
]
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
// src/presentation/components/ContextIndicator.tsx
|
|
4099
|
+
import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
4100
|
+
import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
4101
|
+
import {
|
|
4102
|
+
Tooltip,
|
|
4103
|
+
TooltipContent,
|
|
4104
|
+
TooltipProvider,
|
|
4105
|
+
TooltipTrigger
|
|
4106
|
+
} from "@contractspec/lib.ui-kit-web/ui/tooltip";
|
|
4107
|
+
import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
|
|
4108
|
+
import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
|
|
4109
|
+
"use client";
|
|
4110
|
+
function ContextIndicator({
|
|
4111
|
+
summary,
|
|
4112
|
+
active = false,
|
|
4113
|
+
className,
|
|
4114
|
+
showDetails = true
|
|
4115
|
+
}) {
|
|
4116
|
+
if (!summary && !active) {
|
|
4117
|
+
return /* @__PURE__ */ jsxs12("div", {
|
|
4118
|
+
className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
|
|
4119
|
+
children: [
|
|
4120
|
+
/* @__PURE__ */ jsx12(Info, {
|
|
4121
|
+
className: "h-4 w-4"
|
|
4122
|
+
}),
|
|
4123
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4124
|
+
children: "No workspace context"
|
|
4125
|
+
})
|
|
4126
|
+
]
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
const content = /* @__PURE__ */ jsxs12("div", {
|
|
4130
|
+
className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
|
|
4131
|
+
children: [
|
|
4132
|
+
/* @__PURE__ */ jsxs12(Badge2, {
|
|
4133
|
+
variant: active ? "default" : "secondary",
|
|
4134
|
+
className: "flex items-center gap-1",
|
|
4135
|
+
children: [
|
|
4136
|
+
/* @__PURE__ */ jsx12(Zap, {
|
|
4137
|
+
className: "h-3 w-3"
|
|
4138
|
+
}),
|
|
4139
|
+
"Context"
|
|
4140
|
+
]
|
|
4141
|
+
}),
|
|
4142
|
+
summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
|
|
4143
|
+
children: [
|
|
4144
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
4145
|
+
className: "flex items-center gap-1 text-xs",
|
|
4146
|
+
children: [
|
|
4147
|
+
/* @__PURE__ */ jsx12(FolderOpen, {
|
|
4148
|
+
className: "h-3.5 w-3.5"
|
|
4149
|
+
}),
|
|
4150
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4151
|
+
children: summary.name
|
|
4152
|
+
})
|
|
4153
|
+
]
|
|
4154
|
+
}),
|
|
4155
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
4156
|
+
className: "flex items-center gap-1 text-xs",
|
|
4157
|
+
children: [
|
|
4158
|
+
/* @__PURE__ */ jsx12(FileCode, {
|
|
4159
|
+
className: "h-3.5 w-3.5"
|
|
4160
|
+
}),
|
|
4161
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
4162
|
+
children: [
|
|
4163
|
+
summary.specs.total,
|
|
4164
|
+
" specs"
|
|
4165
|
+
]
|
|
4166
|
+
})
|
|
4167
|
+
]
|
|
4168
|
+
})
|
|
4169
|
+
]
|
|
4170
|
+
})
|
|
4171
|
+
]
|
|
4172
|
+
});
|
|
4173
|
+
if (!summary) {
|
|
4174
|
+
return content;
|
|
4175
|
+
}
|
|
4176
|
+
return /* @__PURE__ */ jsx12(TooltipProvider, {
|
|
4177
|
+
children: /* @__PURE__ */ jsxs12(Tooltip, {
|
|
4178
|
+
children: [
|
|
4179
|
+
/* @__PURE__ */ jsx12(TooltipTrigger, {
|
|
4180
|
+
asChild: true,
|
|
4181
|
+
children: content
|
|
4182
|
+
}),
|
|
4183
|
+
/* @__PURE__ */ jsx12(TooltipContent, {
|
|
4184
|
+
side: "bottom",
|
|
4185
|
+
className: "max-w-[300px]",
|
|
4186
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
4187
|
+
className: "flex flex-col gap-2 text-sm",
|
|
4188
|
+
children: [
|
|
4189
|
+
/* @__PURE__ */ jsx12("div", {
|
|
4190
|
+
className: "font-medium",
|
|
4191
|
+
children: summary.name
|
|
4192
|
+
}),
|
|
4193
|
+
/* @__PURE__ */ jsx12("div", {
|
|
4194
|
+
className: "text-muted-foreground text-xs",
|
|
4195
|
+
children: summary.path
|
|
4196
|
+
}),
|
|
4197
|
+
/* @__PURE__ */ jsx12("div", {
|
|
4198
|
+
className: "border-t pt-2",
|
|
4199
|
+
children: /* @__PURE__ */ jsxs12("div", {
|
|
4200
|
+
className: "grid grid-cols-2 gap-1 text-xs",
|
|
4201
|
+
children: [
|
|
4202
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4203
|
+
children: "Commands:"
|
|
4204
|
+
}),
|
|
4205
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4206
|
+
className: "text-right",
|
|
4207
|
+
children: summary.specs.commands
|
|
4208
|
+
}),
|
|
4209
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4210
|
+
children: "Queries:"
|
|
4211
|
+
}),
|
|
4212
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4213
|
+
className: "text-right",
|
|
4214
|
+
children: summary.specs.queries
|
|
4215
|
+
}),
|
|
4216
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4217
|
+
children: "Events:"
|
|
4218
|
+
}),
|
|
4219
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4220
|
+
className: "text-right",
|
|
4221
|
+
children: summary.specs.events
|
|
4222
|
+
}),
|
|
4223
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4224
|
+
children: "Presentations:"
|
|
4225
|
+
}),
|
|
4226
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4227
|
+
className: "text-right",
|
|
4228
|
+
children: summary.specs.presentations
|
|
4229
|
+
})
|
|
4230
|
+
]
|
|
4231
|
+
})
|
|
4232
|
+
}),
|
|
4233
|
+
/* @__PURE__ */ jsxs12("div", {
|
|
4234
|
+
className: "border-t pt-2 text-xs",
|
|
4235
|
+
children: [
|
|
4236
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
4237
|
+
children: [
|
|
4238
|
+
summary.files.total,
|
|
4239
|
+
" files"
|
|
4240
|
+
]
|
|
4241
|
+
}),
|
|
4242
|
+
/* @__PURE__ */ jsx12("span", {
|
|
4243
|
+
className: "mx-1",
|
|
4244
|
+
children: "\u2022"
|
|
4245
|
+
}),
|
|
4246
|
+
/* @__PURE__ */ jsxs12("span", {
|
|
4247
|
+
children: [
|
|
4248
|
+
summary.files.specFiles,
|
|
4249
|
+
" spec files"
|
|
4250
|
+
]
|
|
4251
|
+
})
|
|
4252
|
+
]
|
|
4253
|
+
})
|
|
4254
|
+
]
|
|
4255
|
+
})
|
|
4256
|
+
})
|
|
4257
|
+
]
|
|
4258
|
+
})
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
2136
4261
|
// src/presentation/hooks/useProviders.tsx
|
|
2137
|
-
import * as
|
|
4262
|
+
import * as React14 from "react";
|
|
2138
4263
|
import {
|
|
2139
4264
|
getAvailableProviders,
|
|
2140
4265
|
getModelsForProvider as getModelsForProvider2
|
|
2141
4266
|
} from "@contractspec/lib.ai-providers";
|
|
2142
4267
|
"use client";
|
|
2143
4268
|
function useProviders() {
|
|
2144
|
-
const [providers, setProviders] =
|
|
2145
|
-
const [isLoading, setIsLoading] =
|
|
2146
|
-
const loadProviders =
|
|
4269
|
+
const [providers, setProviders] = React14.useState([]);
|
|
4270
|
+
const [isLoading, setIsLoading] = React14.useState(true);
|
|
4271
|
+
const loadProviders = React14.useCallback(async () => {
|
|
2147
4272
|
setIsLoading(true);
|
|
2148
4273
|
try {
|
|
2149
4274
|
const available = getAvailableProviders();
|
|
@@ -2158,12 +4283,12 @@ function useProviders() {
|
|
|
2158
4283
|
setIsLoading(false);
|
|
2159
4284
|
}
|
|
2160
4285
|
}, []);
|
|
2161
|
-
|
|
4286
|
+
React14.useEffect(() => {
|
|
2162
4287
|
loadProviders();
|
|
2163
4288
|
}, [loadProviders]);
|
|
2164
|
-
const availableProviders =
|
|
2165
|
-
const isAvailable =
|
|
2166
|
-
const getModelsCallback =
|
|
4289
|
+
const availableProviders = React14.useMemo(() => providers.filter((p) => p.available), [providers]);
|
|
4290
|
+
const isAvailable = React14.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
|
|
4291
|
+
const getModelsCallback = React14.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
|
|
2167
4292
|
return {
|
|
2168
4293
|
providers,
|
|
2169
4294
|
availableProviders,
|
|
@@ -2507,15 +4632,64 @@ var ChatErrorEvent = defineEvent({
|
|
|
2507
4632
|
}
|
|
2508
4633
|
})
|
|
2509
4634
|
});
|
|
4635
|
+
// src/adapters/ai-sdk-bundle-adapter.ts
|
|
4636
|
+
import { compilePlannerPrompt as compilePlannerPrompt2 } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
4637
|
+
function createAiSdkBundleAdapter(deps) {
|
|
4638
|
+
const { provider, onPatchProposal } = deps;
|
|
4639
|
+
return {
|
|
4640
|
+
startThread(_args) {
|
|
4641
|
+
return null;
|
|
4642
|
+
},
|
|
4643
|
+
async requestPatches(args) {
|
|
4644
|
+
const proposals = [];
|
|
4645
|
+
const captureProposal = (p) => {
|
|
4646
|
+
proposals.push(p);
|
|
4647
|
+
onPatchProposal?.(p);
|
|
4648
|
+
};
|
|
4649
|
+
const plannerInput = buildPlannerPromptInput(args.currentPlan);
|
|
4650
|
+
const systemPrompt = compilePlannerPrompt2(plannerInput);
|
|
4651
|
+
const service = new ChatService({
|
|
4652
|
+
provider,
|
|
4653
|
+
systemPrompt,
|
|
4654
|
+
surfacePlanConfig: {
|
|
4655
|
+
plan: args.currentPlan,
|
|
4656
|
+
onPatchProposal: captureProposal
|
|
4657
|
+
}
|
|
4658
|
+
});
|
|
4659
|
+
await service.send({
|
|
4660
|
+
content: args.userMessage
|
|
4661
|
+
});
|
|
4662
|
+
return proposals;
|
|
4663
|
+
}
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
4666
|
+
// src/core/agent-adapter.ts
|
|
4667
|
+
function createChatAgentAdapter(agent) {
|
|
4668
|
+
return {
|
|
4669
|
+
async generate({ prompt, signal }) {
|
|
4670
|
+
const result = await agent.generate({ prompt, signal });
|
|
4671
|
+
return {
|
|
4672
|
+
text: result.text,
|
|
4673
|
+
toolCalls: result.toolCalls,
|
|
4674
|
+
toolResults: result.toolResults,
|
|
4675
|
+
usage: result.usage
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
2510
4680
|
export {
|
|
2511
4681
|
validateProvider,
|
|
2512
4682
|
useProviders,
|
|
4683
|
+
useMessageSelection,
|
|
4684
|
+
useConversations,
|
|
2513
4685
|
useCompletion,
|
|
2514
4686
|
useChat,
|
|
2515
4687
|
supportsLocalMode,
|
|
2516
4688
|
listOllamaModels,
|
|
2517
4689
|
isStudioAvailable,
|
|
4690
|
+
isPresentationToolResult,
|
|
2518
4691
|
isOllamaRunning,
|
|
4692
|
+
isFormToolResult,
|
|
2519
4693
|
hasCredentials,
|
|
2520
4694
|
getRecommendedModels,
|
|
2521
4695
|
getModelsForProvider3 as getModelsForProvider,
|
|
@@ -2528,7 +4702,11 @@ export {
|
|
|
2528
4702
|
createProvider2 as createProvider,
|
|
2529
4703
|
createNodeFileOperations,
|
|
2530
4704
|
createContextBuilder,
|
|
4705
|
+
createChatAgentAdapter,
|
|
4706
|
+
createAiSdkBundleAdapter,
|
|
2531
4707
|
WorkspaceContext,
|
|
4708
|
+
ToolResultRenderer,
|
|
4709
|
+
ThinkingLevelPicker,
|
|
2532
4710
|
StreamMessageContract,
|
|
2533
4711
|
SendMessageOutputModel,
|
|
2534
4712
|
SendMessageInputModel,
|
|
@@ -2550,10 +4728,14 @@ export {
|
|
|
2550
4728
|
ContextIndicator,
|
|
2551
4729
|
ContextBuilder,
|
|
2552
4730
|
CodePreview,
|
|
4731
|
+
ChatWithSidebar,
|
|
4732
|
+
ChatWithExport,
|
|
4733
|
+
ChatSidebar,
|
|
2553
4734
|
ChatMessageModel,
|
|
2554
4735
|
ChatMessage as ChatMessageComponent,
|
|
2555
4736
|
ChatMessage,
|
|
2556
4737
|
ChatInput,
|
|
4738
|
+
ChatExportToolbar,
|
|
2557
4739
|
ChatErrorEvent,
|
|
2558
4740
|
ChatConversationModel,
|
|
2559
4741
|
ChatContainer,
|