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