@contractspec/module.ai-chat 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/ai-chat.feature.d.ts +12 -0
- package/dist/ai-chat.feature.d.ts.map +1 -0
- package/dist/ai-chat.feature.js +95 -0
- package/dist/ai-chat.feature.js.map +1 -0
- package/dist/ai-chat.operations.d.ts +243 -0
- package/dist/ai-chat.operations.d.ts.map +1 -0
- package/dist/ai-chat.operations.js +174 -0
- package/dist/ai-chat.operations.js.map +1 -0
- package/dist/context/context-builder.d.ts +57 -0
- package/dist/context/context-builder.d.ts.map +1 -0
- package/dist/context/context-builder.js +148 -0
- package/dist/context/context-builder.js.map +1 -0
- package/dist/context/file-operations.d.ts +100 -0
- package/dist/context/file-operations.d.ts.map +1 -0
- package/dist/context/file-operations.js +175 -0
- package/dist/context/file-operations.js.map +1 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.js +5 -0
- package/dist/context/workspace-context.d.ts +117 -0
- package/dist/context/workspace-context.d.ts.map +1 -0
- package/dist/context/workspace-context.js +124 -0
- package/dist/context/workspace-context.js.map +1 -0
- package/dist/core/chat-service.d.ts +73 -0
- package/dist/core/chat-service.d.ts.map +1 -0
- package/dist/core/chat-service.js +227 -0
- package/dist/core/chat-service.js.map +1 -0
- package/dist/core/conversation-store.d.ts +74 -0
- package/dist/core/conversation-store.d.ts.map +1 -0
- package/dist/core/conversation-store.js +109 -0
- package/dist/core/conversation-store.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +4 -0
- package/dist/core/message-types.d.ts +150 -0
- package/dist/core/message-types.d.ts.map +1 -0
- package/dist/events.d.ts +115 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +100 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +23 -0
- package/dist/libs/schema/dist/EnumType.js +2 -0
- package/dist/libs/schema/dist/FieldType.js +50 -0
- package/dist/libs/schema/dist/FieldType.js.map +1 -0
- package/dist/libs/schema/dist/GraphQLSchemaType.js +1 -0
- package/dist/libs/schema/dist/JsonSchemaType.js +1 -0
- package/dist/libs/schema/dist/ScalarTypeEnum.js +237 -0
- package/dist/libs/schema/dist/ScalarTypeEnum.js.map +1 -0
- package/dist/libs/schema/dist/SchemaModel.js +40 -0
- package/dist/libs/schema/dist/SchemaModel.js.map +1 -0
- package/dist/libs/schema/dist/ZodSchemaType.js +1 -0
- package/dist/libs/schema/dist/entity/defineEntity.js +1 -0
- package/dist/libs/schema/dist/entity/index.js +2 -0
- package/dist/libs/schema/dist/entity/types.js +1 -0
- package/dist/libs/schema/dist/index.js +9 -0
- package/dist/presentation/components/ChatContainer.d.ts +21 -0
- package/dist/presentation/components/ChatContainer.d.ts.map +1 -0
- package/dist/presentation/components/ChatContainer.js +63 -0
- package/dist/presentation/components/ChatContainer.js.map +1 -0
- package/dist/presentation/components/ChatInput.d.ts +35 -0
- package/dist/presentation/components/ChatInput.d.ts.map +1 -0
- package/dist/presentation/components/ChatInput.js +149 -0
- package/dist/presentation/components/ChatInput.js.map +1 -0
- package/dist/presentation/components/ChatMessage.d.ts +24 -0
- package/dist/presentation/components/ChatMessage.d.ts.map +1 -0
- package/dist/presentation/components/ChatMessage.js +136 -0
- package/dist/presentation/components/ChatMessage.js.map +1 -0
- package/dist/presentation/components/CodePreview.d.ts +40 -0
- package/dist/presentation/components/CodePreview.d.ts.map +1 -0
- package/dist/presentation/components/CodePreview.js +127 -0
- package/dist/presentation/components/CodePreview.js.map +1 -0
- package/dist/presentation/components/ContextIndicator.d.ts +26 -0
- package/dist/presentation/components/ContextIndicator.d.ts.map +1 -0
- package/dist/presentation/components/ContextIndicator.js +97 -0
- package/dist/presentation/components/ContextIndicator.js.map +1 -0
- package/dist/presentation/components/ModelPicker.d.ts +39 -0
- package/dist/presentation/components/ModelPicker.d.ts.map +1 -0
- package/dist/presentation/components/ModelPicker.js +202 -0
- package/dist/presentation/components/ModelPicker.js.map +1 -0
- package/dist/presentation/components/index.d.ts +7 -0
- package/dist/presentation/components/index.js +8 -0
- package/dist/presentation/hooks/index.d.ts +3 -0
- package/dist/presentation/hooks/index.js +4 -0
- package/dist/presentation/hooks/useChat.d.ts +67 -0
- package/dist/presentation/hooks/useChat.d.ts.map +1 -0
- package/dist/presentation/hooks/useChat.js +172 -0
- package/dist/presentation/hooks/useChat.js.map +1 -0
- package/dist/presentation/hooks/useProviders.d.ts +38 -0
- package/dist/presentation/hooks/useProviders.d.ts.map +1 -0
- package/dist/presentation/hooks/useProviders.js +41 -0
- package/dist/presentation/hooks/useProviders.js.map +1 -0
- package/dist/presentation/index.d.ts +11 -0
- package/dist/presentation/index.js +12 -0
- package/dist/providers/chat-utilities.d.ts +15 -0
- package/dist/providers/chat-utilities.d.ts.map +1 -0
- package/dist/providers/chat-utilities.js +17 -0
- package/dist/providers/chat-utilities.js.map +1 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/schema.d.ts +222 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +102 -0
- package/dist/schema.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "zod";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./EnumType.js";
|
|
2
|
+
import { FieldType } from "./FieldType.js";
|
|
3
|
+
import "./GraphQLSchemaType.js";
|
|
4
|
+
import "./JsonSchemaType.js";
|
|
5
|
+
import { ScalarTypeEnum } from "./ScalarTypeEnum.js";
|
|
6
|
+
import { SchemaModel, defineSchemaModel } from "./SchemaModel.js";
|
|
7
|
+
import "./ZodSchemaType.js";
|
|
8
|
+
import "./entity/defineEntity.js";
|
|
9
|
+
import "./entity/index.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/presentation/components/ChatContainer.d.ts
|
|
5
|
+
interface ChatContainerProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Show scroll-to-bottom button when scrolled up */
|
|
9
|
+
showScrollButton?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Container component for chat messages with scrolling
|
|
13
|
+
*/
|
|
14
|
+
declare function ChatContainer({
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
showScrollButton
|
|
18
|
+
}: ChatContainerProps): react_jsx_runtime0.JSX.Element;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ChatContainer };
|
|
21
|
+
//# sourceMappingURL=ChatContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatContainer.d.ts","names":[],"sources":["../../../src/presentation/components/ChatContainer.tsx"],"sourcesContent":[],"mappings":";;;;UAMiB,kBAAA;YACL,KAAA,CAAM;;EADD;EAUD,gBAAa,CAAA,EAAA,OAAA;;;;;AAIR,iBAJL,aAAA,CAIK;EAAA,QAAA;EAAA,SAAA;EAAA;AAAA,CAAA,EAAlB,kBAAkB,CAAA,EAAA,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
|
|
5
|
+
import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/presentation/components/ChatContainer.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Container component for chat messages with scrolling
|
|
11
|
+
*/
|
|
12
|
+
function ChatContainer({ children, className, showScrollButton = true }) {
|
|
13
|
+
const scrollRef = React.useRef(null);
|
|
14
|
+
const [showScrollDown, setShowScrollDown] = React.useState(false);
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
const container = scrollRef.current;
|
|
17
|
+
if (!container) return;
|
|
18
|
+
if (container.scrollHeight - container.scrollTop <= container.clientHeight + 100) container.scrollTop = container.scrollHeight;
|
|
19
|
+
}, [children]);
|
|
20
|
+
const handleScroll = React.useCallback((event) => {
|
|
21
|
+
const container = event.currentTarget;
|
|
22
|
+
setShowScrollDown(!(container.scrollHeight - container.scrollTop <= container.clientHeight + 100));
|
|
23
|
+
}, []);
|
|
24
|
+
const scrollToBottom = React.useCallback(() => {
|
|
25
|
+
const container = scrollRef.current;
|
|
26
|
+
if (container) container.scrollTo({
|
|
27
|
+
top: container.scrollHeight,
|
|
28
|
+
behavior: "smooth"
|
|
29
|
+
});
|
|
30
|
+
}, []);
|
|
31
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
32
|
+
className: cn("relative flex flex-1 flex-col", className),
|
|
33
|
+
children: [/* @__PURE__ */ jsx(ScrollArea, {
|
|
34
|
+
ref: scrollRef,
|
|
35
|
+
className: "flex-1",
|
|
36
|
+
onScroll: handleScroll,
|
|
37
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
38
|
+
className: "flex flex-col gap-4 p-4",
|
|
39
|
+
children
|
|
40
|
+
})
|
|
41
|
+
}), showScrollButton && showScrollDown && /* @__PURE__ */ jsxs("button", {
|
|
42
|
+
onClick: scrollToBottom,
|
|
43
|
+
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"),
|
|
44
|
+
"aria-label": "Scroll to bottom",
|
|
45
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
46
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
47
|
+
width: "16",
|
|
48
|
+
height: "16",
|
|
49
|
+
viewBox: "0 0 24 24",
|
|
50
|
+
fill: "none",
|
|
51
|
+
stroke: "currentColor",
|
|
52
|
+
strokeWidth: "2",
|
|
53
|
+
strokeLinecap: "round",
|
|
54
|
+
strokeLinejoin: "round",
|
|
55
|
+
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
|
|
56
|
+
}), "New messages"]
|
|
57
|
+
})]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { ChatContainer };
|
|
63
|
+
//# sourceMappingURL=ChatContainer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatContainer.js","names":[],"sources":["../../../src/presentation/components/ChatContainer.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { ScrollArea } from '@contractspec/lib.ui-kit-web/ui/scroll-area';\nimport { cn } from '@contractspec/lib.ui-kit-web/ui/utils';\n\nexport interface ChatContainerProps {\n children: React.ReactNode;\n className?: string;\n /** Show scroll-to-bottom button when scrolled up */\n showScrollButton?: boolean;\n}\n\n/**\n * Container component for chat messages with scrolling\n */\nexport function ChatContainer({\n children,\n className,\n showScrollButton = true,\n}: ChatContainerProps) {\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const [showScrollDown, setShowScrollDown] = React.useState(false);\n\n // Auto-scroll to bottom when children change\n React.useEffect(() => {\n const container = scrollRef.current;\n if (!container) return;\n\n // Check if user has scrolled up\n const isAtBottom =\n container.scrollHeight - container.scrollTop <=\n container.clientHeight + 100;\n\n if (isAtBottom) {\n container.scrollTop = container.scrollHeight;\n }\n }, [children]);\n\n // Track scroll position for scroll-to-bottom button\n const handleScroll = React.useCallback(\n (event: React.UIEvent<HTMLDivElement>) => {\n const container = event.currentTarget;\n const isAtBottom =\n container.scrollHeight - container.scrollTop <=\n container.clientHeight + 100;\n setShowScrollDown(!isAtBottom);\n },\n []\n );\n\n const scrollToBottom = React.useCallback(() => {\n const container = scrollRef.current;\n if (container) {\n container.scrollTo({\n top: container.scrollHeight,\n behavior: 'smooth',\n });\n }\n }, []);\n\n return (\n <div className={cn('relative flex flex-1 flex-col', className)}>\n <ScrollArea ref={scrollRef} className=\"flex-1\" onScroll={handleScroll}>\n <div className=\"flex flex-col gap-4 p-4\">{children}</div>\n </ScrollArea>\n\n {showScrollButton && showScrollDown && (\n <button\n onClick={scrollToBottom}\n className={cn(\n 'absolute bottom-4 left-1/2 -translate-x-1/2',\n 'bg-primary text-primary-foreground',\n 'rounded-full px-3 py-1.5 text-sm font-medium shadow-lg',\n 'hover:bg-primary/90 transition-colors',\n 'flex items-center gap-1.5'\n )}\n aria-label=\"Scroll to bottom\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n New messages\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAgBA,SAAgB,cAAc,EAC5B,UACA,WACA,mBAAmB,QACE;CACrB,MAAM,YAAY,MAAM,OAAuB,KAAK;CACpD,MAAM,CAAC,gBAAgB,qBAAqB,MAAM,SAAS,MAAM;AAGjE,OAAM,gBAAgB;EACpB,MAAM,YAAY,UAAU;AAC5B,MAAI,CAAC,UAAW;AAOhB,MAHE,UAAU,eAAe,UAAU,aACnC,UAAU,eAAe,IAGzB,WAAU,YAAY,UAAU;IAEjC,CAAC,SAAS,CAAC;CAGd,MAAM,eAAe,MAAM,aACxB,UAAyC;EACxC,MAAM,YAAY,MAAM;AAIxB,oBAAkB,EAFhB,UAAU,eAAe,UAAU,aACnC,UAAU,eAAe,KACG;IAEhC,EAAE,CACH;CAED,MAAM,iBAAiB,MAAM,kBAAkB;EAC7C,MAAM,YAAY,UAAU;AAC5B,MAAI,UACF,WAAU,SAAS;GACjB,KAAK,UAAU;GACf,UAAU;GACX,CAAC;IAEH,EAAE,CAAC;AAEN,QACE,qBAAC;EAAI,WAAW,GAAG,iCAAiC,UAAU;aAC5D,oBAAC;GAAW,KAAK;GAAW,WAAU;GAAS,UAAU;aACvD,oBAAC;IAAI,WAAU;IAA2B;KAAe;IAC9C,EAEZ,oBAAoB,kBACnB,qBAAC;GACC,SAAS;GACT,WAAW,GACT,+CACA,sCACA,0DACA,yCACA,4BACD;GACD,cAAW;cAEX,oBAAC;IACC,OAAM;IACN,OAAM;IACN,QAAO;IACP,SAAQ;IACR,MAAK;IACL,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;cAEf,oBAAC,UAAK,GAAE,iBAAiB;KACrB;IAEC;GAEP"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ChatAttachment } from "../../core/message-types.js";
|
|
2
|
+
import * as react_jsx_runtime2 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/presentation/components/ChatInput.d.ts
|
|
5
|
+
interface ChatInputProps {
|
|
6
|
+
/** Called when a message is sent */
|
|
7
|
+
onSend: (content: string, attachments?: ChatAttachment[]) => void;
|
|
8
|
+
/** Whether input is disabled (e.g., during streaming) */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Whether currently loading/streaming */
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Additional class name */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Show attachment button */
|
|
17
|
+
showAttachments?: boolean;
|
|
18
|
+
/** Max attachments allowed */
|
|
19
|
+
maxAttachments?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Chat input component with attachment support
|
|
23
|
+
*/
|
|
24
|
+
declare function ChatInput({
|
|
25
|
+
onSend,
|
|
26
|
+
disabled,
|
|
27
|
+
isLoading,
|
|
28
|
+
placeholder,
|
|
29
|
+
className,
|
|
30
|
+
showAttachments,
|
|
31
|
+
maxAttachments
|
|
32
|
+
}: ChatInputProps): react_jsx_runtime2.JSX.Element;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { ChatInput };
|
|
35
|
+
//# sourceMappingURL=ChatInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInput.d.ts","names":[],"sources":["../../../src/presentation/components/ChatInput.tsx"],"sourcesContent":[],"mappings":";;;;UASiB,cAAA;;0CAEyB;EAFzB;EAoBD,QAAA,CAAA,EAAA,OAAS;EACvB;EACA,SAAA,CAAA,EAAA,OAAA;EACA;EACA,WAAA,CAAA,EAAA,MAAA;EACA;EACA,SAAA,CAAA,EAAA,MAAA;EACA;EACC,eAAA,CAAA,EAAA,OAAA;EAAc;EAAA,cAAA,CAAA,EAAA,MAAA;;;;;iBARD,SAAA;;;;;;;;GAQb,iBAAc,kBAAA,CAAA,GAAA,CAAA"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Code, FileText, Loader2, Paperclip, Send, X } from "lucide-react";
|
|
7
|
+
import { Button, Textarea } from "@contractspec/lib.design-system";
|
|
8
|
+
|
|
9
|
+
//#region src/presentation/components/ChatInput.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Chat input component with attachment support
|
|
12
|
+
*/
|
|
13
|
+
function ChatInput({ onSend, disabled = false, isLoading = false, placeholder = "Type a message...", className, showAttachments = true, maxAttachments = 5 }) {
|
|
14
|
+
const [content, setContent] = React.useState("");
|
|
15
|
+
const [attachments, setAttachments] = React.useState([]);
|
|
16
|
+
const textareaRef = React.useRef(null);
|
|
17
|
+
const fileInputRef = React.useRef(null);
|
|
18
|
+
const canSend = content.trim().length > 0 || attachments.length > 0;
|
|
19
|
+
const handleSubmit = React.useCallback((e) => {
|
|
20
|
+
e?.preventDefault();
|
|
21
|
+
if (!canSend || disabled || isLoading) return;
|
|
22
|
+
onSend(content.trim(), attachments.length > 0 ? attachments : void 0);
|
|
23
|
+
setContent("");
|
|
24
|
+
setAttachments([]);
|
|
25
|
+
textareaRef.current?.focus();
|
|
26
|
+
}, [
|
|
27
|
+
canSend,
|
|
28
|
+
content,
|
|
29
|
+
attachments,
|
|
30
|
+
disabled,
|
|
31
|
+
isLoading,
|
|
32
|
+
onSend
|
|
33
|
+
]);
|
|
34
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
35
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
handleSubmit();
|
|
38
|
+
}
|
|
39
|
+
}, [handleSubmit]);
|
|
40
|
+
const handleFileSelect = React.useCallback(async (e) => {
|
|
41
|
+
const files = e.target.files;
|
|
42
|
+
if (!files) return;
|
|
43
|
+
const newAttachments = [];
|
|
44
|
+
for (const file of Array.from(files)) {
|
|
45
|
+
if (attachments.length + newAttachments.length >= maxAttachments) break;
|
|
46
|
+
const content$1 = await file.text();
|
|
47
|
+
const extension = file.name.split(".").pop()?.toLowerCase() ?? "";
|
|
48
|
+
const isCode = [
|
|
49
|
+
"ts",
|
|
50
|
+
"tsx",
|
|
51
|
+
"js",
|
|
52
|
+
"jsx",
|
|
53
|
+
"py",
|
|
54
|
+
"go",
|
|
55
|
+
"rs",
|
|
56
|
+
"java"
|
|
57
|
+
].includes(extension);
|
|
58
|
+
newAttachments.push({
|
|
59
|
+
id: `att_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
|
|
60
|
+
type: isCode ? "code" : "file",
|
|
61
|
+
name: file.name,
|
|
62
|
+
content: content$1,
|
|
63
|
+
mimeType: file.type,
|
|
64
|
+
size: file.size
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
68
|
+
e.target.value = "";
|
|
69
|
+
}, [attachments.length, maxAttachments]);
|
|
70
|
+
const removeAttachment = React.useCallback((id) => {
|
|
71
|
+
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
72
|
+
}, []);
|
|
73
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
74
|
+
className: cn("flex flex-col gap-2", className),
|
|
75
|
+
children: [
|
|
76
|
+
attachments.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
77
|
+
className: "flex flex-wrap gap-2",
|
|
78
|
+
children: attachments.map((attachment) => /* @__PURE__ */ jsxs("div", {
|
|
79
|
+
className: cn("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
|
|
80
|
+
children: [
|
|
81
|
+
attachment.type === "code" ? /* @__PURE__ */ jsx(Code, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(FileText, { className: "h-3.5 w-3.5" }),
|
|
82
|
+
/* @__PURE__ */ jsx("span", {
|
|
83
|
+
className: "max-w-[150px] truncate",
|
|
84
|
+
children: attachment.name
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ jsx("button", {
|
|
87
|
+
type: "button",
|
|
88
|
+
onClick: () => removeAttachment(attachment.id),
|
|
89
|
+
className: "hover:text-foreground",
|
|
90
|
+
"aria-label": `Remove ${attachment.name}`,
|
|
91
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
}, attachment.id))
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsxs("form", {
|
|
97
|
+
onSubmit: handleSubmit,
|
|
98
|
+
className: "flex items-end gap-2",
|
|
99
|
+
children: [
|
|
100
|
+
showAttachments && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
101
|
+
ref: fileInputRef,
|
|
102
|
+
type: "file",
|
|
103
|
+
multiple: true,
|
|
104
|
+
accept: ".ts,.tsx,.js,.jsx,.json,.md,.txt,.py,.go,.rs,.java,.yaml,.yml",
|
|
105
|
+
onChange: handleFileSelect,
|
|
106
|
+
className: "hidden",
|
|
107
|
+
"aria-label": "Attach files"
|
|
108
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
109
|
+
type: "button",
|
|
110
|
+
variant: "ghost",
|
|
111
|
+
size: "sm",
|
|
112
|
+
onPress: () => fileInputRef.current?.click(),
|
|
113
|
+
disabled: disabled || attachments.length >= maxAttachments,
|
|
114
|
+
"aria-label": "Attach files",
|
|
115
|
+
children: /* @__PURE__ */ jsx(Paperclip, { className: "h-4 w-4" })
|
|
116
|
+
})] }),
|
|
117
|
+
/* @__PURE__ */ jsx("div", {
|
|
118
|
+
className: "relative flex-1",
|
|
119
|
+
children: /* @__PURE__ */ jsx(Textarea, {
|
|
120
|
+
value: content,
|
|
121
|
+
onChange: (e) => setContent(e.target.value),
|
|
122
|
+
onKeyDown: handleKeyDown,
|
|
123
|
+
placeholder,
|
|
124
|
+
disabled,
|
|
125
|
+
className: cn("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
|
|
126
|
+
rows: 1,
|
|
127
|
+
"aria-label": "Chat message"
|
|
128
|
+
})
|
|
129
|
+
}),
|
|
130
|
+
/* @__PURE__ */ jsx(Button, {
|
|
131
|
+
type: "submit",
|
|
132
|
+
disabled: !canSend || disabled || isLoading,
|
|
133
|
+
size: "sm",
|
|
134
|
+
"aria-label": isLoading ? "Sending..." : "Send message",
|
|
135
|
+
children: isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ jsx("p", {
|
|
140
|
+
className: "text-muted-foreground text-xs",
|
|
141
|
+
children: "Press Enter to send, Shift+Enter for new line"
|
|
142
|
+
})
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
export { ChatInput };
|
|
149
|
+
//# sourceMappingURL=ChatInput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatInput.js","names":["newAttachments: ChatAttachment[]","content"],"sources":["../../../src/presentation/components/ChatInput.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { cn } from '@contractspec/lib.ui-kit-web/ui/utils';\nimport { Textarea } from '@contractspec/lib.design-system';\nimport { Button } from '@contractspec/lib.design-system';\nimport { Send, Paperclip, X, Loader2, FileText, Code } from 'lucide-react';\nimport type { ChatAttachment } from '../../core/message-types';\n\nexport interface ChatInputProps {\n /** Called when a message is sent */\n onSend: (content: string, attachments?: ChatAttachment[]) => void;\n /** Whether input is disabled (e.g., during streaming) */\n disabled?: boolean;\n /** Whether currently loading/streaming */\n isLoading?: boolean;\n /** Placeholder text */\n placeholder?: string;\n /** Additional class name */\n className?: string;\n /** Show attachment button */\n showAttachments?: boolean;\n /** Max attachments allowed */\n maxAttachments?: number;\n}\n\n/**\n * Chat input component with attachment support\n */\nexport function ChatInput({\n onSend,\n disabled = false,\n isLoading = false,\n placeholder = 'Type a message...',\n className,\n showAttachments = true,\n maxAttachments = 5,\n}: ChatInputProps) {\n const [content, setContent] = React.useState('');\n const [attachments, setAttachments] = React.useState<ChatAttachment[]>([]);\n const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n\n const canSend = content.trim().length > 0 || attachments.length > 0;\n\n const handleSubmit = React.useCallback(\n (e?: React.FormEvent) => {\n e?.preventDefault();\n if (!canSend || disabled || isLoading) return;\n\n onSend(content.trim(), attachments.length > 0 ? attachments : undefined);\n setContent('');\n setAttachments([]);\n\n // Focus back on textarea\n textareaRef.current?.focus();\n },\n [canSend, content, attachments, disabled, isLoading, onSend]\n );\n\n const handleKeyDown = React.useCallback(\n (e: React.KeyboardEvent) => {\n // Submit on Enter (without Shift)\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n },\n [handleSubmit]\n );\n\n const handleFileSelect = React.useCallback(\n async (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n if (!files) return;\n\n const newAttachments: ChatAttachment[] = [];\n\n for (const file of Array.from(files)) {\n if (attachments.length + newAttachments.length >= maxAttachments) break;\n\n const content = await file.text();\n const extension = file.name.split('.').pop()?.toLowerCase() ?? '';\n const isCode = [\n 'ts',\n 'tsx',\n 'js',\n 'jsx',\n 'py',\n 'go',\n 'rs',\n 'java',\n ].includes(extension);\n\n newAttachments.push({\n id: `att_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,\n type: isCode ? 'code' : 'file',\n name: file.name,\n content,\n mimeType: file.type,\n size: file.size,\n });\n }\n\n setAttachments((prev) => [...prev, ...newAttachments]);\n\n // Reset file input\n e.target.value = '';\n },\n [attachments.length, maxAttachments]\n );\n\n const removeAttachment = React.useCallback((id: string) => {\n setAttachments((prev) => prev.filter((a) => a.id !== id));\n }, []);\n\n return (\n <div className={cn('flex flex-col gap-2', className)}>\n {/* Attachments preview */}\n {attachments.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {attachments.map((attachment) => (\n <div\n key={attachment.id}\n className={cn(\n 'flex items-center gap-1.5 rounded-md px-2 py-1',\n 'bg-muted text-muted-foreground text-sm'\n )}\n >\n {attachment.type === 'code' ? (\n <Code className=\"h-3.5 w-3.5\" />\n ) : (\n <FileText className=\"h-3.5 w-3.5\" />\n )}\n <span className=\"max-w-[150px] truncate\">{attachment.name}</span>\n <button\n type=\"button\"\n onClick={() => removeAttachment(attachment.id)}\n className=\"hover:text-foreground\"\n aria-label={`Remove ${attachment.name}`}\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Input form */}\n <form onSubmit={handleSubmit} className=\"flex items-end gap-2\">\n {/* Attachment button */}\n {showAttachments && (\n <>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept=\".ts,.tsx,.js,.jsx,.json,.md,.txt,.py,.go,.rs,.java,.yaml,.yml\"\n onChange={handleFileSelect}\n className=\"hidden\"\n aria-label=\"Attach files\"\n />\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onPress={() => fileInputRef.current?.click()}\n disabled={disabled || attachments.length >= maxAttachments}\n aria-label=\"Attach files\"\n >\n <Paperclip className=\"h-4 w-4\" />\n </Button>\n </>\n )}\n\n {/* Text input */}\n <div className=\"relative flex-1\">\n <Textarea\n value={content}\n onChange={(e) => setContent(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n className={cn(\n 'max-h-[200px] min-h-[44px] resize-none pr-12',\n 'focus-visible:ring-1'\n )}\n rows={1}\n aria-label=\"Chat message\"\n />\n </div>\n\n {/* Send button */}\n <Button\n type=\"submit\"\n disabled={!canSend || disabled || isLoading}\n size=\"sm\"\n aria-label={isLoading ? 'Sending...' : 'Send message'}\n >\n {isLoading ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Send className=\"h-4 w-4\" />\n )}\n </Button>\n </form>\n\n {/* Helper text */}\n <p className=\"text-muted-foreground text-xs\">\n Press Enter to send, Shift+Enter for new line\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChatMessage as ChatMessage$1 } from "../../core/message-types.js";
|
|
2
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/presentation/components/ChatMessage.d.ts
|
|
5
|
+
interface ChatMessageProps {
|
|
6
|
+
message: ChatMessage$1;
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Show copy button */
|
|
9
|
+
showCopy?: boolean;
|
|
10
|
+
/** Show avatar */
|
|
11
|
+
showAvatar?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Chat message component
|
|
15
|
+
*/
|
|
16
|
+
declare function ChatMessage({
|
|
17
|
+
message,
|
|
18
|
+
className,
|
|
19
|
+
showCopy,
|
|
20
|
+
showAvatar
|
|
21
|
+
}: ChatMessageProps): react_jsx_runtime1.JSX.Element;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ChatMessage };
|
|
24
|
+
//# sourceMappingURL=ChatMessage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatMessage.d.ts","names":[],"sources":["../../../src/presentation/components/ChatMessage.tsx"],"sourcesContent":[],"mappings":";;;;UAWiB,gBAAA;WACN;;EADM;EA+ED,QAAA,CAAA,EAAA,OAAW;EACzB;EACA,UAAA,CAAA,EAAA,OAAA;;;;;AAGiB,iBALH,WAAA,CAKG;EAAA,OAAA;EAAA,SAAA;EAAA,QAAA;EAAA;AAAA,CAAA,EAAhB,gBAAgB,CAAA,EAAA,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CodePreview } from "./CodePreview.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
|
|
8
|
+
import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
|
|
9
|
+
import { AlertCircle, Bot, Check, Copy, User } from "lucide-react";
|
|
10
|
+
import { Button } from "@contractspec/lib.design-system";
|
|
11
|
+
|
|
12
|
+
//#region src/presentation/components/ChatMessage.tsx
|
|
13
|
+
/**
|
|
14
|
+
* Extract code blocks from message content
|
|
15
|
+
*/
|
|
16
|
+
function extractCodeBlocks(content) {
|
|
17
|
+
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
18
|
+
const blocks = [];
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = codeBlockRegex.exec(content)) !== null) blocks.push({
|
|
21
|
+
language: match[1] ?? "text",
|
|
22
|
+
code: match[2] ?? "",
|
|
23
|
+
raw: match[0]
|
|
24
|
+
});
|
|
25
|
+
return blocks;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render message content with code blocks
|
|
29
|
+
*/
|
|
30
|
+
function MessageContent({ content }) {
|
|
31
|
+
const codeBlocks = extractCodeBlocks(content);
|
|
32
|
+
if (codeBlocks.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
33
|
+
className: "whitespace-pre-wrap",
|
|
34
|
+
children: content
|
|
35
|
+
});
|
|
36
|
+
let remaining = content;
|
|
37
|
+
const parts = [];
|
|
38
|
+
let key = 0;
|
|
39
|
+
for (const block of codeBlocks) {
|
|
40
|
+
const [before, after] = remaining.split(block.raw);
|
|
41
|
+
if (before) parts.push(/* @__PURE__ */ jsx("p", {
|
|
42
|
+
className: "whitespace-pre-wrap",
|
|
43
|
+
children: before.trim()
|
|
44
|
+
}, key++));
|
|
45
|
+
parts.push(/* @__PURE__ */ jsx(CodePreview, {
|
|
46
|
+
code: block.code,
|
|
47
|
+
language: block.language,
|
|
48
|
+
className: "my-2"
|
|
49
|
+
}, key++));
|
|
50
|
+
remaining = after ?? "";
|
|
51
|
+
}
|
|
52
|
+
if (remaining.trim()) parts.push(/* @__PURE__ */ jsx("p", {
|
|
53
|
+
className: "whitespace-pre-wrap",
|
|
54
|
+
children: remaining.trim()
|
|
55
|
+
}, key++));
|
|
56
|
+
return /* @__PURE__ */ jsx(Fragment, { children: parts });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Chat message component
|
|
60
|
+
*/
|
|
61
|
+
function ChatMessage({ message, className, showCopy = true, showAvatar = true }) {
|
|
62
|
+
const [copied, setCopied] = React.useState(false);
|
|
63
|
+
const isUser = message.role === "user";
|
|
64
|
+
const isError = message.status === "error";
|
|
65
|
+
const isStreaming = message.status === "streaming";
|
|
66
|
+
const handleCopy = React.useCallback(async () => {
|
|
67
|
+
await navigator.clipboard.writeText(message.content);
|
|
68
|
+
setCopied(true);
|
|
69
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
70
|
+
}, [message.content]);
|
|
71
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
72
|
+
className: cn("group flex gap-3", isUser && "flex-row-reverse", className),
|
|
73
|
+
children: [showAvatar && /* @__PURE__ */ jsx(Avatar, {
|
|
74
|
+
className: "h-8 w-8 shrink-0",
|
|
75
|
+
children: /* @__PURE__ */ jsx(AvatarFallback, {
|
|
76
|
+
className: cn(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
|
|
77
|
+
children: isUser ? /* @__PURE__ */ jsx(User, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Bot, { className: "h-4 w-4" })
|
|
78
|
+
})
|
|
79
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
80
|
+
className: cn("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx("div", {
|
|
83
|
+
className: cn("rounded-2xl px-4 py-2", isUser ? "bg-primary text-primary-foreground" : "bg-muted text-foreground", isError && "border-destructive bg-destructive/10 border"),
|
|
84
|
+
children: isError && message.error ? /* @__PURE__ */ jsxs("div", {
|
|
85
|
+
className: "flex items-start gap-2",
|
|
86
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { className: "text-destructive mt-0.5 h-4 w-4 shrink-0" }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
87
|
+
className: "text-destructive font-medium",
|
|
88
|
+
children: message.error.code
|
|
89
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
90
|
+
className: "text-muted-foreground text-sm",
|
|
91
|
+
children: message.error.message
|
|
92
|
+
})] })]
|
|
93
|
+
}) : isStreaming && !message.content ? /* @__PURE__ */ jsxs("div", {
|
|
94
|
+
className: "flex flex-col gap-2",
|
|
95
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" })]
|
|
96
|
+
}) : /* @__PURE__ */ jsx(MessageContent, { content: message.content })
|
|
97
|
+
}),
|
|
98
|
+
/* @__PURE__ */ jsxs("div", {
|
|
99
|
+
className: cn("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsx("span", { children: new Date(message.createdAt).toLocaleTimeString([], {
|
|
102
|
+
hour: "2-digit",
|
|
103
|
+
minute: "2-digit"
|
|
104
|
+
}) }),
|
|
105
|
+
message.usage && /* @__PURE__ */ jsxs("span", { children: [message.usage.inputTokens + message.usage.outputTokens, " tokens"] }),
|
|
106
|
+
showCopy && !isUser && message.content && /* @__PURE__ */ jsx(Button, {
|
|
107
|
+
variant: "ghost",
|
|
108
|
+
size: "sm",
|
|
109
|
+
className: "h-6 w-6 p-0",
|
|
110
|
+
onPress: handleCopy,
|
|
111
|
+
"aria-label": copied ? "Copied" : "Copy message",
|
|
112
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
}),
|
|
116
|
+
message.reasoning && /* @__PURE__ */ jsxs("details", {
|
|
117
|
+
className: "text-muted-foreground mt-2 text-sm",
|
|
118
|
+
children: [/* @__PURE__ */ jsx("summary", {
|
|
119
|
+
className: "cursor-pointer hover:underline",
|
|
120
|
+
children: "View reasoning"
|
|
121
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
122
|
+
className: "bg-muted mt-1 rounded-md p-2",
|
|
123
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
124
|
+
className: "whitespace-pre-wrap",
|
|
125
|
+
children: message.reasoning
|
|
126
|
+
})
|
|
127
|
+
})]
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
})]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { ChatMessage };
|
|
136
|
+
//# sourceMappingURL=ChatMessage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatMessage.js","names":["blocks: { language: string; code: string; raw: string }[]","parts: React.ReactNode[]"],"sources":["../../../src/presentation/components/ChatMessage.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { cn } from '@contractspec/lib.ui-kit-web/ui/utils';\nimport { Avatar, AvatarFallback } from '@contractspec/lib.ui-kit-web/ui/avatar';\nimport { Skeleton } from '@contractspec/lib.ui-kit-web/ui/skeleton';\nimport { Bot, User, AlertCircle, Copy, Check } from 'lucide-react';\nimport { Button } from '@contractspec/lib.design-system';\nimport type { ChatMessage as ChatMessageType } from '../../core/message-types';\nimport { CodePreview } from './CodePreview';\n\nexport interface ChatMessageProps {\n message: ChatMessageType;\n className?: string;\n /** Show copy button */\n showCopy?: boolean;\n /** Show avatar */\n showAvatar?: boolean;\n}\n\n/**\n * Extract code blocks from message content\n */\nfunction extractCodeBlocks(\n content: string\n): { language: string; code: string; raw: string }[] {\n const codeBlockRegex = /```(\\w+)?\\n([\\s\\S]*?)```/g;\n const blocks: { language: string; code: string; raw: string }[] = [];\n let match;\n\n while ((match = codeBlockRegex.exec(content)) !== null) {\n blocks.push({\n language: match[1] ?? 'text',\n code: match[2] ?? '',\n raw: match[0],\n });\n }\n\n return blocks;\n}\n\n/**\n * Render message content with code blocks\n */\nfunction MessageContent({ content }: { content: string }) {\n const codeBlocks = extractCodeBlocks(content);\n\n if (codeBlocks.length === 0) {\n return <p className=\"whitespace-pre-wrap\">{content}</p>;\n }\n\n // Split content by code blocks and render\n let remaining = content;\n const parts: React.ReactNode[] = [];\n let key = 0;\n\n for (const block of codeBlocks) {\n const [before, after] = remaining.split(block.raw);\n if (before) {\n parts.push(\n <p key={key++} className=\"whitespace-pre-wrap\">\n {before.trim()}\n </p>\n );\n }\n parts.push(\n <CodePreview\n key={key++}\n code={block.code}\n language={block.language}\n className=\"my-2\"\n />\n );\n remaining = after ?? '';\n }\n\n if (remaining.trim()) {\n parts.push(\n <p key={key++} className=\"whitespace-pre-wrap\">\n {remaining.trim()}\n </p>\n );\n }\n\n return <>{parts}</>;\n}\n\n/**\n * Chat message component\n */\nexport function ChatMessage({\n message,\n className,\n showCopy = true,\n showAvatar = true,\n}: ChatMessageProps) {\n const [copied, setCopied] = React.useState(false);\n\n const isUser = message.role === 'user';\n const isError = message.status === 'error';\n const isStreaming = message.status === 'streaming';\n\n const handleCopy = React.useCallback(async () => {\n await navigator.clipboard.writeText(message.content);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n }, [message.content]);\n\n return (\n <div\n className={cn(\n 'group flex gap-3',\n isUser && 'flex-row-reverse',\n className\n )}\n >\n {showAvatar && (\n <Avatar className=\"h-8 w-8 shrink-0\">\n <AvatarFallback\n className={cn(\n isUser ? 'bg-primary text-primary-foreground' : 'bg-muted'\n )}\n >\n {isUser ? (\n <User className=\"h-4 w-4\" />\n ) : (\n <Bot className=\"h-4 w-4\" />\n )}\n </AvatarFallback>\n </Avatar>\n )}\n\n <div\n className={cn('flex max-w-[80%] flex-col gap-1', isUser && 'items-end')}\n >\n <div\n className={cn(\n 'rounded-2xl px-4 py-2',\n isUser\n ? 'bg-primary text-primary-foreground'\n : 'bg-muted text-foreground',\n isError && 'border-destructive bg-destructive/10 border'\n )}\n >\n {isError && message.error ? (\n <div className=\"flex items-start gap-2\">\n <AlertCircle className=\"text-destructive mt-0.5 h-4 w-4 shrink-0\" />\n <div>\n <p className=\"text-destructive font-medium\">\n {message.error.code}\n </p>\n <p className=\"text-muted-foreground text-sm\">\n {message.error.message}\n </p>\n </div>\n </div>\n ) : isStreaming && !message.content ? (\n <div className=\"flex flex-col gap-2\">\n <Skeleton className=\"h-4 w-48\" />\n <Skeleton className=\"h-4 w-32\" />\n </div>\n ) : (\n <MessageContent content={message.content} />\n )}\n </div>\n\n {/* Message meta */}\n <div\n className={cn(\n 'flex items-center gap-2 text-xs',\n 'text-muted-foreground opacity-0 transition-opacity',\n 'group-hover:opacity-100'\n )}\n >\n <span>\n {new Date(message.createdAt).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n })}\n </span>\n\n {message.usage && (\n <span>\n {message.usage.inputTokens + message.usage.outputTokens} tokens\n </span>\n )}\n\n {showCopy && !isUser && message.content && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-6 w-6 p-0\"\n onPress={handleCopy}\n aria-label={copied ? 'Copied' : 'Copy message'}\n >\n {copied ? (\n <Check className=\"h-3 w-3\" />\n ) : (\n <Copy className=\"h-3 w-3\" />\n )}\n </Button>\n )}\n </div>\n\n {/* Reasoning (for models that support it) */}\n {message.reasoning && (\n <details className=\"text-muted-foreground mt-2 text-sm\">\n <summary className=\"cursor-pointer hover:underline\">\n View reasoning\n </summary>\n <div className=\"bg-muted mt-1 rounded-md p-2\">\n <p className=\"whitespace-pre-wrap\">{message.reasoning}</p>\n </div>\n </details>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/presentation/components/CodePreview.d.ts
|
|
4
|
+
interface CodePreviewProps {
|
|
5
|
+
/** Code content */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Programming language */
|
|
8
|
+
language?: string;
|
|
9
|
+
/** File name */
|
|
10
|
+
filename?: string;
|
|
11
|
+
/** Additional class name */
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Show copy button */
|
|
14
|
+
showCopy?: boolean;
|
|
15
|
+
/** Show execute button (for applicable languages) */
|
|
16
|
+
showExecute?: boolean;
|
|
17
|
+
/** Called when execute is clicked */
|
|
18
|
+
onExecute?: (code: string) => void;
|
|
19
|
+
/** Show download button */
|
|
20
|
+
showDownload?: boolean;
|
|
21
|
+
/** Max height before scroll */
|
|
22
|
+
maxHeight?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Code preview component with syntax highlighting placeholder
|
|
26
|
+
*/
|
|
27
|
+
declare function CodePreview({
|
|
28
|
+
code,
|
|
29
|
+
language,
|
|
30
|
+
filename,
|
|
31
|
+
className,
|
|
32
|
+
showCopy,
|
|
33
|
+
showExecute,
|
|
34
|
+
onExecute,
|
|
35
|
+
showDownload,
|
|
36
|
+
maxHeight
|
|
37
|
+
}: CodePreviewProps): react_jsx_runtime0.JSX.Element;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { CodePreview };
|
|
40
|
+
//# sourceMappingURL=CodePreview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodePreview.d.ts","names":[],"sources":["../../../src/presentation/components/CodePreview.tsx"],"sourcesContent":[],"mappings":";;;UAOiB,gBAAA;;;EAAA;EAgDD,QAAA,CAAA,EAAA,MAAW;EACzB;EACA,QAAA,CAAA,EAAA,MAAA;EACA;EACA,SAAA,CAAA,EAAA,MAAA;EACA;EACA,QAAA,CAAA,EAAA,OAAA;EACA;EACA,WAAA,CAAA,EAAA,OAAA;EACA;EACC,SAAA,CAAA,EAAA,CAAA,IAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAAgB;EAAA,YAAA,CAAA,EAAA,OAAA;;;;;;;iBAVH,WAAA;;;;;;;;;;GAUb,mBAAgB,kBAAA,CAAA,GAAA,CAAA"}
|