@schandlergarcia/sf-web-components 1.9.38 → 1.9.39
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/package.json +4 -1
- package/scripts/postinstall.mjs +36 -17
- package/src/components/library/cards/ActionList.jsx +38 -0
- package/src/components/library/cards/ActivityCard.jsx +56 -0
- package/src/components/library/cards/BaseCard.jsx +109 -0
- package/src/components/library/cards/CalloutCard.jsx +37 -0
- package/src/components/library/cards/ChartCard.jsx +105 -0
- package/src/components/library/cards/FeedPanel.jsx +39 -0
- package/src/components/library/cards/ListCard.jsx +193 -0
- package/src/components/library/cards/MetricCard.jsx +109 -0
- package/src/components/library/cards/MetricsStrip.jsx +78 -0
- package/src/components/library/cards/SectionCard.jsx +83 -0
- package/src/components/library/cards/SemanticMetricCard.jsx +52 -0
- package/src/components/library/cards/SemanticMetricCardWithLoading.jsx +23 -0
- package/src/components/library/cards/SemanticTableCard.jsx +48 -0
- package/src/components/library/cards/SemanticTableCardWithLoading.jsx +22 -0
- package/src/components/library/cards/StatusCard.jsx +220 -0
- package/src/components/library/cards/TableCard.jsx +337 -0
- package/src/components/library/cards/WidgetCard.jsx +90 -0
- package/src/components/library/charts/D3Chart.jsx +109 -0
- package/src/components/library/charts/D3ChartTemplates.jsx +126 -0
- package/src/components/library/charts/GeoMap.jsx +293 -0
- package/src/components/library/chat/ChatBar.jsx +256 -0
- package/src/components/library/chat/ChatInput.jsx +89 -0
- package/src/components/library/chat/ChatMessage.jsx +178 -0
- package/src/components/library/chat/ChatMessageList.jsx +73 -0
- package/src/components/library/chat/ChatPanel.jsx +97 -0
- package/src/components/library/chat/ChatSuggestions.jsx +28 -0
- package/src/components/library/chat/ChatToolCall.jsx +100 -0
- package/src/components/library/chat/ChatTypingIndicator.jsx +23 -0
- package/src/components/library/chat/ChatWelcome.jsx +43 -0
- package/src/components/library/chat/index.jsx +10 -0
- package/src/components/library/chat/useChatState.jsx +130 -0
- package/src/components/library/data/DataModeProvider.jsx +67 -0
- package/src/components/library/data/DataModeToggle.jsx +36 -0
- package/src/components/library/data/chartDataProvider.jsx +61 -0
- package/src/components/library/data/filterUtils.jsx +141 -0
- package/src/components/library/data/useDataSource.jsx +33 -0
- package/src/components/library/data/usePageFilters.jsx +99 -0
- package/src/components/library/filters/FilterBar.jsx +95 -0
- package/src/components/library/filters/SearchFilter.jsx +36 -0
- package/src/components/library/filters/SelectFilter.jsx +55 -0
- package/src/components/library/filters/ToggleFilter.jsx +52 -0
- package/src/components/library/filters/index.jsx +4 -0
- package/src/components/library/forms/FormField.jsx +291 -0
- package/src/components/library/forms/FormModal.jsx +201 -0
- package/src/components/library/forms/FormRenderer.jsx +46 -0
- package/src/components/library/forms/FormSection.jsx +69 -0
- package/src/components/library/forms/index.jsx +5 -0
- package/src/components/library/forms/useFormState.jsx +165 -0
- package/src/components/library/heroui/Accordion.jsx +26 -0
- package/src/components/library/heroui/Alert.jsx +8 -0
- package/src/components/library/heroui/Badge.jsx +8 -0
- package/src/components/library/heroui/Breadcrumbs.jsx +22 -0
- package/src/components/library/heroui/Button.jsx +58 -0
- package/src/components/library/heroui/Card.jsx +8 -0
- package/src/components/library/heroui/Collapsible.jsx +42 -0
- package/src/components/library/heroui/DatePicker.jsx +34 -0
- package/src/components/library/heroui/Dialog.jsx +37 -0
- package/src/components/library/heroui/Drawer.jsx +32 -0
- package/src/components/library/heroui/Dropdown.jsx +28 -0
- package/src/components/library/heroui/Field.jsx +51 -0
- package/src/components/library/heroui/Input.jsx +6 -0
- package/src/components/library/heroui/Kbd.jsx +8 -0
- package/src/components/library/heroui/Meter.jsx +8 -0
- package/src/components/library/heroui/Modal.jsx +32 -0
- package/src/components/library/heroui/Pagination.jsx +8 -0
- package/src/components/library/heroui/Popover.jsx +64 -0
- package/src/components/library/heroui/ProgressBar.jsx +8 -0
- package/src/components/library/heroui/ProgressCircle.jsx +8 -0
- package/src/components/library/heroui/ScrollShadow.jsx +8 -0
- package/src/components/library/heroui/Select.jsx +37 -0
- package/src/components/library/heroui/Separator.jsx +8 -0
- package/src/components/library/heroui/Skeleton.jsx +8 -0
- package/src/components/library/heroui/Tabs.jsx +26 -0
- package/src/components/library/heroui/Toast.jsx +25 -0
- package/src/components/library/heroui/Toggle.jsx +14 -0
- package/src/components/library/heroui/Tooltip.jsx +21 -0
- package/src/components/library/index.jsx +146 -0
- package/src/components/library/layout/PageContainer.jsx +11 -0
- package/src/components/library/skeletons/CardSkeleton.jsx +30 -0
- package/src/components/library/theme/AppThemeProvider.jsx +67 -0
- package/src/components/library/theme/tokens.jsx +72 -0
- package/src/components/library/ui/Alert.jsx +80 -0
- package/src/components/library/ui/Avatar.jsx +44 -0
- package/src/components/library/ui/BreadcrumbExtras.tsx +120 -0
- package/src/components/library/ui/Button.jsx +61 -0
- package/src/components/library/ui/Card.jsx +117 -0
- package/src/components/library/ui/Checkbox.jsx +17 -0
- package/src/components/library/ui/Chip.jsx +38 -0
- package/src/components/library/ui/Collapsible.tsx +31 -0
- package/src/components/library/ui/Container.jsx +56 -0
- package/src/components/library/ui/DatePicker.tsx +34 -0
- package/src/components/library/ui/Dialog.tsx +141 -0
- package/src/components/library/ui/EmptyState.jsx +46 -0
- package/src/components/library/ui/Field.tsx +82 -0
- package/src/components/library/ui/FieldGroup.jsx +17 -0
- package/src/components/library/ui/Input.jsx +21 -0
- package/src/components/library/ui/Label.jsx +22 -0
- package/src/components/library/ui/PaginationExtras.tsx +142 -0
- package/src/components/library/ui/Popover.tsx +39 -0
- package/src/components/library/ui/Select.tsx +113 -0
- package/src/components/library/ui/Spinner.d.ts +10 -0
- package/src/components/library/ui/Spinner.jsx +64 -0
- package/src/components/library/ui/Text.jsx +46 -0
- package/src/components/library/ui/Toggle.jsx +42 -0
- package/src/components/workspace/ComponentRegistry.jsx +297 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/utils.ts +6 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderSchemaComponent } from "@/components/workspace/ComponentRegistry";
|
|
3
|
+
import ChatToolCall from "./ChatToolCall";
|
|
4
|
+
import { UserCircleIcon, CpuChipIcon } from "@heroicons/react/24/solid";
|
|
5
|
+
|
|
6
|
+
function cx(...classes) {
|
|
7
|
+
return classes.filter(Boolean).join(" ");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lightweight inline formatter for assistant messages.
|
|
12
|
+
* Handles code blocks, inline code, bold, italic, and line breaks.
|
|
13
|
+
*/
|
|
14
|
+
function formatContent(text) {
|
|
15
|
+
if (!text) return null;
|
|
16
|
+
const parts = [];
|
|
17
|
+
let key = 0;
|
|
18
|
+
|
|
19
|
+
const codeBlockRegex = /```(\w*)\n?([\s\S]*?)```/g;
|
|
20
|
+
let lastIndex = 0;
|
|
21
|
+
let match;
|
|
22
|
+
|
|
23
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
24
|
+
if (match.index > lastIndex) {
|
|
25
|
+
parts.push(
|
|
26
|
+
<span key={key++}>{formatInline(text.slice(lastIndex, match.index))}</span>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
parts.push(
|
|
30
|
+
<pre
|
|
31
|
+
key={key++}
|
|
32
|
+
className="my-2 overflow-x-auto rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs leading-relaxed dark:border-slate-700 dark:bg-slate-800/50"
|
|
33
|
+
>
|
|
34
|
+
<code>{match[2]}</code>
|
|
35
|
+
</pre>
|
|
36
|
+
);
|
|
37
|
+
lastIndex = match.index + match[0].length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (lastIndex < text.length) {
|
|
41
|
+
parts.push(
|
|
42
|
+
<span key={key++}>{formatInline(text.slice(lastIndex))}</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatInline(text) {
|
|
50
|
+
const tokens = text.split(/(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*)/g);
|
|
51
|
+
return tokens.map((token, i) => {
|
|
52
|
+
if (token.startsWith("**") && token.endsWith("**")) {
|
|
53
|
+
return <strong key={i} className="font-semibold">{token.slice(2, -2)}</strong>;
|
|
54
|
+
}
|
|
55
|
+
if (token.startsWith("*") && token.endsWith("*")) {
|
|
56
|
+
return <em key={i}>{token.slice(1, -1)}</em>;
|
|
57
|
+
}
|
|
58
|
+
if (token.startsWith("`") && token.endsWith("`")) {
|
|
59
|
+
return (
|
|
60
|
+
<code
|
|
61
|
+
key={i}
|
|
62
|
+
className="rounded bg-slate-100 px-1 py-0.5 text-xs font-mono dark:bg-slate-800"
|
|
63
|
+
>
|
|
64
|
+
{token.slice(1, -1)}
|
|
65
|
+
</code>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return token.split("\n").map((line, j, arr) => (
|
|
69
|
+
<React.Fragment key={`${i}-${j}`}>
|
|
70
|
+
{line}
|
|
71
|
+
{j < arr.length - 1 ? <br /> : null}
|
|
72
|
+
</React.Fragment>
|
|
73
|
+
));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Renders a single chat message.
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} message — { id, role, content, components?, toolCalls?, isError?, isStreaming?, timestamp? }
|
|
81
|
+
* @param {React.ReactNode} avatar — custom avatar override
|
|
82
|
+
*/
|
|
83
|
+
export default function ChatMessage({ message, avatar }) {
|
|
84
|
+
const isUser = message.role === "user";
|
|
85
|
+
const isSystem = message.role === "system";
|
|
86
|
+
const isAssistant = message.role === "assistant";
|
|
87
|
+
|
|
88
|
+
const defaultAvatar = isUser ? (
|
|
89
|
+
<UserCircleIcon className="h-7 w-7 text-slate-400 dark:text-slate-500" />
|
|
90
|
+
) : (
|
|
91
|
+
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-brand-100 dark:bg-brand-900/40">
|
|
92
|
+
<CpuChipIcon className="h-4 w-4 text-brand-600 dark:text-brand-400" />
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (isSystem) {
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
className={cx(
|
|
100
|
+
"mx-auto max-w-lg rounded-lg px-4 py-2 text-center text-xs",
|
|
101
|
+
message.isError
|
|
102
|
+
? "bg-red-50 text-red-600 dark:bg-red-950/30 dark:text-red-400"
|
|
103
|
+
: "text-slate-400 dark:text-slate-500"
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{message.content}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
className={cx(
|
|
114
|
+
"flex gap-3",
|
|
115
|
+
isUser ? "flex-row-reverse" : "flex-row"
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{/* Avatar */}
|
|
119
|
+
<div className="shrink-0 pt-0.5">{avatar ?? defaultAvatar}</div>
|
|
120
|
+
|
|
121
|
+
{/* Bubble */}
|
|
122
|
+
<div
|
|
123
|
+
className={cx(
|
|
124
|
+
"max-w-[80%] space-y-2",
|
|
125
|
+
isUser ? "items-end" : "items-start"
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
{/* Text content */}
|
|
129
|
+
{message.content ? (
|
|
130
|
+
<div
|
|
131
|
+
className={cx(
|
|
132
|
+
"rounded-2xl px-4 py-2.5 text-sm leading-relaxed",
|
|
133
|
+
isUser
|
|
134
|
+
? "rounded-tr-md bg-brand-600 text-white dark:bg-brand-500"
|
|
135
|
+
: "rounded-tl-md bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-100",
|
|
136
|
+
message.isStreaming && "animate-pulse"
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{isAssistant ? formatContent(message.content) : message.content}
|
|
140
|
+
</div>
|
|
141
|
+
) : null}
|
|
142
|
+
|
|
143
|
+
{/* Tool calls */}
|
|
144
|
+
{message.toolCalls?.length ? (
|
|
145
|
+
<div className="space-y-1.5 pl-1">
|
|
146
|
+
{message.toolCalls.map((tc, idx) => (
|
|
147
|
+
<ChatToolCall key={tc.id ?? idx} toolCall={tc} />
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
) : null}
|
|
151
|
+
|
|
152
|
+
{/* Inline components — render real command center components */}
|
|
153
|
+
{message.components?.length ? (
|
|
154
|
+
<div className="w-full space-y-3 pt-1">
|
|
155
|
+
{message.components.map((comp, idx) =>
|
|
156
|
+
renderSchemaComponent(comp, idx)
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
) : null}
|
|
160
|
+
|
|
161
|
+
{/* Timestamp */}
|
|
162
|
+
{message.timestamp ? (
|
|
163
|
+
<div
|
|
164
|
+
className={cx(
|
|
165
|
+
"px-1 text-[10px] text-slate-400 dark:text-slate-500",
|
|
166
|
+
isUser ? "text-right" : "text-left"
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
{new Date(message.timestamp).toLocaleTimeString([], {
|
|
170
|
+
hour: "2-digit",
|
|
171
|
+
minute: "2-digit",
|
|
172
|
+
})}
|
|
173
|
+
</div>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from "react";
|
|
2
|
+
import ChatMessage from "./ChatMessage";
|
|
3
|
+
import ChatTypingIndicator from "./ChatTypingIndicator";
|
|
4
|
+
import ChatSuggestions from "./ChatSuggestions";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scrollable message area with auto-scroll to latest message.
|
|
8
|
+
*
|
|
9
|
+
* @param {Array} messages — array of message objects
|
|
10
|
+
* @param {boolean} isLoading — show typing indicator
|
|
11
|
+
* @param {boolean} isStreaming — agent is streaming (show different indicator text)
|
|
12
|
+
* @param {string[]} suggestions — follow-up suggestions shown after last assistant message
|
|
13
|
+
* @param {Function} onSuggestion — (text) => void
|
|
14
|
+
* @param {Function} renderAvatar — (message) => ReactNode, optional per-message avatar
|
|
15
|
+
*/
|
|
16
|
+
export default function ChatMessageList({
|
|
17
|
+
messages = [],
|
|
18
|
+
isLoading = false,
|
|
19
|
+
isStreaming = false,
|
|
20
|
+
suggestions = [],
|
|
21
|
+
onSuggestion,
|
|
22
|
+
renderAvatar,
|
|
23
|
+
}) {
|
|
24
|
+
const bottomRef = useRef(null);
|
|
25
|
+
const containerRef = useRef(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
29
|
+
}, [messages.length, isLoading, isStreaming]);
|
|
30
|
+
|
|
31
|
+
const lastMessage = messages[messages.length - 1];
|
|
32
|
+
const showSuggestions =
|
|
33
|
+
suggestions.length > 0 &&
|
|
34
|
+
!isLoading &&
|
|
35
|
+
!isStreaming &&
|
|
36
|
+
lastMessage?.role === "assistant";
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
ref={containerRef}
|
|
41
|
+
className="flex-1 overflow-y-auto px-4 py-4"
|
|
42
|
+
>
|
|
43
|
+
<div className="mx-auto max-w-3xl space-y-4">
|
|
44
|
+
{messages.map((msg) => (
|
|
45
|
+
<ChatMessage
|
|
46
|
+
key={msg.id}
|
|
47
|
+
message={msg}
|
|
48
|
+
avatar={renderAvatar?.(msg)}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
|
|
52
|
+
{showSuggestions ? (
|
|
53
|
+
<div className="pl-10">
|
|
54
|
+
<ChatSuggestions
|
|
55
|
+
suggestions={suggestions}
|
|
56
|
+
onSelect={onSuggestion}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
|
|
61
|
+
{isLoading && !isStreaming ? (
|
|
62
|
+
<ChatTypingIndicator label="Thinking" />
|
|
63
|
+
) : null}
|
|
64
|
+
|
|
65
|
+
{isStreaming ? (
|
|
66
|
+
<ChatTypingIndicator label="Generating" />
|
|
67
|
+
) : null}
|
|
68
|
+
|
|
69
|
+
<div ref={bottomRef} />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ChatMessageList from "./ChatMessageList";
|
|
3
|
+
import ChatInput from "./ChatInput";
|
|
4
|
+
import ChatWelcome from "./ChatWelcome";
|
|
5
|
+
import useChatState from "./useChatState";
|
|
6
|
+
import { TrashIcon } from "@heroicons/react/24/outline";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* All-in-one chat panel. Composes ChatMessageList, ChatInput, ChatWelcome,
|
|
10
|
+
* and useChatState into a single drop-in component.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} title — panel header title
|
|
13
|
+
* @param {Function} onSend — async (userMessage, history, helpers) => assistantMessage?
|
|
14
|
+
* @param {Array} initialMessages — seed messages
|
|
15
|
+
* @param {string} welcomeTitle — welcome screen heading
|
|
16
|
+
* @param {string} welcomeSubtitle — welcome screen description
|
|
17
|
+
* @param {string[]} suggestions — starter and follow-up prompts
|
|
18
|
+
* @param {string} placeholder — input placeholder
|
|
19
|
+
* @param {string} className — additional classes on the root container
|
|
20
|
+
* @param {Function} renderAvatar — (message) => ReactNode
|
|
21
|
+
* @param {boolean} showHeader — show the title bar (default true)
|
|
22
|
+
*/
|
|
23
|
+
export default function ChatPanel({
|
|
24
|
+
title = "AI Assistant",
|
|
25
|
+
onSend,
|
|
26
|
+
initialMessages = [],
|
|
27
|
+
welcomeTitle,
|
|
28
|
+
welcomeSubtitle,
|
|
29
|
+
suggestions = [],
|
|
30
|
+
placeholder,
|
|
31
|
+
className = "",
|
|
32
|
+
renderAvatar,
|
|
33
|
+
showHeader = true,
|
|
34
|
+
}) {
|
|
35
|
+
const chat = useChatState({ initialMessages, onSend });
|
|
36
|
+
|
|
37
|
+
const isEmpty = chat.messages.length === 0;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={[
|
|
42
|
+
"flex flex-col overflow-hidden rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900",
|
|
43
|
+
className,
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ")}
|
|
47
|
+
>
|
|
48
|
+
{/* Header */}
|
|
49
|
+
{showHeader ? (
|
|
50
|
+
<div className="flex items-center justify-between border-b border-slate-100 px-4 py-3 dark:border-slate-800">
|
|
51
|
+
<h3 className="text-sm font-semibold text-slate-900 dark:text-slate-50">
|
|
52
|
+
{title}
|
|
53
|
+
</h3>
|
|
54
|
+
{chat.messages.length > 0 ? (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={chat.clearMessages}
|
|
58
|
+
className="rounded p-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
|
|
59
|
+
aria-label="Clear chat"
|
|
60
|
+
>
|
|
61
|
+
<TrashIcon className="h-4 w-4" />
|
|
62
|
+
</button>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
) : null}
|
|
66
|
+
|
|
67
|
+
{/* Messages or welcome */}
|
|
68
|
+
{isEmpty ? (
|
|
69
|
+
<ChatWelcome
|
|
70
|
+
title={welcomeTitle}
|
|
71
|
+
subtitle={welcomeSubtitle}
|
|
72
|
+
suggestions={suggestions}
|
|
73
|
+
onSuggestion={(text) => chat.sendMessage(text)}
|
|
74
|
+
/>
|
|
75
|
+
) : (
|
|
76
|
+
<ChatMessageList
|
|
77
|
+
messages={chat.messages}
|
|
78
|
+
isLoading={chat.isLoading}
|
|
79
|
+
isStreaming={chat.isStreaming}
|
|
80
|
+
suggestions={suggestions}
|
|
81
|
+
onSuggestion={(text) => chat.sendMessage(text)}
|
|
82
|
+
renderAvatar={renderAvatar}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Input */}
|
|
87
|
+
<div className="border-t border-slate-100 p-3 dark:border-slate-800">
|
|
88
|
+
<ChatInput
|
|
89
|
+
onSend={(content) => chat.sendMessage(content)}
|
|
90
|
+
disabled={chat.isLoading}
|
|
91
|
+
isLoading={chat.isLoading}
|
|
92
|
+
placeholder={placeholder}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SparklesIcon } from "@heroicons/react/24/outline";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Quick-action prompt buttons. Place above the input or after an assistant message.
|
|
6
|
+
*
|
|
7
|
+
* @param {string[]} suggestions — prompt strings
|
|
8
|
+
* @param {Function} onSelect — (suggestion) => void
|
|
9
|
+
*/
|
|
10
|
+
export default function ChatSuggestions({ suggestions = [], onSelect }) {
|
|
11
|
+
if (!suggestions.length) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex flex-wrap gap-2">
|
|
15
|
+
{suggestions.map((text) => (
|
|
16
|
+
<button
|
|
17
|
+
key={text}
|
|
18
|
+
type="button"
|
|
19
|
+
onClick={() => onSelect?.(text)}
|
|
20
|
+
className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 shadow-sm transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:border-brand-700 dark:hover:bg-brand-950/30 dark:hover:text-brand-300"
|
|
21
|
+
>
|
|
22
|
+
<SparklesIcon className="h-3 w-3" aria-hidden="true" />
|
|
23
|
+
{text}
|
|
24
|
+
</button>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import Spinner from "../ui/Spinner";
|
|
3
|
+
import {
|
|
4
|
+
WrenchScrewdriverIcon,
|
|
5
|
+
CheckCircleIcon,
|
|
6
|
+
XCircleIcon,
|
|
7
|
+
ChevronDownIcon,
|
|
8
|
+
} from "@heroicons/react/24/outline";
|
|
9
|
+
|
|
10
|
+
const STATUS_CONFIG = {
|
|
11
|
+
running: {
|
|
12
|
+
icon: <Spinner size="xs" tone="brand" label="Running" />,
|
|
13
|
+
text: "text-brand-600 dark:text-brand-400",
|
|
14
|
+
bg: "bg-brand-50 border-brand-100 dark:bg-brand-950/20 dark:border-brand-900/30",
|
|
15
|
+
label: "Running",
|
|
16
|
+
},
|
|
17
|
+
complete: {
|
|
18
|
+
icon: <CheckCircleIcon className="h-3.5 w-3.5 text-emerald-500" />,
|
|
19
|
+
text: "text-emerald-700 dark:text-emerald-300",
|
|
20
|
+
bg: "bg-emerald-50 border-emerald-100 dark:bg-emerald-950/20 dark:border-emerald-900/30",
|
|
21
|
+
label: "Complete",
|
|
22
|
+
},
|
|
23
|
+
error: {
|
|
24
|
+
icon: <XCircleIcon className="h-3.5 w-3.5 text-red-500" />,
|
|
25
|
+
text: "text-red-700 dark:text-red-300",
|
|
26
|
+
bg: "bg-red-50 border-red-100 dark:bg-red-950/20 dark:border-red-900/30",
|
|
27
|
+
label: "Failed",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Displays an agent tool call / function execution step.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} toolCall — { id?, name, args?, status: "running"|"complete"|"error", result? }
|
|
35
|
+
*/
|
|
36
|
+
export default function ChatToolCall({ toolCall }) {
|
|
37
|
+
const [expanded, setExpanded] = useState(false);
|
|
38
|
+
const config = STATUS_CONFIG[toolCall.status] ?? STATUS_CONFIG.running;
|
|
39
|
+
const hasDetails = toolCall.args || toolCall.result;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={[
|
|
44
|
+
"rounded-lg border text-xs",
|
|
45
|
+
config.bg,
|
|
46
|
+
].join(" ")}
|
|
47
|
+
>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={() => hasDetails && setExpanded(!expanded)}
|
|
51
|
+
className={[
|
|
52
|
+
"flex w-full items-center gap-2 px-3 py-1.5",
|
|
53
|
+
hasDetails ? "cursor-pointer" : "cursor-default",
|
|
54
|
+
].join(" ")}
|
|
55
|
+
>
|
|
56
|
+
{config.icon}
|
|
57
|
+
<WrenchScrewdriverIcon className="h-3 w-3 text-slate-400 dark:text-slate-500" />
|
|
58
|
+
<span className={["font-medium", config.text].join(" ")}>
|
|
59
|
+
{toolCall.name}
|
|
60
|
+
</span>
|
|
61
|
+
<span className="text-slate-400 dark:text-slate-500">
|
|
62
|
+
— {config.label}
|
|
63
|
+
</span>
|
|
64
|
+
{hasDetails ? (
|
|
65
|
+
<ChevronDownIcon
|
|
66
|
+
className={[
|
|
67
|
+
"ml-auto h-3 w-3 text-slate-400 transition-transform dark:text-slate-500",
|
|
68
|
+
expanded ? "rotate-180" : "",
|
|
69
|
+
].join(" ")}
|
|
70
|
+
/>
|
|
71
|
+
) : null}
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
{expanded && hasDetails ? (
|
|
75
|
+
<div className="border-t border-inherit px-3 py-2 font-mono text-[11px] leading-relaxed text-slate-600 dark:text-slate-300">
|
|
76
|
+
{toolCall.args ? (
|
|
77
|
+
<div className="mb-1">
|
|
78
|
+
<span className="font-sans font-semibold text-slate-500 dark:text-slate-400">
|
|
79
|
+
Args:{" "}
|
|
80
|
+
</span>
|
|
81
|
+
{typeof toolCall.args === "string"
|
|
82
|
+
? toolCall.args
|
|
83
|
+
: JSON.stringify(toolCall.args, null, 2)}
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
{toolCall.result ? (
|
|
87
|
+
<div>
|
|
88
|
+
<span className="font-sans font-semibold text-slate-500 dark:text-slate-400">
|
|
89
|
+
Result:{" "}
|
|
90
|
+
</span>
|
|
91
|
+
{typeof toolCall.result === "string"
|
|
92
|
+
? toolCall.result
|
|
93
|
+
: JSON.stringify(toolCall.result, null, 2)}
|
|
94
|
+
</div>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CpuChipIcon } from "@heroicons/react/24/solid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Animated typing indicator shown while the agent is processing.
|
|
6
|
+
*/
|
|
7
|
+
export default function ChatTypingIndicator({ label = "Thinking" }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-start gap-3">
|
|
10
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-brand-100 dark:bg-brand-900/40">
|
|
11
|
+
<CpuChipIcon className="h-4 w-4 text-brand-600 dark:text-brand-400" />
|
|
12
|
+
</div>
|
|
13
|
+
<div className="flex items-center gap-2 rounded-2xl rounded-tl-md bg-slate-100 px-4 py-3 dark:bg-slate-800">
|
|
14
|
+
<div className="flex gap-1">
|
|
15
|
+
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 dark:bg-slate-500 [animation-delay:0ms]" />
|
|
16
|
+
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 dark:bg-slate-500 [animation-delay:150ms]" />
|
|
17
|
+
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 dark:bg-slate-500 [animation-delay:300ms]" />
|
|
18
|
+
</div>
|
|
19
|
+
<span className="text-xs text-slate-400 dark:text-slate-500">{label}</span>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CpuChipIcon } from "@heroicons/react/24/solid";
|
|
3
|
+
import ChatSuggestions from "./ChatSuggestions";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Empty-state welcome screen shown before the first message.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} title — welcome heading
|
|
9
|
+
* @param {string} subtitle — description text
|
|
10
|
+
* @param {string[]} suggestions — starter prompt suggestions
|
|
11
|
+
* @param {Function} onSuggestion — (text) => void
|
|
12
|
+
* @param {React.ReactNode} icon — custom icon override
|
|
13
|
+
*/
|
|
14
|
+
export default function ChatWelcome({
|
|
15
|
+
title = "How can I help?",
|
|
16
|
+
subtitle = "Ask me anything about your data, or try one of the suggestions below.",
|
|
17
|
+
suggestions = [],
|
|
18
|
+
onSuggestion,
|
|
19
|
+
icon,
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-4 px-6 py-12 text-center">
|
|
23
|
+
{icon ?? (
|
|
24
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-brand-100 dark:bg-brand-900/40">
|
|
25
|
+
<CpuChipIcon className="h-7 w-7 text-brand-600 dark:text-brand-400" />
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
<div>
|
|
29
|
+
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-50">
|
|
30
|
+
{title}
|
|
31
|
+
</h3>
|
|
32
|
+
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
|
33
|
+
{subtitle}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
{suggestions.length ? (
|
|
37
|
+
<div className="mt-2">
|
|
38
|
+
<ChatSuggestions suggestions={suggestions} onSelect={onSuggestion} />
|
|
39
|
+
</div>
|
|
40
|
+
) : null}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as ChatPanel } from "./ChatPanel";
|
|
2
|
+
export { default as ChatBar } from "./ChatBar";
|
|
3
|
+
export { default as ChatMessageList } from "./ChatMessageList";
|
|
4
|
+
export { default as ChatMessage } from "./ChatMessage";
|
|
5
|
+
export { default as ChatInput } from "./ChatInput";
|
|
6
|
+
export { default as ChatTypingIndicator } from "./ChatTypingIndicator";
|
|
7
|
+
export { default as ChatSuggestions } from "./ChatSuggestions";
|
|
8
|
+
export { default as ChatToolCall } from "./ChatToolCall";
|
|
9
|
+
export { default as ChatWelcome } from "./ChatWelcome";
|
|
10
|
+
export { default as useChatState } from "./useChatState";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
let _nextId = 1;
|
|
4
|
+
function uid() {
|
|
5
|
+
return `msg-${Date.now()}-${_nextId++}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Core state management hook for AI chat.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {Array} options.initialMessages — seed the conversation
|
|
13
|
+
* @param {Function} options.onSend — async (userMessage, allMessages) => assistantMessage | void
|
|
14
|
+
* Return a message object to add it, or use appendChunk / addMessage for streaming.
|
|
15
|
+
*
|
|
16
|
+
* @returns {Object}
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const chat = useChatState({
|
|
20
|
+
* onSend: async (msg, history) => {
|
|
21
|
+
* const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: history }) });
|
|
22
|
+
* const data = await res.json();
|
|
23
|
+
* return { role: "assistant", content: data.reply, components: data.components };
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export default function useChatState({ initialMessages = [], onSend } = {}) {
|
|
28
|
+
const [messages, setMessages] = useState(() =>
|
|
29
|
+
initialMessages.map((m) => ({ id: uid(), timestamp: new Date().toISOString(), ...m }))
|
|
30
|
+
);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
33
|
+
const [error, setError] = useState(null);
|
|
34
|
+
const onSendRef = useRef(onSend);
|
|
35
|
+
onSendRef.current = onSend;
|
|
36
|
+
|
|
37
|
+
const addMessage = useCallback((msg) => {
|
|
38
|
+
const full = { id: uid(), timestamp: new Date().toISOString(), ...msg };
|
|
39
|
+
setMessages((prev) => [...prev, full]);
|
|
40
|
+
return full.id;
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const updateMessage = useCallback((id, updates) => {
|
|
44
|
+
setMessages((prev) =>
|
|
45
|
+
prev.map((m) => (m.id === id ? { ...m, ...updates } : m))
|
|
46
|
+
);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const appendChunk = useCallback((id, chunk) => {
|
|
50
|
+
setMessages((prev) =>
|
|
51
|
+
prev.map((m) =>
|
|
52
|
+
m.id === id ? { ...m, content: (m.content ?? "") + chunk } : m
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const removeMessage = useCallback((id) => {
|
|
58
|
+
setMessages((prev) => prev.filter((m) => m.id !== id));
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const clearMessages = useCallback(() => {
|
|
62
|
+
setMessages([]);
|
|
63
|
+
setError(null);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const sendMessage = useCallback(
|
|
67
|
+
async (content, extra = {}) => {
|
|
68
|
+
if (!content?.trim()) return;
|
|
69
|
+
setError(null);
|
|
70
|
+
|
|
71
|
+
const userMsg = { role: "user", content: content.trim(), ...extra };
|
|
72
|
+
const userId = uid();
|
|
73
|
+
const fullUser = { ...userMsg, id: userId, timestamp: new Date().toISOString() };
|
|
74
|
+
|
|
75
|
+
setMessages((prev) => [...prev, fullUser]);
|
|
76
|
+
setIsLoading(true);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const allMsgs = [...messages, fullUser];
|
|
80
|
+
const result = await onSendRef.current?.(fullUser, allMsgs, {
|
|
81
|
+
addMessage,
|
|
82
|
+
updateMessage,
|
|
83
|
+
appendChunk,
|
|
84
|
+
setStreaming: setIsStreaming,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (result && typeof result === "object" && result.role) {
|
|
88
|
+
addMessage(result);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setError(err?.message ?? "Failed to send message");
|
|
92
|
+
addMessage({
|
|
93
|
+
role: "system",
|
|
94
|
+
content: err?.message ?? "Something went wrong. Please try again.",
|
|
95
|
+
isError: true,
|
|
96
|
+
});
|
|
97
|
+
} finally {
|
|
98
|
+
setIsLoading(false);
|
|
99
|
+
setIsStreaming(false);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[messages, addMessage, updateMessage, appendChunk]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const retryLast = useCallback(() => {
|
|
106
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
107
|
+
if (!lastUser) return;
|
|
108
|
+
|
|
109
|
+
const idx = messages.lastIndexOf(lastUser);
|
|
110
|
+
setMessages(messages.slice(0, idx));
|
|
111
|
+
setError(null);
|
|
112
|
+
|
|
113
|
+
sendMessage(lastUser.content);
|
|
114
|
+
}, [messages, sendMessage]);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
messages,
|
|
118
|
+
isLoading,
|
|
119
|
+
isStreaming,
|
|
120
|
+
error,
|
|
121
|
+
sendMessage,
|
|
122
|
+
addMessage,
|
|
123
|
+
updateMessage,
|
|
124
|
+
appendChunk,
|
|
125
|
+
removeMessage,
|
|
126
|
+
clearMessages,
|
|
127
|
+
retryLast,
|
|
128
|
+
setError,
|
|
129
|
+
};
|
|
130
|
+
}
|