@databricks/appkit-ui 0.20.3 → 0.22.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.
Files changed (63) hide show
  1. package/CLAUDE.md +1 -0
  2. package/README.md +3 -20
  3. package/dist/cli/commands/setup.js +2 -2
  4. package/dist/cli/commands/setup.js.map +1 -1
  5. package/dist/js/config.d.ts +24 -0
  6. package/dist/js/config.d.ts.map +1 -0
  7. package/dist/js/config.js +49 -0
  8. package/dist/js/config.js.map +1 -0
  9. package/dist/js/index.d.ts +2 -1
  10. package/dist/js/index.js +2 -1
  11. package/dist/react/charts/options.d.ts.map +1 -1
  12. package/dist/react/charts/options.js +3 -1
  13. package/dist/react/charts/options.js.map +1 -1
  14. package/dist/react/genie/genie-chart-inference.d.ts +6 -1
  15. package/dist/react/genie/genie-chart-inference.d.ts.map +1 -1
  16. package/dist/react/genie/genie-chart-inference.js +60 -6
  17. package/dist/react/genie/genie-chart-inference.js.map +1 -1
  18. package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
  19. package/dist/react/genie/genie-chat-message-list.js +5 -4
  20. package/dist/react/genie/genie-chat-message-list.js.map +1 -1
  21. package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
  22. package/dist/react/genie/genie-chat-message.js +7 -6
  23. package/dist/react/genie/genie-chat-message.js.map +1 -1
  24. package/dist/react/genie/genie-query-visualization.d.ts.map +1 -1
  25. package/dist/react/genie/genie-query-visualization.js +66 -15
  26. package/dist/react/genie/genie-query-visualization.js.map +1 -1
  27. package/dist/react/genie/index.d.ts +2 -2
  28. package/dist/react/genie/index.js +1 -1
  29. package/dist/react/genie/types.d.ts +2 -1
  30. package/dist/react/genie/types.d.ts.map +1 -1
  31. package/dist/react/genie/types.js +6 -0
  32. package/dist/react/genie/types.js.map +1 -0
  33. package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
  34. package/dist/react/genie/use-genie-chat.js +60 -23
  35. package/dist/react/genie/use-genie-chat.js.map +1 -1
  36. package/dist/react/hooks/index.d.ts +2 -1
  37. package/dist/react/hooks/index.js +1 -0
  38. package/dist/react/hooks/use-plugin-config.d.ts +25 -0
  39. package/dist/react/hooks/use-plugin-config.d.ts.map +1 -0
  40. package/dist/react/hooks/use-plugin-config.js +32 -0
  41. package/dist/react/hooks/use-plugin-config.js.map +1 -0
  42. package/dist/react/index.d.ts +4 -3
  43. package/dist/react/index.js +5 -4
  44. package/dist/react/table/data-table.js +1 -1
  45. package/dist/react/ui/index.js +1 -1
  46. package/dist/shared/src/index.d.ts +1 -1
  47. package/dist/shared/src/plugin.d.ts +12 -1
  48. package/dist/shared/src/plugin.d.ts.map +1 -0
  49. package/docs/api/appkit/Class.Plugin.md +75 -17
  50. package/docs/app-management.md +1 -1
  51. package/docs/architecture.md +1 -1
  52. package/docs/development/ai-assisted-development.md +2 -2
  53. package/docs/development/local-development.md +1 -1
  54. package/docs/development/remote-bridge.md +1 -1
  55. package/docs/development/templates.md +93 -0
  56. package/docs/development.md +1 -1
  57. package/docs/plugins/caching.md +3 -1
  58. package/docs/plugins/execution-context.md +1 -1
  59. package/docs/plugins/lakebase.md +1 -1
  60. package/docs.md +2 -2
  61. package/llms.txt +1 -0
  62. package/package.json +60 -58
  63. package/sbom.cdx.json +1 -0
@@ -4,19 +4,20 @@ import { Avatar, AvatarFallback } from "../ui/avatar.js";
4
4
  import { GenieQueryVisualization } from "./genie-query-visualization.js";
5
5
  import { useMemo } from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
+ import DOMPurify from "dompurify";
7
8
  import { marked } from "marked";
8
9
 
9
10
  //#region src/react/genie/genie-chat-message.tsx
10
11
  /**
11
12
  * Using `marked` instead of `react-markdown` because `react-markdown` depends on
12
13
  * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.
13
- * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.
14
+ * Output is sanitized with DOMPurify before being passed to `dangerouslySetInnerHTML`.
14
15
  */
15
16
  marked.setOptions({
16
17
  breaks: true,
17
18
  gfm: true
18
19
  });
19
- const markdownStyles = cn("text-sm", "[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0", "[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto", "[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded", "[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1", "[&_table]:border-collapse [&_th]:border [&_td]:border", "[&_th]:border-border [&_td]:border-border", "[&_a]:underline");
20
+ const markdownStyles = cn("text-sm break-words", "[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0", "[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto", "[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded", "[&_table]:text-xs [&_table]:block [&_table]:overflow-x-auto [&_table]:max-w-full", "[&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1", "[&_table]:border-collapse [&_th]:border [&_td]:border", "[&_th]:border-border [&_td]:border-border", "[&_a]:underline");
20
21
  function isQueryAttachment(att) {
21
22
  return !!(att.query?.title || att.query?.query);
22
23
  }
@@ -24,7 +25,7 @@ function isQueryAttachment(att) {
24
25
  function GenieChatMessage({ message, className }) {
25
26
  const isUser = message.role === "user";
26
27
  const queryAttachments = message.attachments.filter(isQueryAttachment);
27
- const html = useMemo(() => message.content ? marked.parse(message.content) : "", [message.content]);
28
+ const html = useMemo(() => message.content ? DOMPurify.sanitize(marked.parse(message.content)) : "", [message.content]);
28
29
  return /* @__PURE__ */ jsxs("div", {
29
30
  className: cn("flex gap-3", isUser ? "flex-row-reverse" : "flex-row", className),
30
31
  children: [/* @__PURE__ */ jsx(Avatar, {
@@ -34,9 +35,9 @@ function GenieChatMessage({ message, className }) {
34
35
  children: isUser ? "You" : "AI"
35
36
  })
36
37
  }), /* @__PURE__ */ jsxs("div", {
37
- className: cn("flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden", isUser ? "items-end" : "items-start"),
38
+ className: "flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden",
38
39
  children: [/* @__PURE__ */ jsxs(Card, {
39
- className: cn("px-4 py-3 max-w-full overflow-hidden", isUser ? "bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30" : "bg-muted"),
40
+ className: cn("w-full px-4 py-3 overflow-hidden", isUser ? "bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30" : "bg-muted"),
40
41
  children: [html && /* @__PURE__ */ jsx("div", {
41
42
  className: markdownStyles,
42
43
  dangerouslySetInnerHTML: { __html: html }
@@ -67,7 +68,7 @@ function GenieChatMessage({ message, className }) {
67
68
  })]
68
69
  })] })
69
70
  }), queryResult != null && /* @__PURE__ */ jsx(Card, {
70
- className: "px-4 py-3 overflow-hidden",
71
+ className: "w-full px-4 py-3 overflow-hidden",
71
72
  children: /* @__PURE__ */ jsx(GenieQueryVisualization, { data: queryResult })
72
73
  })]
73
74
  }, key);
@@ -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 { 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\ninterface 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\n ? \"bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30\"\n : \"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,SACI,yHACA,WACL;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"}
1
+ {"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import DOMPurify from \"dompurify\";\nimport { 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 * Output is sanitized with DOMPurify before being passed to `dangerouslySetInnerHTML`.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm break-words\",\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 [&_table]:block [&_table]:overflow-x-auto [&_table]:max-w-full\",\n \"[&_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\ninterface 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 () =>\n message.content\n ? DOMPurify.sanitize(marked.parse(message.content) as string)\n : \"\",\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 className=\"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\">\n <Card\n className={cn(\n \"w-full px-4 py-3 overflow-hidden\",\n isUser\n ? \"bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30\"\n : \"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=\"w-full 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":";;;;;;;;;;;;;;;AAcA,OAAO,WAAW;CAAE,QAAQ;CAAM,KAAK;CAAM,CAAC;AAE9C,MAAM,iBAAiB,GACrB,uBACA,kDACA,gGACA,6EACA,oFACA,mDACA,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,cAET,QAAQ,UACJ,UAAU,SAAS,OAAO,MAAM,QAAQ,QAAQ,CAAW,GAC3D,IACN,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;GAAI,WAAU;cACb,qBAAC;IACC,WAAW,GACT,oCACA,SACI,yHACA,WACL;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"}
@@ -1 +1 @@
1
- {"version":3,"file":"genie-query-visualization.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"mappings":";;;;;UAmBU,4BAAA;;EAER,IAAA,EAAM,sBAAA;;EAEN,SAAA;AAAA;;;;;;;;iBAUc,uBAAA,CAAA;EACd,IAAA;EACA;AAAA,GACC,4BAAA,GAA4B,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"genie-query-visualization.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"mappings":";;;;;UA6CU,4BAAA;;EAER,IAAA,EAAM,sBAAA;;EAEN,SAAA;AAAA;;;;;;;;iBAUc,uBAAA,CAAA;EACd,IAAA;EACA;AAAA,GACC,4BAAA,GAA4B,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1,15 +1,29 @@
1
1
  import { BaseChart } from "../charts/base.js";
2
2
  import { ChartErrorBoundary } from "../charts/chart-error-boundary.js";
3
- import { inferChartType } from "./genie-chart-inference.js";
3
+ import { cn } from "../lib/utils.js";
4
+ import { Button } from "../ui/button.js";
5
+ import { getCompatibleChartTypes, inferChartType } from "./genie-chart-inference.js";
6
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
4
7
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
5
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
6
9
  import { transformGenieData } from "./genie-query-transform.js";
7
- import { useMemo } from "react";
10
+ import { useMemo, useState } from "react";
8
11
  import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { BarChart3Icon, ChevronDownIcon } from "lucide-react";
9
13
 
10
14
  //#region src/react/genie/genie-query-visualization.tsx
11
15
  const TABLE_ROW_LIMIT = 50;
12
16
  const CHART_HEIGHT = 250;
17
+ const CHART_TYPE_LABELS = {
18
+ bar: "Bar",
19
+ line: "Line",
20
+ area: "Area",
21
+ pie: "Pie",
22
+ donut: "Donut",
23
+ scatter: "Scatter",
24
+ radar: "Radar",
25
+ heatmap: "Heatmap"
26
+ };
13
27
  /**
14
28
  * Renders a chart + data table for a Genie query result.
15
29
  *
@@ -19,11 +33,23 @@ const CHART_HEIGHT = 250;
19
33
  */
20
34
  function GenieQueryVisualization({ data, className }) {
21
35
  const transformed = useMemo(() => transformGenieData(data), [data]);
22
- const inference = useMemo(() => transformed ? inferChartType(transformed.rows, transformed.columns) : null, [transformed]);
36
+ const { inference, compatibleTypes } = useMemo(() => {
37
+ if (!transformed) return {
38
+ inference: null,
39
+ compatibleTypes: []
40
+ };
41
+ const { rows, columns } = transformed;
42
+ return {
43
+ inference: inferChartType(rows, columns),
44
+ compatibleTypes: getCompatibleChartTypes(rows, columns)
45
+ };
46
+ }, [transformed]);
47
+ const [chartTypeOverride, setChartTypeOverride] = useState(null);
23
48
  if (!transformed || transformed.rows.length === 0) return null;
24
49
  const { rows, columns } = transformed;
25
50
  const truncated = rows.length > TABLE_ROW_LIMIT;
26
51
  const displayRows = truncated ? rows.slice(0, TABLE_ROW_LIMIT) : rows;
52
+ const activeChartType = chartTypeOverride && compatibleTypes.includes(chartTypeOverride) ? chartTypeOverride : inference?.chartType ?? null;
27
53
  const dataTable = /* @__PURE__ */ jsxs("div", {
28
54
  className: "overflow-auto max-h-[300px]",
29
55
  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", {
@@ -37,40 +63,65 @@ function GenieQueryVisualization({ data, className }) {
37
63
  ]
38
64
  })]
39
65
  });
