@databricks/appkit-ui 0.16.0 → 0.18.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/CLAUDE.md +1 -0
- package/dist/react/charts/base.js +3 -2
- package/dist/react/charts/base.js.map +1 -1
- package/dist/react/charts/normalize.d.ts.map +1 -1
- package/dist/react/charts/normalize.js +3 -1
- package/dist/react/charts/normalize.js.map +1 -1
- package/dist/react/charts/options.d.ts +1 -0
- package/dist/react/charts/options.d.ts.map +1 -1
- package/dist/react/charts/options.js +13 -8
- package/dist/react/charts/options.js.map +1 -1
- package/dist/react/charts/utils.d.ts.map +1 -1
- package/dist/react/charts/utils.js +23 -1
- package/dist/react/charts/utils.js.map +1 -1
- package/dist/react/genie/genie-chart-inference.d.ts +17 -0
- package/dist/react/genie/genie-chart-inference.d.ts.map +1 -0
- package/dist/react/genie/genie-chart-inference.js +75 -0
- package/dist/react/genie/genie-chart-inference.js.map +1 -0
- package/dist/react/genie/genie-chat-message-list.d.ts +7 -1
- package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message-list.js +92 -17
- package/dist/react/genie/genie-chat-message-list.js.map +1 -1
- package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message.js +26 -15
- package/dist/react/genie/genie-chat-message.js.map +1 -1
- package/dist/react/genie/genie-chat.js +4 -2
- package/dist/react/genie/genie-chat.js.map +1 -1
- package/dist/react/genie/genie-query-transform.d.ts +31 -0
- package/dist/react/genie/genie-query-transform.d.ts.map +1 -0
- package/dist/react/genie/genie-query-transform.js +79 -0
- package/dist/react/genie/genie-query-transform.js.map +1 -0
- package/dist/react/genie/genie-query-visualization.d.ts +25 -0
- package/dist/react/genie/genie-query-visualization.d.ts.map +1 -0
- package/dist/react/genie/genie-query-visualization.js +79 -0
- package/dist/react/genie/genie-query-visualization.js.map +1 -0
- package/dist/react/genie/index.d.ts +4 -1
- package/dist/react/genie/index.js +3 -0
- package/dist/react/genie/types.d.ts +9 -3
- package/dist/react/genie/types.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.js +119 -41
- package/dist/react/genie/use-genie-chat.js.map +1 -1
- package/dist/react/index.d.ts +5 -2
- package/dist/react/index.js +6 -3
- package/dist/react/table/data-table.js +1 -1
- package/dist/react/ui/index.js +2 -2
- package/dist/shared/src/genie.d.ts +22 -2
- package/dist/shared/src/genie.d.ts.map +1 -1
- package/dist/shared/src/index.d.ts +1 -1
- package/docs/api/appkit-ui/genie/GenieChatMessageList.md +7 -5
- package/docs/api/appkit-ui/genie/GenieQueryVisualization.md +29 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { ScrollArea } from "../ui/scroll-area.js";
|
|
|
3
3
|
import { Skeleton } from "../ui/skeleton.js";
|
|
4
4
|
import { Spinner } from "../ui/spinner.js";
|
|
5
5
|
import { GenieChatMessage } from "./genie-chat-message.js";
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
|
|
9
9
|
//#region src/react/genie/genie-chat-message-list.tsx
|
|
@@ -16,27 +16,102 @@ const STATUS_LABELS = {
|
|
|
16
16
|
function formatStatus(status) {
|
|
17
17
|
return STATUS_LABELS[status] ?? status.replace(/_/g, " ").toLowerCase();
|
|
18
18
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
if (last?.role === "assistant" && last.id === "") return /* @__PURE__ */ jsxs("div", {
|
|
22
|
-
className: "flex items-center gap-2 text-sm text-muted-foreground px-11",
|
|
23
|
-
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", { children: formatStatus(last.status) })]
|
|
24
|
-
});
|
|
25
|
-
return null;
|
|
19
|
+
function getViewport(scrollRef) {
|
|
20
|
+
return scrollRef.current?.querySelector("[data-slot=\"scroll-area-viewport\"]");
|
|
26
21
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Manages scroll position: scrolls to bottom on append/initial load,
|
|
24
|
+
* preserves position when older messages are prepended.
|
|
25
|
+
*/
|
|
26
|
+
function useScrollManagement(scrollRef, messages, status) {
|
|
27
|
+
const prevFirstMessageIdRef = useRef(null);
|
|
28
|
+
const prevScrollHeightRef = useRef(0);
|
|
29
|
+
const prevMessageCountRef = useRef(0);
|
|
30
30
|
useEffect(() => {
|
|
31
|
-
const viewport = scrollRef
|
|
32
|
-
if (viewport)
|
|
31
|
+
const viewport = getViewport(scrollRef);
|
|
32
|
+
if (!viewport) return;
|
|
33
|
+
const observer = new ResizeObserver(() => {
|
|
34
|
+
prevScrollHeightRef.current = viewport.scrollHeight;
|
|
35
|
+
});
|
|
36
|
+
observer.observe(viewport);
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, [scrollRef]);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const viewport = getViewport(scrollRef);
|
|
41
|
+
if (!viewport) return;
|
|
42
|
+
const count = messages.length;
|
|
43
|
+
const countChanged = count !== prevMessageCountRef.current;
|
|
44
|
+
prevMessageCountRef.current = count;
|
|
45
|
+
if (!countChanged) {
|
|
46
|
+
prevScrollHeightRef.current = viewport.scrollHeight;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const firstMessageId = messages[0]?.id ?? null;
|
|
50
|
+
if (prevFirstMessageIdRef.current !== null && firstMessageId !== prevFirstMessageIdRef.current && prevScrollHeightRef.current > 0) {
|
|
51
|
+
const delta = viewport.scrollHeight - prevScrollHeightRef.current;
|
|
52
|
+
viewport.scrollTop += delta;
|
|
53
|
+
} else viewport.scrollTop = viewport.scrollHeight;
|
|
54
|
+
prevFirstMessageIdRef.current = firstMessageId;
|
|
55
|
+
prevScrollHeightRef.current = viewport.scrollHeight;
|
|
33
56
|
}, [messages.length, status]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Observes a sentinel element at the top of the scroll area and triggers
|
|
60
|
+
* `onFetchPreviousPage` when the user scrolls to the top (only if content overflows).
|
|
61
|
+
* Returns a ref to attach to the sentinel element.
|
|
62
|
+
*/
|
|
63
|
+
function useLoadOlderOnScroll(scrollRef, shouldObserve, onFetchPreviousPage) {
|
|
64
|
+
const sentinelRef = useRef(null);
|
|
65
|
+
const onFetchPreviousPageRef = useRef(onFetchPreviousPage);
|
|
66
|
+
onFetchPreviousPageRef.current = onFetchPreviousPage;
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const sentinel = sentinelRef.current;
|
|
69
|
+
const viewport = getViewport(scrollRef);
|
|
70
|
+
if (!sentinel || !viewport || !shouldObserve) return;
|
|
71
|
+
let armed = false;
|
|
72
|
+
const frameId = requestAnimationFrame(() => {
|
|
73
|
+
armed = true;
|
|
74
|
+
});
|
|
75
|
+
const observer = new IntersectionObserver((entries) => {
|
|
76
|
+
if (!armed) return;
|
|
77
|
+
const isScrollable = viewport.scrollHeight > viewport.clientHeight;
|
|
78
|
+
if (entries[0]?.isIntersecting && isScrollable) onFetchPreviousPageRef.current?.();
|
|
79
|
+
}, {
|
|
80
|
+
root: viewport,
|
|
81
|
+
threshold: 0
|
|
82
|
+
});
|
|
83
|
+
observer.observe(sentinel);
|
|
84
|
+
return () => {
|
|
85
|
+
cancelAnimationFrame(frameId);
|
|
86
|
+
observer.disconnect();
|
|
87
|
+
};
|
|
88
|
+
}, [scrollRef, shouldObserve]);
|
|
89
|
+
return sentinelRef;
|
|
90
|
+
}
|
|
91
|
+
/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */
|
|
92
|
+
function GenieChatMessageList({ messages, status, className, hasPreviousPage = false, onFetchPreviousPage }) {
|
|
93
|
+
const scrollRef = useRef(null);
|
|
94
|
+
const sentinelRef = useLoadOlderOnScroll(scrollRef, hasPreviousPage && status !== "loading-older", onFetchPreviousPage);
|
|
95
|
+
useScrollManagement(scrollRef, messages, status);
|
|
96
|
+
const lastMessage = messages[messages.length - 1];
|
|
97
|
+
const showStreamingIndicator = status === "streaming" && lastMessage?.role === "assistant" && lastMessage.id === "";
|
|
34
98
|
return /* @__PURE__ */ jsx(ScrollArea, {
|
|
35
99
|
ref: scrollRef,
|
|
36
100
|
className: cn("flex-1 min-h-0 p-4", className),
|
|
37
101
|
children: /* @__PURE__ */ jsxs("div", {
|
|
38
102
|
className: "flex flex-col gap-4",
|
|
39
103
|
children: [
|
|
104
|
+
hasPreviousPage && /* @__PURE__ */ jsx("div", {
|
|
105
|
+
ref: sentinelRef,
|
|
106
|
+
className: "h-px"
|
|
107
|
+
}),
|
|
108
|
+
status === "loading-older" && /* @__PURE__ */ jsxs("div", {
|
|
109
|
+
className: "flex items-center justify-center gap-2 py-2",
|
|
110
|
+
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", {
|
|
111
|
+
className: "text-sm text-muted-foreground",
|
|
112
|
+
children: "Loading older messages..."
|
|
113
|
+
})]
|
|
114
|
+
}),
|
|
40
115
|
status === "loading-history" && messages.length === 0 && /* @__PURE__ */ jsxs("div", {
|
|
41
116
|
className: "flex flex-col gap-4",
|
|
42
117
|
children: [
|
|
@@ -45,11 +120,11 @@ function GenieChatMessageList({ messages, status, className }) {
|
|
|
45
120
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-2/3 self-end" })
|
|
46
121
|
]
|
|
47
122
|
}),
|
|
48
|
-
messages.map((msg) => {
|
|
49
|
-
|
|
50
|
-
|
|
123
|
+
messages.filter((msg) => msg.role !== "assistant" || msg.id !== "" || msg.content).map((msg) => /* @__PURE__ */ jsx(GenieChatMessage, { message: msg }, msg.id)),
|
|
124
|
+
showStreamingIndicator && /* @__PURE__ */ jsxs("div", {
|
|
125
|
+
className: "flex items-center gap-2 text-sm text-muted-foreground px-11",
|
|
126
|
+
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", { children: formatStatus(lastMessage.status) })]
|
|
51
127
|
}),
|
|
52
|
-
status === "streaming" && messages.length > 0 && /* @__PURE__ */ jsx(StreamingIndicator, { messages }),
|
|
53
128
|
messages.length === 0 && status === "idle" && /* @__PURE__ */ jsx("div", {
|
|
54
129
|
className: "flex items-center justify-center h-full text-muted-foreground text-sm py-12",
|
|
55
130
|
children: "Start a conversation by typing a question below."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message-list.js","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { Spinner } from \"../ui/spinner\";\nimport { GenieChatMessage } from \"./genie-chat-message\";\nimport type { GenieChatStatus, GenieMessageItem } from \"./types\";\n\nexport interface GenieChatMessageListProps {\n /** Array of messages to display */\n messages: GenieMessageItem[];\n /** Current chat status (controls loading indicators and skeleton placeholders) */\n status: GenieChatStatus;\n /** Additional CSS class for the scroll area */\n className?: string;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n ASKING_AI: \"Asking AI...\",\n EXECUTING_QUERY: \"Executing query...\",\n FILTERING_RESULTS: \"Filtering results...\",\n COMPLETED: \"Done\",\n};\n\nfunction formatStatus(status: string): string {\n return STATUS_LABELS[status] ?? status.replace(/_/g, \" \").toLowerCase();\n}\n\nfunction StreamingIndicator({ messages }: { messages: GenieMessageItem[] }) {\n const last = messages[messages.length - 1];\n if (last?.role === \"assistant\" && last.id === \"\") {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(last.status)}</span>\n </div>\n );\n }\n return null;\n}\n\n/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */\nexport function GenieChatMessageList({\n messages,\n status,\n className,\n}: GenieChatMessageListProps) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Scroll only the ScrollArea viewport, not the page\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentional triggers for auto-scroll\n useEffect(() => {\n const viewport = scrollRef.current?.querySelector<HTMLElement>(\n '[data-slot=\"scroll-area-viewport\"]',\n );\n if (viewport) {\n viewport.scrollTop = viewport.scrollHeight;\n }\n }, [messages.length, status]);\n\n return (\n <ScrollArea ref={scrollRef} className={cn(\"flex-1 min-h-0 p-4\", className)}>\n <div className=\"flex flex-col gap-4\">\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages.map((msg) => {\n if (msg.role === \"assistant\" && msg.id === \"\" && !msg.content) {\n return null;\n }\n return <GenieChatMessage key={msg.id} message={msg} />;\n })}\n\n {status === \"streaming\" && messages.length > 0 && (\n <StreamingIndicator messages={messages} />\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":";;;;;;;;;AAiBA,MAAM,gBAAwC;CAC5C,WAAW;CACX,iBAAiB;CACjB,mBAAmB;CACnB,WAAW;CACZ;AAED,SAAS,aAAa,QAAwB;AAC5C,QAAO,cAAc,WAAW,OAAO,QAAQ,MAAM,IAAI,CAAC,aAAa;;AAGzE,SAAS,mBAAmB,EAAE,YAA8C;CAC1E,MAAM,OAAO,SAAS,SAAS,SAAS;AACxC,KAAI,MAAM,SAAS,eAAe,KAAK,OAAO,GAC5C,QACE,qBAAC;EAAI,WAAU;aACb,oBAAC,WAAQ,WAAU,YAAY,EAC/B,oBAAC,oBAAM,aAAa,KAAK,OAAO,GAAQ;GACpC;AAGV,QAAO;;;AAIT,SAAgB,qBAAqB,EACnC,UACA,QACA,aAC4B;CAC5B,MAAM,YAAY,OAAuB,KAAK;AAI9C,iBAAgB;EACd,MAAM,WAAW,UAAU,SAAS,cAClC,uCACD;AACD,MAAI,SACF,UAAS,YAAY,SAAS;IAE/B,CAAC,SAAS,QAAQ,OAAO,CAAC;AAE7B,QACE,oBAAC;EAAW,KAAK;EAAW,WAAW,GAAG,sBAAsB,UAAU;YACxE,qBAAC;GAAI,WAAU;;IACZ,WAAW,qBAAqB,SAAS,WAAW,KACnD,qBAAC;KAAI,WAAU;;MACb,oBAAC,YAAS,WAAU,eAAe;MACnC,oBAAC,YAAS,WAAU,0BAA0B;MAC9C,oBAAC,YAAS,WAAU,wBAAwB;;MACxC;IAGP,SAAS,KAAK,QAAQ;AACrB,SAAI,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,CAAC,IAAI,QACpD,QAAO;AAET,YAAO,oBAAC,oBAA8B,SAAS,OAAjB,IAAI,GAAoB;MACtD;IAED,WAAW,eAAe,SAAS,SAAS,KAC3C,oBAAC,sBAA6B,WAAY;IAG3C,SAAS,WAAW,KAAK,WAAW,UACnC,oBAAC;KAAI,WAAU;eAA8E;MAEvF;;IAEJ;GACK"}
|
|
1
|
+
{"version":3,"file":"genie-chat-message-list.js","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { Spinner } from \"../ui/spinner\";\nimport { GenieChatMessage } from \"./genie-chat-message\";\nimport type { GenieChatStatus, GenieMessageItem } from \"./types\";\n\nexport interface GenieChatMessageListProps {\n /** Array of messages to display */\n messages: GenieMessageItem[];\n /** Current chat status (controls loading indicators and skeleton placeholders) */\n status: GenieChatStatus;\n /** Additional CSS class for the scroll area */\n className?: string;\n /** Whether a previous page of older messages exists */\n hasPreviousPage?: boolean;\n /** Callback to fetch the previous page of messages */\n onFetchPreviousPage?: () => void;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n ASKING_AI: \"Asking AI...\",\n EXECUTING_QUERY: \"Executing query...\",\n FILTERING_RESULTS: \"Filtering results...\",\n COMPLETED: \"Done\",\n};\n\nfunction formatStatus(status: string): string {\n return STATUS_LABELS[status] ?? status.replace(/_/g, \" \").toLowerCase();\n}\n\nfunction getViewport(scrollRef: React.RefObject<HTMLDivElement | null>) {\n return scrollRef.current?.querySelector<HTMLElement>(\n '[data-slot=\"scroll-area-viewport\"]',\n );\n}\n\n/**\n * Manages scroll position: scrolls to bottom on append/initial load,\n * preserves position when older messages are prepended.\n */\nfunction useScrollManagement(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n messages: GenieMessageItem[],\n status: GenieChatStatus,\n) {\n const prevFirstMessageIdRef = useRef<string | null>(null);\n const prevScrollHeightRef = useRef(0);\n const prevMessageCountRef = useRef(0);\n\n // Keep prevScrollHeightRef fresh when async content (images, embeds)\n // changes the viewport height between renders.\n useEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const observer = new ResizeObserver(() => {\n prevScrollHeightRef.current = viewport.scrollHeight;\n });\n observer.observe(viewport);\n return () => observer.disconnect();\n }, [scrollRef]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: react to message count AND status so prevScrollHeightRef stays accurate when the loading indicator appears/disappears\n useLayoutEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const count = messages.length;\n const countChanged = count !== prevMessageCountRef.current;\n prevMessageCountRef.current = count;\n\n // Nothing to do if message count didn't change (e.g. status-only transition)\n if (!countChanged) {\n prevScrollHeightRef.current = viewport.scrollHeight;\n return;\n }\n\n const firstMessageId = messages[0]?.id ?? null;\n const wasPrepend =\n prevFirstMessageIdRef.current !== null &&\n firstMessageId !== prevFirstMessageIdRef.current;\n\n if (wasPrepend && prevScrollHeightRef.current > 0) {\n // Older messages prepended — preserve scroll position\n const delta = viewport.scrollHeight - prevScrollHeightRef.current;\n viewport.scrollTop += delta;\n } else {\n // Messages appended or initial load — scroll to bottom\n viewport.scrollTop = viewport.scrollHeight;\n }\n\n prevFirstMessageIdRef.current = firstMessageId;\n prevScrollHeightRef.current = viewport.scrollHeight;\n }, [messages.length, status]);\n}\n\n/**\n * Observes a sentinel element at the top of the scroll area and triggers\n * `onFetchPreviousPage` when the user scrolls to the top (only if content overflows).\n * Returns a ref to attach to the sentinel element.\n */\nfunction useLoadOlderOnScroll(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n shouldObserve: boolean,\n onFetchPreviousPage?: () => void,\n) {\n const sentinelRef = useRef<HTMLDivElement>(null);\n const onFetchPreviousPageRef = useRef(onFetchPreviousPage);\n onFetchPreviousPageRef.current = onFetchPreviousPage;\n\n useEffect(() => {\n const sentinel = sentinelRef.current;\n const viewport = getViewport(scrollRef);\n if (!sentinel || !viewport || !shouldObserve) return;\n\n // The observer fires synchronously on observe() if the sentinel is\n // already visible. We arm it on the next frame so that synchronous\n // initial fire is ignored, but a real intersection (user genuinely\n // at the top on a short conversation) triggers on subsequent frames.\n let armed = false;\n const frameId = requestAnimationFrame(() => {\n armed = true;\n });\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (!armed) return;\n const isScrollable = viewport.scrollHeight > viewport.clientHeight;\n if (entries[0]?.isIntersecting && isScrollable) {\n onFetchPreviousPageRef.current?.();\n }\n },\n { root: viewport, threshold: 0 },\n );\n\n observer.observe(sentinel);\n return () => {\n cancelAnimationFrame(frameId);\n observer.disconnect();\n };\n }, [scrollRef, shouldObserve]);\n\n return sentinelRef;\n}\n\n/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */\nexport function GenieChatMessageList({\n messages,\n status,\n className,\n hasPreviousPage = false,\n onFetchPreviousPage,\n}: GenieChatMessageListProps) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const sentinelRef = useLoadOlderOnScroll(\n scrollRef,\n hasPreviousPage && status !== \"loading-older\",\n onFetchPreviousPage,\n );\n useScrollManagement(scrollRef, messages, status);\n\n const lastMessage = messages[messages.length - 1];\n const showStreamingIndicator =\n status === \"streaming\" &&\n lastMessage?.role === \"assistant\" &&\n lastMessage.id === \"\";\n\n return (\n <ScrollArea ref={scrollRef} className={cn(\"flex-1 min-h-0 p-4\", className)}>\n <div className=\"flex flex-col gap-4\">\n {hasPreviousPage && <div ref={sentinelRef} className=\"h-px\" />}\n\n {status === \"loading-older\" && (\n <div className=\"flex items-center justify-center gap-2 py-2\">\n <Spinner className=\"h-3 w-3\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading older messages...\n </span>\n </div>\n )}\n\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages\n .filter(\n (msg) => msg.role !== \"assistant\" || msg.id !== \"\" || msg.content,\n )\n .map((msg) => (\n <GenieChatMessage key={msg.id} message={msg} />\n ))}\n\n {showStreamingIndicator && (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(lastMessage.status)}</span>\n </div>\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":";;;;;;;;;AAqBA,MAAM,gBAAwC;CAC5C,WAAW;CACX,iBAAiB;CACjB,mBAAmB;CACnB,WAAW;CACZ;AAED,SAAS,aAAa,QAAwB;AAC5C,QAAO,cAAc,WAAW,OAAO,QAAQ,MAAM,IAAI,CAAC,aAAa;;AAGzE,SAAS,YAAY,WAAmD;AACtE,QAAO,UAAU,SAAS,cACxB,uCACD;;;;;;AAOH,SAAS,oBACP,WACA,UACA,QACA;CACA,MAAM,wBAAwB,OAAsB,KAAK;CACzD,MAAM,sBAAsB,OAAO,EAAE;CACrC,MAAM,sBAAsB,OAAO,EAAE;AAIrC,iBAAgB;EACd,MAAM,WAAW,YAAY,UAAU;AACvC,MAAI,CAAC,SAAU;EAEf,MAAM,WAAW,IAAI,qBAAqB;AACxC,uBAAoB,UAAU,SAAS;IACvC;AACF,WAAS,QAAQ,SAAS;AAC1B,eAAa,SAAS,YAAY;IACjC,CAAC,UAAU,CAAC;AAGf,uBAAsB;EACpB,MAAM,WAAW,YAAY,UAAU;AACvC,MAAI,CAAC,SAAU;EAEf,MAAM,QAAQ,SAAS;EACvB,MAAM,eAAe,UAAU,oBAAoB;AACnD,sBAAoB,UAAU;AAG9B,MAAI,CAAC,cAAc;AACjB,uBAAoB,UAAU,SAAS;AACvC;;EAGF,MAAM,iBAAiB,SAAS,IAAI,MAAM;AAK1C,MAHE,sBAAsB,YAAY,QAClC,mBAAmB,sBAAsB,WAEzB,oBAAoB,UAAU,GAAG;GAEjD,MAAM,QAAQ,SAAS,eAAe,oBAAoB;AAC1D,YAAS,aAAa;QAGtB,UAAS,YAAY,SAAS;AAGhC,wBAAsB,UAAU;AAChC,sBAAoB,UAAU,SAAS;IACtC,CAAC,SAAS,QAAQ,OAAO,CAAC;;;;;;;AAQ/B,SAAS,qBACP,WACA,eACA,qBACA;CACA,MAAM,cAAc,OAAuB,KAAK;CAChD,MAAM,yBAAyB,OAAO,oBAAoB;AAC1D,wBAAuB,UAAU;AAEjC,iBAAgB;EACd,MAAM,WAAW,YAAY;EAC7B,MAAM,WAAW,YAAY,UAAU;AACvC,MAAI,CAAC,YAAY,CAAC,YAAY,CAAC,cAAe;EAM9C,IAAI,QAAQ;EACZ,MAAM,UAAU,4BAA4B;AAC1C,WAAQ;IACR;EAEF,MAAM,WAAW,IAAI,sBAClB,YAAY;AACX,OAAI,CAAC,MAAO;GACZ,MAAM,eAAe,SAAS,eAAe,SAAS;AACtD,OAAI,QAAQ,IAAI,kBAAkB,aAChC,wBAAuB,WAAW;KAGtC;GAAE,MAAM;GAAU,WAAW;GAAG,CACjC;AAED,WAAS,QAAQ,SAAS;AAC1B,eAAa;AACX,wBAAqB,QAAQ;AAC7B,YAAS,YAAY;;IAEtB,CAAC,WAAW,cAAc,CAAC;AAE9B,QAAO;;;AAIT,SAAgB,qBAAqB,EACnC,UACA,QACA,WACA,kBAAkB,OAClB,uBAC4B;CAC5B,MAAM,YAAY,OAAuB,KAAK;CAE9C,MAAM,cAAc,qBAClB,WACA,mBAAmB,WAAW,iBAC9B,oBACD;AACD,qBAAoB,WAAW,UAAU,OAAO;CAEhD,MAAM,cAAc,SAAS,SAAS,SAAS;CAC/C,MAAM,yBACJ,WAAW,eACX,aAAa,SAAS,eACtB,YAAY,OAAO;AAErB,QACE,oBAAC;EAAW,KAAK;EAAW,WAAW,GAAG,sBAAsB,UAAU;YACxE,qBAAC;GAAI,WAAU;;IACZ,mBAAmB,oBAAC;KAAI,KAAK;KAAa,WAAU;MAAS;IAE7D,WAAW,mBACV,qBAAC;KAAI,WAAU;gBACb,oBAAC,WAAQ,WAAU,YAAY,EAC/B,oBAAC;MAAK,WAAU;gBAAgC;OAEzC;MACH;IAGP,WAAW,qBAAqB,SAAS,WAAW,KACnD,qBAAC;KAAI,WAAU;;MACb,oBAAC,YAAS,WAAU,eAAe;MACnC,oBAAC,YAAS,WAAU,0BAA0B;MAC9C,oBAAC,YAAS,WAAU,wBAAwB;;MACxC;IAGP,SACE,QACE,QAAQ,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,IAAI,QAC3D,CACA,KAAK,QACJ,oBAAC,oBAA8B,SAAS,OAAjB,IAAI,GAAoB,CAC/C;IAEH,0BACC,qBAAC;KAAI,WAAU;gBACb,oBAAC,WAAQ,WAAU,YAAY,EAC/B,oBAAC,oBAAM,aAAa,YAAY,OAAO,GAAQ;MAC3C;IAGP,SAAS,WAAW,KAAK,WAAW,UACnC,oBAAC;KAAI,WAAU;eAA8E;MAEvF;;IAEJ;GACK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;UA0BiB,qBAAA;;EAEf,OAAA,EAAS,gBAAA;EAFM;EAIf,SAAA;AAAA;;iBAQc,gBAAA,CAAA;EACd,OAAA;EACA;AAAA,GACC,qBAAA,GAAqB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { Avatar, AvatarFallback } from "../ui/avatar.js";
|
|
3
3
|
import { Card } from "../ui/card.js";
|
|
4
|
+
import { GenieQueryVisualization } from "./genie-query-visualization.js";
|
|
4
5
|
import { useMemo } from "react";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
import { marked } from "marked";
|
|
@@ -45,22 +46,32 @@ function GenieChatMessage({ message, className }) {
|
|
|
45
46
|
})]
|
|
46
47
|
}), queryAttachments.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
47
48
|
className: "flex flex-col gap-2 w-full min-w-0",
|
|
48
|
-
children: queryAttachments.map((att) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
children: queryAttachments.map((att) => {
|
|
50
|
+
const key = att.attachmentId ?? "query";
|
|
51
|
+
const queryResult = att.attachmentId ? message.queryResults.get(att.attachmentId) : void 0;
|
|
52
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
53
|
+
className: "flex flex-col gap-2",
|
|
54
|
+
children: [/* @__PURE__ */ jsx(Card, {
|
|
55
|
+
className: "px-4 py-3 text-xs overflow-hidden shadow-none",
|
|
56
|
+
children: /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", {
|
|
57
|
+
className: "cursor-pointer select-none font-medium",
|
|
58
|
+
children: att.query?.title ?? "SQL Query"
|
|
59
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
60
|
+
className: "mt-2 flex flex-col gap-1",
|
|
61
|
+
children: [att.query?.description && /* @__PURE__ */ jsx("span", {
|
|
62
|
+
className: "text-muted-foreground",
|
|
63
|
+
children: att.query.description
|
|
64
|
+
}), att.query?.query && /* @__PURE__ */ jsx("pre", {
|
|
65
|
+
className: "mt-1 p-2 rounded bg-background text-[11px] whitespace-pre-wrap break-all",
|
|
66
|
+
children: att.query.query
|
|
67
|
+
})]
|
|
68
|
+
})] })
|
|
69
|
+
}), queryResult != null && /* @__PURE__ */ jsx(Card, {
|
|
70
|
+
className: "px-4 py-3 overflow-hidden",
|
|
71
|
+
children: /* @__PURE__ */ jsx(GenieQueryVisualization, { data: queryResult })
|
|
61
72
|
})]
|
|
62
|
-
}
|
|
63
|
-
}
|
|
73
|
+
}, key);
|
|
74
|
+
})
|
|
64
75
|
})]
|
|
65
76
|
})]
|
|
66
77
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import { marked } from \"marked\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar, AvatarFallback } from \"../ui/avatar\";\nimport { Card } from \"../ui/card\";\nimport type { GenieAttachmentResponse, GenieMessageItem } from \"./types\";\n\n/**\n * Using `marked` instead of `react-markdown` because `react-markdown` depends on\n * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.\n * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm\",\n \"[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0\",\n \"[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto\",\n \"[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded\",\n \"[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1\",\n \"[&_table]:border-collapse [&_th]:border [&_td]:border\",\n \"[&_th]:border-border [&_td]:border-border\",\n \"[&_a]:underline\",\n);\n\nexport interface GenieChatMessageProps {\n /** The message object to render */\n message: GenieMessageItem;\n /** Additional CSS class */\n className?: string;\n}\n\nfunction isQueryAttachment(att: GenieAttachmentResponse): boolean {\n return !!(att.query?.title || att.query?.query);\n}\n\n/** Renders a single Genie message bubble with optional expandable SQL query attachments. */\nexport function GenieChatMessage({\n message,\n className,\n}: GenieChatMessageProps) {\n const isUser = message.role === \"user\";\n const queryAttachments = message.attachments.filter(isQueryAttachment);\n const html = useMemo(\n () => (message.content ? (marked.parse(message.content) as string) : \"\"),\n [message.content],\n );\n\n return (\n <div\n className={cn(\n \"flex gap-3\",\n isUser ? \"flex-row-reverse\" : \"flex-row\",\n className,\n )}\n >\n <Avatar className=\"h-8 w-8 shrink-0 mt-1\">\n <AvatarFallback\n className={cn(\n \"text-xs font-medium\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {isUser ? \"You\" : \"AI\"}\n </AvatarFallback>\n </Avatar>\n\n <div\n className={cn(\n \"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\",\n isUser ? \"items-end\" : \"items-start\",\n )}\n >\n <Card\n className={cn(\n \"px-4 py-3 max-w-full overflow-hidden\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {html && (\n <div\n className={markdownStyles}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {message.error && (\n <p className=\"text-sm text-destructive mt-1\">{message.error}</p>\n )}\n </Card>\n\n {queryAttachments.length > 0 && (\n <div className=\"flex flex-col gap-2 w-full min-w-0\">\n {queryAttachments.map((att) =>
|
|
1
|
+
{"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import { marked } from \"marked\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar, AvatarFallback } from \"../ui/avatar\";\nimport { Card } from \"../ui/card\";\nimport { GenieQueryVisualization } from \"./genie-query-visualization\";\nimport type { GenieAttachmentResponse, GenieMessageItem } from \"./types\";\n\n/**\n * Using `marked` instead of `react-markdown` because `react-markdown` depends on\n * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.\n * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm\",\n \"[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0\",\n \"[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto\",\n \"[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded\",\n \"[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1\",\n \"[&_table]:border-collapse [&_th]:border [&_td]:border\",\n \"[&_th]:border-border [&_td]:border-border\",\n \"[&_a]:underline\",\n);\n\nexport interface GenieChatMessageProps {\n /** The message object to render */\n message: GenieMessageItem;\n /** Additional CSS class */\n className?: string;\n}\n\nfunction isQueryAttachment(att: GenieAttachmentResponse): boolean {\n return !!(att.query?.title || att.query?.query);\n}\n\n/** Renders a single Genie message bubble with optional expandable SQL query attachments. */\nexport function GenieChatMessage({\n message,\n className,\n}: GenieChatMessageProps) {\n const isUser = message.role === \"user\";\n const queryAttachments = message.attachments.filter(isQueryAttachment);\n const html = useMemo(\n () => (message.content ? (marked.parse(message.content) as string) : \"\"),\n [message.content],\n );\n\n return (\n <div\n className={cn(\n \"flex gap-3\",\n isUser ? \"flex-row-reverse\" : \"flex-row\",\n className,\n )}\n >\n <Avatar className=\"h-8 w-8 shrink-0 mt-1\">\n <AvatarFallback\n className={cn(\n \"text-xs font-medium\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {isUser ? \"You\" : \"AI\"}\n </AvatarFallback>\n </Avatar>\n\n <div\n className={cn(\n \"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\",\n isUser ? \"items-end\" : \"items-start\",\n )}\n >\n <Card\n className={cn(\n \"px-4 py-3 max-w-full overflow-hidden\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {html && (\n <div\n className={markdownStyles}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {message.error && (\n <p className=\"text-sm text-destructive mt-1\">{message.error}</p>\n )}\n </Card>\n\n {queryAttachments.length > 0 && (\n <div className=\"flex flex-col gap-2 w-full min-w-0\">\n {queryAttachments.map((att) => {\n const key = att.attachmentId ?? \"query\";\n const queryResult = att.attachmentId\n ? message.queryResults.get(att.attachmentId)\n : undefined;\n\n return (\n <div key={key} className=\"flex flex-col gap-2\">\n <Card className=\"px-4 py-3 text-xs overflow-hidden shadow-none\">\n <details>\n <summary className=\"cursor-pointer select-none font-medium\">\n {att.query?.title ?? \"SQL Query\"}\n </summary>\n <div className=\"mt-2 flex flex-col gap-1\">\n {att.query?.description && (\n <span className=\"text-muted-foreground\">\n {att.query.description}\n </span>\n )}\n {att.query?.query && (\n <pre className=\"mt-1 p-2 rounded bg-background text-[11px] whitespace-pre-wrap break-all\">\n {att.query.query}\n </pre>\n )}\n </div>\n </details>\n </Card>\n {queryResult != null && (\n <Card className=\"px-4 py-3 overflow-hidden\">\n <GenieQueryVisualization data={queryResult} />\n </Card>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,OAAO,WAAW;CAAE,QAAQ;CAAM,KAAK;CAAM,CAAC;AAE9C,MAAM,iBAAiB,GACrB,WACA,kDACA,gGACA,6EACA,qEACA,yDACA,6CACA,kBACD;AASD,SAAS,kBAAkB,KAAuC;AAChE,QAAO,CAAC,EAAE,IAAI,OAAO,SAAS,IAAI,OAAO;;;AAI3C,SAAgB,iBAAiB,EAC/B,SACA,aACwB;CACxB,MAAM,SAAS,QAAQ,SAAS;CAChC,MAAM,mBAAmB,QAAQ,YAAY,OAAO,kBAAkB;CACtE,MAAM,OAAO,cACJ,QAAQ,UAAW,OAAO,MAAM,QAAQ,QAAQ,GAAc,IACrE,CAAC,QAAQ,QAAQ,CAClB;AAED,QACE,qBAAC;EACC,WAAW,GACT,cACA,SAAS,qBAAqB,YAC9B,UACD;aAED,oBAAC;GAAO,WAAU;aAChB,oBAAC;IACC,WAAW,GACT,uBACA,SAAS,uCAAuC,WACjD;cAEA,SAAS,QAAQ;KACH;IACV,EAET,qBAAC;GACC,WAAW,GACT,2DACA,SAAS,cAAc,cACxB;cAED,qBAAC;IACC,WAAW,GACT,wCACA,SAAS,uCAAuC,WACjD;eAEA,QACC,oBAAC;KACC,WAAW;KACX,yBAAyB,EAAE,QAAQ,MAAM;MACzC,EAGH,QAAQ,SACP,oBAAC;KAAE,WAAU;eAAiC,QAAQ;MAAU;KAE7D,EAEN,iBAAiB,SAAS,KACzB,oBAAC;IAAI,WAAU;cACZ,iBAAiB,KAAK,QAAQ;KAC7B,MAAM,MAAM,IAAI,gBAAgB;KAChC,MAAM,cAAc,IAAI,eACpB,QAAQ,aAAa,IAAI,IAAI,aAAa,GAC1C;AAEJ,YACE,qBAAC;MAAc,WAAU;iBACvB,oBAAC;OAAK,WAAU;iBACd,qBAAC,wBACC,oBAAC;QAAQ,WAAU;kBAChB,IAAI,OAAO,SAAS;SACb,EACV,qBAAC;QAAI,WAAU;mBACZ,IAAI,OAAO,eACV,oBAAC;SAAK,WAAU;mBACb,IAAI,MAAM;UACN,EAER,IAAI,OAAO,SACV,oBAAC;SAAI,WAAU;mBACZ,IAAI,MAAM;UACP;SAEJ,IACE;QACL,EACN,eAAe,QACd,oBAAC;OAAK,WAAU;iBACd,oBAAC,2BAAwB,MAAM,cAAe;QACzC;QAvBD,IAyBJ;MAER;KACE;IAEJ;GACF"}
|
|
@@ -8,7 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
//#region src/react/genie/genie-chat.tsx
|
|
9
9
|
/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */
|
|
10
10
|
function GenieChat({ alias, basePath, placeholder, className }) {
|
|
11
|
-
const { messages, status, error, sendMessage, reset } = useGenieChat({
|
|
11
|
+
const { messages, status, error, sendMessage, reset, hasPreviousPage, fetchPreviousPage } = useGenieChat({
|
|
12
12
|
alias,
|
|
13
13
|
basePath
|
|
14
14
|
});
|
|
@@ -27,7 +27,9 @@ function GenieChat({ alias, basePath, placeholder, className }) {
|
|
|
27
27
|
}),
|
|
28
28
|
/* @__PURE__ */ jsx(GenieChatMessageList, {
|
|
29
29
|
messages,
|
|
30
|
-
status
|
|
30
|
+
status,
|
|
31
|
+
hasPreviousPage,
|
|
32
|
+
onFetchPreviousPage: fetchPreviousPage
|
|
31
33
|
}),
|
|
32
34
|
error && /* @__PURE__ */ jsx("div", {
|
|
33
35
|
className: "shrink-0 px-4 py-2 text-sm text-destructive bg-destructive/10 border-t",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat.js","names":[],"sources":["../../../src/react/genie/genie-chat.tsx"],"sourcesContent":["import { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { GenieChatInput } from \"./genie-chat-input\";\nimport { GenieChatMessageList } from \"./genie-chat-message-list\";\nimport type { GenieChatProps } from \"./types\";\nimport { useGenieChat } from \"./use-genie-chat\";\n\n/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */\nexport function GenieChat({\n alias,\n basePath,\n placeholder,\n className,\n}: GenieChatProps) {\n const {
|
|
1
|
+
{"version":3,"file":"genie-chat.js","names":[],"sources":["../../../src/react/genie/genie-chat.tsx"],"sourcesContent":["import { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { GenieChatInput } from \"./genie-chat-input\";\nimport { GenieChatMessageList } from \"./genie-chat-message-list\";\nimport type { GenieChatProps } from \"./types\";\nimport { useGenieChat } from \"./use-genie-chat\";\n\n/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */\nexport function GenieChat({\n alias,\n basePath,\n placeholder,\n className,\n}: GenieChatProps) {\n const {\n messages,\n status,\n error,\n sendMessage,\n reset,\n hasPreviousPage,\n fetchPreviousPage,\n } = useGenieChat({\n alias,\n basePath,\n });\n\n return (\n <div className={cn(\"flex flex-col h-full overflow-hidden\", className)}>\n {messages.length > 0 && (\n <div className=\"shrink-0 flex justify-end px-4 pt-3 pb-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={reset}\n className=\"text-xs text-muted-foreground\"\n >\n New conversation\n </Button>\n </div>\n )}\n\n <GenieChatMessageList\n messages={messages}\n status={status}\n hasPreviousPage={hasPreviousPage}\n onFetchPreviousPage={fetchPreviousPage}\n />\n\n {error && (\n <div className=\"shrink-0 px-4 py-2 text-sm text-destructive bg-destructive/10 border-t\">\n {error}\n </div>\n )}\n\n <GenieChatInput\n onSend={sendMessage}\n disabled={status === \"streaming\" || status === \"loading-history\"}\n placeholder={placeholder}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,UAAU,EACxB,OACA,UACA,aACA,aACiB;CACjB,MAAM,EACJ,UACA,QACA,OACA,aACA,OACA,iBACA,sBACE,aAAa;EACf;EACA;EACD,CAAC;AAEF,QACE,qBAAC;EAAI,WAAW,GAAG,wCAAwC,UAAU;;GAClE,SAAS,SAAS,KACjB,oBAAC;IAAI,WAAU;cACb,oBAAC;KACC,SAAQ;KACR,MAAK;KACL,SAAS;KACT,WAAU;eACX;MAEQ;KACL;GAGR,oBAAC;IACW;IACF;IACS;IACjB,qBAAqB;KACrB;GAED,SACC,oBAAC;IAAI,WAAU;cACZ;KACG;GAGR,oBAAC;IACC,QAAQ;IACR,UAAU,WAAW,eAAe,WAAW;IAClC;KACb;;GACE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { GenieStatementResponse } from "../../shared/src/genie.js";
|
|
2
|
+
import "../../shared/src/index.js";
|
|
3
|
+
|
|
4
|
+
//#region src/react/genie/genie-query-transform.d.ts
|
|
5
|
+
type ColumnCategory = "numeric" | "date" | "string";
|
|
6
|
+
interface GenieColumnMeta {
|
|
7
|
+
name: string;
|
|
8
|
+
typeName: string;
|
|
9
|
+
category: ColumnCategory;
|
|
10
|
+
}
|
|
11
|
+
interface TransformedGenieData {
|
|
12
|
+
rows: Record<string, unknown>[];
|
|
13
|
+
columns: GenieColumnMeta[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Transform a Genie statement_response into chart-ready rows + column metadata.
|
|
17
|
+
*
|
|
18
|
+
* Expects `data` to have the shape:
|
|
19
|
+
* ```
|
|
20
|
+
* {
|
|
21
|
+
* manifest: { schema: { columns: [{ name, type_name }, ...] } },
|
|
22
|
+
* result: { data_array: [["val", ...], ...] }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` when the data is empty or malformed.
|
|
27
|
+
*/
|
|
28
|
+
declare function transformGenieData(data: GenieStatementResponse | null | undefined): TransformedGenieData | null;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ColumnCategory, GenieColumnMeta, TransformedGenieData, transformGenieData };
|
|
31
|
+
//# sourceMappingURL=genie-query-transform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genie-query-transform.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-transform.ts"],"mappings":";;;;KA8BY,cAAA;AAAA,UAEK,eAAA;EACf,IAAA;EACA,QAAA;EACA,QAAA,EAAU,cAAA;AAAA;AAAA,UAGK,oBAAA;EACf,IAAA,EAAM,MAAA;EACN,OAAA,EAAS,eAAA;AAAA;;;;;;AAuCX;;;;;;;;iBAAgB,kBAAA,CACd,IAAA,EAAM,sBAAA,sBACL,oBAAA"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/react/genie/genie-query-transform.ts
|
|
2
|
+
const NUMERIC_SQL_TYPES = new Set([
|
|
3
|
+
"DECIMAL",
|
|
4
|
+
"INT",
|
|
5
|
+
"INTEGER",
|
|
6
|
+
"BIGINT",
|
|
7
|
+
"LONG",
|
|
8
|
+
"SMALLINT",
|
|
9
|
+
"TINYINT",
|
|
10
|
+
"FLOAT",
|
|
11
|
+
"DOUBLE",
|
|
12
|
+
"SHORT",
|
|
13
|
+
"BYTE"
|
|
14
|
+
]);
|
|
15
|
+
const DATE_SQL_TYPES = new Set([
|
|
16
|
+
"DATE",
|
|
17
|
+
"TIMESTAMP",
|
|
18
|
+
"TIMESTAMP_NTZ"
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Classify a SQL type_name into a high-level category.
|
|
22
|
+
*/
|
|
23
|
+
function classifySqlType(typeName) {
|
|
24
|
+
const upper = typeName.toUpperCase();
|
|
25
|
+
if (NUMERIC_SQL_TYPES.has(upper)) return "numeric";
|
|
26
|
+
if (DATE_SQL_TYPES.has(upper)) return "date";
|
|
27
|
+
return "string";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse a single cell value based on its column category.
|
|
31
|
+
*/
|
|
32
|
+
function parseValue(raw, category) {
|
|
33
|
+
if (raw == null) return null;
|
|
34
|
+
if (category === "numeric") {
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
return Number.isNaN(n) ? null : n;
|
|
37
|
+
}
|
|
38
|
+
return raw;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Transform a Genie statement_response into chart-ready rows + column metadata.
|
|
42
|
+
*
|
|
43
|
+
* Expects `data` to have the shape:
|
|
44
|
+
* ```
|
|
45
|
+
* {
|
|
46
|
+
* manifest: { schema: { columns: [{ name, type_name }, ...] } },
|
|
47
|
+
* result: { data_array: [["val", ...], ...] }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Returns `null` when the data is empty or malformed.
|
|
52
|
+
*/
|
|
53
|
+
function transformGenieData(data) {
|
|
54
|
+
if (!data) return null;
|
|
55
|
+
const rawColumns = data.manifest?.schema?.columns;
|
|
56
|
+
if (!rawColumns || rawColumns.length === 0) return null;
|
|
57
|
+
const dataArray = data.result?.data_array;
|
|
58
|
+
if (!dataArray || dataArray.length === 0) return null;
|
|
59
|
+
const columns = rawColumns.map((col) => ({
|
|
60
|
+
name: col.name,
|
|
61
|
+
typeName: col.type_name,
|
|
62
|
+
category: classifySqlType(col.type_name)
|
|
63
|
+
}));
|
|
64
|
+
return {
|
|
65
|
+
rows: dataArray.map((row) => {
|
|
66
|
+
const record = {};
|
|
67
|
+
for (let i = 0; i < columns.length; i++) {
|
|
68
|
+
const col = columns[i];
|
|
69
|
+
record[col.name] = parseValue(row[i] ?? null, col.category);
|
|
70
|
+
}
|
|
71
|
+
return record;
|
|
72
|
+
}),
|
|
73
|
+
columns
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { transformGenieData };
|
|
79
|
+
//# sourceMappingURL=genie-query-transform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genie-query-transform.js","names":[],"sources":["../../../src/react/genie/genie-query-transform.ts"],"sourcesContent":["/**\n * Converts Genie's statement_response data into a flat record array\n * suitable for charting.\n *\n * The Genie API returns `{ manifest.schema.columns, result.data_array }`\n * where each column carries a SQL `type_name`. This module parses values\n * according to those types so downstream chart code receives proper\n * numbers and strings.\n */\n\nimport type { GenieStatementResponse } from \"shared\";\n\n// SQL type_name values that map to numeric JS values\nconst NUMERIC_SQL_TYPES = new Set([\n \"DECIMAL\",\n \"INT\",\n \"INTEGER\",\n \"BIGINT\",\n \"LONG\",\n \"SMALLINT\",\n \"TINYINT\",\n \"FLOAT\",\n \"DOUBLE\",\n \"SHORT\",\n \"BYTE\",\n]);\n\n// SQL type_name values that map to date/timestamp strings\nconst DATE_SQL_TYPES = new Set([\"DATE\", \"TIMESTAMP\", \"TIMESTAMP_NTZ\"]);\n\nexport type ColumnCategory = \"numeric\" | \"date\" | \"string\";\n\nexport interface GenieColumnMeta {\n name: string;\n typeName: string;\n category: ColumnCategory;\n}\n\nexport interface TransformedGenieData {\n rows: Record<string, unknown>[];\n columns: GenieColumnMeta[];\n}\n\n/**\n * Classify a SQL type_name into a high-level category.\n */\nexport function classifySqlType(typeName: string): ColumnCategory {\n const upper = typeName.toUpperCase();\n if (NUMERIC_SQL_TYPES.has(upper)) return \"numeric\";\n if (DATE_SQL_TYPES.has(upper)) return \"date\";\n return \"string\";\n}\n\n/**\n * Parse a single cell value based on its column category.\n */\nfunction parseValue(raw: string | null, category: ColumnCategory): unknown {\n if (raw == null) return null;\n if (category === \"numeric\") {\n const n = Number(raw);\n return Number.isNaN(n) ? null : n;\n }\n // Dates and strings stay as strings — normalizeChartData detects ISO dates\n return raw;\n}\n\n/**\n * Transform a Genie statement_response into chart-ready rows + column metadata.\n *\n * Expects `data` to have the shape:\n * ```\n * {\n * manifest: { schema: { columns: [{ name, type_name }, ...] } },\n * result: { data_array: [[\"val\", ...], ...] }\n * }\n * ```\n *\n * Returns `null` when the data is empty or malformed.\n */\nexport function transformGenieData(\n data: GenieStatementResponse | null | undefined,\n): TransformedGenieData | null {\n if (!data) return null;\n\n const rawColumns = data.manifest?.schema?.columns;\n if (!rawColumns || rawColumns.length === 0) {\n return null;\n }\n\n const dataArray = data.result?.data_array;\n if (!dataArray || dataArray.length === 0) {\n return null;\n }\n\n const columns: GenieColumnMeta[] = rawColumns.map((col) => ({\n name: col.name,\n typeName: col.type_name,\n category: classifySqlType(col.type_name),\n }));\n\n const rows: Record<string, unknown>[] = dataArray.map((row) => {\n const record: Record<string, unknown> = {};\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i];\n record[col.name] = parseValue(row[i] ?? null, col.category);\n }\n return record;\n });\n\n return { rows, columns };\n}\n"],"mappings":";AAaA,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAGF,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAQ;CAAa;CAAgB,CAAC;;;;AAkBtE,SAAgB,gBAAgB,UAAkC;CAChE,MAAM,QAAQ,SAAS,aAAa;AACpC,KAAI,kBAAkB,IAAI,MAAM,CAAE,QAAO;AACzC,KAAI,eAAe,IAAI,MAAM,CAAE,QAAO;AACtC,QAAO;;;;;AAMT,SAAS,WAAW,KAAoB,UAAmC;AACzE,KAAI,OAAO,KAAM,QAAO;AACxB,KAAI,aAAa,WAAW;EAC1B,MAAM,IAAI,OAAO,IAAI;AACrB,SAAO,OAAO,MAAM,EAAE,GAAG,OAAO;;AAGlC,QAAO;;;;;;;;;;;;;;;AAgBT,SAAgB,mBACd,MAC6B;AAC7B,KAAI,CAAC,KAAM,QAAO;CAElB,MAAM,aAAa,KAAK,UAAU,QAAQ;AAC1C,KAAI,CAAC,cAAc,WAAW,WAAW,EACvC,QAAO;CAGT,MAAM,YAAY,KAAK,QAAQ;AAC/B,KAAI,CAAC,aAAa,UAAU,WAAW,EACrC,QAAO;CAGT,MAAM,UAA6B,WAAW,KAAK,SAAS;EAC1D,MAAM,IAAI;EACV,UAAU,IAAI;EACd,UAAU,gBAAgB,IAAI,UAAU;EACzC,EAAE;AAWH,QAAO;EAAE,MAT+B,UAAU,KAAK,QAAQ;GAC7D,MAAM,SAAkC,EAAE;AAC1C,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;IACvC,MAAM,MAAM,QAAQ;AACpB,WAAO,IAAI,QAAQ,WAAW,IAAI,MAAM,MAAM,IAAI,SAAS;;AAE7D,UAAO;IACP;EAEa;EAAS"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GenieStatementResponse } from "../../shared/src/genie.js";
|
|
2
|
+
import "../../shared/src/index.js";
|
|
3
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/react/genie/genie-query-visualization.d.ts
|
|
6
|
+
interface GenieQueryVisualizationProps {
|
|
7
|
+
/** Raw statement_response from the Genie API */
|
|
8
|
+
data: GenieStatementResponse;
|
|
9
|
+
/** Additional CSS classes */
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Renders a chart + data table for a Genie query result.
|
|
14
|
+
*
|
|
15
|
+
* - When a chart type can be inferred: shows Tabs with "Chart" (default) and "Table"
|
|
16
|
+
* - When no chart fits: shows only the data table
|
|
17
|
+
* - When data is empty/malformed: renders nothing
|
|
18
|
+
*/
|
|
19
|
+
declare function GenieQueryVisualization({
|
|
20
|
+
data,
|
|
21
|
+
className
|
|
22
|
+
}: GenieQueryVisualizationProps): react_jsx_runtime0.JSX.Element | null;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { GenieQueryVisualization };
|
|
25
|
+
//# sourceMappingURL=genie-query-visualization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genie-query-visualization.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"mappings":";;;;;UAmBiB,4BAAA;;EAEf,IAAA,EAAM,sBAAA;;EAEN,SAAA;AAAA;;;;;;;;iBAUc,uBAAA,CAAA;EACd,IAAA;EACA;AAAA,GACC,4BAAA,GAA4B,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BaseChart } from "../charts/base.js";
|
|
2
|
+
import { ChartErrorBoundary } from "../charts/chart-error-boundary.js";
|
|
3
|
+
import { inferChartType } from "./genie-chart-inference.js";
|
|
4
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
|
|
6
|
+
import { transformGenieData } from "./genie-query-transform.js";
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
|
|
10
|
+
//#region src/react/genie/genie-query-visualization.tsx
|
|
11
|
+
const TABLE_ROW_LIMIT = 50;
|
|
12
|
+
const CHART_HEIGHT = 250;
|
|
13
|
+
/**
|
|
14
|
+
* Renders a chart + data table for a Genie query result.
|
|
15
|
+
*
|
|
16
|
+
* - When a chart type can be inferred: shows Tabs with "Chart" (default) and "Table"
|
|
17
|
+
* - When no chart fits: shows only the data table
|
|
18
|
+
* - When data is empty/malformed: renders nothing
|
|
19
|
+
*/
|
|
20
|
+
function GenieQueryVisualization({ data, className }) {
|
|
21
|
+
const transformed = useMemo(() => transformGenieData(data), [data]);
|
|
22
|
+
const inference = useMemo(() => transformed ? inferChartType(transformed.rows, transformed.columns) : null, [transformed]);
|
|
23
|
+
if (!transformed || transformed.rows.length === 0) return null;
|
|
24
|
+
const { rows, columns } = transformed;
|
|
25
|
+
const truncated = rows.length > TABLE_ROW_LIMIT;
|
|
26
|
+
const displayRows = truncated ? rows.slice(0, TABLE_ROW_LIMIT) : rows;
|
|
27
|
+
const dataTable = /* @__PURE__ */ jsxs("div", {
|
|
28
|
+
className: "overflow-auto max-h-[300px]",
|
|
29
|
+
children: [/* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((col) => /* @__PURE__ */ jsx(TableHead, { children: col.name }, col.name)) }) }), /* @__PURE__ */ jsx(TableBody, { children: displayRows.map((row, i) => /* @__PURE__ */ jsx(TableRow, { children: columns.map((col) => /* @__PURE__ */ jsx(TableCell, { children: row[col.name] != null ? String(row[col.name]) : "" }, col.name)) }, i)) })] }), truncated && /* @__PURE__ */ jsxs("p", {
|
|
30
|
+
className: "text-xs text-muted-foreground px-2 py-1",
|
|
31
|
+
children: [
|
|
32
|
+
"Showing ",
|
|
33
|
+
TABLE_ROW_LIMIT,
|
|
34
|
+
" of ",
|
|
35
|
+
rows.length,
|
|
36
|
+
" rows"
|
|
37
|
+
]
|
|
38
|
+
})]
|
|
39
|
+
});
|
|
40
|
+
if (!inference) return /* @__PURE__ */ jsx("div", {
|
|
41
|
+
className,
|
|
42
|
+
children: dataTable
|
|
43
|
+
});
|
|
44
|
+
return /* @__PURE__ */ jsxs(Tabs, {
|
|
45
|
+
defaultValue: "chart",
|
|
46
|
+
className,
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [/* @__PURE__ */ jsx(TabsTrigger, {
|
|
49
|
+
value: "chart",
|
|
50
|
+
children: "Chart"
|
|
51
|
+
}), /* @__PURE__ */ jsx(TabsTrigger, {
|
|
52
|
+
value: "table",
|
|
53
|
+
children: "Table"
|
|
54
|
+
})] }),
|
|
55
|
+
/* @__PURE__ */ jsx(TabsContent, {
|
|
56
|
+
value: "chart",
|
|
57
|
+
children: /* @__PURE__ */ jsx(ChartErrorBoundary, {
|
|
58
|
+
fallback: dataTable,
|
|
59
|
+
children: /* @__PURE__ */ jsx(BaseChart, {
|
|
60
|
+
data: rows,
|
|
61
|
+
chartType: inference.chartType,
|
|
62
|
+
xKey: inference.xKey,
|
|
63
|
+
yKey: inference.yKey,
|
|
64
|
+
height: CHART_HEIGHT,
|
|
65
|
+
showLegend: Array.isArray(inference.yKey)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ jsx(TabsContent, {
|
|
70
|
+
value: "table",
|
|
71
|
+
children: dataTable
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { GenieQueryVisualization };
|
|
79
|
+
//# sourceMappingURL=genie-query-visualization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genie-query-visualization.js","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { GenieStatementResponse } from \"shared\";\nimport { BaseChart } from \"../charts/base\";\nimport { ChartErrorBoundary } from \"../charts/chart-error-boundary\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"../ui/table\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\";\nimport { inferChartType } from \"./genie-chart-inference\";\nimport { transformGenieData } from \"./genie-query-transform\";\n\nconst TABLE_ROW_LIMIT = 50;\nconst CHART_HEIGHT = 250;\n\nexport interface GenieQueryVisualizationProps {\n /** Raw statement_response from the Genie API */\n data: GenieStatementResponse;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Renders a chart + data table for a Genie query result.\n *\n * - When a chart type can be inferred: shows Tabs with \"Chart\" (default) and \"Table\"\n * - When no chart fits: shows only the data table\n * - When data is empty/malformed: renders nothing\n */\nexport function GenieQueryVisualization({\n data,\n className,\n}: GenieQueryVisualizationProps) {\n const transformed = useMemo(() => transformGenieData(data), [data]);\n const inference = useMemo(\n () =>\n transformed\n ? inferChartType(transformed.rows, transformed.columns)\n : null,\n [transformed],\n );\n\n if (!transformed || transformed.rows.length === 0) return null;\n\n const { rows, columns } = transformed;\n const truncated = rows.length > TABLE_ROW_LIMIT;\n const displayRows = truncated ? rows.slice(0, TABLE_ROW_LIMIT) : rows;\n\n const dataTable = (\n <div className=\"overflow-auto max-h-[300px]\">\n <Table>\n <TableHeader>\n <TableRow>\n {columns.map((col) => (\n <TableHead key={col.name}>{col.name}</TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {displayRows.map((row, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabular data rows have no unique identifier\n <TableRow key={i}>\n {columns.map((col) => (\n <TableCell key={col.name}>\n {row[col.name] != null ? String(row[col.name]) : \"\"}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n {truncated && (\n <p className=\"text-xs text-muted-foreground px-2 py-1\">\n Showing {TABLE_ROW_LIMIT} of {rows.length} rows\n </p>\n )}\n </div>\n );\n\n if (!inference) {\n return <div className={className}>{dataTable}</div>;\n }\n\n return (\n <Tabs defaultValue=\"chart\" className={className}>\n <TabsList>\n <TabsTrigger value=\"chart\">Chart</TabsTrigger>\n <TabsTrigger value=\"table\">Table</TabsTrigger>\n </TabsList>\n <TabsContent value=\"chart\">\n <ChartErrorBoundary fallback={dataTable}>\n <BaseChart\n data={rows}\n chartType={inference.chartType}\n xKey={inference.xKey}\n yKey={inference.yKey}\n height={CHART_HEIGHT}\n showLegend={Array.isArray(inference.yKey)}\n />\n </ChartErrorBoundary>\n </TabsContent>\n <TabsContent value=\"table\">{dataTable}</TabsContent>\n </Tabs>\n );\n}\n"],"mappings":";;;;;;;;;;AAgBA,MAAM,kBAAkB;AACxB,MAAM,eAAe;;;;;;;;AAgBrB,SAAgB,wBAAwB,EACtC,MACA,aAC+B;CAC/B,MAAM,cAAc,cAAc,mBAAmB,KAAK,EAAE,CAAC,KAAK,CAAC;CACnE,MAAM,YAAY,cAEd,cACI,eAAe,YAAY,MAAM,YAAY,QAAQ,GACrD,MACN,CAAC,YAAY,CACd;AAED,KAAI,CAAC,eAAe,YAAY,KAAK,WAAW,EAAG,QAAO;CAE1D,MAAM,EAAE,MAAM,YAAY;CAC1B,MAAM,YAAY,KAAK,SAAS;CAChC,MAAM,cAAc,YAAY,KAAK,MAAM,GAAG,gBAAgB,GAAG;CAEjE,MAAM,YACJ,qBAAC;EAAI,WAAU;aACb,qBAAC,oBACC,oBAAC,yBACC,oBAAC,sBACE,QAAQ,KAAK,QACZ,oBAAC,uBAA0B,IAAI,QAAf,IAAI,KAA4B,CAChD,GACO,GACC,EACd,oBAAC,uBACE,YAAY,KAAK,KAAK,MAErB,oBAAC,sBACE,QAAQ,KAAK,QACZ,oBAAC,uBACE,IAAI,IAAI,SAAS,OAAO,OAAO,IAAI,IAAI,MAAM,GAAG,MADnC,IAAI,KAER,CACZ,IALW,EAMJ,CACX,GACQ,IACN,EACP,aACC,qBAAC;GAAE,WAAU;;IAA0C;IAC5C;IAAgB;IAAK,KAAK;IAAO;;IACxC;GAEF;AAGR,KAAI,CAAC,UACH,QAAO,oBAAC;EAAe;YAAY;GAAgB;AAGrD,QACE,qBAAC;EAAK,cAAa;EAAmB;;GACpC,qBAAC,uBACC,oBAAC;IAAY,OAAM;cAAQ;KAAmB,EAC9C,oBAAC;IAAY,OAAM;cAAQ;KAAmB,IACrC;GACX,oBAAC;IAAY,OAAM;cACjB,oBAAC;KAAmB,UAAU;eAC5B,oBAAC;MACC,MAAM;MACN,WAAW,UAAU;MACrB,MAAM,UAAU;MAChB,MAAM,UAAU;MAChB,QAAQ;MACR,YAAY,MAAM,QAAQ,UAAU,KAAK;OACzC;MACiB;KACT;GACd,oBAAC;IAAY,OAAM;cAAS;KAAwB;;GAC/C"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { GenieAttachmentResponse, GenieMessageResponse, GenieStreamEvent } from "../../shared/src/genie.js";
|
|
1
|
+
import { GenieAttachmentResponse, GenieMessageResponse, GenieStatementResponse, GenieStreamEvent } from "../../shared/src/genie.js";
|
|
2
|
+
import { ColumnCategory, GenieColumnMeta, TransformedGenieData, transformGenieData } from "./genie-query-transform.js";
|
|
3
|
+
import { ChartInference, inferChartType } from "./genie-chart-inference.js";
|
|
2
4
|
import { GenieChatProps, GenieChatStatus, GenieMessageItem, UseGenieChatOptions, UseGenieChatReturn } from "./types.js";
|
|
3
5
|
import { GenieChat } from "./genie-chat.js";
|
|
4
6
|
import { GenieChatInput } from "./genie-chat-input.js";
|
|
5
7
|
import { GenieChatMessage } from "./genie-chat-message.js";
|
|
6
8
|
import { GenieChatMessageList } from "./genie-chat-message-list.js";
|
|
9
|
+
import { GenieQueryVisualization } from "./genie-query-visualization.js";
|
|
7
10
|
import { useGenieChat } from "./use-genie-chat.js";
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { inferChartType } from "./genie-chart-inference.js";
|
|
1
2
|
import { GenieChatInput } from "./genie-chat-input.js";
|
|
3
|
+
import { transformGenieData } from "./genie-query-transform.js";
|
|
4
|
+
import { GenieQueryVisualization } from "./genie-query-visualization.js";
|
|
2
5
|
import { GenieChatMessage } from "./genie-chat-message.js";
|
|
3
6
|
import { GenieChatMessageList } from "./genie-chat-message-list.js";
|
|
4
7
|
import { useGenieChat } from "./use-genie-chat.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { GenieAttachmentResponse, GenieMessageResponse, GenieStreamEvent } from "../../shared/src/genie.js";
|
|
1
|
+
import { GenieAttachmentResponse, GenieMessageResponse, GenieStatementResponse, GenieStreamEvent } from "../../shared/src/genie.js";
|
|
2
2
|
import "../../shared/src/index.js";
|
|
3
3
|
|
|
4
4
|
//#region src/react/genie/types.d.ts
|
|
5
|
-
type GenieChatStatus = "idle" | "loading-history" | "streaming" | "error";
|
|
5
|
+
type GenieChatStatus = "idle" | "loading-history" | "loading-older" | "streaming" | "error";
|
|
6
6
|
interface GenieMessageItem {
|
|
7
7
|
id: string;
|
|
8
8
|
role: "user" | "assistant";
|
|
9
9
|
content: string;
|
|
10
10
|
status: string;
|
|
11
11
|
attachments: GenieAttachmentResponse[];
|
|
12
|
-
queryResults: Map<string,
|
|
12
|
+
queryResults: Map<string, GenieStatementResponse>;
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
15
|
interface UseGenieChatOptions {
|
|
@@ -29,6 +29,12 @@ interface UseGenieChatReturn {
|
|
|
29
29
|
error: string | null;
|
|
30
30
|
sendMessage: (content: string) => void;
|
|
31
31
|
reset: () => void;
|
|
32
|
+
/** Whether a previous page of older messages exists */
|
|
33
|
+
hasPreviousPage: boolean;
|
|
34
|
+
/** Whether a previous page is currently being fetched */
|
|
35
|
+
isFetchingPreviousPage: boolean;
|
|
36
|
+
/** Fetch the previous page of older messages */
|
|
37
|
+
fetchPreviousPage: () => void;
|
|
32
38
|
}
|
|
33
39
|
interface GenieChatProps {
|
|
34
40
|
/** Genie space alias (must match a key registered with the genie plugin on the server) */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/genie/types.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/genie/types.ts"],"mappings":";;;;KASY,eAAA;AAAA,UAOK,gBAAA;EACf,EAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,WAAA,EAAa,uBAAA;EACb,YAAA,EAAc,GAAA,SAAY,sBAAA;EAC1B,KAAA;AAAA;AAAA,UAGe,mBAAA;EAJW;EAM1B,KAAA;EANiB;EAQjB,QAAA;EAbA;EAeA,YAAA;EAbA;EAeA,YAAA;AAAA;AAAA,UAGe,kBAAA;EACf,QAAA,EAAU,gBAAA;EACV,MAAA,EAAQ,eAAA;EACR,cAAA;EACA,KAAA;EACA,WAAA,GAAc,OAAA;EACd,KAAA;EAjBe;EAmBf,eAAA;;EAEA,sBAAA;EAnBA;EAqBA,iBAAA;AAAA;AAAA,UAGe,cAAA;EAlBH;EAoBZ,KAAA;EAjBe;EAmBf,QAAA;;EAEA,WAAA;EApBA;EAsBA,SAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"use-genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"mappings":";;;;;AAqJA;;;;;;;iBAAgB,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,kBAAA"}
|