@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.
Files changed (109) hide show
  1. package/package.json +4 -1
  2. package/scripts/postinstall.mjs +36 -17
  3. package/src/components/library/cards/ActionList.jsx +38 -0
  4. package/src/components/library/cards/ActivityCard.jsx +56 -0
  5. package/src/components/library/cards/BaseCard.jsx +109 -0
  6. package/src/components/library/cards/CalloutCard.jsx +37 -0
  7. package/src/components/library/cards/ChartCard.jsx +105 -0
  8. package/src/components/library/cards/FeedPanel.jsx +39 -0
  9. package/src/components/library/cards/ListCard.jsx +193 -0
  10. package/src/components/library/cards/MetricCard.jsx +109 -0
  11. package/src/components/library/cards/MetricsStrip.jsx +78 -0
  12. package/src/components/library/cards/SectionCard.jsx +83 -0
  13. package/src/components/library/cards/SemanticMetricCard.jsx +52 -0
  14. package/src/components/library/cards/SemanticMetricCardWithLoading.jsx +23 -0
  15. package/src/components/library/cards/SemanticTableCard.jsx +48 -0
  16. package/src/components/library/cards/SemanticTableCardWithLoading.jsx +22 -0
  17. package/src/components/library/cards/StatusCard.jsx +220 -0
  18. package/src/components/library/cards/TableCard.jsx +337 -0
  19. package/src/components/library/cards/WidgetCard.jsx +90 -0
  20. package/src/components/library/charts/D3Chart.jsx +109 -0
  21. package/src/components/library/charts/D3ChartTemplates.jsx +126 -0
  22. package/src/components/library/charts/GeoMap.jsx +293 -0
  23. package/src/components/library/chat/ChatBar.jsx +256 -0
  24. package/src/components/library/chat/ChatInput.jsx +89 -0
  25. package/src/components/library/chat/ChatMessage.jsx +178 -0
  26. package/src/components/library/chat/ChatMessageList.jsx +73 -0
  27. package/src/components/library/chat/ChatPanel.jsx +97 -0
  28. package/src/components/library/chat/ChatSuggestions.jsx +28 -0
  29. package/src/components/library/chat/ChatToolCall.jsx +100 -0
  30. package/src/components/library/chat/ChatTypingIndicator.jsx +23 -0
  31. package/src/components/library/chat/ChatWelcome.jsx +43 -0
  32. package/src/components/library/chat/index.jsx +10 -0
  33. package/src/components/library/chat/useChatState.jsx +130 -0
  34. package/src/components/library/data/DataModeProvider.jsx +67 -0
  35. package/src/components/library/data/DataModeToggle.jsx +36 -0
  36. package/src/components/library/data/chartDataProvider.jsx +61 -0
  37. package/src/components/library/data/filterUtils.jsx +141 -0
  38. package/src/components/library/data/useDataSource.jsx +33 -0
  39. package/src/components/library/data/usePageFilters.jsx +99 -0
  40. package/src/components/library/filters/FilterBar.jsx +95 -0
  41. package/src/components/library/filters/SearchFilter.jsx +36 -0
  42. package/src/components/library/filters/SelectFilter.jsx +55 -0
  43. package/src/components/library/filters/ToggleFilter.jsx +52 -0
  44. package/src/components/library/filters/index.jsx +4 -0
  45. package/src/components/library/forms/FormField.jsx +291 -0
  46. package/src/components/library/forms/FormModal.jsx +201 -0
  47. package/src/components/library/forms/FormRenderer.jsx +46 -0
  48. package/src/components/library/forms/FormSection.jsx +69 -0
  49. package/src/components/library/forms/index.jsx +5 -0
  50. package/src/components/library/forms/useFormState.jsx +165 -0
  51. package/src/components/library/heroui/Accordion.jsx +26 -0
  52. package/src/components/library/heroui/Alert.jsx +8 -0
  53. package/src/components/library/heroui/Badge.jsx +8 -0
  54. package/src/components/library/heroui/Breadcrumbs.jsx +22 -0
  55. package/src/components/library/heroui/Button.jsx +58 -0
  56. package/src/components/library/heroui/Card.jsx +8 -0
  57. package/src/components/library/heroui/Collapsible.jsx +42 -0
  58. package/src/components/library/heroui/DatePicker.jsx +34 -0
  59. package/src/components/library/heroui/Dialog.jsx +37 -0
  60. package/src/components/library/heroui/Drawer.jsx +32 -0
  61. package/src/components/library/heroui/Dropdown.jsx +28 -0
  62. package/src/components/library/heroui/Field.jsx +51 -0
  63. package/src/components/library/heroui/Input.jsx +6 -0
  64. package/src/components/library/heroui/Kbd.jsx +8 -0
  65. package/src/components/library/heroui/Meter.jsx +8 -0
  66. package/src/components/library/heroui/Modal.jsx +32 -0
  67. package/src/components/library/heroui/Pagination.jsx +8 -0
  68. package/src/components/library/heroui/Popover.jsx +64 -0
  69. package/src/components/library/heroui/ProgressBar.jsx +8 -0
  70. package/src/components/library/heroui/ProgressCircle.jsx +8 -0
  71. package/src/components/library/heroui/ScrollShadow.jsx +8 -0
  72. package/src/components/library/heroui/Select.jsx +37 -0
  73. package/src/components/library/heroui/Separator.jsx +8 -0
  74. package/src/components/library/heroui/Skeleton.jsx +8 -0
  75. package/src/components/library/heroui/Tabs.jsx +26 -0
  76. package/src/components/library/heroui/Toast.jsx +25 -0
  77. package/src/components/library/heroui/Toggle.jsx +14 -0
  78. package/src/components/library/heroui/Tooltip.jsx +21 -0
  79. package/src/components/library/index.jsx +146 -0
  80. package/src/components/library/layout/PageContainer.jsx +11 -0
  81. package/src/components/library/skeletons/CardSkeleton.jsx +30 -0
  82. package/src/components/library/theme/AppThemeProvider.jsx +67 -0
  83. package/src/components/library/theme/tokens.jsx +72 -0
  84. package/src/components/library/ui/Alert.jsx +80 -0
  85. package/src/components/library/ui/Avatar.jsx +44 -0
  86. package/src/components/library/ui/BreadcrumbExtras.tsx +120 -0
  87. package/src/components/library/ui/Button.jsx +61 -0
  88. package/src/components/library/ui/Card.jsx +117 -0
  89. package/src/components/library/ui/Checkbox.jsx +17 -0
  90. package/src/components/library/ui/Chip.jsx +38 -0
  91. package/src/components/library/ui/Collapsible.tsx +31 -0
  92. package/src/components/library/ui/Container.jsx +56 -0
  93. package/src/components/library/ui/DatePicker.tsx +34 -0
  94. package/src/components/library/ui/Dialog.tsx +141 -0
  95. package/src/components/library/ui/EmptyState.jsx +46 -0
  96. package/src/components/library/ui/Field.tsx +82 -0
  97. package/src/components/library/ui/FieldGroup.jsx +17 -0
  98. package/src/components/library/ui/Input.jsx +21 -0
  99. package/src/components/library/ui/Label.jsx +22 -0
  100. package/src/components/library/ui/PaginationExtras.tsx +142 -0
  101. package/src/components/library/ui/Popover.tsx +39 -0
  102. package/src/components/library/ui/Select.tsx +113 -0
  103. package/src/components/library/ui/Spinner.d.ts +10 -0
  104. package/src/components/library/ui/Spinner.jsx +64 -0
  105. package/src/components/library/ui/Text.jsx +46 -0
  106. package/src/components/library/ui/Toggle.jsx +42 -0
  107. package/src/components/workspace/ComponentRegistry.jsx +297 -0
  108. package/src/lib/index.ts +1 -0
  109. 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
+ }