40
- if (!inference) return /* @__PURE__ */ jsx("div", {
41
- className,
66
+ if (!inference || !activeChartType) return /* @__PURE__ */ jsx("div", {
67
+ className: cn("min-w-0", className),
42
68
  children: dataTable
43
69
  });
44
70
  return /* @__PURE__ */ jsxs(Tabs, {
45
71
  defaultValue: "chart",
46
- className,
47
- children: [
48
- /* @__PURE__ */ jsxs(TabsList, { children: [/* @__PURE__ */ jsx(TabsTrigger, {
72
+ className: cn("min-w-0", className),
73
+ children: [/* @__PURE__ */ jsxs("div", {
74
+ className: "flex items-center justify-between",
75
+ children: [/* @__PURE__ */ jsxs(TabsList, { children: [/* @__PURE__ */ jsx(TabsTrigger, {
49
76
  value: "chart",
50
77
  children: "Chart"
51
78
  }), /* @__PURE__ */ jsx(TabsTrigger, {
52
79
  value: "table",
53
80
  children: "Table"
54
- })] }),
55
- /* @__PURE__ */ jsx(TabsContent, {
81
+ })] }), compatibleTypes.length > 1 && /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
82
+ asChild: true,
83
+ children: /* @__PURE__ */ jsxs(Button, {
84
+ variant: "ghost",
85
+ size: "icon-sm",
86
+ "aria-label": "Change chart type",
87
+ className: "gap-0.5",
88
+ children: [/* @__PURE__ */ jsx(BarChart3Icon, { className: "size-3.5" }), /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3" })]
89
+ })
90
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
91
+ align: "end",
92
+ children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Chart type" }), /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
93
+ value: activeChartType,
94
+ onValueChange: (v) => setChartTypeOverride(v),
95
+ children: compatibleTypes.map((type) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, {
96
+ value: type,
97
+ children: CHART_TYPE_LABELS[type]
98
+ }, type))
99
+ })]
100
+ })] })]
101
+ }), /* @__PURE__ */ jsxs("div", {
102
+ className: "grid min-w-0 [&>*]:col-start-1 [&>*]:row-start-1 [&>*]:min-w-0",
103
+ children: [/* @__PURE__ */ jsx(TabsContent, {
56
104
  value: "chart",
105
+ forceMount: true,
106
+ className: "data-[state=inactive]:invisible",
57
107
  children: /* @__PURE__ */ jsx(ChartErrorBoundary, {
58
108
  fallback: dataTable,
59
109
  children: /* @__PURE__ */ jsx(BaseChart, {
60
110
  data: rows,
61
- chartType: inference.chartType,
111
+ chartType: activeChartType,
62
112
  xKey: inference.xKey,
63
113
  yKey: inference.yKey,
64
114
  height: CHART_HEIGHT,
65
115
  showLegend: Array.isArray(inference.yKey)
66
116
  })
67
117
  })
68
- }),
69
- /* @__PURE__ */ jsx(TabsContent, {
118
+ }), /* @__PURE__ */ jsx(TabsContent, {
70
119
  value: "table",
120
+ forceMount: true,
121
+ className: "data-[state=inactive]:invisible",
71
122
  children: dataTable
72
- })
73
- ]
123
+ })]
124
+ })]
74
125
  });
75
126
  }
76
127
 
@@ -1 +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\ninterface 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
+ {"version":3,"file":"genie-query-visualization.js","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"sourcesContent":["import { BarChart3Icon, ChevronDownIcon } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport type { GenieStatementResponse } from \"shared\";\nimport { BaseChart } from \"../charts/base\";\nimport { ChartErrorBoundary } from \"../charts/chart-error-boundary\";\nimport type { ChartType } from \"../charts/types\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuLabel,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\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 {\n getCompatibleChartTypes,\n inferChartType,\n} from \"./genie-chart-inference\";\nimport { transformGenieData } from \"./genie-query-transform\";\n\nconst TABLE_ROW_LIMIT = 50;\nconst CHART_HEIGHT = 250;\n\nconst CHART_TYPE_LABELS: Record<ChartType, string> = {\n bar: \"Bar\",\n line: \"Line\",\n area: \"Area\",\n pie: \"Pie\",\n donut: \"Donut\",\n scatter: \"Scatter\",\n radar: \"Radar\",\n heatmap: \"Heatmap\",\n};\n\ninterface 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, compatibleTypes } = useMemo(() => {\n if (!transformed)\n return { inference: null, compatibleTypes: [] as ChartType[] };\n const { rows, columns } = transformed;\n return {\n inference: inferChartType(rows, columns),\n compatibleTypes: getCompatibleChartTypes(rows, columns),\n };\n }, [transformed]);\n\n const [chartTypeOverride, setChartTypeOverride] = useState<ChartType | null>(\n null,\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 activeChartType =\n chartTypeOverride && compatibleTypes.includes(chartTypeOverride)\n ? chartTypeOverride\n : (inference?.chartType ?? null);\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 || !activeChartType) {\n return <div className={cn(\"min-w-0\", className)}>{dataTable}</div>;\n }\n\n return (\n <Tabs defaultValue=\"chart\" className={cn(\"min-w-0\", className)}>\n <div className=\"flex items-center justify-between\">\n <TabsList>\n <TabsTrigger value=\"chart\">Chart</TabsTrigger>\n <TabsTrigger value=\"table\">Table</TabsTrigger>\n </TabsList>\n {compatibleTypes.length > 1 && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Change chart type\"\n className=\"gap-0.5\"\n >\n <BarChart3Icon className=\"size-3.5\" />\n <ChevronDownIcon className=\"size-3\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuLabel>Chart type</DropdownMenuLabel>\n <DropdownMenuRadioGroup\n value={activeChartType}\n onValueChange={(v) => setChartTypeOverride(v as ChartType)}\n >\n {compatibleTypes.map((type) => (\n <DropdownMenuRadioItem key={type} value={type}>\n {CHART_TYPE_LABELS[type]}\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n <div className=\"grid min-w-0 [&>*]:col-start-1 [&>*]:row-start-1 [&>*]:min-w-0\">\n <TabsContent\n value=\"chart\"\n forceMount\n className=\"data-[state=inactive]:invisible\"\n >\n <ChartErrorBoundary fallback={dataTable}>\n <BaseChart\n data={rows}\n chartType={activeChartType}\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\n value=\"table\"\n forceMount\n className=\"data-[state=inactive]:invisible\"\n >\n {dataTable}\n </TabsContent>\n </div>\n </Tabs>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA+BA,MAAM,kBAAkB;AACxB,MAAM,eAAe;AAErB,MAAM,oBAA+C;CACnD,KAAK;CACL,MAAM;CACN,MAAM;CACN,KAAK;CACL,OAAO;CACP,SAAS;CACT,OAAO;CACP,SAAS;CACV;;;;;;;;AAgBD,SAAgB,wBAAwB,EACtC,MACA,aAC+B;CAC/B,MAAM,cAAc,cAAc,mBAAmB,KAAK,EAAE,CAAC,KAAK,CAAC;CACnE,MAAM,EAAE,WAAW,oBAAoB,cAAc;AACnD,MAAI,CAAC,YACH,QAAO;GAAE,WAAW;GAAM,iBAAiB,EAAE;GAAiB;EAChE,MAAM,EAAE,MAAM,YAAY;AAC1B,SAAO;GACL,WAAW,eAAe,MAAM,QAAQ;GACxC,iBAAiB,wBAAwB,MAAM,QAAQ;GACxD;IACA,CAAC,YAAY,CAAC;CAEjB,MAAM,CAAC,mBAAmB,wBAAwB,SAChD,KACD;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,kBACJ,qBAAqB,gBAAgB,SAAS,kBAAkB,GAC5D,oBACC,WAAW,aAAa;CAE/B,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,aAAa,CAAC,gBACjB,QAAO,oBAAC;EAAI,WAAW,GAAG,WAAW,UAAU;YAAG;GAAgB;AAGpE,QACE,qBAAC;EAAK,cAAa;EAAQ,WAAW,GAAG,WAAW,UAAU;aAC5D,qBAAC;GAAI,WAAU;cACb,qBAAC,uBACC,oBAAC;IAAY,OAAM;cAAQ;KAAmB,EAC9C,oBAAC;IAAY,OAAM;cAAQ;KAAmB,IACrC,EACV,gBAAgB,SAAS,KACxB,qBAAC,2BACC,oBAAC;IAAoB;cACnB,qBAAC;KACC,SAAQ;KACR,MAAK;KACL,cAAW;KACX,WAAU;gBAEV,oBAAC,iBAAc,WAAU,aAAa,EACtC,oBAAC,mBAAgB,WAAU,WAAW;MAC/B;KACW,EACtB,qBAAC;IAAoB,OAAM;eACzB,oBAAC,+BAAkB,eAA8B,EACjD,oBAAC;KACC,OAAO;KACP,gBAAgB,MAAM,qBAAqB,EAAe;eAEzD,gBAAgB,KAAK,SACpB,oBAAC;MAAiC,OAAO;gBACtC,kBAAkB;QADO,KAEJ,CACxB;MACqB;KACL,IACT;IAEb,EACN,qBAAC;GAAI,WAAU;cACb,oBAAC;IACC,OAAM;IACN;IACA,WAAU;cAEV,oBAAC;KAAmB,UAAU;eAC5B,oBAAC;MACC,MAAM;MACN,WAAW;MACX,MAAM,UAAU;MAChB,MAAM,UAAU;MAChB,QAAQ;MACR,YAAY,MAAM,QAAQ,UAAU,KAAK;OACzC;MACiB;KACT,EACd,oBAAC;IACC,OAAM;IACN;IACA,WAAU;cAET;KACW;IACV;GACD"}
@@ -1,7 +1,7 @@
1
1
  import { GenieAttachmentResponse, GenieMessageResponse, GenieStatementResponse, GenieStreamEvent } from "../../shared/src/genie.js";
2
2
  import { ColumnCategory, GenieColumnMeta, TransformedGenieData, transformGenieData } from "./genie-query-transform.js";
3
- import { ChartInference, inferChartType } from "./genie-chart-inference.js";
4
- import { GenieChatProps, GenieChatStatus, GenieMessageItem, UseGenieChatOptions, UseGenieChatReturn } from "./types.js";
3
+ import { ChartInference, getCompatibleChartTypes, inferChartType } from "./genie-chart-inference.js";
4
+ import { GenieChatProps, GenieChatStatus, GenieMessageItem, TERMINAL_STATUSES, UseGenieChatOptions, UseGenieChatReturn } from "./types.js";
5
5
  import { GenieChat } from "./genie-chat.js";
6
6
  import { GenieChatInput } from "./genie-chat-input.js";
7
7
  import { GenieChatMessage } from "./genie-chat-message.js";
@@ -1,4 +1,4 @@
1
- import { inferChartType } from "./genie-chart-inference.js";
1
+ import { getCompatibleChartTypes, inferChartType } from "./genie-chart-inference.js";
2
2
  import { GenieChatInput } from "./genie-chat-input.js";
3
3
  import { transformGenieData } from "./genie-query-transform.js";
4
4
  import { GenieQueryVisualization } from "./genie-query-visualization.js";
@@ -2,6 +2,7 @@ import { GenieAttachmentResponse, GenieMessageResponse, GenieStatementResponse,
2
2
  import "../../shared/src/index.js";
3
3
 
4
4
  //#region src/react/genie/types.d.ts
5
+ declare const TERMINAL_STATUSES: Set<string>;
5
6
  type GenieChatStatus = "idle" | "loading-history" | "loading-older" | "streaming" | "error";
6
7
  interface GenieMessageItem {
7
8
  id: string;
@@ -47,5 +48,5 @@ interface GenieChatProps {
47
48
  className?: string;
48
49
  }
49
50
  //#endregion
50
- export { GenieChatProps, GenieChatStatus, GenieMessageItem, UseGenieChatOptions, UseGenieChatReturn };
51
+ export { GenieChatProps, GenieChatStatus, GenieMessageItem, TERMINAL_STATUSES, UseGenieChatOptions, UseGenieChatReturn };
51
52
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/genie/types.ts"],"mappings":";;;;cASa,iBAAA,EAAiB,GAAA;AAAA,KAElB,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;EAVA;EAYf,KAAA;;EAEA,QAAA;EAR0B;EAU1B,YAAA;EAViB;EAYjB,YAAA;AAAA;AAAA,UAGe,kBAAA;EACf,QAAA,EAAU,gBAAA;EACV,MAAA,EAAQ,eAAA;EACR,cAAA;EACA,KAAA;EACA,WAAA,GAAc,OAAA;EACd,KAAA;EArB0B;EAuB1B,eAAA;EAtBK;EAwBL,sBAAA;EArBe;EAuBf,iBAAA;AAAA;AAAA,UAGe,cAAA;EAxBf;EA0BA,KAAA;EAtBA;EAwBA,QAAA;EAtBY;EAwBZ,WAAA;EArBe;EAuBf,SAAA;AAAA"}
@@ -0,0 +1,6 @@
1
+ //#region src/react/genie/types.ts
2
+ const TERMINAL_STATUSES = new Set(["COMPLETED", "FAILED"]);
3
+
4
+ //#endregion
5
+ export { TERMINAL_STATUSES };
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/react/genie/types.ts"],"sourcesContent":["import type { GenieAttachmentResponse, GenieStatementResponse } from \"shared\";\n\nexport type {\n GenieAttachmentResponse,\n GenieMessageResponse,\n GenieStatementResponse,\n GenieStreamEvent,\n} from \"shared\";\n\nexport const TERMINAL_STATUSES = new Set([\"COMPLETED\", \"FAILED\"]);\n\nexport type GenieChatStatus =\n | \"idle\"\n | \"loading-history\"\n | \"loading-older\"\n | \"streaming\"\n | \"error\";\n\nexport interface GenieMessageItem {\n id: string;\n role: \"user\" | \"assistant\";\n content: string;\n status: string;\n attachments: GenieAttachmentResponse[];\n queryResults: Map<string, GenieStatementResponse>;\n error?: string;\n}\n\nexport interface UseGenieChatOptions {\n /** Genie space alias (maps to backend route param) */\n alias: string;\n /** Base API path. Default: \"/api/genie\" */\n basePath?: string;\n /** Read/write conversationId from URL search params. Default: true */\n persistInUrl?: boolean;\n /** URL search param name. Default: \"conversationId\" */\n urlParamName?: string;\n}\n\nexport interface UseGenieChatReturn {\n messages: GenieMessageItem[];\n status: GenieChatStatus;\n conversationId: string | null;\n error: string | null;\n sendMessage: (content: string) => void;\n reset: () => void;\n /** Whether a previous page of older messages exists */\n hasPreviousPage: boolean;\n /** Whether a previous page is currently being fetched */\n isFetchingPreviousPage: boolean;\n /** Fetch the previous page of older messages */\n fetchPreviousPage: () => void;\n}\n\nexport interface GenieChatProps {\n /** Genie space alias (must match a key registered with the genie plugin on the server) */\n alias: string;\n /** Base API path */\n basePath?: string;\n /** Placeholder text for the input */\n placeholder?: string;\n /** Additional CSS class for the root container */\n className?: string;\n}\n"],"mappings":";AASA,MAAa,oBAAoB,IAAI,IAAI,CAAC,aAAa,SAAS,CAAC"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"use-genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"mappings":";;;;;AA0KA;;;;;;;iBAAgB,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,kBAAA"}
@@ -1,5 +1,6 @@
1
1
  import { connectSSE } from "../../js/sse/connect-sse.js";
2
2
  import "../../js/index.js";
3
+ import { TERMINAL_STATUSES } from "./types.js";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
4
5
 
5
6
  //#region src/react/genie/use-genie-chat.ts
@@ -49,9 +50,22 @@ function makeAssistantItem(msg) {
49
50
  /**
50
51
  * The API bundles user question (content) and AI answer (attachments) in one message.
51
52
  * Split into separate user + assistant items for display.
53
+ *
54
+ * When a message is still in-progress (non-terminal status) and has no
55
+ * attachments yet, we emit an empty assistant placeholder so the UI can
56
+ * show a loading indicator and later poll for the completed response.
52
57
  */
53
58
  function messageResultToItems(msg) {
54
- if (!((msg.attachments?.length ?? 0) > 0)) return [makeUserItem(msg)];
59
+ const hasAttachments = (msg.attachments?.length ?? 0) > 0;
60
+ if (!hasAttachments && TERMINAL_STATUSES.has(msg.status)) return [makeUserItem(msg)];
61
+ if (!hasAttachments) return [makeUserItem(msg, "-user"), {
62
+ id: msg.messageId,
63
+ role: "assistant",
64
+ content: "",
65
+ status: msg.status,
66
+ attachments: [],
67
+ queryResults: /* @__PURE__ */ new Map()
68
+ }];
55
69
  return [makeUserItem(msg, "-user"), makeAssistantItem(msg)];
56
70
  }
57
71
  /**
@@ -118,12 +132,12 @@ function useGenieChat(options) {
118
132
  const conversationIdRef = useRef(null);
119
133
  const nextPageTokenRef = useRef(null);
120
134
  const isLoadingOlderRef = useRef(false);
135
+ const processStreamEventRef = useRef(() => {});
121
136
  useEffect(() => {
122
137
  conversationIdRef.current = conversationId;
123
138
  nextPageTokenRef.current = nextPageToken;
124
139
  }, [conversationId, nextPageToken]);
125
- /** Process SSE events during live message streaming (sendMessage). */
126
- const processStreamEvent = useCallback((event) => {
140
+ processStreamEventRef.current = useCallback((event) => {
127
141
  switch (event.type) {
128
142
  case "message_start":
129
143
  setConversationId(event.conversationId);
@@ -140,15 +154,13 @@ function useGenieChat(options) {
140
154
  });
141
155
  break;
142
156
  case "message_result": {
143
- const msg = event.message;
144
- if ((msg.attachments?.length ?? 0) > 0) {
145
- const item = makeAssistantItem(msg);
146
- setMessages((prev) => {
147
- const last = prev[prev.length - 1];
148
- if (last?.role === "assistant" && last.id === "") return [...prev.slice(0, -1), item];
149
- return [...prev, item];
150
- });
151
- }
157
+ const item = makeAssistantItem(event.message);
158
+ setMessages((prev) => {
159
+ const last = prev[prev.length - 1];
160
+ if (!last || last.role !== "assistant") return prev;
161
+ if (last.id === event.message.messageId || last.id === "") return [...prev.slice(0, -1), item];
162
+ return prev;
163
+ });
152
164
  break;
153
165
  }
154
166
  case "query_result":
@@ -213,7 +225,7 @@ function useGenieChat(options) {
213
225
  signal: abortController.signal,
214
226
  onMessage: async (message) => {
215
227
  try {
216
- processStreamEvent(JSON.parse(message.data));
228
+ processStreamEventRef.current(JSON.parse(message.data));
217
229
  } catch {}
218
230
  },
219
231
  onError: (err) => {
@@ -227,12 +239,12 @@ function useGenieChat(options) {
227
239
  }
228
240
  }).then(() => {
229
241
  if (!abortController.signal.aborted) setStatus((prev) => prev === "error" ? "error" : "idle");
242
+ }).catch(() => {
243
+ if (abortController.signal.aborted) return;
244
+ setError("Connection error. Please try again.");
245
+ setStatus("error");
230
246
  });
231
- }, [
232
- alias,
233
- basePath,
234
- processStreamEvent
235
- ]);
247
+ }, [alias, basePath]);
236
248
  /** Creates an AbortController, stores it in the given ref, and fetches a conversation page. */
237
249
  const fetchPage = useCallback((controllerRef, convId, options) => {
238
250
  controllerRef.current?.abort();
@@ -256,6 +268,30 @@ function useGenieChat(options) {
256
268
  abortController
257
269
  };
258
270
  }, [alias, basePath]);
271
+ const pollPendingMessage = useCallback((convId, messageId, parentAbortController) => {
272
+ setStatus("streaming");
273
+ const requestId = crypto.randomUUID();
274
+ connectSSE({
275
+ url: `${basePath}/${encodeURIComponent(alias)}/conversations/${encodeURIComponent(convId)}/messages/${encodeURIComponent(messageId)}?requestId=${encodeURIComponent(requestId)}`,
276
+ signal: parentAbortController.signal,
277
+ onMessage: async (message) => {
278
+ try {
279
+ processStreamEventRef.current(JSON.parse(message.data));
280
+ } catch {}
281
+ },
282
+ onError: (err) => {
283
+ if (parentAbortController.signal.aborted) return;
284
+ setError(err instanceof Error ? err.message : "Failed to poll pending message.");
285
+ setStatus("error");
286
+ }
287
+ }).then(() => {
288
+ if (!parentAbortController.signal.aborted) setStatus((prev) => prev === "error" ? "error" : "idle");
289
+ }).catch(() => {
290
+ if (parentAbortController.signal.aborted) return;
291
+ setError("Failed to poll pending message.");
292
+ setStatus("error");
293
+ });
294
+ }, [alias, basePath]);
259
295
  const loadHistory = useCallback((convId) => {
260
296
  paginationAbortRef.current?.abort();
261
297
  setStatus("loading-history");
@@ -264,12 +300,13 @@ function useGenieChat(options) {
264
300
  setConversationId(convId);
265
301
  const { promise, abortController } = fetchPage(abortControllerRef, convId, { errorMessage: "Failed to load conversation history." });
266
302
  promise.then((items) => {
267
- if (!abortController.signal.aborted) {
268
- setMessages(items);
269
- setStatus((prev) => prev === "error" ? "error" : "idle");
270
- }
303
+ if (abortController.signal.aborted) return;
304
+ setMessages(items);
305
+ const lastItem = items[items.length - 1];
306
+ if (lastItem?.role === "assistant" && !TERMINAL_STATUSES.has(lastItem.status)) pollPendingMessage(convId, lastItem.id, abortController);
307
+ else setStatus((prev) => prev === "error" ? "error" : "idle");
271
308
  });
272
- }, [fetchPage]);
309
+ }, [fetchPage, pollPendingMessage]);
273
310
  const fetchPreviousPage = useCallback(() => {
274
311
  if (!nextPageTokenRef.current || !conversationIdRef.current || isLoadingOlderRef.current) return;
275
312
  isLoadingOlderRef.current = true;
@@ -1 +1 @@
1
- {"version":3,"file":"use-genie-chat.js","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { connectSSE } from \"@/js\";\nimport type {\n GenieChatStatus,\n GenieMessageItem,\n GenieMessageResponse,\n GenieStreamEvent,\n UseGenieChatOptions,\n UseGenieChatReturn,\n} from \"./types\";\n\nfunction getUrlParam(name: string): string | null {\n return new URLSearchParams(window.location.search).get(name);\n}\n\nfunction setUrlParam(name: string, value: string): void {\n const url = new URL(window.location.href);\n url.searchParams.set(name, value);\n window.history.replaceState({}, \"\", url.toString());\n}\n\nfunction removeUrlParam(name: string): void {\n const url = new URL(window.location.href);\n url.searchParams.delete(name);\n window.history.replaceState({}, \"\", url.toString());\n}\n\n/**\n * The Genie API puts the user's question in `message.content` and the\n * actual AI answer in text attachments. Extract the text attachment\n * content so we display the real answer, not the question echo.\n */\nfunction extractAssistantContent(msg: GenieMessageResponse): string {\n const textParts = (msg.attachments ?? [])\n .map((att) => att.text?.content)\n .filter(Boolean) as string[];\n return textParts.length > 0 ? textParts.join(\"\\n\\n\") : msg.content;\n}\n\nfunction makeUserItem(\n msg: GenieMessageResponse,\n idSuffix = \"\",\n): GenieMessageItem {\n return {\n id: `${msg.messageId}${idSuffix}`,\n role: \"user\",\n content: msg.content,\n status: msg.status,\n attachments: [],\n queryResults: new Map(),\n };\n}\n\nfunction makeAssistantItem(msg: GenieMessageResponse): GenieMessageItem {\n return {\n id: msg.messageId,\n role: \"assistant\",\n content: extractAssistantContent(msg),\n status: msg.status,\n attachments: msg.attachments ?? [],\n queryResults: new Map(),\n error: msg.error,\n };\n}\n\n/**\n * The API bundles user question (content) and AI answer (attachments) in one message.\n * Split into separate user + assistant items for display.\n */\nfunction messageResultToItems(msg: GenieMessageResponse): GenieMessageItem[] {\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n if (!hasAttachments) return [makeUserItem(msg)];\n return [makeUserItem(msg, \"-user\"), makeAssistantItem(msg)];\n}\n\n/**\n * Streams a conversation page via SSE. Collects message items and query\n * results into a buffer and returns them when the stream completes.\n */\nfunction fetchConversationPage(\n basePath: string,\n alias: string,\n convId: string,\n options: {\n pageToken?: string;\n signal?: AbortSignal;\n onPaginationInfo?: (nextPageToken: string | null) => void;\n onError?: (error: string) => void;\n onConnectionError?: (err: unknown) => void;\n },\n): Promise<GenieMessageItem[]> {\n const params = new URLSearchParams({\n requestId: crypto.randomUUID(),\n });\n if (options.pageToken) {\n params.set(\"pageToken\", options.pageToken);\n }\n\n const items: GenieMessageItem[] = [];\n return connectSSE({\n url: `${basePath}/${encodeURIComponent(alias)}/conversations/${encodeURIComponent(convId)}?${params}`,\n signal: options.signal,\n onMessage: async (message) => {\n try {\n const event = JSON.parse(message.data) as GenieStreamEvent;\n switch (event.type) {\n case \"message_result\":\n items.push(...messageResultToItems(event.message));\n break;\n case \"query_result\":\n for (let i = items.length - 1; i >= 0; i--) {\n const item = items[i];\n if (\n item.attachments.some(\n (a) => a.attachmentId === event.attachmentId,\n )\n ) {\n item.queryResults.set(event.attachmentId, event.data);\n break;\n }\n }\n break;\n case \"history_info\":\n options.onPaginationInfo?.(event.nextPageToken);\n break;\n case \"error\":\n options.onError?.(event.error);\n break;\n }\n } catch {\n // Malformed SSE data\n }\n },\n onError: (err) => options.onConnectionError?.(err),\n }).then(() => items);\n}\n\n/** Minimum time (ms) to hold the loading-older state so scroll inertia settles before prepending messages. */\nconst MIN_PREVIOUS_PAGE_LOAD_MS = 800;\n\n/**\n * Manages the full Genie chat lifecycle:\n * SSE streaming, conversation persistence via URL, and history replay.\n *\n * @example\n * ```tsx\n * const { messages, status, sendMessage, reset } = useGenieChat({ alias: \"demo\" });\n * ```\n */\nexport function useGenieChat(options: UseGenieChatOptions): UseGenieChatReturn {\n const {\n alias,\n basePath = \"/api/genie\",\n persistInUrl = true,\n urlParamName = \"conversationId\",\n } = options;\n\n const [messages, setMessages] = useState<GenieMessageItem[]>([]);\n const [status, setStatus] = useState<GenieChatStatus>(\"idle\");\n const [conversationId, setConversationId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [nextPageToken, setNextPageToken] = useState<string | null>(null);\n\n const hasPreviousPage = nextPageToken !== null;\n const isFetchingPreviousPage = status === \"loading-older\";\n\n const abortControllerRef = useRef<AbortController | null>(null);\n const paginationAbortRef = useRef<AbortController | null>(null);\n const conversationIdRef = useRef<string | null>(null);\n const nextPageTokenRef = useRef<string | null>(null);\n const isLoadingOlderRef = useRef(false);\n\n useEffect(() => {\n conversationIdRef.current = conversationId;\n nextPageTokenRef.current = nextPageToken;\n }, [conversationId, nextPageToken]);\n\n /** Process SSE events during live message streaming (sendMessage). */\n const processStreamEvent = useCallback(\n (event: GenieStreamEvent) => {\n switch (event.type) {\n case \"message_start\": {\n setConversationId(event.conversationId);\n if (persistInUrl) {\n setUrlParam(urlParamName, event.conversationId);\n }\n break;\n }\n\n case \"status\": {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (last?.role === \"assistant\") {\n return [...prev.slice(0, -1), { ...last, status: event.status }];\n }\n return prev;\n });\n break;\n }\n\n case \"message_result\": {\n const msg = event.message;\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n\n if (hasAttachments) {\n // During streaming we already appended the user message locally,\n // so only handle assistant results. Messages without attachments\n // are the user-message echo from the API — skip those.\n const item = makeAssistantItem(msg);\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (last?.role === \"assistant\" && last.id === \"\") {\n return [...prev.slice(0, -1), item];\n }\n return [...prev, item];\n });\n }\n break;\n }\n\n case \"query_result\": {\n setMessages((prev) => {\n // Reverse scan — query results typically match recent messages\n for (let i = prev.length - 1; i >= 0; i--) {\n const msg = prev[i];\n if (\n msg.attachments.some(\n (a) => a.attachmentId === event.attachmentId,\n )\n ) {\n const updated = prev.slice();\n updated[i] = {\n ...msg,\n queryResults: new Map(msg.queryResults).set(\n event.attachmentId,\n event.data,\n ),\n };\n return updated;\n }\n }\n return prev;\n });\n break;\n }\n\n case \"error\": {\n setError(event.error);\n setStatus(\"error\");\n break;\n }\n }\n },\n [persistInUrl, urlParamName],\n );\n\n const sendMessage = useCallback(\n (content: string) => {\n const trimmed = content.trim();\n if (!trimmed) return;\n\n abortControllerRef.current?.abort();\n paginationAbortRef.current?.abort();\n setError(null);\n setStatus(\"streaming\");\n\n const userMessage: GenieMessageItem = {\n id: crypto.randomUUID(),\n role: \"user\",\n content: trimmed,\n status: \"COMPLETED\",\n attachments: [],\n queryResults: new Map(),\n };\n\n const assistantPlaceholder: GenieMessageItem = {\n id: \"\",\n role: \"assistant\",\n content: \"\",\n status: \"ASKING_AI\",\n attachments: [],\n queryResults: new Map(),\n };\n\n setMessages((prev) => [...prev, userMessage, assistantPlaceholder]);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n const requestId = crypto.randomUUID();\n\n connectSSE({\n url: `${basePath}/${encodeURIComponent(alias)}/messages?requestId=${encodeURIComponent(requestId)}`,\n payload: {\n content: trimmed,\n conversationId: conversationIdRef.current ?? undefined,\n },\n signal: abortController.signal,\n onMessage: async (message) => {\n try {\n processStreamEvent(JSON.parse(message.data) as GenieStreamEvent);\n } catch {\n // Malformed SSE data\n }\n },\n onError: (err) => {\n if (abortController.signal.aborted) return;\n setError(\n err instanceof Error\n ? err.message\n : \"Connection error. Please try again.\",\n );\n setStatus(\"error\");\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n return last?.role === \"assistant\" && last.id === \"\"\n ? prev.slice(0, -1)\n : prev;\n });\n },\n }).then(() => {\n if (!abortController.signal.aborted) {\n setStatus((prev) => (prev === \"error\" ? \"error\" : \"idle\"));\n }\n });\n },\n [alias, basePath, processStreamEvent],\n );\n\n /** Creates an AbortController, stores it in the given ref, and fetches a conversation page. */\n const fetchPage = useCallback(\n (\n controllerRef: { current: AbortController | null },\n convId: string,\n options?: { pageToken?: string; errorMessage?: string },\n ) => {\n controllerRef.current?.abort();\n const abortController = new AbortController();\n controllerRef.current = abortController;\n\n const promise = fetchConversationPage(basePath, alias, convId, {\n pageToken: options?.pageToken,\n signal: abortController.signal,\n onPaginationInfo: setNextPageToken,\n onError: (msg) => {\n setError(msg);\n setStatus(\"error\");\n },\n onConnectionError: (err) => {\n if (abortController.signal.aborted) return;\n setError(\n err instanceof Error\n ? err.message\n : (options?.errorMessage ?? \"Failed to load messages.\"),\n );\n setStatus(\"error\");\n },\n });\n\n return { promise, abortController };\n },\n [alias, basePath],\n );\n\n const loadHistory = useCallback(\n (convId: string) => {\n paginationAbortRef.current?.abort();\n setStatus(\"loading-history\");\n setError(null);\n setMessages([]);\n setConversationId(convId);\n\n const { promise, abortController } = fetchPage(\n abortControllerRef,\n convId,\n { errorMessage: \"Failed to load conversation history.\" },\n );\n promise.then((items) => {\n if (!abortController.signal.aborted) {\n setMessages(items);\n setStatus((prev) => (prev === \"error\" ? \"error\" : \"idle\"));\n }\n });\n },\n [fetchPage],\n );\n\n const fetchPreviousPage = useCallback(() => {\n if (\n !nextPageTokenRef.current ||\n !conversationIdRef.current ||\n isLoadingOlderRef.current\n )\n return;\n\n isLoadingOlderRef.current = true;\n setStatus(\"loading-older\");\n setError(null);\n\n const startTime = Date.now();\n const { promise, abortController } = fetchPage(\n paginationAbortRef,\n conversationIdRef.current,\n {\n pageToken: nextPageTokenRef.current,\n errorMessage: \"Failed to load older messages.\",\n },\n );\n promise\n .then(async (items) => {\n if (abortController.signal.aborted) return;\n const elapsed = Date.now() - startTime;\n if (elapsed < MIN_PREVIOUS_PAGE_LOAD_MS) {\n await new Promise((r) =>\n setTimeout(r, MIN_PREVIOUS_PAGE_LOAD_MS - elapsed),\n );\n }\n if (abortController.signal.aborted) return;\n if (items.length > 0) {\n setMessages((prev) => [...items, ...prev]);\n }\n setStatus((current) =>\n current === \"loading-older\" ? \"idle\" : current,\n );\n })\n .finally(() => {\n isLoadingOlderRef.current = false;\n });\n }, [fetchPage]);\n\n const reset = useCallback(() => {\n abortControllerRef.current?.abort();\n paginationAbortRef.current?.abort();\n setMessages([]);\n setConversationId(null);\n setError(null);\n setStatus(\"idle\");\n setNextPageToken(null);\n if (persistInUrl) {\n removeUrlParam(urlParamName);\n }\n }, [persistInUrl, urlParamName]);\n\n useEffect(() => {\n if (!persistInUrl) return;\n const existingId = getUrlParam(urlParamName);\n if (existingId) {\n loadHistory(existingId);\n }\n return () => {\n abortControllerRef.current?.abort();\n paginationAbortRef.current?.abort();\n };\n }, [persistInUrl, urlParamName, loadHistory]);\n\n return {\n messages,\n status,\n conversationId,\n error,\n sendMessage,\n reset,\n hasPreviousPage,\n isFetchingPreviousPage,\n fetchPreviousPage,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-genie-chat.js","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { connectSSE } from \"@/js\";\nimport {\n type GenieChatStatus,\n type GenieMessageItem,\n type GenieMessageResponse,\n type GenieStreamEvent,\n TERMINAL_STATUSES,\n type UseGenieChatOptions,\n type UseGenieChatReturn,\n} from \"./types\";\n\nfunction getUrlParam(name: string): string | null {\n return new URLSearchParams(window.location.search).get(name);\n}\n\nfunction setUrlParam(name: string, value: string): void {\n const url = new URL(window.location.href);\n url.searchParams.set(name, value);\n window.history.replaceState({}, \"\", url.toString());\n}\n\nfunction removeUrlParam(name: string): void {\n const url = new URL(window.location.href);\n url.searchParams.delete(name);\n window.history.replaceState({}, \"\", url.toString());\n}\n\n/**\n * The Genie API puts the user's question in `message.content` and the\n * actual AI answer in text attachments. Extract the text attachment\n * content so we display the real answer, not the question echo.\n */\nfunction extractAssistantContent(msg: GenieMessageResponse): string {\n const textParts = (msg.attachments ?? [])\n .map((att) => att.text?.content)\n .filter(Boolean) as string[];\n return textParts.length > 0 ? textParts.join(\"\\n\\n\") : msg.content;\n}\n\nfunction makeUserItem(\n msg: GenieMessageResponse,\n idSuffix = \"\",\n): GenieMessageItem {\n return {\n id: `${msg.messageId}${idSuffix}`,\n role: \"user\",\n content: msg.content,\n status: msg.status,\n attachments: [],\n queryResults: new Map(),\n };\n}\n\nfunction makeAssistantItem(msg: GenieMessageResponse): GenieMessageItem {\n return {\n id: msg.messageId,\n role: \"assistant\",\n content: extractAssistantContent(msg),\n status: msg.status,\n attachments: msg.attachments ?? [],\n queryResults: new Map(),\n error: msg.error,\n };\n}\n\n/**\n * The API bundles user question (content) and AI answer (attachments) in one message.\n * Split into separate user + assistant items for display.\n *\n * When a message is still in-progress (non-terminal status) and has no\n * attachments yet, we emit an empty assistant placeholder so the UI can\n * show a loading indicator and later poll for the completed response.\n */\nfunction messageResultToItems(msg: GenieMessageResponse): GenieMessageItem[] {\n const hasAttachments = (msg.attachments?.length ?? 0) > 0;\n\n if (!hasAttachments && TERMINAL_STATUSES.has(msg.status)) {\n return [makeUserItem(msg)];\n }\n if (!hasAttachments) {\n return [\n makeUserItem(msg, \"-user\"),\n {\n id: msg.messageId,\n role: \"assistant\",\n content: \"\",\n status: msg.status,\n attachments: [],\n queryResults: new Map(),\n },\n ];\n }\n return [makeUserItem(msg, \"-user\"), makeAssistantItem(msg)];\n}\n\n/**\n * Streams a conversation page via SSE. Collects message items and query\n * results into a buffer and returns them when the stream completes.\n */\nfunction fetchConversationPage(\n basePath: string,\n alias: string,\n convId: string,\n options: {\n pageToken?: string;\n signal?: AbortSignal;\n onPaginationInfo?: (nextPageToken: string | null) => void;\n onError?: (error: string) => void;\n onConnectionError?: (err: unknown) => void;\n },\n): Promise<GenieMessageItem[]> {\n const params = new URLSearchParams({\n requestId: crypto.randomUUID(),\n });\n if (options.pageToken) {\n params.set(\"pageToken\", options.pageToken);\n }\n\n const items: GenieMessageItem[] = [];\n return connectSSE({\n url: `${basePath}/${encodeURIComponent(alias)}/conversations/${encodeURIComponent(convId)}?${params}`,\n signal: options.signal,\n onMessage: async (message) => {\n try {\n const event = JSON.parse(message.data) as GenieStreamEvent;\n switch (event.type) {\n case \"message_result\":\n items.push(...messageResultToItems(event.message));\n break;\n case \"query_result\":\n for (let i = items.length - 1; i >= 0; i--) {\n const item = items[i];\n if (\n item.attachments.some(\n (a) => a.attachmentId === event.attachmentId,\n )\n ) {\n item.queryResults.set(event.attachmentId, event.data);\n break;\n }\n }\n break;\n case \"history_info\":\n options.onPaginationInfo?.(event.nextPageToken);\n break;\n case \"error\":\n options.onError?.(event.error);\n break;\n }\n } catch {\n // Malformed SSE data\n }\n },\n onError: (err) => options.onConnectionError?.(err),\n }).then(() => items);\n}\n\n/** Minimum time (ms) to hold the loading-older state so scroll inertia settles before prepending messages. */\nconst MIN_PREVIOUS_PAGE_LOAD_MS = 800;\n\n/**\n * Manages the full Genie chat lifecycle:\n * SSE streaming, conversation persistence via URL, and history replay.\n *\n * @example\n * ```tsx\n * const { messages, status, sendMessage, reset } = useGenieChat({ alias: \"demo\" });\n * ```\n */\nexport function useGenieChat(options: UseGenieChatOptions): UseGenieChatReturn {\n const {\n alias,\n basePath = \"/api/genie\",\n persistInUrl = true,\n urlParamName = \"conversationId\",\n } = options;\n\n const [messages, setMessages] = useState<GenieMessageItem[]>([]);\n const [status, setStatus] = useState<GenieChatStatus>(\"idle\");\n const [conversationId, setConversationId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [nextPageToken, setNextPageToken] = useState<string | null>(null);\n\n const hasPreviousPage = nextPageToken !== null;\n const isFetchingPreviousPage = status === \"loading-older\";\n\n const abortControllerRef = useRef<AbortController | null>(null);\n const paginationAbortRef = useRef<AbortController | null>(null);\n const conversationIdRef = useRef<string | null>(null);\n const nextPageTokenRef = useRef<string | null>(null);\n const isLoadingOlderRef = useRef(false);\n const processStreamEventRef = useRef<(event: GenieStreamEvent) => void>(\n () => {},\n );\n\n useEffect(() => {\n conversationIdRef.current = conversationId;\n nextPageTokenRef.current = nextPageToken;\n }, [conversationId, nextPageToken]);\n\n /** Process SSE events during live message streaming (sendMessage). */\n const processStreamEvent = useCallback(\n (event: GenieStreamEvent) => {\n switch (event.type) {\n case \"message_start\": {\n setConversationId(event.conversationId);\n if (persistInUrl) {\n setUrlParam(urlParamName, event.conversationId);\n }\n break;\n }\n\n case \"status\": {\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (last?.role === \"assistant\") {\n return [...prev.slice(0, -1), { ...last, status: event.status }];\n }\n return prev;\n });\n break;\n }\n\n case \"message_result\": {\n const item = makeAssistantItem(event.message);\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n if (!last || last.role !== \"assistant\") return prev;\n\n if (last.id === event.message.messageId || last.id === \"\") {\n return [...prev.slice(0, -1), item];\n }\n\n return prev;\n });\n break;\n }\n\n case \"query_result\": {\n setMessages((prev) => {\n // Reverse scan — query results typically match recent messages\n for (let i = prev.length - 1; i >= 0; i--) {\n const msg = prev[i];\n if (\n msg.attachments.some(\n (a) => a.attachmentId === event.attachmentId,\n )\n ) {\n const updated = prev.slice();\n updated[i] = {\n ...msg,\n queryResults: new Map(msg.queryResults).set(\n event.attachmentId,\n event.data,\n ),\n };\n return updated;\n }\n }\n return prev;\n });\n break;\n }\n\n case \"error\": {\n setError(event.error);\n setStatus(\"error\");\n break;\n }\n }\n },\n [persistInUrl, urlParamName],\n );\n\n processStreamEventRef.current = processStreamEvent;\n\n const sendMessage = useCallback(\n (content: string) => {\n const trimmed = content.trim();\n if (!trimmed) return;\n\n abortControllerRef.current?.abort();\n paginationAbortRef.current?.abort();\n setError(null);\n setStatus(\"streaming\");\n\n const userMessage: GenieMessageItem = {\n id: crypto.randomUUID(),\n role: \"user\",\n content: trimmed,\n status: \"COMPLETED\",\n attachments: [],\n queryResults: new Map(),\n };\n\n const assistantPlaceholder: GenieMessageItem = {\n id: \"\",\n role: \"assistant\",\n content: \"\",\n status: \"ASKING_AI\",\n attachments: [],\n queryResults: new Map(),\n };\n\n setMessages((prev) => [...prev, userMessage, assistantPlaceholder]);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n const requestId = crypto.randomUUID();\n\n connectSSE({\n url: `${basePath}/${encodeURIComponent(alias)}/messages?requestId=${encodeURIComponent(requestId)}`,\n payload: {\n content: trimmed,\n conversationId: conversationIdRef.current ?? undefined,\n },\n signal: abortController.signal,\n onMessage: async (message) => {\n try {\n processStreamEventRef.current(\n JSON.parse(message.data) as GenieStreamEvent,\n );\n } catch {\n // Malformed SSE data\n }\n },\n onError: (err) => {\n if (abortController.signal.aborted) return;\n setError(\n err instanceof Error\n ? err.message\n : \"Connection error. Please try again.\",\n );\n setStatus(\"error\");\n setMessages((prev) => {\n const last = prev[prev.length - 1];\n return last?.role === \"assistant\" && last.id === \"\"\n ? prev.slice(0, -1)\n : prev;\n });\n },\n })\n .then(() => {\n if (!abortController.signal.aborted) {\n setStatus((prev) => (prev === \"error\" ? \"error\" : \"idle\"));\n }\n })\n .catch(() => {\n if (abortController.signal.aborted) return;\n setError(\"Connection error. Please try again.\");\n setStatus(\"error\");\n });\n },\n [alias, basePath],\n );\n\n /** Creates an AbortController, stores it in the given ref, and fetches a conversation page. */\n const fetchPage = useCallback(\n (\n controllerRef: { current: AbortController | null },\n convId: string,\n options?: { pageToken?: string; errorMessage?: string },\n ) => {\n controllerRef.current?.abort();\n const abortController = new AbortController();\n controllerRef.current = abortController;\n\n const promise = fetchConversationPage(basePath, alias, convId, {\n pageToken: options?.pageToken,\n signal: abortController.signal,\n onPaginationInfo: setNextPageToken,\n onError: (msg) => {\n setError(msg);\n setStatus(\"error\");\n },\n onConnectionError: (err) => {\n if (abortController.signal.aborted) return;\n setError(\n err instanceof Error\n ? err.message\n : (options?.errorMessage ?? \"Failed to load messages.\"),\n );\n setStatus(\"error\");\n },\n });\n\n return { promise, abortController };\n },\n [alias, basePath],\n );\n\n const pollPendingMessage = useCallback(\n (\n convId: string,\n messageId: string,\n parentAbortController: AbortController,\n ) => {\n setStatus(\"streaming\");\n\n const requestId = crypto.randomUUID();\n const url =\n `${basePath}/${encodeURIComponent(alias)}/conversations/${encodeURIComponent(convId)}` +\n `/messages/${encodeURIComponent(messageId)}?requestId=${encodeURIComponent(requestId)}`;\n\n connectSSE({\n url,\n signal: parentAbortController.signal,\n onMessage: async (message) => {\n try {\n processStreamEventRef.current(\n JSON.parse(message.data) as GenieStreamEvent,\n );\n } catch {\n // Malformed SSE data\n }\n },\n onError: (err) => {\n if (parentAbortController.signal.aborted) return;\n setError(\n err instanceof Error\n ? err.message\n : \"Failed to poll pending message.\",\n );\n setStatus(\"error\");\n },\n })\n .then(() => {\n if (!parentAbortController.signal.aborted) {\n setStatus((prev) => (prev === \"error\" ? \"error\" : \"idle\"));\n }\n })\n .catch(() => {\n if (parentAbortController.signal.aborted) return;\n setError(\"Failed to poll pending message.\");\n setStatus(\"error\");\n });\n },\n [alias, basePath],\n );\n\n const loadHistory = useCallback(\n (convId: string) => {\n paginationAbortRef.current?.abort();\n setStatus(\"loading-history\");\n setError(null);\n setMessages([]);\n setConversationId(convId);\n\n const { promise, abortController } = fetchPage(\n abortControllerRef,\n convId,\n { errorMessage: \"Failed to load conversation history.\" },\n );\n promise.then((items) => {\n if (abortController.signal.aborted) return;\n setMessages(items);\n\n const lastItem = items[items.length - 1];\n if (\n lastItem?.role === \"assistant\" &&\n !TERMINAL_STATUSES.has(lastItem.status)\n ) {\n pollPendingMessage(convId, lastItem.id, abortController);\n } else {\n setStatus((prev) => (prev === \"error\" ? \"error\" : \"idle\"));\n }\n });\n },\n [fetchPage, pollPendingMessage],\n );\n\n const fetchPreviousPage = useCallback(() => {\n if (\n !nextPageTokenRef.current ||\n !conversationIdRef.current ||\n isLoadingOlderRef.current\n )\n return;\n\n isLoadingOlderRef.current = true;\n setStatus(\"loading-older\");\n setError(null);\n\n const startTime = Date.now();\n const { promise, abortController } = fetchPage(\n paginationAbortRef,\n conversationIdRef.current,\n {\n pageToken: nextPageTokenRef.current,\n errorMessage: \"Failed to load older messages.\",\n },\n );\n promise\n .then(async (items) => {\n if (abortController.signal.aborted) return;\n const elapsed = Date.now() - startTime;\n if (elapsed < MIN_PREVIOUS_PAGE_LOAD_MS) {\n await new Promise((r) =>\n setTimeout(r, MIN_PREVIOUS_PAGE_LOAD_MS - elapsed),\n );\n }\n if (abortController.signal.aborted) return;\n if (items.length > 0) {\n setMessages((prev) => [...items, ...prev]);\n }\n setStatus((current) =>\n current === \"loading-older\" ? \"idle\" : current,\n );\n })\n .finally(() => {\n isLoadingOlderRef.current = false;\n });\n }, [fetchPage]);\n\n const reset = useCallback(() => {\n abortControllerRef.current?.abort();\n paginationAbortRef.current?.abort();\n setMessages([]);\n setConversationId(null);\n setError(null);\n setStatus(\"idle\");\n setNextPageToken(null);\n if (persistInUrl) {\n removeUrlParam(urlParamName);\n }\n }, [persistInUrl, urlParamName]);\n\n useEffect(() => {\n if (!persistInUrl) return;\n const existingId = getUrlParam(urlParamName);\n if (existingId) {\n loadHistory(existingId);\n }\n return () => {\n abortControllerRef.current?.abort();\n paginationAbortRef.current?.abort();\n };\n }, [persistInUrl, urlParamName, loadHistory]);\n\n return {\n messages,\n status,\n conversationId,\n error,\n sendMessage,\n reset,\n hasPreviousPage,\n isFetchingPreviousPage,\n fetchPreviousPage,\n };\n}\n"],"mappings":";;;;;;AAYA,SAAS,YAAY,MAA6B;AAChD,QAAO,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAAC,IAAI,KAAK;;AAG9D,SAAS,YAAY,MAAc,OAAqB;CACtD,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,aAAa,IAAI,MAAM,MAAM;AACjC,QAAO,QAAQ,aAAa,EAAE,EAAE,IAAI,IAAI,UAAU,CAAC;;AAGrD,SAAS,eAAe,MAAoB;CAC1C,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,aAAa,OAAO,KAAK;AAC7B,QAAO,QAAQ,aAAa,EAAE,EAAE,IAAI,IAAI,UAAU,CAAC;;;;;;;AAQrD,SAAS,wBAAwB,KAAmC;CAClE,MAAM,aAAa,IAAI,eAAe,EAAE,EACrC,KAAK,QAAQ,IAAI,MAAM,QAAQ,CAC/B,OAAO,QAAQ;AAClB,QAAO,UAAU,SAAS,IAAI,UAAU,KAAK,OAAO,GAAG,IAAI;;AAG7D,SAAS,aACP,KACA,WAAW,IACO;AAClB,QAAO;EACL,IAAI,GAAG,IAAI,YAAY;EACvB,MAAM;EACN,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,aAAa,EAAE;EACf,8BAAc,IAAI,KAAK;EACxB;;AAGH,SAAS,kBAAkB,KAA6C;AACtE,QAAO;EACL,IAAI,IAAI;EACR,MAAM;EACN,SAAS,wBAAwB,IAAI;EACrC,QAAQ,IAAI;EACZ,aAAa,IAAI,eAAe,EAAE;EAClC,8BAAc,IAAI,KAAK;EACvB,OAAO,IAAI;EACZ;;;;;;;;;;AAWH,SAAS,qBAAqB,KAA+C;CAC3E,MAAM,kBAAkB,IAAI,aAAa,UAAU,KAAK;AAExD,KAAI,CAAC,kBAAkB,kBAAkB,IAAI,IAAI,OAAO,CACtD,QAAO,CAAC,aAAa,IAAI,CAAC;AAE5B,KAAI,CAAC,eACH,QAAO,CACL,aAAa,KAAK,QAAQ,EAC1B;EACE,IAAI,IAAI;EACR,MAAM;EACN,SAAS;EACT,QAAQ,IAAI;EACZ,aAAa,EAAE;EACf,8BAAc,IAAI,KAAK;EACxB,CACF;AAEH,QAAO,CAAC,aAAa,KAAK,QAAQ,EAAE,kBAAkB,IAAI,CAAC;;;;;;AAO7D,SAAS,sBACP,UACA,OACA,QACA,SAO6B;CAC7B,MAAM,SAAS,IAAI,gBAAgB,EACjC,WAAW,OAAO,YAAY,EAC/B,CAAC;AACF,KAAI,QAAQ,UACV,QAAO,IAAI,aAAa,QAAQ,UAAU;CAG5C,MAAM,QAA4B,EAAE;AACpC,QAAO,WAAW;EAChB,KAAK,GAAG,SAAS,GAAG,mBAAmB,MAAM,CAAC,iBAAiB,mBAAmB,OAAO,CAAC,GAAG;EAC7F,QAAQ,QAAQ;EAChB,WAAW,OAAO,YAAY;AAC5B,OAAI;IACF,MAAM,QAAQ,KAAK,MAAM,QAAQ,KAAK;AACtC,YAAQ,MAAM,MAAd;KACE,KAAK;AACH,YAAM,KAAK,GAAG,qBAAqB,MAAM,QAAQ,CAAC;AAClD;KACF,KAAK;AACH,WAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;OAC1C,MAAM,OAAO,MAAM;AACnB,WACE,KAAK,YAAY,MACd,MAAM,EAAE,iBAAiB,MAAM,aACjC,EACD;AACA,aAAK,aAAa,IAAI,MAAM,cAAc,MAAM,KAAK;AACrD;;;AAGJ;KACF,KAAK;AACH,cAAQ,mBAAmB,MAAM,cAAc;AAC/C;KACF,KAAK;AACH,cAAQ,UAAU,MAAM,MAAM;AAC9B;;WAEE;;EAIV,UAAU,QAAQ,QAAQ,oBAAoB,IAAI;EACnD,CAAC,CAAC,WAAW,MAAM;;;AAItB,MAAM,4BAA4B;;;;;;;;;;AAWlC,SAAgB,aAAa,SAAkD;CAC7E,MAAM,EACJ,OACA,WAAW,cACX,eAAe,MACf,eAAe,qBACb;CAEJ,MAAM,CAAC,UAAU,eAAe,SAA6B,EAAE,CAAC;CAChE,MAAM,CAAC,QAAQ,aAAa,SAA0B,OAAO;CAC7D,MAAM,CAAC,gBAAgB,qBAAqB,SAAwB,KAAK;CACzE,MAAM,CAAC,OAAO,YAAY,SAAwB,KAAK;CACvD,MAAM,CAAC,eAAe,oBAAoB,SAAwB,KAAK;CAEvE,MAAM,kBAAkB,kBAAkB;CAC1C,MAAM,yBAAyB,WAAW;CAE1C,MAAM,qBAAqB,OAA+B,KAAK;CAC/D,MAAM,qBAAqB,OAA+B,KAAK;CAC/D,MAAM,oBAAoB,OAAsB,KAAK;CACrD,MAAM,mBAAmB,OAAsB,KAAK;CACpD,MAAM,oBAAoB,OAAO,MAAM;CACvC,MAAM,wBAAwB,aACtB,GACP;AAED,iBAAgB;AACd,oBAAkB,UAAU;AAC5B,mBAAiB,UAAU;IAC1B,CAAC,gBAAgB,cAAc,CAAC;AA4EnC,uBAAsB,UAzEK,aACxB,UAA4B;AAC3B,UAAQ,MAAM,MAAd;GACE,KAAK;AACH,sBAAkB,MAAM,eAAe;AACvC,QAAI,aACF,aAAY,cAAc,MAAM,eAAe;AAEjD;GAGF,KAAK;AACH,iBAAa,SAAS;KACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,SAAI,MAAM,SAAS,YACjB,QAAO,CAAC,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;MAAE,GAAG;MAAM,QAAQ,MAAM;MAAQ,CAAC;AAElE,YAAO;MACP;AACF;GAGF,KAAK,kBAAkB;IACrB,MAAM,OAAO,kBAAkB,MAAM,QAAQ;AAC7C,iBAAa,SAAS;KACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,SAAI,CAAC,QAAQ,KAAK,SAAS,YAAa,QAAO;AAE/C,SAAI,KAAK,OAAO,MAAM,QAAQ,aAAa,KAAK,OAAO,GACrD,QAAO,CAAC,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE,KAAK;AAGrC,YAAO;MACP;AACF;;GAGF,KAAK;AACH,iBAAa,SAAS;AAEpB,UAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK;MACzC,MAAM,MAAM,KAAK;AACjB,UACE,IAAI,YAAY,MACb,MAAM,EAAE,iBAAiB,MAAM,aACjC,EACD;OACA,MAAM,UAAU,KAAK,OAAO;AAC5B,eAAQ,KAAK;QACX,GAAG;QACH,cAAc,IAAI,IAAI,IAAI,aAAa,CAAC,IACtC,MAAM,cACN,MAAM,KACP;QACF;AACD,cAAO;;;AAGX,YAAO;MACP;AACF;GAGF,KAAK;AACH,aAAS,MAAM,MAAM;AACrB,cAAU,QAAQ;AAClB;;IAIN,CAAC,cAAc,aAAa,CAC7B;CAID,MAAM,cAAc,aACjB,YAAoB;EACnB,MAAM,UAAU,QAAQ,MAAM;AAC9B,MAAI,CAAC,QAAS;AAEd,qBAAmB,SAAS,OAAO;AACnC,qBAAmB,SAAS,OAAO;AACnC,WAAS,KAAK;AACd,YAAU,YAAY;EAEtB,MAAM,cAAgC;GACpC,IAAI,OAAO,YAAY;GACvB,MAAM;GACN,SAAS;GACT,QAAQ;GACR,aAAa,EAAE;GACf,8BAAc,IAAI,KAAK;GACxB;EAED,MAAM,uBAAyC;GAC7C,IAAI;GACJ,MAAM;GACN,SAAS;GACT,QAAQ;GACR,aAAa,EAAE;GACf,8BAAc,IAAI,KAAK;GACxB;AAED,eAAa,SAAS;GAAC,GAAG;GAAM;GAAa;GAAqB,CAAC;EAEnE,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,qBAAmB,UAAU;EAE7B,MAAM,YAAY,OAAO,YAAY;AAErC,aAAW;GACT,KAAK,GAAG,SAAS,GAAG,mBAAmB,MAAM,CAAC,sBAAsB,mBAAmB,UAAU;GACjG,SAAS;IACP,SAAS;IACT,gBAAgB,kBAAkB,WAAW;IAC9C;GACD,QAAQ,gBAAgB;GACxB,WAAW,OAAO,YAAY;AAC5B,QAAI;AACF,2BAAsB,QACpB,KAAK,MAAM,QAAQ,KAAK,CACzB;YACK;;GAIV,UAAU,QAAQ;AAChB,QAAI,gBAAgB,OAAO,QAAS;AACpC,aACE,eAAe,QACX,IAAI,UACJ,sCACL;AACD,cAAU,QAAQ;AAClB,iBAAa,SAAS;KACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,YAAO,MAAM,SAAS,eAAe,KAAK,OAAO,KAC7C,KAAK,MAAM,GAAG,GAAG,GACjB;MACJ;;GAEL,CAAC,CACC,WAAW;AACV,OAAI,CAAC,gBAAgB,OAAO,QAC1B,YAAW,SAAU,SAAS,UAAU,UAAU,OAAQ;IAE5D,CACD,YAAY;AACX,OAAI,gBAAgB,OAAO,QAAS;AACpC,YAAS,sCAAsC;AAC/C,aAAU,QAAQ;IAClB;IAEN,CAAC,OAAO,SAAS,CAClB;;CAGD,MAAM,YAAY,aAEd,eACA,QACA,YACG;AACH,gBAAc,SAAS,OAAO;EAC9B,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,gBAAc,UAAU;AAqBxB,SAAO;GAAE,SAnBO,sBAAsB,UAAU,OAAO,QAAQ;IAC7D,WAAW,SAAS;IACpB,QAAQ,gBAAgB;IACxB,kBAAkB;IAClB,UAAU,QAAQ;AAChB,cAAS,IAAI;AACb,eAAU,QAAQ;;IAEpB,oBAAoB,QAAQ;AAC1B,SAAI,gBAAgB,OAAO,QAAS;AACpC,cACE,eAAe,QACX,IAAI,UACH,SAAS,gBAAgB,2BAC/B;AACD,eAAU,QAAQ;;IAErB,CAAC;GAEgB;GAAiB;IAErC,CAAC,OAAO,SAAS,CAClB;CAED,MAAM,qBAAqB,aAEvB,QACA,WACA,0BACG;AACH,YAAU,YAAY;EAEtB,MAAM,YAAY,OAAO,YAAY;AAKrC,aAAW;GACT,KAJA,GAAG,SAAS,GAAG,mBAAmB,MAAM,CAAC,iBAAiB,mBAAmB,OAAO,aACvE,mBAAmB,UAAU,CAAC,aAAa,mBAAmB,UAAU;GAIrF,QAAQ,sBAAsB;GAC9B,WAAW,OAAO,YAAY;AAC5B,QAAI;AACF,2BAAsB,QACpB,KAAK,MAAM,QAAQ,KAAK,CACzB;YACK;;GAIV,UAAU,QAAQ;AAChB,QAAI,sBAAsB,OAAO,QAAS;AAC1C,aACE,eAAe,QACX,IAAI,UACJ,kCACL;AACD,cAAU,QAAQ;;GAErB,CAAC,CACC,WAAW;AACV,OAAI,CAAC,sBAAsB,OAAO,QAChC,YAAW,SAAU,SAAS,UAAU,UAAU,OAAQ;IAE5D,CACD,YAAY;AACX,OAAI,sBAAsB,OAAO,QAAS;AAC1C,YAAS,kCAAkC;AAC3C,aAAU,QAAQ;IAClB;IAEN,CAAC,OAAO,SAAS,CAClB;CAED,MAAM,cAAc,aACjB,WAAmB;AAClB,qBAAmB,SAAS,OAAO;AACnC,YAAU,kBAAkB;AAC5B,WAAS,KAAK;AACd,cAAY,EAAE,CAAC;AACf,oBAAkB,OAAO;EAEzB,MAAM,EAAE,SAAS,oBAAoB,UACnC,oBACA,QACA,EAAE,cAAc,wCAAwC,CACzD;AACD,UAAQ,MAAM,UAAU;AACtB,OAAI,gBAAgB,OAAO,QAAS;AACpC,eAAY,MAAM;GAElB,MAAM,WAAW,MAAM,MAAM,SAAS;AACtC,OACE,UAAU,SAAS,eACnB,CAAC,kBAAkB,IAAI,SAAS,OAAO,CAEvC,oBAAmB,QAAQ,SAAS,IAAI,gBAAgB;OAExD,YAAW,SAAU,SAAS,UAAU,UAAU,OAAQ;IAE5D;IAEJ,CAAC,WAAW,mBAAmB,CAChC;CAED,MAAM,oBAAoB,kBAAkB;AAC1C,MACE,CAAC,iBAAiB,WAClB,CAAC,kBAAkB,WACnB,kBAAkB,QAElB;AAEF,oBAAkB,UAAU;AAC5B,YAAU,gBAAgB;AAC1B,WAAS,KAAK;EAEd,MAAM,YAAY,KAAK,KAAK;EAC5B,MAAM,EAAE,SAAS,oBAAoB,UACnC,oBACA,kBAAkB,SAClB;GACE,WAAW,iBAAiB;GAC5B,cAAc;GACf,CACF;AACD,UACG,KAAK,OAAO,UAAU;AACrB,OAAI,gBAAgB,OAAO,QAAS;GACpC,MAAM,UAAU,KAAK,KAAK,GAAG;AAC7B,OAAI,UAAU,0BACZ,OAAM,IAAI,SAAS,MACjB,WAAW,GAAG,4BAA4B,QAAQ,CACnD;AAEH,OAAI,gBAAgB,OAAO,QAAS;AACpC,OAAI,MAAM,SAAS,EACjB,cAAa,SAAS,CAAC,GAAG,OAAO,GAAG,KAAK,CAAC;AAE5C,cAAW,YACT,YAAY,kBAAkB,SAAS,QACxC;IACD,CACD,cAAc;AACb,qBAAkB,UAAU;IAC5B;IACH,CAAC,UAAU,CAAC;CAEf,MAAM,QAAQ,kBAAkB;AAC9B,qBAAmB,SAAS,OAAO;AACnC,qBAAmB,SAAS,OAAO;AACnC,cAAY,EAAE,CAAC;AACf,oBAAkB,KAAK;AACvB,WAAS,KAAK;AACd,YAAU,OAAO;AACjB,mBAAiB,KAAK;AACtB,MAAI,aACF,gBAAe,aAAa;IAE7B,CAAC,cAAc,aAAa,CAAC;AAEhC,iBAAgB;AACd,MAAI,CAAC,aAAc;EACnB,MAAM,aAAa,YAAY,aAAa;AAC5C,MAAI,WACF,aAAY,WAAW;AAEzB,eAAa;AACX,sBAAmB,SAAS,OAAO;AACnC,sBAAmB,SAAS,OAAO;;IAEpC;EAAC;EAAc;EAAc;EAAY,CAAC;AAE7C,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
@@ -1,3 +1,4 @@
1
1
  import { UseChartDataOptions, UseChartDataResult, useChartData } from "./use-chart-data.js";
2
2
  import { AnalyticsFormat, InferResultByFormat, InferRowType, PluginRegistry, QueryRegistry, TypedArrowTable, UseAnalyticsQueryOptions, UseAnalyticsQueryResult } from "./types.js";
3
- import { useAnalyticsQuery } from "./use-analytics-query.js";
3
+ import { useAnalyticsQuery } from "./use-analytics-query.js";
4
+ import { usePluginClientConfig } from "./use-plugin-config.js";
@@ -1,2 +1,3 @@
1
1
  import { useAnalyticsQuery } from "./use-analytics-query.js";
2
2
  import { useChartData } from "./use-chart-data.js";
3
+ import { usePluginClientConfig } from "./use-plugin-config.js";