@databricks/appkit-ui 0.61.0 → 0.62.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 (117) hide show
  1. package/CLAUDE.md +1 -0
  2. package/NOTICE.md +1 -0
  3. package/dist/cli/commands/codemod/index.js.map +1 -1
  4. package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
  5. package/dist/cli/commands/docs.js.map +1 -1
  6. package/dist/cli/commands/doctor/bundle.js.map +1 -1
  7. package/dist/cli/commands/doctor/index.js.map +1 -1
  8. package/dist/cli/commands/doctor/report.js.map +1 -1
  9. package/dist/cli/commands/doctor/resolve-targets.js +4 -6
  10. package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
  11. package/dist/cli/commands/doctor/run.js.map +1 -1
  12. package/dist/cli/commands/generate-types.js.map +1 -1
  13. package/dist/cli/commands/lint.js.map +1 -1
  14. package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
  15. package/dist/cli/commands/plugin/create/create.js.map +1 -1
  16. package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
  17. package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
  18. package/dist/cli/commands/plugin/index.js.map +1 -1
  19. package/dist/cli/commands/plugin/list/list.js.map +1 -1
  20. package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
  21. package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
  22. package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
  23. package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
  24. package/dist/cli/commands/registry/add.js.map +1 -1
  25. package/dist/cli/commands/registry/client.js.map +1 -1
  26. package/dist/cli/commands/registry/config-writer.js.map +1 -1
  27. package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
  28. package/dist/cli/commands/registry/env-writer.js.map +1 -1
  29. package/dist/cli/commands/registry/index.js.map +1 -1
  30. package/dist/cli/commands/registry/info.js.map +1 -1
  31. package/dist/cli/commands/registry/list.js.map +1 -1
  32. package/dist/cli/commands/registry/requirements.js.map +1 -1
  33. package/dist/cli/commands/registry/server-register.js.map +1 -1
  34. package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
  35. package/dist/cli/commands/setup.js.map +1 -1
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/js/arrow/arrow-client.d.ts.map +1 -1
  38. package/dist/js/arrow/arrow-client.js.map +1 -1
  39. package/dist/react/charts/area/index.d.ts.map +1 -1
  40. package/dist/react/charts/area/index.js.map +1 -1
  41. package/dist/react/charts/bar/index.d.ts.map +1 -1
  42. package/dist/react/charts/bar/index.js.map +1 -1
  43. package/dist/react/charts/base.d.ts.map +1 -1
  44. package/dist/react/charts/base.js +1 -1
  45. package/dist/react/charts/base.js.map +1 -1
  46. package/dist/react/charts/heatmap/index.d.ts.map +1 -1
  47. package/dist/react/charts/heatmap/index.js.map +1 -1
  48. package/dist/react/charts/line/index.d.ts.map +1 -1
  49. package/dist/react/charts/line/index.js.map +1 -1
  50. package/dist/react/charts/normalize.d.ts.map +1 -1
  51. package/dist/react/charts/normalize.js.map +1 -1
  52. package/dist/react/charts/pie/index.d.ts.map +1 -1
  53. package/dist/react/charts/pie/index.js.map +1 -1
  54. package/dist/react/charts/radar/index.d.ts.map +1 -1
  55. package/dist/react/charts/radar/index.js.map +1 -1
  56. package/dist/react/charts/scatter/index.d.ts.map +1 -1
  57. package/dist/react/charts/scatter/index.js.map +1 -1
  58. package/dist/react/charts/theme.d.ts.map +1 -1
  59. package/dist/react/charts/theme.js.map +1 -1
  60. package/dist/react/charts/wrapper.d.ts.map +1 -1
  61. package/dist/react/charts/wrapper.js.map +1 -1
  62. package/dist/react/file-browser/directory-list.d.ts.map +1 -1
  63. package/dist/react/file-browser/directory-list.js.map +1 -1
  64. package/dist/react/file-browser/file-breadcrumb.d.ts.map +1 -1
  65. package/dist/react/file-browser/file-breadcrumb.js.map +1 -1
  66. package/dist/react/file-browser/file-entry.d.ts.map +1 -1
  67. package/dist/react/file-browser/file-entry.js.map +1 -1
  68. package/dist/react/file-browser/file-preview-panel.d.ts.map +1 -1
  69. package/dist/react/file-browser/file-preview-panel.js.map +1 -1
  70. package/dist/react/file-browser/new-folder-input.d.ts.map +1 -1
  71. package/dist/react/file-browser/new-folder-input.js.map +1 -1
  72. package/dist/react/genie/genie-chat-input.d.ts.map +1 -1
  73. package/dist/react/genie/genie-chat-input.js.map +1 -1
  74. package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
  75. package/dist/react/genie/genie-chat-message-list.js.map +1 -1
  76. package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
  77. package/dist/react/genie/genie-chat-message.js.map +1 -1
  78. package/dist/react/genie/genie-query-visualization.d.ts.map +1 -1
  79. package/dist/react/genie/genie-query-visualization.js.map +1 -1
  80. package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
  81. package/dist/react/genie/use-genie-chat.js.map +1 -1
  82. package/dist/react/hooks/types.d.ts.map +1 -1
  83. package/dist/react/hooks/use-agent-chat.d.ts.map +1 -1
  84. package/dist/react/hooks/use-agent-chat.js.map +1 -1
  85. package/dist/react/hooks/use-ai-search-query.d.ts.map +1 -1
  86. package/dist/react/hooks/use-ai-search-query.js.map +1 -1
  87. package/dist/react/hooks/use-analytics-query.d.ts.map +1 -1
  88. package/dist/react/hooks/use-analytics-query.js.map +1 -1
  89. package/dist/react/hooks/use-analytics-warehouse-status.js.map +1 -1
  90. package/dist/react/hooks/use-chart-data.d.ts.map +1 -1
  91. package/dist/react/hooks/use-chart-data.js.map +1 -1
  92. package/dist/react/hooks/use-metric-view.d.ts.map +1 -1
  93. package/dist/react/hooks/use-metric-view.js.map +1 -1
  94. package/dist/react/hooks/use-plugin-config.d.ts.map +1 -1
  95. package/dist/react/hooks/use-plugin-config.js.map +1 -1
  96. package/dist/react/hooks/use-serving-invoke.d.ts.map +1 -1
  97. package/dist/react/hooks/use-serving-invoke.js.map +1 -1
  98. package/dist/react/hooks/use-serving-stream.d.ts.map +1 -1
  99. package/dist/react/hooks/use-serving-stream.js.map +1 -1
  100. package/dist/react/resource-status-indicator.d.ts.map +1 -1
  101. package/dist/react/resource-status-indicator.js.map +1 -1
  102. package/dist/react/table/data-table.d.ts.map +1 -1
  103. package/dist/react/table/data-table.js.map +1 -1
  104. package/dist/react/table/table-wrapper.js.map +1 -1
  105. package/dist/react/ui-variants/variants.d.ts.map +1 -1
  106. package/dist/react/ui-variants/variants.js.map +1 -1
  107. package/dist/schemas/manifest.d.ts.map +1 -1
  108. package/dist/schemas/manifest.js.map +1 -1
  109. package/dist/schemas/metric-fqn.js.map +1 -1
  110. package/dist/shared/src/plugin.d.ts.map +1 -1
  111. package/dist/styles.css +0 -3
  112. package/dist/workspace-client/legacy.js.map +1 -1
  113. package/dist/workspace-client/types.d.ts.map +1 -1
  114. package/docs/plugins/testing.md +245 -0
  115. package/llms.txt +1 -0
  116. package/package.json +1 -1
  117. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"new-folder-input.js","names":[],"sources":["../../../src/react/file-browser/new-folder-input.tsx"],"sourcesContent":["import { FolderPlus, Loader2, X } from \"lucide-react\";\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport type { FileBrowserLabels } from \"./types\";\n\n/** Props for the NewFolderInput component */\nexport interface NewFolderInputProps\n extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\"> {\n /** Current folder name value */\n value: string;\n /** Called when folder name changes */\n onChange: (value: string) => void;\n /** Called when the user confirms creation */\n onCreate: () => void;\n /** Called when the user cancels */\n onCancel: () => void;\n /** Whether folder creation is in progress */\n creating?: boolean;\n /** Auto-focus the input on mount */\n autoFocus?: boolean;\n /** Customizable labels */\n labels?: Pick<FileBrowserLabels, \"create\" | \"folderNamePlaceholder\">;\n}\n\n/** Inline folder-name input with create/cancel actions */\nexport function NewFolderInput({\n value,\n onChange,\n onCreate,\n onCancel,\n creating,\n autoFocus = true,\n labels,\n className,\n ...props\n}: NewFolderInputProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (autoFocus) {\n inputRef.current?.focus();\n }\n }, [autoFocus]);\n\n return (\n <div\n data-slot=\"new-folder-input\"\n className={cn(\n \"flex items-center gap-2 px-4 py-3 border-b bg-muted/30\",\n className,\n )}\n {...props}\n >\n <FolderPlus className=\"h-5 w-5 text-blue-500 shrink-0\" />\n <input\n ref={inputRef}\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") onCreate();\n if (e.key === \"Escape\") onCancel();\n }}\n placeholder={labels?.folderNamePlaceholder ?? \"Folder name\"}\n className=\"flex-1 text-sm bg-background border rounded px-2 py-1 outline-none focus:ring-1 focus:ring-ring\"\n disabled={creating}\n />\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={creating || !value.trim()}\n onClick={onCreate}\n >\n {creating ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n (labels?.create ?? \"Create\")\n )}\n </Button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA0BA,SAAgB,eAAe,EAC7B,OACA,UACA,UACA,UACA,UACA,YAAY,MACZ,QACA,WACA,GAAG,SACmB;CACtB,MAAM,WAAW,OAAyB,KAAK;AAE/C,iBAAgB;AACd,MAAI,UACF,UAAS,SAAS,OAAO;IAE1B,CAAC,UAAU,CAAC;AAEf,QACE,qBAAC;EACC,aAAU;EACV,WAAW,GACT,0DACA,UACD;EACD,GAAI;;GAEJ,oBAAC,cAAW,WAAU,mCAAmC;GACzD,oBAAC;IACC,KAAK;IACL,MAAK;IACE;IACP,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM;IACzC,YAAY,MAAM;AAChB,SAAI,EAAE,QAAQ,QAAS,WAAU;AACjC,SAAI,EAAE,QAAQ,SAAU,WAAU;;IAEpC,aAAa,QAAQ,yBAAyB;IAC9C,WAAU;IACV,UAAU;KACV;GACF,oBAAC;IACC,SAAQ;IACR,MAAK;IACL,UAAU,YAAY,CAAC,MAAM,MAAM;IACnC,SAAS;cAER,WACC,oBAAC,WAAQ,WAAU,yBAAyB,GAE3C,QAAQ,UAAU;KAEd;GACT,oBAAC;IACC,MAAK;IACL,SAAS;IACT,WAAU;cAEV,oBAAC,KAAE,WAAU,YAAY;KAClB;;GACL"}
1
+ {"version":3,"file":"new-folder-input.js","names":[],"sources":["../../../src/react/file-browser/new-folder-input.tsx"],"sourcesContent":["import { FolderPlus, Loader2, X } from \"lucide-react\";\nimport { useEffect, useRef } from \"react\";\n\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport type { FileBrowserLabels } from \"./types\";\n\n/** Props for the NewFolderInput component */\nexport interface NewFolderInputProps extends Omit<\n React.ComponentProps<\"div\">,\n \"children\" | \"onChange\"\n> {\n /** Current folder name value */\n value: string;\n /** Called when folder name changes */\n onChange: (value: string) => void;\n /** Called when the user confirms creation */\n onCreate: () => void;\n /** Called when the user cancels */\n onCancel: () => void;\n /** Whether folder creation is in progress */\n creating?: boolean;\n /** Auto-focus the input on mount */\n autoFocus?: boolean;\n /** Customizable labels */\n labels?: Pick<FileBrowserLabels, \"create\" | \"folderNamePlaceholder\">;\n}\n\n/** Inline folder-name input with create/cancel actions */\nexport function NewFolderInput({\n value,\n onChange,\n onCreate,\n onCancel,\n creating,\n autoFocus = true,\n labels,\n className,\n ...props\n}: NewFolderInputProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (autoFocus) {\n inputRef.current?.focus();\n }\n }, [autoFocus]);\n\n return (\n <div\n data-slot=\"new-folder-input\"\n className={cn(\n \"flex items-center gap-2 px-4 py-3 border-b bg-muted/30\",\n className,\n )}\n {...props}\n >\n <FolderPlus className=\"h-5 w-5 text-blue-500 shrink-0\" />\n <input\n ref={inputRef}\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") onCreate();\n if (e.key === \"Escape\") onCancel();\n }}\n placeholder={labels?.folderNamePlaceholder ?? \"Folder name\"}\n className=\"flex-1 text-sm bg-background border rounded px-2 py-1 outline-none focus:ring-1 focus:ring-ring\"\n disabled={creating}\n />\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={creating || !value.trim()}\n onClick={onCreate}\n >\n {creating ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n (labels?.create ?? \"Create\")\n )}\n </Button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA6BA,SAAgB,eAAe,EAC7B,OACA,UACA,UACA,UACA,UACA,YAAY,MACZ,QACA,WACA,GAAG,SACmB;CACtB,MAAM,WAAW,OAAyB,KAAK;AAE/C,iBAAgB;AACd,MAAI,UACF,UAAS,SAAS,OAAO;IAE1B,CAAC,UAAU,CAAC;AAEf,QACE,qBAAC;EACC,aAAU;EACV,WAAW,GACT,0DACA,UACD;EACD,GAAI;;GAEJ,oBAAC,cAAW,WAAU,mCAAmC;GACzD,oBAAC;IACC,KAAK;IACL,MAAK;IACE;IACP,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM;IACzC,YAAY,MAAM;AAChB,SAAI,EAAE,QAAQ,QAAS,WAAU;AACjC,SAAI,EAAE,QAAQ,SAAU,WAAU;;IAEpC,aAAa,QAAQ,yBAAyB;IAC9C,WAAU;IACV,UAAU;KACV;GACF,oBAAC;IACC,SAAQ;IACR,MAAK;IACL,UAAU,YAAY,CAAC,MAAM,MAAM;IACnC,SAAS;cAER,WACC,oBAAC,WAAQ,WAAU,yBAAyB,GAE3C,QAAQ,UAAU;KAEd;GACT,oBAAC;IACC,MAAK;IACL,SAAS;IACT,WAAU;cAEV,oBAAC,KAAE,WAAU,YAAY;KAClB;;GACL"}
@@ -1 +1 @@
1
- {"version":3,"file":"genie-chat-input.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"mappings":";;;UAIU,mBAAA;;EAER,MAAA,GAAS,OAAA;EAFD;EAIR,QAAA;;EAEA,WAAA;EAJA;EAMA,SAAA;AAAA;;iBAIc,cAAA,CAAA;EACd,MAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,mBAAA,GAAmB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"genie-chat-input.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"mappings":";;;UAKU,mBAAA;;EAER,MAAA,GAAS,OAAA;EAFD;EAIR,QAAA;;EAEA,WAAA;EAJA;EAMA,SAAA;AAAA;;iBAIc,cAAA,CAAA;EACd,MAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,mBAAA,GAAmB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"genie-chat-input.js","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"sourcesContent":["import { type KeyboardEvent, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\n\ninterface GenieChatInputProps {\n /** Callback fired when the user submits a message */\n onSend: (content: string) => void;\n /** Disable the input and send button */\n disabled?: boolean;\n /** Placeholder text shown in the textarea */\n placeholder?: string;\n /** Additional CSS class for the container */\n className?: string;\n}\n\n/** Auto-expanding textarea input with a send button for chat messages. Submits on Enter (Shift+Enter for newline). */\nexport function GenieChatInput({\n onSend,\n disabled = false,\n placeholder = \"Ask a question...\",\n className,\n}: GenieChatInputProps) {\n const [value, setValue] = useState(\"\");\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleSubmit = () => {\n const trimmed = value.trim();\n if (!trimmed || disabled) return;\n onSend(trimmed);\n setValue(\"\");\n if (textareaRef.current) {\n textareaRef.current.style.height = \"auto\";\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n const MAX_HEIGHT = 200;\n\n const handleInput = () => {\n const textarea = textareaRef.current;\n if (textarea) {\n textarea.style.height = \"auto\";\n const clamped = Math.min(textarea.scrollHeight, MAX_HEIGHT);\n textarea.style.height = `${clamped}px`;\n textarea.style.overflowY =\n textarea.scrollHeight > MAX_HEIGHT ? \"auto\" : \"hidden\";\n }\n };\n\n return (\n <div className={cn(\"flex gap-2 p-4 border-t shrink-0\", className)}>\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n disabled={disabled}\n rows={1}\n className={cn(\n \"flex-1 resize-none overflow-hidden rounded-md border border-input bg-background px-3 py-2\",\n \"text-sm placeholder:text-muted-foreground\",\n \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n )}\n />\n <Button\n onClick={handleSubmit}\n disabled={disabled || !value.trim()}\n size=\"default\"\n className=\"self-end\"\n >\n Send\n </Button>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,eAAe,EAC7B,QACA,WAAW,OACX,cAAc,qBACd,aACsB;CACtB,MAAM,CAAC,OAAO,YAAY,SAAS,GAAG;CACtC,MAAM,cAAc,OAA4B,KAAK;CAErD,MAAM,qBAAqB;EACzB,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,WAAW,SAAU;AAC1B,SAAO,QAAQ;AACf,WAAS,GAAG;AACZ,MAAI,YAAY,QACd,aAAY,QAAQ,MAAM,SAAS;;CAIvC,MAAM,iBAAiB,MAA0C;AAC/D,MAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,KAAE,gBAAgB;AAClB,iBAAc;;;CAIlB,MAAM,aAAa;CAEnB,MAAM,oBAAoB;EACxB,MAAM,WAAW,YAAY;AAC7B,MAAI,UAAU;AACZ,YAAS,MAAM,SAAS;GACxB,MAAM,UAAU,KAAK,IAAI,SAAS,cAAc,WAAW;AAC3D,YAAS,MAAM,SAAS,GAAG,QAAQ;AACnC,YAAS,MAAM,YACb,SAAS,eAAe,aAAa,SAAS;;;AAIpD,QACE,qBAAC;EAAI,WAAW,GAAG,oCAAoC,UAAU;aAC/D,oBAAC;GACC,KAAK;GACE;GACP,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM;GACzC,WAAW;GACX,SAAS;GACI;GACH;GACV,MAAM;GACN,WAAW,GACT,6FACA,6CACA,2EACA,kDACD;IACD,EACF,oBAAC;GACC,SAAS;GACT,UAAU,YAAY,CAAC,MAAM,MAAM;GACnC,MAAK;GACL,WAAU;aACX;IAEQ;GACL"}
1
+ {"version":3,"file":"genie-chat-input.js","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"sourcesContent":["import { type KeyboardEvent, useRef, useState } from \"react\";\n\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\n\ninterface GenieChatInputProps {\n /** Callback fired when the user submits a message */\n onSend: (content: string) => void;\n /** Disable the input and send button */\n disabled?: boolean;\n /** Placeholder text shown in the textarea */\n placeholder?: string;\n /** Additional CSS class for the container */\n className?: string;\n}\n\n/** Auto-expanding textarea input with a send button for chat messages. Submits on Enter (Shift+Enter for newline). */\nexport function GenieChatInput({\n onSend,\n disabled = false,\n placeholder = \"Ask a question...\",\n className,\n}: GenieChatInputProps) {\n const [value, setValue] = useState(\"\");\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleSubmit = () => {\n const trimmed = value.trim();\n if (!trimmed || disabled) return;\n onSend(trimmed);\n setValue(\"\");\n if (textareaRef.current) {\n textareaRef.current.style.height = \"auto\";\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n const MAX_HEIGHT = 200;\n\n const handleInput = () => {\n const textarea = textareaRef.current;\n if (textarea) {\n textarea.style.height = \"auto\";\n const clamped = Math.min(textarea.scrollHeight, MAX_HEIGHT);\n textarea.style.height = `${clamped}px`;\n textarea.style.overflowY =\n textarea.scrollHeight > MAX_HEIGHT ? \"auto\" : \"hidden\";\n }\n };\n\n return (\n <div className={cn(\"flex gap-2 p-4 border-t shrink-0\", className)}>\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n disabled={disabled}\n rows={1}\n className={cn(\n \"flex-1 resize-none overflow-hidden rounded-md border border-input bg-background px-3 py-2\",\n \"text-sm placeholder:text-muted-foreground\",\n \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n )}\n />\n <Button\n onClick={handleSubmit}\n disabled={disabled || !value.trim()}\n size=\"default\"\n className=\"self-end\"\n >\n Send\n </Button>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAiBA,SAAgB,eAAe,EAC7B,QACA,WAAW,OACX,cAAc,qBACd,aACsB;CACtB,MAAM,CAAC,OAAO,YAAY,SAAS,GAAG;CACtC,MAAM,cAAc,OAA4B,KAAK;CAErD,MAAM,qBAAqB;EACzB,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,WAAW,SAAU;AAC1B,SAAO,QAAQ;AACf,WAAS,GAAG;AACZ,MAAI,YAAY,QACd,aAAY,QAAQ,MAAM,SAAS;;CAIvC,MAAM,iBAAiB,MAA0C;AAC/D,MAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,KAAE,gBAAgB;AAClB,iBAAc;;;CAIlB,MAAM,aAAa;CAEnB,MAAM,oBAAoB;EACxB,MAAM,WAAW,YAAY;AAC7B,MAAI,UAAU;AACZ,YAAS,MAAM,SAAS;GACxB,MAAM,UAAU,KAAK,IAAI,SAAS,cAAc,WAAW;AAC3D,YAAS,MAAM,SAAS,GAAG,QAAQ;AACnC,YAAS,MAAM,YACb,SAAS,eAAe,aAAa,SAAS;;;AAIpD,QACE,qBAAC;EAAI,WAAW,GAAG,oCAAoC,UAAU;aAC/D,oBAAC;GACC,KAAK;GACE;GACP,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM;GACzC,WAAW;GACX,SAAS;GACI;GACH;GACV,MAAM;GACN,WAAW,GACT,6FACA,6CACA,2EACA,kDACD;IACD,EACF,oBAAC;GACC,SAAS;GACT,UAAU,YAAY,CAAC,MAAM,MAAM;GACnC,MAAK;GACL,WAAU;aACX;IAEQ;GACL"}
@@ -1 +1 @@
1
- {"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"mappings":";;;;UAYU,yBAAA;;EAER,QAAA,EAAU,gBAAA;EAFF;EAIR,MAAA,EAAQ,eAAA;;EAER,SAAA;EAJA;EAMA,eAAA;EAJA;EAMA,mBAAA;AAAA;;iBAkIc,oBAAA,CAAA;EACd,QAAA;EACA,MAAA;EACA,SAAA;EACA,eAAA;EACA;AAAA,GACC,yBAAA,GAAyB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"mappings":";;;;UAaU,yBAAA;;EAER,QAAA,EAAU,gBAAA;EAFF;EAIR,MAAA,EAAQ,eAAA;;EAER,SAAA;EAJA;EAMA,eAAA;EAJA;EAMA,mBAAA;AAAA;;iBAiIc,oBAAA,CAAA;EACd,QAAA;EACA,MAAA;EACA,SAAA;EACA,eAAA;EACA;AAAA,GACC,yBAAA,GAAyB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1 +1 @@
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 {\n type GenieChatStatus,\n type GenieMessageItem,\n TERMINAL_STATUSES,\n} from \"./types\";\n\ninterface 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.content &&\n !TERMINAL_STATUSES.has(lastMessage.status);\n\n return (\n <ScrollArea\n ref={scrollRef}\n className={cn(\n \"flex-1 min-h-0 p-4 [&_[data-slot=scroll-area-viewport]>div]:!block\",\n className,\n )}\n >\n <div className=\"flex flex-col gap-4 min-w-0\">\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) =>\n msg.role !== \"assistant\" ||\n msg.content ||\n (msg.id !== \"\" && TERMINAL_STATUSES.has(msg.status)),\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":"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"}
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\";\n\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 {\n type GenieChatStatus,\n type GenieMessageItem,\n TERMINAL_STATUSES,\n} from \"./types\";\n\ninterface 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 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.content &&\n !TERMINAL_STATUSES.has(lastMessage.status);\n\n return (\n <ScrollArea\n ref={scrollRef}\n className={cn(\n \"flex-1 min-h-0 p-4 [&_[data-slot=scroll-area-viewport]>div]:!block\",\n className,\n )}\n >\n <div className=\"flex flex-col gap-4 min-w-0\">\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) =>\n msg.role !== \"assistant\" ||\n msg.content ||\n (msg.id !== \"\" && TERMINAL_STATUSES.has(msg.status)),\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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;UA4BU,qBAAA;;EAER,OAAA,EAAS,gBAAA;EAFD;EAIR,SAAA;AAAA;;iBAQc,gBAAA,CAAA;EACd,OAAA;EACA;AAAA,GACC,qBAAA,GAAqB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;UA6BU,qBAAA;;EAER,OAAA,EAAS,gBAAA;EAFD;EAIR,SAAA;AAAA;;iBAQc,gBAAA,CAAA;EACd,OAAA;EACA;AAAA,GACC,qBAAA,GAAqB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1 +1 @@
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
+ {"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\";\n\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":";;;;;;;;;;;;;;;AAeA,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":";;;;;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
+ {"version":3,"file":"genie-query-visualization.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"mappings":";;;;;UA8CU,4BAAA;;EAER,IAAA,EAAM,sBAAA;;EAEN,SAAA;AAAA;;;;;;;;iBAUc,uBAAA,CAAA;EACd,IAAA;EACA;AAAA,GACC,4BAAA,GAA4B,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1 +1 @@
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
+ {"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\";\n\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 <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":";;;;;;;;;;;;;;AAgCA,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,MACrB,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 +1 @@
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
+ {"version":3,"file":"use-genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"mappings":";;;;;AA4KA;;;;;;;iBAAgB,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,kBAAA"}
@@ -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 {\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":"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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\";\n\nimport { connectSSE } from \"@/js\";\n\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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/hooks/types.ts"],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/hooks/types.ts"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-chat.d.ts","names":[],"sources":["../../../src/react/hooks/use-agent-chat.ts"],"mappings":";;AAeA;;;;;;;;;;;UAAiB,cAAA;EACf,IAAA;EACA,KAAA;EACA,OAAA;EACA,IAAA;IACE,IAAA;IACA,EAAA;IACA,OAAA;IACA,IAAA;IACA,SAAA;IACA,MAAA;IACA,MAAA;EAAA;EAEF,OAAA;EACA,IAAA,GAAO,MAAA;EACP,KAAA;EACA,eAAA;EACA,YAAA;EAEA,WAAA;EACA,SAAA;EACA,SAAA;EACA,IAAA;EACA,WAAA,GAAc,MAAA;AAAA;AAAA,UAGC,mBAAA;EAmBG;;;;AAGpB;EAhBE,KAAA;;;;;;EAMA,QAAA;EA2BA;;;;;;EApBA,OAAA,IAAW,KAAA,EAAO,cAAA;AAAA;AAAA,UAGH,kBAAA;EA+DW;EA7D1B,OAAA;EA8DA;;;;;;EAvDA,MAAA,EAAQ,cAAA;EAuDR;;;;;EAjDA,QAAA;EAoDC;EAlDD,WAAA;EAkDyC;EAhDzC,KAAA;;;;;EAKA,IAAA,GAAO,OAAA,aAAoB,OAAA;;;;;;EAM3B,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCc,YAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA;AAAA,GACC,mBAAA,GAAsB,kBAAA"}
1
+ {"version":3,"file":"use-agent-chat.d.ts","names":[],"sources":["../../../src/react/hooks/use-agent-chat.ts"],"mappings":";;AAgBA;;;;;;;;;;;UAAiB,cAAA;EACf,IAAA;EACA,KAAA;EACA,OAAA;EACA,IAAA;IACE,IAAA;IACA,EAAA;IACA,OAAA;IACA,IAAA;IACA,SAAA;IACA,MAAA;IACA,MAAA;EAAA;EAEF,OAAA;EACA,IAAA,GAAO,MAAA;EACP,KAAA;EACA,eAAA;EACA,YAAA;EAEA,WAAA;EACA,SAAA;EACA,SAAA;EACA,IAAA;EACA,WAAA,GAAc,MAAA;AAAA;AAAA,UAGC,mBAAA;EAmBG;;;;AAGpB;EAhBE,KAAA;;;;;;EAMA,QAAA;EA2BA;;;;;;EApBA,OAAA,IAAW,KAAA,EAAO,cAAA;AAAA;AAAA,UAGH,kBAAA;EA+DW;EA7D1B,OAAA;EA8DA;;;;;;EAvDA,MAAA,EAAQ,cAAA;EAuDR;;;;;EAjDA,QAAA;EAoDC;EAlDD,WAAA;EAkDyC;EAhDzC,KAAA;;;;;EAKA,IAAA,GAAO,OAAA,aAAoB,OAAA;;;;;;EAM3B,KAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiCc,YAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA;AAAA,GACC,mBAAA,GAAsB,kBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-agent-chat.js","names":[],"sources":["../../../src/react/hooks/use-agent-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { connectSSE } from \"@/js\";\n\n/**\n * One Responses-API-shaped event yielded by the agents plugin SSE stream.\n *\n * The hook handles the two paths every chat UI needs — accumulating\n * `content` from `response.output_text.delta` and capturing `threadId`\n * from `appkit.metadata` — and surfaces everything else (tool calls,\n * approval gates, status events, etc.) through {@link UseAgentChatOptions.onEvent}.\n *\n * Fields beyond `type` are intentionally loose because the agents plugin\n * forwards adapter-specific shapes verbatim. Treat unknown fields as\n * opaque pass-through.\n */\nexport interface AgentChatEvent {\n type: string;\n delta?: string;\n item_id?: string;\n item?: {\n type?: string;\n id?: string;\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n status?: string;\n };\n content?: string;\n data?: Record<string, unknown>;\n error?: string;\n sequence_number?: number;\n output_index?: number;\n // `appkit.approval_pending` payload\n approval_id?: string;\n stream_id?: string;\n tool_name?: string;\n args?: unknown;\n annotations?: Record<string, unknown>;\n}\n\nexport interface UseAgentChatOptions {\n /**\n * Agent name registered with the `agents()` plugin (e.g. `\"assistant\"`,\n * `\"helper\"`). Send-time payload includes this so the plugin routes the\n * turn to the right `AgentDefinition`.\n */\n agent: string;\n /**\n * Override the chat endpoint. Default `\"/api/agents/chat\"` matches the\n * route the agents plugin mounts under its prefix. Useful when the\n * server mounts under a non-default base path or when proxying.\n */\n endpoint?: string;\n /**\n * Called for every parsed SSE event before any state update. Use this\n * to drive tool-call rows, approval cards, inspectors, or anything\n * beyond the streaming text content. Errors thrown here are swallowed\n * so a buggy handler can't kill the stream.\n */\n onEvent?: (event: AgentChatEvent) => void;\n}\n\nexport interface UseAgentChatResult {\n /** Accumulated assistant text from `response.output_text.delta` events. */\n content: string;\n /**\n * Every parsed event, in order. Provided for components that need to\n * render historical tool calls or replay state after a remount —\n * lighter than re-deriving from message history. For one-off side\n * effects prefer {@link UseAgentChatOptions.onEvent}.\n */\n events: AgentChatEvent[];\n /**\n * Thread id captured from the first `appkit.metadata` event of the\n * stream. Subsequent `send()` calls automatically forward this so the\n * server reuses the same thread.\n */\n threadId: string | null;\n /** True while an SSE stream is open. */\n isStreaming: boolean;\n /** Last error message (cleared on next successful `send()`). */\n error: string | null;\n /**\n * Send a user turn and stream the response. Aborts any in-flight\n * stream. Resolves when the stream completes (success or error).\n */\n send: (message: string) => Promise<void>;\n /**\n * Discard accumulated content, events, and threadId. Aborts any\n * in-flight stream. Use when switching agents or starting a fresh\n * conversation.\n */\n reset: () => void;\n}\n\n/**\n * React hook for chatting with an agent registered via the `agents()`\n * plugin. Wraps {@link connectSSE} (which owns the buffer cap, abort\n * composition, retry/backoff, and frame parsing) with the small amount\n * of stateful glue every chat UI needs: accumulated assistant text,\n * thread id, streaming flag, and an event callback.\n *\n * The hook is intentionally lower-level than a full chat component —\n * it owns one stream at a time, not a multi-turn message history. The\n * caller composes its own messages array (typically a `useState`) and\n * appends to it via the `onEvent` callback for tool calls and via the\n * `content` field for assistant text.\n *\n * @example\n * ```tsx\n * function Chat({ agent }: { agent: string }) {\n * const [messages, setMessages] = useState<Message[]>([]);\n * const { content, threadId, isStreaming, send, reset } = useAgentChat({\n * agent,\n * onEvent(ev) {\n * if (ev.type === \"response.output_item.added\" && ev.item?.type === \"function_call\") {\n * setMessages((m) => [...m, { role: \"tool\", name: ev.item?.name, args: ev.item?.arguments }]);\n * }\n * },\n * });\n * // `content` reflects the latest assistant turn; reset() between conversations.\n * // ...\n * }\n * ```\n */\nexport function useAgentChat({\n agent,\n endpoint = \"/api/agents/chat\",\n onEvent,\n}: UseAgentChatOptions): UseAgentChatResult {\n const [content, setContent] = useState(\"\");\n const [events, setEvents] = useState<AgentChatEvent[]>([]);\n const [threadId, setThreadId] = useState<string | null>(null);\n const [isStreaming, setIsStreaming] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Refs avoid the standard \"stale closure\" problem with `send` and\n // `onEvent`: `send` is a stable callback that reads the latest\n // threadId/onEvent without re-mounting connectSSE on every render.\n const threadIdRef = useRef<string | null>(null);\n const contentRef = useRef(\"\");\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const reset = useCallback(() => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n threadIdRef.current = null;\n contentRef.current = \"\";\n setContent(\"\");\n setEvents([]);\n setThreadId(null);\n setIsStreaming(false);\n setError(null);\n }, []);\n\n const send = useCallback(\n async (message: string) => {\n // Abort any previous stream — only one chat turn in flight per hook.\n abortControllerRef.current?.abort();\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n contentRef.current = \"\";\n setContent(\"\");\n setEvents([]);\n setError(null);\n setIsStreaming(true);\n\n const payload = {\n message,\n agent,\n ...(threadIdRef.current ? { threadId: threadIdRef.current } : {}),\n };\n\n try {\n await connectSSE({\n url: endpoint,\n payload,\n signal: controller.signal,\n // Chat turns aren't idempotent — re-sending the payload after a\n // transient failure would either duplicate the user message or\n // depend on server-side Last-Event-ID resumption (the agents\n // plugin's StreamManager supports it, but failure-mode auditing\n // is easier with retries off by default; callers can re-enable\n // via the underlying connectSSE once they understand the\n // resumption contract on their endpoint).\n maxRetries: 0,\n onMessage: async ({ data }) => {\n if (controller.signal.aborted) return;\n if (!data || data === \"[DONE]\") return;\n let event: AgentChatEvent;\n try {\n event = JSON.parse(data) as AgentChatEvent;\n } catch {\n // Skip malformed payloads — the rest of the stream is\n // still useful and the agents plugin recovers on the\n // next event boundary.\n return;\n }\n if (!event.type) return;\n\n // Best-effort: never let an onEvent throw break the stream.\n try {\n onEventRef.current?.(event);\n } catch {\n // swallow\n }\n\n setEvents((prev) => [...prev, event]);\n\n if (event.type === \"appkit.metadata\") {\n const tid = event.data?.threadId;\n if (typeof tid === \"string\") {\n threadIdRef.current = tid;\n setThreadId(tid);\n }\n } else if (\n event.type === \"response.output_text.delta\" &&\n typeof event.delta === \"string\"\n ) {\n contentRef.current += event.delta;\n setContent(contentRef.current);\n }\n },\n onError: (err) => {\n if (controller.signal.aborted) return;\n setError(err instanceof Error ? err.message : \"Chat stream failed\");\n },\n });\n } catch (err) {\n if (!controller.signal.aborted) {\n setError(err instanceof Error ? err.message : \"Chat stream failed\");\n }\n } finally {\n if (abortControllerRef.current === controller) {\n abortControllerRef.current = null;\n }\n setIsStreaming(false);\n }\n },\n [agent, endpoint],\n );\n\n // Abort any in-flight stream when the component unmounts.\n useEffect(() => {\n return () => {\n abortControllerRef.current?.abort();\n };\n }, []);\n\n return {\n content,\n events,\n threadId,\n isStreaming,\n error,\n send,\n reset,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HA,SAAgB,aAAa,EAC3B,OACA,WAAW,oBACX,WAC0C;CAC1C,MAAM,CAAC,SAAS,cAAc,SAAS,GAAG;CAC1C,MAAM,CAAC,QAAQ,aAAa,SAA2B,EAAE,CAAC;CAC1D,MAAM,CAAC,UAAU,eAAe,SAAwB,KAAK;CAC7D,MAAM,CAAC,aAAa,kBAAkB,SAAS,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,SAAwB,KAAK;CAKvD,MAAM,cAAc,OAAsB,KAAK;CAC/C,MAAM,aAAa,OAAO,GAAG;CAC7B,MAAM,aAAa,OAAO,QAAQ;AAClC,YAAW,UAAU;CACrB,MAAM,qBAAqB,OAA+B,KAAK;CAE/D,MAAM,QAAQ,kBAAkB;AAC9B,qBAAmB,SAAS,OAAO;AACnC,qBAAmB,UAAU;AAC7B,cAAY,UAAU;AACtB,aAAW,UAAU;AACrB,aAAW,GAAG;AACd,YAAU,EAAE,CAAC;AACb,cAAY,KAAK;AACjB,iBAAe,MAAM;AACrB,WAAS,KAAK;IACb,EAAE,CAAC;CAEN,MAAM,OAAO,YACX,OAAO,YAAoB;AAEzB,qBAAmB,SAAS,OAAO;EACnC,MAAM,aAAa,IAAI,iBAAiB;AACxC,qBAAmB,UAAU;AAE7B,aAAW,UAAU;AACrB,aAAW,GAAG;AACd,YAAU,EAAE,CAAC;AACb,WAAS,KAAK;AACd,iBAAe,KAAK;EAEpB,MAAM,UAAU;GACd;GACA;GACA,GAAI,YAAY,UAAU,EAAE,UAAU,YAAY,SAAS,GAAG,EAAE;GACjE;AAED,MAAI;AACF,SAAM,WAAW;IACf,KAAK;IACL;IACA,QAAQ,WAAW;IAQnB,YAAY;IACZ,WAAW,OAAO,EAAE,WAAW;AAC7B,SAAI,WAAW,OAAO,QAAS;AAC/B,SAAI,CAAC,QAAQ,SAAS,SAAU;KAChC,IAAI;AACJ,SAAI;AACF,cAAQ,KAAK,MAAM,KAAK;aAClB;AAIN;;AAEF,SAAI,CAAC,MAAM,KAAM;AAGjB,SAAI;AACF,iBAAW,UAAU,MAAM;aACrB;AAIR,gBAAW,SAAS,CAAC,GAAG,MAAM,MAAM,CAAC;AAErC,SAAI,MAAM,SAAS,mBAAmB;MACpC,MAAM,MAAM,MAAM,MAAM;AACxB,UAAI,OAAO,QAAQ,UAAU;AAC3B,mBAAY,UAAU;AACtB,mBAAY,IAAI;;gBAGlB,MAAM,SAAS,gCACf,OAAO,MAAM,UAAU,UACvB;AACA,iBAAW,WAAW,MAAM;AAC5B,iBAAW,WAAW,QAAQ;;;IAGlC,UAAU,QAAQ;AAChB,SAAI,WAAW,OAAO,QAAS;AAC/B,cAAS,eAAe,QAAQ,IAAI,UAAU,qBAAqB;;IAEtE,CAAC;WACK,KAAK;AACZ,OAAI,CAAC,WAAW,OAAO,QACrB,UAAS,eAAe,QAAQ,IAAI,UAAU,qBAAqB;YAE7D;AACR,OAAI,mBAAmB,YAAY,WACjC,oBAAmB,UAAU;AAE/B,kBAAe,MAAM;;IAGzB,CAAC,OAAO,SAAS,CAClB;AAGD,iBAAgB;AACd,eAAa;AACX,sBAAmB,SAAS,OAAO;;IAEpC,EAAE,CAAC;AAEN,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
1
+ {"version":3,"file":"use-agent-chat.js","names":[],"sources":["../../../src/react/hooks/use-agent-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { connectSSE } from \"@/js\";\n\n/**\n * One Responses-API-shaped event yielded by the agents plugin SSE stream.\n *\n * The hook handles the two paths every chat UI needs — accumulating\n * `content` from `response.output_text.delta` and capturing `threadId`\n * from `appkit.metadata` — and surfaces everything else (tool calls,\n * approval gates, status events, etc.) through {@link UseAgentChatOptions.onEvent}.\n *\n * Fields beyond `type` are intentionally loose because the agents plugin\n * forwards adapter-specific shapes verbatim. Treat unknown fields as\n * opaque pass-through.\n */\nexport interface AgentChatEvent {\n type: string;\n delta?: string;\n item_id?: string;\n item?: {\n type?: string;\n id?: string;\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n status?: string;\n };\n content?: string;\n data?: Record<string, unknown>;\n error?: string;\n sequence_number?: number;\n output_index?: number;\n // `appkit.approval_pending` payload\n approval_id?: string;\n stream_id?: string;\n tool_name?: string;\n args?: unknown;\n annotations?: Record<string, unknown>;\n}\n\nexport interface UseAgentChatOptions {\n /**\n * Agent name registered with the `agents()` plugin (e.g. `\"assistant\"`,\n * `\"helper\"`). Send-time payload includes this so the plugin routes the\n * turn to the right `AgentDefinition`.\n */\n agent: string;\n /**\n * Override the chat endpoint. Default `\"/api/agents/chat\"` matches the\n * route the agents plugin mounts under its prefix. Useful when the\n * server mounts under a non-default base path or when proxying.\n */\n endpoint?: string;\n /**\n * Called for every parsed SSE event before any state update. Use this\n * to drive tool-call rows, approval cards, inspectors, or anything\n * beyond the streaming text content. Errors thrown here are swallowed\n * so a buggy handler can't kill the stream.\n */\n onEvent?: (event: AgentChatEvent) => void;\n}\n\nexport interface UseAgentChatResult {\n /** Accumulated assistant text from `response.output_text.delta` events. */\n content: string;\n /**\n * Every parsed event, in order. Provided for components that need to\n * render historical tool calls or replay state after a remount —\n * lighter than re-deriving from message history. For one-off side\n * effects prefer {@link UseAgentChatOptions.onEvent}.\n */\n events: AgentChatEvent[];\n /**\n * Thread id captured from the first `appkit.metadata` event of the\n * stream. Subsequent `send()` calls automatically forward this so the\n * server reuses the same thread.\n */\n threadId: string | null;\n /** True while an SSE stream is open. */\n isStreaming: boolean;\n /** Last error message (cleared on next successful `send()`). */\n error: string | null;\n /**\n * Send a user turn and stream the response. Aborts any in-flight\n * stream. Resolves when the stream completes (success or error).\n */\n send: (message: string) => Promise<void>;\n /**\n * Discard accumulated content, events, and threadId. Aborts any\n * in-flight stream. Use when switching agents or starting a fresh\n * conversation.\n */\n reset: () => void;\n}\n\n/**\n * React hook for chatting with an agent registered via the `agents()`\n * plugin. Wraps {@link connectSSE} (which owns the buffer cap, abort\n * composition, retry/backoff, and frame parsing) with the small amount\n * of stateful glue every chat UI needs: accumulated assistant text,\n * thread id, streaming flag, and an event callback.\n *\n * The hook is intentionally lower-level than a full chat component —\n * it owns one stream at a time, not a multi-turn message history. The\n * caller composes its own messages array (typically a `useState`) and\n * appends to it via the `onEvent` callback for tool calls and via the\n * `content` field for assistant text.\n *\n * @example\n * ```tsx\n * function Chat({ agent }: { agent: string }) {\n * const [messages, setMessages] = useState<Message[]>([]);\n * const { content, threadId, isStreaming, send, reset } = useAgentChat({\n * agent,\n * onEvent(ev) {\n * if (ev.type === \"response.output_item.added\" && ev.item?.type === \"function_call\") {\n * setMessages((m) => [...m, { role: \"tool\", name: ev.item?.name, args: ev.item?.arguments }]);\n * }\n * },\n * });\n * // `content` reflects the latest assistant turn; reset() between conversations.\n * // ...\n * }\n * ```\n */\nexport function useAgentChat({\n agent,\n endpoint = \"/api/agents/chat\",\n onEvent,\n}: UseAgentChatOptions): UseAgentChatResult {\n const [content, setContent] = useState(\"\");\n const [events, setEvents] = useState<AgentChatEvent[]>([]);\n const [threadId, setThreadId] = useState<string | null>(null);\n const [isStreaming, setIsStreaming] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Refs avoid the standard \"stale closure\" problem with `send` and\n // `onEvent`: `send` is a stable callback that reads the latest\n // threadId/onEvent without re-mounting connectSSE on every render.\n const threadIdRef = useRef<string | null>(null);\n const contentRef = useRef(\"\");\n const onEventRef = useRef(onEvent);\n onEventRef.current = onEvent;\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const reset = useCallback(() => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n threadIdRef.current = null;\n contentRef.current = \"\";\n setContent(\"\");\n setEvents([]);\n setThreadId(null);\n setIsStreaming(false);\n setError(null);\n }, []);\n\n const send = useCallback(\n async (message: string) => {\n // Abort any previous stream — only one chat turn in flight per hook.\n abortControllerRef.current?.abort();\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n contentRef.current = \"\";\n setContent(\"\");\n setEvents([]);\n setError(null);\n setIsStreaming(true);\n\n const payload = {\n message,\n agent,\n ...(threadIdRef.current ? { threadId: threadIdRef.current } : {}),\n };\n\n try {\n await connectSSE({\n url: endpoint,\n payload,\n signal: controller.signal,\n // Chat turns aren't idempotent — re-sending the payload after a\n // transient failure would either duplicate the user message or\n // depend on server-side Last-Event-ID resumption (the agents\n // plugin's StreamManager supports it, but failure-mode auditing\n // is easier with retries off by default; callers can re-enable\n // via the underlying connectSSE once they understand the\n // resumption contract on their endpoint).\n maxRetries: 0,\n onMessage: async ({ data }) => {\n if (controller.signal.aborted) return;\n if (!data || data === \"[DONE]\") return;\n let event: AgentChatEvent;\n try {\n event = JSON.parse(data) as AgentChatEvent;\n } catch {\n // Skip malformed payloads — the rest of the stream is\n // still useful and the agents plugin recovers on the\n // next event boundary.\n return;\n }\n if (!event.type) return;\n\n // Best-effort: never let an onEvent throw break the stream.\n try {\n onEventRef.current?.(event);\n } catch {\n // swallow\n }\n\n setEvents((prev) => [...prev, event]);\n\n if (event.type === \"appkit.metadata\") {\n const tid = event.data?.threadId;\n if (typeof tid === \"string\") {\n threadIdRef.current = tid;\n setThreadId(tid);\n }\n } else if (\n event.type === \"response.output_text.delta\" &&\n typeof event.delta === \"string\"\n ) {\n contentRef.current += event.delta;\n setContent(contentRef.current);\n }\n },\n onError: (err) => {\n if (controller.signal.aborted) return;\n setError(err instanceof Error ? err.message : \"Chat stream failed\");\n },\n });\n } catch (err) {\n if (!controller.signal.aborted) {\n setError(err instanceof Error ? err.message : \"Chat stream failed\");\n }\n } finally {\n if (abortControllerRef.current === controller) {\n abortControllerRef.current = null;\n }\n setIsStreaming(false);\n }\n },\n [agent, endpoint],\n );\n\n // Abort any in-flight stream when the component unmounts.\n useEffect(() => {\n return () => {\n abortControllerRef.current?.abort();\n };\n }, []);\n\n return {\n content,\n events,\n threadId,\n isStreaming,\n error,\n send,\n reset,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+HA,SAAgB,aAAa,EAC3B,OACA,WAAW,oBACX,WAC0C;CAC1C,MAAM,CAAC,SAAS,cAAc,SAAS,GAAG;CAC1C,MAAM,CAAC,QAAQ,aAAa,SAA2B,EAAE,CAAC;CAC1D,MAAM,CAAC,UAAU,eAAe,SAAwB,KAAK;CAC7D,MAAM,CAAC,aAAa,kBAAkB,SAAS,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,SAAwB,KAAK;CAKvD,MAAM,cAAc,OAAsB,KAAK;CAC/C,MAAM,aAAa,OAAO,GAAG;CAC7B,MAAM,aAAa,OAAO,QAAQ;AAClC,YAAW,UAAU;CACrB,MAAM,qBAAqB,OAA+B,KAAK;CAE/D,MAAM,QAAQ,kBAAkB;AAC9B,qBAAmB,SAAS,OAAO;AACnC,qBAAmB,UAAU;AAC7B,cAAY,UAAU;AACtB,aAAW,UAAU;AACrB,aAAW,GAAG;AACd,YAAU,EAAE,CAAC;AACb,cAAY,KAAK;AACjB,iBAAe,MAAM;AACrB,WAAS,KAAK;IACb,EAAE,CAAC;CAEN,MAAM,OAAO,YACX,OAAO,YAAoB;AAEzB,qBAAmB,SAAS,OAAO;EACnC,MAAM,aAAa,IAAI,iBAAiB;AACxC,qBAAmB,UAAU;AAE7B,aAAW,UAAU;AACrB,aAAW,GAAG;AACd,YAAU,EAAE,CAAC;AACb,WAAS,KAAK;AACd,iBAAe,KAAK;EAEpB,MAAM,UAAU;GACd;GACA;GACA,GAAI,YAAY,UAAU,EAAE,UAAU,YAAY,SAAS,GAAG,EAAE;GACjE;AAED,MAAI;AACF,SAAM,WAAW;IACf,KAAK;IACL;IACA,QAAQ,WAAW;IAQnB,YAAY;IACZ,WAAW,OAAO,EAAE,WAAW;AAC7B,SAAI,WAAW,OAAO,QAAS;AAC/B,SAAI,CAAC,QAAQ,SAAS,SAAU;KAChC,IAAI;AACJ,SAAI;AACF,cAAQ,KAAK,MAAM,KAAK;aAClB;AAIN;;AAEF,SAAI,CAAC,MAAM,KAAM;AAGjB,SAAI;AACF,iBAAW,UAAU,MAAM;aACrB;AAIR,gBAAW,SAAS,CAAC,GAAG,MAAM,MAAM,CAAC;AAErC,SAAI,MAAM,SAAS,mBAAmB;MACpC,MAAM,MAAM,MAAM,MAAM;AACxB,UAAI,OAAO,QAAQ,UAAU;AAC3B,mBAAY,UAAU;AACtB,mBAAY,IAAI;;gBAGlB,MAAM,SAAS,gCACf,OAAO,MAAM,UAAU,UACvB;AACA,iBAAW,WAAW,MAAM;AAC5B,iBAAW,WAAW,QAAQ;;;IAGlC,UAAU,QAAQ;AAChB,SAAI,WAAW,OAAO,QAAS;AAC/B,cAAS,eAAe,QAAQ,IAAI,UAAU,qBAAqB;;IAEtE,CAAC;WACK,KAAK;AACZ,OAAI,CAAC,WAAW,OAAO,QACrB,UAAS,eAAe,QAAQ,IAAI,UAAU,qBAAqB;YAE7D;AACR,OAAI,mBAAmB,YAAY,WACjC,oBAAmB,UAAU;AAE/B,kBAAe,MAAM;;IAGzB,CAAC,OAAO,SAAS,CAClB;AAGD,iBAAgB;AACd,eAAa;AACX,sBAAmB,SAAS,OAAO;;IAEpC,EAAE,CAAC;AAEN,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-ai-search-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-ai-search-query.ts"],"mappings":";;;UASiB,uBAAA;;AAAjB;;;EAKE,KAAA;AAAA;AAAA,UAGe,sBAAA,WACL,MAAA,oBAA0B,MAAA;EADC;EAIrC,MAAA,GACE,KAAA,WAAgB,eAAA,KACb,OAAA,CAAQ,gBAAA,CAAiB,CAAA;EALpB;EAOV,IAAA,EAAM,gBAAA,CAAiB,CAAA;EAHL;EAKlB,OAAA;EAJa;EAMb,KAAA;EAJuB;EAMvB,KAAA;EAES;EAAT,OAAA,EAAS,oBAAA;AAAA;;;;;;iBAQK,gBAAA,WACJ,MAAA,oBAA0B,MAAA,kBAAA,CACpC,OAAA,GAAS,uBAAA,GAA+B,sBAAA,CAAuB,CAAA"}
1
+ {"version":3,"file":"use-ai-search-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-ai-search-query.ts"],"mappings":";;;UAUiB,uBAAA;;AAAjB;;;EAKE,KAAA;AAAA;AAAA,UAGe,sBAAA,WACL,MAAA,oBAA0B,MAAA;EADC;EAIrC,MAAA,GACE,KAAA,WAAgB,eAAA,KACb,OAAA,CAAQ,gBAAA,CAAiB,CAAA;EALpB;EAOV,IAAA,EAAM,gBAAA,CAAiB,CAAA;EAHL;EAKlB,OAAA;EAJa;EAMb,KAAA;EAJuB;EAMvB,KAAA;EAES;EAAT,OAAA,EAAS,oBAAA;AAAA;;;;;;iBAQK,gBAAA,WACJ,MAAA,oBAA0B,MAAA,kBAAA,CACpC,OAAA,GAAS,uBAAA,GAA+B,sBAAA,CAAuB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-ai-search-query.js","names":[],"sources":["../../../src/react/hooks/use-ai-search-query.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type {\n AiSearchClientConfig,\n AiSearchIndexSummary,\n AiSearchRequest,\n AiSearchResponse,\n} from \"./types\";\nimport { usePluginClientConfig } from \"./use-plugin-config\";\n\nexport interface UseAiSearchQueryOptions {\n /**\n * Index alias to query. Defaults to the first index exposed by the plugin's\n * `clientConfig()`, so a single-index app needs no alias.\n */\n alias?: string;\n}\n\nexport interface UseAiSearchQueryResult<\n T extends Record<string, unknown> = Record<string, unknown>,\n> {\n /** Run a search. Pass query text, or a full request for filters/paging control. */\n search: (\n query: string | AiSearchRequest,\n ) => Promise<AiSearchResponse<T> | null>;\n /** Latest response, null until the first successful search. */\n data: AiSearchResponse<T> | null;\n /** Whether a search is in progress. */\n loading: boolean;\n /** Error message, if any. */\n error: string | null;\n /** The resolved alias this hook queries. */\n alias: string | null;\n /** All configured indexes, for building a selector. */\n indexes: AiSearchIndexSummary[];\n}\n\n/**\n * Hook for querying a Databricks AI Search index. Reads the available indexes\n * from the ai-search plugin's `clientConfig()` and POSTs to\n * `/api/ai-search/{alias}/query`, so the UI never hardcodes an endpoint alias.\n */\nexport function useAiSearchQuery<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseAiSearchQueryOptions = {}): UseAiSearchQueryResult<T> {\n const config = usePluginClientConfig<AiSearchClientConfig>(\"aiSearch\");\n const indexes = config.indexes ?? [];\n\n const alias = options.alias ?? indexes[0]?.alias ?? null;\n\n // Config validation error; null for a valid alias.\n let aliasError: string | null = null;\n if (!alias) {\n aliasError = \"No AI Search indexes are configured.\";\n } else if (options.alias && !indexes.some((i) => i.alias === options.alias)) {\n const available = indexes.map((i) => i.alias).join(\", \") || \"none\";\n aliasError = `Unknown AI Search index \"${options.alias}\". Available: ${available}`;\n }\n\n const [data, setData] = useState<AiSearchResponse<T> | null>(null);\n const [loading, setLoading] = useState(false);\n const [fetchError, setFetchError] = useState<string | null>(null);\n const error = fetchError ?? aliasError;\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const search = useCallback(\n (query: string | AiSearchRequest): Promise<AiSearchResponse<T> | null> => {\n if (aliasError || !alias) {\n return Promise.resolve(null);\n }\n\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setLoading(true);\n setFetchError(null);\n setData(null);\n\n const body: AiSearchRequest =\n typeof query === \"string\" ? { queryText: query } : query;\n\n return fetch(`/api/ai-search/${encodeURIComponent(alias)}/query`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: abortController.signal,\n })\n .then(async (res) => {\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new Error(errorBody?.error || `HTTP ${res.status}`);\n }\n return res.json();\n })\n .then((result: AiSearchResponse<T>) => {\n if (abortController.signal.aborted) return null;\n setData(result);\n setLoading(false);\n return result;\n })\n .catch((err: Error) => {\n if (abortController.signal.aborted) return null;\n setFetchError(err.message || \"Search failed\");\n setLoading(false);\n return null;\n });\n },\n [alias, aliasError],\n );\n\n // Reset when the target alias changes: abort any in-flight request (its\n // result would otherwise land under the new alias) and clear stale data +\n // fetch error. `error` re-derives from the new alias's validation state.\n // biome-ignore lint/correctness/useExhaustiveDependencies: `alias` is the intended trigger — the effect runs to reset on every alias change, not because it reads alias.\n useEffect(() => {\n abortControllerRef.current?.abort();\n setData(null);\n setLoading(false);\n setFetchError(null);\n }, [alias]);\n\n useEffect(() => () => abortControllerRef.current?.abort(), []);\n\n return { search, data, loading, error, alias, indexes };\n}\n"],"mappings":";;;;;;;;;AAyCA,SAAgB,iBAEd,UAAmC,EAAE,EAA6B;CAElE,MAAM,UADS,sBAA4C,WAAW,CAC/C,WAAW,EAAE;CAEpC,MAAM,QAAQ,QAAQ,SAAS,QAAQ,IAAI,SAAS;CAGpD,IAAI,aAA4B;AAChC,KAAI,CAAC,MACH,cAAa;UACJ,QAAQ,SAAS,CAAC,QAAQ,MAAM,MAAM,EAAE,UAAU,QAAQ,MAAM,EAAE;EAC3E,MAAM,YAAY,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI;AAC5D,eAAa,4BAA4B,QAAQ,MAAM,gBAAgB;;CAGzE,MAAM,CAAC,MAAM,WAAW,SAAqC,KAAK;CAClE,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,CAAC,YAAY,iBAAiB,SAAwB,KAAK;CACjE,MAAM,QAAQ,cAAc;CAC5B,MAAM,qBAAqB,OAA+B,KAAK;CAE/D,MAAM,SAAS,aACZ,UAAyE;AACxE,MAAI,cAAc,CAAC,MACjB,QAAO,QAAQ,QAAQ,KAAK;AAG9B,qBAAmB,SAAS,OAAO;EACnC,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,qBAAmB,UAAU;AAE7B,aAAW,KAAK;AAChB,gBAAc,KAAK;AACnB,UAAQ,KAAK;EAEb,MAAM,OACJ,OAAO,UAAU,WAAW,EAAE,WAAW,OAAO,GAAG;AAErD,SAAO,MAAM,kBAAkB,mBAAmB,MAAM,CAAC,SAAS;GAChE,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,KAAK,UAAU,KAAK;GAC1B,QAAQ,gBAAgB;GACzB,CAAC,CACC,KAAK,OAAO,QAAQ;AACnB,OAAI,CAAC,IAAI,IAAI;IACX,MAAM,YAAY,MAAM,IAAI,MAAM,CAAC,YAAY,KAAK;AACpD,UAAM,IAAI,MAAM,WAAW,SAAS,QAAQ,IAAI,SAAS;;AAE3D,UAAO,IAAI,MAAM;IACjB,CACD,MAAM,WAAgC;AACrC,OAAI,gBAAgB,OAAO,QAAS,QAAO;AAC3C,WAAQ,OAAO;AACf,cAAW,MAAM;AACjB,UAAO;IACP,CACD,OAAO,QAAe;AACrB,OAAI,gBAAgB,OAAO,QAAS,QAAO;AAC3C,iBAAc,IAAI,WAAW,gBAAgB;AAC7C,cAAW,MAAM;AACjB,UAAO;IACP;IAEN,CAAC,OAAO,WAAW,CACpB;AAMD,iBAAgB;AACd,qBAAmB,SAAS,OAAO;AACnC,UAAQ,KAAK;AACb,aAAW,MAAM;AACjB,gBAAc,KAAK;IAClB,CAAC,MAAM,CAAC;AAEX,uBAAsB,mBAAmB,SAAS,OAAO,EAAE,EAAE,CAAC;AAE9D,QAAO;EAAE;EAAQ;EAAM;EAAS;EAAO;EAAO;EAAS"}
1
+ {"version":3,"file":"use-ai-search-query.js","names":[],"sources":["../../../src/react/hooks/use-ai-search-query.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport type {\n AiSearchClientConfig,\n AiSearchIndexSummary,\n AiSearchRequest,\n AiSearchResponse,\n} from \"./types\";\nimport { usePluginClientConfig } from \"./use-plugin-config\";\n\nexport interface UseAiSearchQueryOptions {\n /**\n * Index alias to query. Defaults to the first index exposed by the plugin's\n * `clientConfig()`, so a single-index app needs no alias.\n */\n alias?: string;\n}\n\nexport interface UseAiSearchQueryResult<\n T extends Record<string, unknown> = Record<string, unknown>,\n> {\n /** Run a search. Pass query text, or a full request for filters/paging control. */\n search: (\n query: string | AiSearchRequest,\n ) => Promise<AiSearchResponse<T> | null>;\n /** Latest response, null until the first successful search. */\n data: AiSearchResponse<T> | null;\n /** Whether a search is in progress. */\n loading: boolean;\n /** Error message, if any. */\n error: string | null;\n /** The resolved alias this hook queries. */\n alias: string | null;\n /** All configured indexes, for building a selector. */\n indexes: AiSearchIndexSummary[];\n}\n\n/**\n * Hook for querying a Databricks AI Search index. Reads the available indexes\n * from the ai-search plugin's `clientConfig()` and POSTs to\n * `/api/ai-search/{alias}/query`, so the UI never hardcodes an endpoint alias.\n */\nexport function useAiSearchQuery<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(options: UseAiSearchQueryOptions = {}): UseAiSearchQueryResult<T> {\n const config = usePluginClientConfig<AiSearchClientConfig>(\"aiSearch\");\n const indexes = config.indexes ?? [];\n\n const alias = options.alias ?? indexes[0]?.alias ?? null;\n\n // Config validation error; null for a valid alias.\n let aliasError: string | null = null;\n if (!alias) {\n aliasError = \"No AI Search indexes are configured.\";\n } else if (options.alias && !indexes.some((i) => i.alias === options.alias)) {\n const available = indexes.map((i) => i.alias).join(\", \") || \"none\";\n aliasError = `Unknown AI Search index \"${options.alias}\". Available: ${available}`;\n }\n\n const [data, setData] = useState<AiSearchResponse<T> | null>(null);\n const [loading, setLoading] = useState(false);\n const [fetchError, setFetchError] = useState<string | null>(null);\n const error = fetchError ?? aliasError;\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const search = useCallback(\n (query: string | AiSearchRequest): Promise<AiSearchResponse<T> | null> => {\n if (aliasError || !alias) {\n return Promise.resolve(null);\n }\n\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setLoading(true);\n setFetchError(null);\n setData(null);\n\n const body: AiSearchRequest =\n typeof query === \"string\" ? { queryText: query } : query;\n\n return fetch(`/api/ai-search/${encodeURIComponent(alias)}/query`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: abortController.signal,\n })\n .then(async (res) => {\n if (!res.ok) {\n const errorBody = await res.json().catch(() => null);\n throw new Error(errorBody?.error || `HTTP ${res.status}`);\n }\n return res.json();\n })\n .then((result: AiSearchResponse<T>) => {\n if (abortController.signal.aborted) return null;\n setData(result);\n setLoading(false);\n return result;\n })\n .catch((err: Error) => {\n if (abortController.signal.aborted) return null;\n setFetchError(err.message || \"Search failed\");\n setLoading(false);\n return null;\n });\n },\n [alias, aliasError],\n );\n\n // Reset when the target alias changes: abort any in-flight request (its\n // result would otherwise land under the new alias) and clear stale data +\n // fetch error. `error` re-derives from the new alias's validation state.\n useEffect(() => {\n abortControllerRef.current?.abort();\n setData(null);\n setLoading(false);\n setFetchError(null);\n }, [alias]);\n\n useEffect(() => () => abortControllerRef.current?.abort(), []);\n\n return { search, data, loading, error, alias, indexes };\n}\n"],"mappings":";;;;;;;;;AA0CA,SAAgB,iBAEd,UAAmC,EAAE,EAA6B;CAElE,MAAM,UADS,sBAA4C,WAAW,CAC/C,WAAW,EAAE;CAEpC,MAAM,QAAQ,QAAQ,SAAS,QAAQ,IAAI,SAAS;CAGpD,IAAI,aAA4B;AAChC,KAAI,CAAC,MACH,cAAa;UACJ,QAAQ,SAAS,CAAC,QAAQ,MAAM,MAAM,EAAE,UAAU,QAAQ,MAAM,EAAE;EAC3E,MAAM,YAAY,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI;AAC5D,eAAa,4BAA4B,QAAQ,MAAM,gBAAgB;;CAGzE,MAAM,CAAC,MAAM,WAAW,SAAqC,KAAK;CAClE,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,CAAC,YAAY,iBAAiB,SAAwB,KAAK;CACjE,MAAM,QAAQ,cAAc;CAC5B,MAAM,qBAAqB,OAA+B,KAAK;CAE/D,MAAM,SAAS,aACZ,UAAyE;AACxE,MAAI,cAAc,CAAC,MACjB,QAAO,QAAQ,QAAQ,KAAK;AAG9B,qBAAmB,SAAS,OAAO;EACnC,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,qBAAmB,UAAU;AAE7B,aAAW,KAAK;AAChB,gBAAc,KAAK;AACnB,UAAQ,KAAK;EAEb,MAAM,OACJ,OAAO,UAAU,WAAW,EAAE,WAAW,OAAO,GAAG;AAErD,SAAO,MAAM,kBAAkB,mBAAmB,MAAM,CAAC,SAAS;GAChE,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,KAAK,UAAU,KAAK;GAC1B,QAAQ,gBAAgB;GACzB,CAAC,CACC,KAAK,OAAO,QAAQ;AACnB,OAAI,CAAC,IAAI,IAAI;IACX,MAAM,YAAY,MAAM,IAAI,MAAM,CAAC,YAAY,KAAK;AACpD,UAAM,IAAI,MAAM,WAAW,SAAS,QAAQ,IAAI,SAAS;;AAE3D,UAAO,IAAI,MAAM;IACjB,CACD,MAAM,WAAgC;AACrC,OAAI,gBAAgB,OAAO,QAAS,QAAO;AAC3C,WAAQ,OAAO;AACf,cAAW,MAAM;AACjB,UAAO;IACP,CACD,OAAO,QAAe;AACrB,OAAI,gBAAgB,OAAO,QAAS,QAAO;AAC3C,iBAAc,IAAI,WAAW,gBAAgB;AAC7C,cAAW,MAAM;AACjB,UAAO;IACP;IAEN,CAAC,OAAO,WAAW,CACpB;AAKD,iBAAgB;AACd,qBAAmB,SAAS,OAAO;AACnC,UAAQ,KAAK;AACb,aAAW,MAAM;AACjB,gBAAc,KAAK;IAClB,CAAC,MAAM,CAAC;AAEX,uBAAsB,mBAAmB,SAAS,OAAO,EAAE,EAAE,CAAC;AAE9D,QAAO;EAAE;EAAQ;EAAM;EAAS;EAAO;EAAO;EAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-analytics-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"mappings":";;;;;AA2MA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iBAAA,wBAEJ,QAAA,GAAW,QAAA,YACX,eAAA,gBAAA,CAEV,QAAA,EAAU,CAAA,EACV,UAAA,GAAa,WAAA,CAAY,CAAA,UACzB,OAAA,GAAS,wBAAA,CAAyB,CAAA,IACjC,uBAAA,CAAwB,mBAAA,CAAoB,CAAA,EAAG,CAAA,EAAG,CAAA"}
1
+ {"version":3,"file":"use-analytics-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"mappings":";;;;;AA6MA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iBAAA,wBAEJ,QAAA,GAAW,QAAA,YACX,eAAA,gBAAA,CAEV,QAAA,EAAU,CAAA,EACV,UAAA,GAAa,WAAA,CAAY,CAAA,UACzB,OAAA,GAAS,wBAAA,CAAyB,CAAA,IACjC,uBAAA,CAAwB,mBAAA,CAAoB,CAAA,EAAG,CAAA,EAAG,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-analytics-query.js","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { ArrowClient, connectSSE } from \"@/js\";\nimport {\n type AnalyticsSseHandlerContext,\n GENERIC_LOAD_ERROR,\n getDevMode,\n handleAnalyticsSseError,\n handleAnalyticsSseMessage,\n userFacingFetchError,\n} from \"./analytics-sse\";\nimport type {\n AnalyticsFormat,\n InferParams,\n InferResultByFormat,\n QueryKey,\n UseAnalyticsQueryOptions,\n UseAnalyticsQueryResult,\n WarehouseStatus,\n} from \"./types\";\nimport { useAnalyticsWarehousePublisher } from \"./use-analytics-warehouse-status\";\nimport { useQueryHMR } from \"./use-query-hmr\";\n\n/** Shallow equality for plain-object query parameters (primitive values only). */\nfunction shallowEqualParams(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (\n a === null ||\n b === null ||\n typeof a !== \"object\" ||\n typeof b !== \"object\"\n ) {\n return false;\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(b, key)) return false;\n if (\n !Object.is(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false;\n }\n }\n return true;\n}\n\n/** Keep structurally-equal params referentially stable across renders. */\nfunction useStableParams<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!shallowEqualParams(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\ninterface ArrowDirectContext {\n url: string;\n payload: string;\n signal: AbortSignal;\n setLoading: (loading: boolean) => void;\n setError: (error: string | null) => void;\n setErrorCode: (code: string | null) => void;\n setData: (data: unknown) => void;\n unpublishWarehouseStatus: () => void;\n}\n\n/**\n * Fetch the real column names for a statement from the fallback endpoint,\n * used when a very wide schema's names didn't fit in the response header.\n * Returns undefined on any failure so decoding falls back to the raw Arrow\n * schema names.\n */\nasync function fetchArrowColumns(\n statementId: string,\n signal: AbortSignal,\n): Promise<string[] | undefined> {\n try {\n const res = await fetch(\n `/api/analytics/columns/${encodeURIComponent(statementId)}`,\n { signal },\n );\n if (!res.ok) return undefined;\n const body = (await res.json()) as { columns?: unknown };\n return Array.isArray(body.columns) ? (body.columns as string[]) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fetch an ARROW_STREAM query result as raw Arrow IPC bytes directly from\n * the query endpoint (no SSE, no second /arrow-result request) and decode\n * it into a Table. The server streams the bytes back as the POST response\n * body; errors before the first byte arrive as a JSON `{ error, errorCode }`.\n */\nasync function fetchArrowDirect(ctx: ArrowDirectContext): Promise<void> {\n try {\n const response = await fetch(ctx.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: ctx.payload,\n signal: ctx.signal,\n });\n if (ctx.signal.aborted) return;\n\n if (!response.ok) {\n let message = GENERIC_LOAD_ERROR;\n let code: string | null = null;\n try {\n const body = (await response.json()) as {\n error?: string;\n errorCode?: string;\n };\n if (body.error) message = body.error;\n if (typeof body.errorCode === \"string\") code = body.errorCode;\n } catch {\n // Non-JSON error body — keep the generic message.\n }\n ctx.setLoading(false);\n ctx.setError(message);\n if (code) ctx.setErrorCode(code);\n ctx.unpublishWarehouseStatus();\n return;\n }\n\n const buffer = await response.arrayBuffer();\n if (ctx.signal.aborted) return;\n // Databricks encodes ARROW_STREAM columns positionally (col_0, …); the\n // server sends the real manifest names so we can relabel the decoded\n // Table (charts look columns up by name). Normally inline in the\n // `X-Appkit-Arrow-Columns` header; for very wide schemas the header\n // carries only a statement-id reference and we fetch the names.\n let columnNames: string[] | undefined;\n const header = response.headers.get(\"X-Appkit-Arrow-Columns\");\n if (header) {\n try {\n columnNames = JSON.parse(decodeURIComponent(header));\n } catch {\n // Malformed header — fall back to the raw Arrow schema names.\n }\n } else {\n const ref = response.headers.get(\"X-Appkit-Arrow-Columns-Ref\");\n if (ref) {\n columnNames = await fetchArrowColumns(ref, ctx.signal);\n }\n }\n const table = await ArrowClient.processArrowBuffer(\n new Uint8Array(buffer),\n columnNames,\n );\n ctx.setData(table);\n ctx.setLoading(false);\n ctx.unpublishWarehouseStatus();\n } catch (error) {\n if (ctx.signal.aborted) return;\n ctx.setLoading(false);\n ctx.unpublishWarehouseStatus();\n ctx.setError(userFacingFetchError(error));\n }\n}\n\n/**\n * Subscribe to an analytics query and return its latest result. JSON_ARRAY\n * results stream over SSE (with warehouse-readiness progress); ARROW_STREAM\n * results are fetched as raw Arrow bytes directly from the query endpoint.\n * Integration hook between client and analytics plugin.\n *\n * The return type is automatically inferred based on the format:\n * - `format: \"JSON_ARRAY\"` (default): Returns typed array from QueryRegistry\n * - `format: \"ARROW_STREAM\"`: Returns TypedArrowTable with row type preserved\n *\n * Note: User context execution is determined by query file naming:\n * - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)\n * - `queryKey.sql`: Executes as service principal\n *\n * @param queryKey - Analytics query identifier\n * @param parameters - Query parameters (type-safe based on QueryRegistry)\n * @param options - Analytics query settings including format\n * @returns Query result state with format-appropriate data type\n *\n * @example JSON_ARRAY format (default)\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params);\n * // data: Array<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n *\n * @example ARROW_STREAM format\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params, { format: \"ARROW_STREAM\" });\n * // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n */\nexport function useAnalyticsQuery<\n T = unknown,\n K extends QueryKey = QueryKey,\n F extends AnalyticsFormat = \"JSON_ARRAY\",\n>(\n queryKey: K,\n parameters?: InferParams<K> | null,\n options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,\n): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {\n const format = options?.format ?? \"JSON_ARRAY\";\n const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;\n const autoStart = options?.autoStart ?? true;\n\n const devMode = getDevMode();\n const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;\n\n type ResultType = InferResultByFormat<T, K, F>;\n const [data, setData] = useState<ResultType | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [errorCode, setErrorCode] = useState<string | null>(null);\n const [warehouseStatus, setWarehouseStatus] =\n useState<WarehouseStatus | null>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const publisherId = useId();\n const {\n publish: publishWarehouseStatus,\n unpublish: unpublishWarehouseStatus,\n } = useAnalyticsWarehousePublisher(publisherId, queryKey);\n\n if (!queryKey || queryKey.trim().length === 0) {\n throw new Error(\n \"useAnalyticsQuery: 'queryKey' must be a non-empty string.\",\n );\n }\n\n const stableParameters = useStableParams(parameters);\n\n const payload = useMemo(() => {\n try {\n const serialized = JSON.stringify({\n parameters: stableParameters,\n format,\n });\n const sizeInBytes = new Blob([serialized]).size;\n if (sizeInBytes > maxParametersSize) {\n throw new Error(\n \"useAnalyticsQuery: Parameters size exceeds the maximum allowed size\",\n );\n }\n\n return serialized;\n } catch (error) {\n console.error(\"useAnalyticsQuery: Failed to serialize parameters\", error);\n return null;\n }\n }, [stableParameters, format, maxParametersSize]);\n\n const start = useCallback(() => {\n if (payload === null) {\n setError(\"Failed to serialize query parameters\");\n return;\n }\n\n abortControllerRef.current?.abort();\n\n setLoading(true);\n setError(null);\n setErrorCode(null);\n setData(null);\n setWarehouseStatus(null);\n publishWarehouseStatus(null);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n // ARROW_STREAM: the server streams raw Arrow IPC bytes back on the query\n // response body (no SSE). Fetch and decode directly.\n if (format === \"ARROW_STREAM\") {\n void fetchArrowDirect({\n url: urlSuffix,\n payload,\n signal: abortController.signal,\n setLoading,\n setError,\n setErrorCode,\n setData: (table) => setData(table as ResultType),\n unpublishWarehouseStatus,\n });\n return;\n }\n\n const sseContext: AnalyticsSseHandlerContext = {\n source: \"useAnalyticsQuery\",\n resource: { queryKey },\n defaultExecutionError: \"Unable to execute query\",\n unpublishOnMalformedMessage: false,\n signal: abortController.signal,\n abort: () => abortController.abort(),\n setLoading,\n setError,\n setErrorCode,\n onWarehouseStatus: (status) => {\n setWarehouseStatus(status);\n publishWarehouseStatus(status);\n },\n onResult: (message) => setData(message.data as ResultType),\n unpublishWarehouseStatus,\n };\n\n connectSSE({\n url: urlSuffix,\n payload,\n signal: abortController.signal,\n onMessage: (message) =>\n handleAnalyticsSseMessage(message.data, sseContext),\n onError: (error) => handleAnalyticsSseError(error, sseContext),\n });\n }, [\n queryKey,\n payload,\n urlSuffix,\n format,\n publishWarehouseStatus,\n unpublishWarehouseStatus,\n ]);\n\n useEffect(() => {\n if (autoStart) {\n start();\n }\n\n return () => {\n abortControllerRef.current?.abort();\n unpublishWarehouseStatus();\n };\n }, [start, autoStart, unpublishWarehouseStatus]);\n\n useQueryHMR(queryKey, start);\n\n return { data, loading, error, errorCode, warehouseStatus };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-analytics-query.js","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { ArrowClient, connectSSE } from \"@/js\";\n\nimport {\n type AnalyticsSseHandlerContext,\n GENERIC_LOAD_ERROR,\n getDevMode,\n handleAnalyticsSseError,\n handleAnalyticsSseMessage,\n userFacingFetchError,\n} from \"./analytics-sse\";\nimport type {\n AnalyticsFormat,\n InferParams,\n InferResultByFormat,\n QueryKey,\n UseAnalyticsQueryOptions,\n UseAnalyticsQueryResult,\n WarehouseStatus,\n} from \"./types\";\nimport { useAnalyticsWarehousePublisher } from \"./use-analytics-warehouse-status\";\nimport { useQueryHMR } from \"./use-query-hmr\";\n\n/** Shallow equality for plain-object query parameters (primitive values only). */\nfunction shallowEqualParams(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (\n a === null ||\n b === null ||\n typeof a !== \"object\" ||\n typeof b !== \"object\"\n ) {\n return false;\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(b, key)) return false;\n if (\n !Object.is(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false;\n }\n }\n return true;\n}\n\n/** Keep structurally-equal params referentially stable across renders. */\nfunction useStableParams<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!shallowEqualParams(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\ninterface ArrowDirectContext {\n url: string;\n payload: string;\n signal: AbortSignal;\n setLoading: (loading: boolean) => void;\n setError: (error: string | null) => void;\n setErrorCode: (code: string | null) => void;\n setData: (data: unknown) => void;\n unpublishWarehouseStatus: () => void;\n}\n\n/**\n * Fetch the real column names for a statement from the fallback endpoint,\n * used when a very wide schema's names didn't fit in the response header.\n * Returns undefined on any failure so decoding falls back to the raw Arrow\n * schema names.\n */\nasync function fetchArrowColumns(\n statementId: string,\n signal: AbortSignal,\n): Promise<string[] | undefined> {\n try {\n const res = await fetch(\n `/api/analytics/columns/${encodeURIComponent(statementId)}`,\n { signal },\n );\n if (!res.ok) return undefined;\n const body = (await res.json()) as { columns?: unknown };\n return Array.isArray(body.columns) ? (body.columns as string[]) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fetch an ARROW_STREAM query result as raw Arrow IPC bytes directly from\n * the query endpoint (no SSE, no second /arrow-result request) and decode\n * it into a Table. The server streams the bytes back as the POST response\n * body; errors before the first byte arrive as a JSON `{ error, errorCode }`.\n */\nasync function fetchArrowDirect(ctx: ArrowDirectContext): Promise<void> {\n try {\n const response = await fetch(ctx.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: ctx.payload,\n signal: ctx.signal,\n });\n if (ctx.signal.aborted) return;\n\n if (!response.ok) {\n let message = GENERIC_LOAD_ERROR;\n let code: string | null = null;\n try {\n const body = (await response.json()) as {\n error?: string;\n errorCode?: string;\n };\n if (body.error) message = body.error;\n if (typeof body.errorCode === \"string\") code = body.errorCode;\n } catch {\n // Non-JSON error body — keep the generic message.\n }\n ctx.setLoading(false);\n ctx.setError(message);\n if (code) ctx.setErrorCode(code);\n ctx.unpublishWarehouseStatus();\n return;\n }\n\n const buffer = await response.arrayBuffer();\n if (ctx.signal.aborted) return;\n // Databricks encodes ARROW_STREAM columns positionally (col_0, …); the\n // server sends the real manifest names so we can relabel the decoded\n // Table (charts look columns up by name). Normally inline in the\n // `X-Appkit-Arrow-Columns` header; for very wide schemas the header\n // carries only a statement-id reference and we fetch the names.\n let columnNames: string[] | undefined;\n const header = response.headers.get(\"X-Appkit-Arrow-Columns\");\n if (header) {\n try {\n columnNames = JSON.parse(decodeURIComponent(header));\n } catch {\n // Malformed header — fall back to the raw Arrow schema names.\n }\n } else {\n const ref = response.headers.get(\"X-Appkit-Arrow-Columns-Ref\");\n if (ref) {\n columnNames = await fetchArrowColumns(ref, ctx.signal);\n }\n }\n const table = await ArrowClient.processArrowBuffer(\n new Uint8Array(buffer),\n columnNames,\n );\n ctx.setData(table);\n ctx.setLoading(false);\n ctx.unpublishWarehouseStatus();\n } catch (error) {\n if (ctx.signal.aborted) return;\n ctx.setLoading(false);\n ctx.unpublishWarehouseStatus();\n ctx.setError(userFacingFetchError(error));\n }\n}\n\n/**\n * Subscribe to an analytics query and return its latest result. JSON_ARRAY\n * results stream over SSE (with warehouse-readiness progress); ARROW_STREAM\n * results are fetched as raw Arrow bytes directly from the query endpoint.\n * Integration hook between client and analytics plugin.\n *\n * The return type is automatically inferred based on the format:\n * - `format: \"JSON_ARRAY\"` (default): Returns typed array from QueryRegistry\n * - `format: \"ARROW_STREAM\"`: Returns TypedArrowTable with row type preserved\n *\n * Note: User context execution is determined by query file naming:\n * - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)\n * - `queryKey.sql`: Executes as service principal\n *\n * @param queryKey - Analytics query identifier\n * @param parameters - Query parameters (type-safe based on QueryRegistry)\n * @param options - Analytics query settings including format\n * @returns Query result state with format-appropriate data type\n *\n * @example JSON_ARRAY format (default)\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params);\n * // data: Array<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n *\n * @example ARROW_STREAM format\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params, { format: \"ARROW_STREAM\" });\n * // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n */\nexport function useAnalyticsQuery<\n T = unknown,\n K extends QueryKey = QueryKey,\n F extends AnalyticsFormat = \"JSON_ARRAY\",\n>(\n queryKey: K,\n parameters?: InferParams<K> | null,\n options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,\n): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {\n const format = options?.format ?? \"JSON_ARRAY\";\n const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;\n const autoStart = options?.autoStart ?? true;\n\n const devMode = getDevMode();\n const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;\n\n type ResultType = InferResultByFormat<T, K, F>;\n const [data, setData] = useState<ResultType | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [errorCode, setErrorCode] = useState<string | null>(null);\n const [warehouseStatus, setWarehouseStatus] =\n useState<WarehouseStatus | null>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const publisherId = useId();\n const {\n publish: publishWarehouseStatus,\n unpublish: unpublishWarehouseStatus,\n } = useAnalyticsWarehousePublisher(publisherId, queryKey);\n\n if (!queryKey || queryKey.trim().length === 0) {\n throw new Error(\n \"useAnalyticsQuery: 'queryKey' must be a non-empty string.\",\n );\n }\n\n const stableParameters = useStableParams(parameters);\n\n const payload = useMemo(() => {\n try {\n const serialized = JSON.stringify({\n parameters: stableParameters,\n format,\n });\n const sizeInBytes = new Blob([serialized]).size;\n if (sizeInBytes > maxParametersSize) {\n throw new Error(\n \"useAnalyticsQuery: Parameters size exceeds the maximum allowed size\",\n );\n }\n\n return serialized;\n } catch (error) {\n console.error(\"useAnalyticsQuery: Failed to serialize parameters\", error);\n return null;\n }\n }, [stableParameters, format, maxParametersSize]);\n\n const start = useCallback(() => {\n if (payload === null) {\n setError(\"Failed to serialize query parameters\");\n return;\n }\n\n abortControllerRef.current?.abort();\n\n setLoading(true);\n setError(null);\n setErrorCode(null);\n setData(null);\n setWarehouseStatus(null);\n publishWarehouseStatus(null);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n // ARROW_STREAM: the server streams raw Arrow IPC bytes back on the query\n // response body (no SSE). Fetch and decode directly.\n if (format === \"ARROW_STREAM\") {\n void fetchArrowDirect({\n url: urlSuffix,\n payload,\n signal: abortController.signal,\n setLoading,\n setError,\n setErrorCode,\n setData: (table) => setData(table as ResultType),\n unpublishWarehouseStatus,\n });\n return;\n }\n\n const sseContext: AnalyticsSseHandlerContext = {\n source: \"useAnalyticsQuery\",\n resource: { queryKey },\n defaultExecutionError: \"Unable to execute query\",\n unpublishOnMalformedMessage: false,\n signal: abortController.signal,\n abort: () => abortController.abort(),\n setLoading,\n setError,\n setErrorCode,\n onWarehouseStatus: (status) => {\n setWarehouseStatus(status);\n publishWarehouseStatus(status);\n },\n onResult: (message) => setData(message.data as ResultType),\n unpublishWarehouseStatus,\n };\n\n connectSSE({\n url: urlSuffix,\n payload,\n signal: abortController.signal,\n onMessage: (message) =>\n handleAnalyticsSseMessage(message.data, sseContext),\n onError: (error) => handleAnalyticsSseError(error, sseContext),\n });\n }, [\n queryKey,\n payload,\n urlSuffix,\n format,\n publishWarehouseStatus,\n unpublishWarehouseStatus,\n ]);\n\n useEffect(() => {\n if (autoStart) {\n start();\n }\n\n return () => {\n abortControllerRef.current?.abort();\n unpublishWarehouseStatus();\n };\n }, [start, autoStart, unpublishWarehouseStatus]);\n\n useQueryHMR(queryKey, start);\n\n return { data, loading, error, errorCode, warehouseStatus };\n}\n"],"mappings":";;;;;;;;;AAgCA,SAAS,mBAAmB,GAAY,GAAqB;AAC3D,KAAI,OAAO,GAAG,GAAG,EAAE,CAAE,QAAO;AAC5B,KACE,MAAM,QACN,MAAM,QACN,OAAO,MAAM,YACb,OAAO,MAAM,SAEb,QAAO;CAET,MAAM,QAAQ,OAAO,KAAK,EAA6B;CACvD,MAAM,QAAQ,OAAO,KAAK,EAA6B;AACvD,KAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,MAAK,MAAM,OAAO,OAAO;AACvB,MAAI,CAAC,OAAO,OAAO,GAAG,IAAI,CAAE,QAAO;AACnC,MACE,CAAC,OAAO,GACL,EAA8B,MAC9B,EAA8B,KAChC,CAED,QAAO;;AAGX,QAAO;;;AAIT,SAAS,gBAAmB,OAAa;CACvC,MAAM,MAAM,OAAU,MAAM;AAC5B,KAAI,CAAC,mBAAmB,IAAI,SAAS,MAAM,CACzC,KAAI,UAAU;AAEhB,QAAO,IAAI;;;;;;;;AAoBb,eAAe,kBACb,aACA,QAC+B;AAC/B,KAAI;EACF,MAAM,MAAM,MAAM,MAChB,0BAA0B,mBAAmB,YAAY,IACzD,EAAE,QAAQ,CACX;AACD,MAAI,CAAC,IAAI,GAAI,QAAO;EACpB,MAAM,OAAQ,MAAM,IAAI,MAAM;AAC9B,SAAO,MAAM,QAAQ,KAAK,QAAQ,GAAI,KAAK,UAAuB;SAC5D;AACN;;;;;;;;;AAUJ,eAAe,iBAAiB,KAAwC;AACtE,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,IAAI,KAAK;GACpC,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,IAAI;GACV,QAAQ,IAAI;GACb,CAAC;AACF,MAAI,IAAI,OAAO,QAAS;AAExB,MAAI,CAAC,SAAS,IAAI;GAChB,IAAI,UAAU;GACd,IAAI,OAAsB;AAC1B,OAAI;IACF,MAAM,OAAQ,MAAM,SAAS,MAAM;AAInC,QAAI,KAAK,MAAO,WAAU,KAAK;AAC/B,QAAI,OAAO,KAAK,cAAc,SAAU,QAAO,KAAK;WAC9C;AAGR,OAAI,WAAW,MAAM;AACrB,OAAI,SAAS,QAAQ;AACrB,OAAI,KAAM,KAAI,aAAa,KAAK;AAChC,OAAI,0BAA0B;AAC9B;;EAGF,MAAM,SAAS,MAAM,SAAS,aAAa;AAC3C,MAAI,IAAI,OAAO,QAAS;EAMxB,IAAI;EACJ,MAAM,SAAS,SAAS,QAAQ,IAAI,yBAAyB;AAC7D,MAAI,OACF,KAAI;AACF,iBAAc,KAAK,MAAM,mBAAmB,OAAO,CAAC;UAC9C;OAGH;GACL,MAAM,MAAM,SAAS,QAAQ,IAAI,6BAA6B;AAC9D,OAAI,IACF,eAAc,MAAM,kBAAkB,KAAK,IAAI,OAAO;;EAG1D,MAAM,QAAQ,MAAM,YAAY,mBAC9B,IAAI,WAAW,OAAO,EACtB,YACD;AACD,MAAI,QAAQ,MAAM;AAClB,MAAI,WAAW,MAAM;AACrB,MAAI,0BAA0B;UACvB,OAAO;AACd,MAAI,IAAI,OAAO,QAAS;AACxB,MAAI,WAAW,MAAM;AACrB,MAAI,0BAA0B;AAC9B,MAAI,SAAS,qBAAqB,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmC7C,SAAgB,kBAKd,UACA,YACA,UAAuC,EAAE,EACc;CACvD,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,oBAAoB,SAAS,qBAAqB,MAAM;CAC9D,MAAM,YAAY,SAAS,aAAa;CAExC,MAAM,UAAU,YAAY;CAC5B,MAAM,YAAY,wBAAwB,mBAAmB,SAAS,GAAG;CAGzE,MAAM,CAAC,MAAM,WAAW,SAA4B,KAAK;CACzD,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,CAAC,OAAO,YAAY,SAAwB,KAAK;CACvD,MAAM,CAAC,WAAW,gBAAgB,SAAwB,KAAK;CAC/D,MAAM,CAAC,iBAAiB,sBACtB,SAAiC,KAAK;CACxC,MAAM,qBAAqB,OAA+B,KAAK;CAG/D,MAAM,EACJ,SAAS,wBACT,WAAW,6BACT,+BAJgB,OAAO,EAIqB,SAAS;AAEzD,KAAI,CAAC,YAAY,SAAS,MAAM,CAAC,WAAW,EAC1C,OAAM,IAAI,MACR,4DACD;CAGH,MAAM,mBAAmB,gBAAgB,WAAW;CAEpD,MAAM,UAAU,cAAc;AAC5B,MAAI;GACF,MAAM,aAAa,KAAK,UAAU;IAChC,YAAY;IACZ;IACD,CAAC;AAEF,OADoB,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,OACzB,kBAChB,OAAM,IAAI,MACR,sEACD;AAGH,UAAO;WACA,OAAO;AACd,WAAQ,MAAM,qDAAqD,MAAM;AACzE,UAAO;;IAER;EAAC;EAAkB;EAAQ;EAAkB,CAAC;CAEjD,MAAM,QAAQ,kBAAkB;AAC9B,MAAI,YAAY,MAAM;AACpB,YAAS,uCAAuC;AAChD;;AAGF,qBAAmB,SAAS,OAAO;AAEnC,aAAW,KAAK;AAChB,WAAS,KAAK;AACd,eAAa,KAAK;AAClB,UAAQ,KAAK;AACb,qBAAmB,KAAK;AACxB,yBAAuB,KAAK;EAE5B,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,qBAAmB,UAAU;AAI7B,MAAI,WAAW,gBAAgB;AAC7B,GAAK,iBAAiB;IACpB,KAAK;IACL;IACA,QAAQ,gBAAgB;IACxB;IACA;IACA;IACA,UAAU,UAAU,QAAQ,MAAoB;IAChD;IACD,CAAC;AACF;;EAGF,MAAM,aAAyC;GAC7C,QAAQ;GACR,UAAU,EAAE,UAAU;GACtB,uBAAuB;GACvB,6BAA6B;GAC7B,QAAQ,gBAAgB;GACxB,aAAa,gBAAgB,OAAO;GACpC;GACA;GACA;GACA,oBAAoB,WAAW;AAC7B,uBAAmB,OAAO;AAC1B,2BAAuB,OAAO;;GAEhC,WAAW,YAAY,QAAQ,QAAQ,KAAmB;GAC1D;GACD;AAED,aAAW;GACT,KAAK;GACL;GACA,QAAQ,gBAAgB;GACxB,YAAY,YACV,0BAA0B,QAAQ,MAAM,WAAW;GACrD,UAAU,UAAU,wBAAwB,OAAO,WAAW;GAC/D,CAAC;IACD;EACD;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;AAEF,iBAAgB;AACd,MAAI,UACF,QAAO;AAGT,eAAa;AACX,sBAAmB,SAAS,OAAO;AACnC,6BAA0B;;IAE3B;EAAC;EAAO;EAAW;EAAyB,CAAC;AAEhD,aAAY,UAAU,MAAM;AAE5B,QAAO;EAAE;EAAM;EAAS;EAAO;EAAW;EAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-analytics-warehouse-status.js","names":[],"sources":["../../../src/react/hooks/use-analytics-warehouse-status.ts"],"sourcesContent":["import { useCallback, useRef } from \"react\";\nimport type { WarehouseStatus } from \"./types\";\nimport {\n type ResourceSeverity,\n useResourceStatusPublisher,\n} from \"./use-resource-status\";\n\nconst ANALYTICS_WAREHOUSE_RESOURCE_KIND = \"warehouse\";\n\n/**\n * - `RUNNING` → `null`; callers `unpublish` instead.\n * - `STARTING` / `STOPPED` / `STOPPING` → `pending`.\n * - `DELETED` / `DELETING` → `error` (config change required).\n */\nfunction severityForWarehouseState(\n state: WarehouseStatus[\"state\"],\n): ResourceSeverity | null {\n switch (state) {\n case \"RUNNING\":\n return null;\n case \"DELETED\":\n case \"DELETING\":\n return \"error\";\n default:\n return \"pending\";\n }\n}\n\n/**\n * Internal hook used by `useAnalyticsQuery` to mirror its current warehouse\n * status into the nearest provider. No-op when no provider is mounted.\n */\nexport function useAnalyticsWarehousePublisher(\n id: string,\n queryKey: string,\n): {\n publish: (status: WarehouseStatus | null) => void;\n unpublish: () => void;\n} {\n const { publish: publishGeneric, unpublish } = useResourceStatusPublisher(\n id,\n queryKey,\n { kindHint: ANALYTICS_WAREHOUSE_RESOURCE_KIND },\n );\n\n // Anchor `startedAt` to the first non-null status so `elapsedMs`\n // advances monotonically across successive `warehouse_status` events.\n const startedAtRef = useRef<number | null>(null);\n\n const publish = useCallback(\n (status: WarehouseStatus | null) => {\n // null covers \"register with no status yet\" *and* RUNNING — both\n // keep the slot registered without contributing to the aggregate.\n const severity = status && severityForWarehouseState(status.state);\n if (!status || !severity) {\n startedAtRef.current = null;\n publishGeneric(null);\n return;\n }\n if (startedAtRef.current === null) {\n startedAtRef.current = Date.now() - Math.max(0, status.elapsedMs);\n }\n publishGeneric({\n kind: ANALYTICS_WAREHOUSE_RESOURCE_KIND,\n state: status.state,\n severity,\n startedAt: startedAtRef.current,\n });\n },\n [publishGeneric],\n );\n\n return { publish, unpublish };\n}\n"],"mappings":";;;;AAOA,MAAM,oCAAoC;;;;;;AAO1C,SAAS,0BACP,OACyB;AACzB,SAAQ,OAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK;EACL,KAAK,WACH,QAAO;EACT,QACE,QAAO;;;;;;;AAQb,SAAgB,+BACd,IACA,UAIA;CACA,MAAM,EAAE,SAAS,gBAAgB,cAAc,2BAC7C,IACA,UACA,EAAE,UAAU,mCAAmC,CAChD;CAID,MAAM,eAAe,OAAsB,KAAK;AAyBhD,QAAO;EAAE,SAvBO,aACb,WAAmC;GAGlC,MAAM,WAAW,UAAU,0BAA0B,OAAO,MAAM;AAClE,OAAI,CAAC,UAAU,CAAC,UAAU;AACxB,iBAAa,UAAU;AACvB,mBAAe,KAAK;AACpB;;AAEF,OAAI,aAAa,YAAY,KAC3B,cAAa,UAAU,KAAK,KAAK,GAAG,KAAK,IAAI,GAAG,OAAO,UAAU;AAEnE,kBAAe;IACb,MAAM;IACN,OAAO,OAAO;IACd;IACA,WAAW,aAAa;IACzB,CAAC;KAEJ,CAAC,eAAe,CACjB;EAEiB;EAAW"}
1
+ {"version":3,"file":"use-analytics-warehouse-status.js","names":[],"sources":["../../../src/react/hooks/use-analytics-warehouse-status.ts"],"sourcesContent":["import { useCallback, useRef } from \"react\";\n\nimport type { WarehouseStatus } from \"./types\";\nimport {\n type ResourceSeverity,\n useResourceStatusPublisher,\n} from \"./use-resource-status\";\n\nconst ANALYTICS_WAREHOUSE_RESOURCE_KIND = \"warehouse\";\n\n/**\n * - `RUNNING` → `null`; callers `unpublish` instead.\n * - `STARTING` / `STOPPED` / `STOPPING` → `pending`.\n * - `DELETED` / `DELETING` → `error` (config change required).\n */\nfunction severityForWarehouseState(\n state: WarehouseStatus[\"state\"],\n): ResourceSeverity | null {\n switch (state) {\n case \"RUNNING\":\n return null;\n case \"DELETED\":\n case \"DELETING\":\n return \"error\";\n default:\n return \"pending\";\n }\n}\n\n/**\n * Internal hook used by `useAnalyticsQuery` to mirror its current warehouse\n * status into the nearest provider. No-op when no provider is mounted.\n */\nexport function useAnalyticsWarehousePublisher(\n id: string,\n queryKey: string,\n): {\n publish: (status: WarehouseStatus | null) => void;\n unpublish: () => void;\n} {\n const { publish: publishGeneric, unpublish } = useResourceStatusPublisher(\n id,\n queryKey,\n { kindHint: ANALYTICS_WAREHOUSE_RESOURCE_KIND },\n );\n\n // Anchor `startedAt` to the first non-null status so `elapsedMs`\n // advances monotonically across successive `warehouse_status` events.\n const startedAtRef = useRef<number | null>(null);\n\n const publish = useCallback(\n (status: WarehouseStatus | null) => {\n // null covers \"register with no status yet\" *and* RUNNING — both\n // keep the slot registered without contributing to the aggregate.\n const severity = status && severityForWarehouseState(status.state);\n if (!status || !severity) {\n startedAtRef.current = null;\n publishGeneric(null);\n return;\n }\n if (startedAtRef.current === null) {\n startedAtRef.current = Date.now() - Math.max(0, status.elapsedMs);\n }\n publishGeneric({\n kind: ANALYTICS_WAREHOUSE_RESOURCE_KIND,\n state: status.state,\n severity,\n startedAt: startedAtRef.current,\n });\n },\n [publishGeneric],\n );\n\n return { publish, unpublish };\n}\n"],"mappings":";;;;AAQA,MAAM,oCAAoC;;;;;;AAO1C,SAAS,0BACP,OACyB;AACzB,SAAQ,OAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK;EACL,KAAK,WACH,QAAO;EACT,QACE,QAAO;;;;;;;AAQb,SAAgB,+BACd,IACA,UAIA;CACA,MAAM,EAAE,SAAS,gBAAgB,cAAc,2BAC7C,IACA,UACA,EAAE,UAAU,mCAAmC,CAChD;CAID,MAAM,eAAe,OAAsB,KAAK;AAyBhD,QAAO;EAAE,SAvBO,aACb,WAAmC;GAGlC,MAAM,WAAW,UAAU,0BAA0B,OAAO,MAAM;AAClE,OAAI,CAAC,UAAU,CAAC,UAAU;AACxB,iBAAa,UAAU;AACvB,mBAAe,KAAK;AACpB;;AAEF,OAAI,aAAa,YAAY,KAC3B,cAAa,UAAU,KAAK,KAAK,GAAG,KAAK,IAAI,GAAG,OAAO,UAAU;AAEnE,kBAAe;IACb,MAAM;IACN,OAAO,OAAO;IACd;IACA,WAAW,aAAa;IACzB,CAAC;KAEJ,CAAC,eAAe,CACjB;EAEiB;EAAW"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-chart-data.d.ts","names":[],"sources":["../../../src/react/hooks/use-chart-data.ts"],"mappings":";;;;UAaiB,mBAAA;;EAEf,QAAA;EAFkC;EAIlC,UAAA,GAAa,MAAA;EAAA;;;;;;;EAQb,MAAA,GAAS,UAAA;EARI;EAUb,WAAA,OAAkB,IAAA,EAAM,CAAA,KAAM,CAAA;AAAA;AAAA,UAGf,kBAAA;EAHA;EAKf,IAAA,EAAM,SAAA;EALY;EAOlB,OAAA;EAP+B;EAS/B,OAAA;EANe;EAQf,KAAA;;EAEA,OAAA;EARA;;;;;;EAeA,eAAA,EAAiB,eAAA;AAAA;;;AAiEnB;;;;;;;;;;;;;;;;;iBAAgB,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,kBAAA"}
1
+ {"version":3,"file":"use-chart-data.d.ts","names":[],"sources":["../../../src/react/hooks/use-chart-data.ts"],"mappings":";;;;UAciB,mBAAA;;EAEf,QAAA;EAFkC;EAIlC,UAAA,GAAa,MAAA;EAAA;;;;;;;EAQb,MAAA,GAAS,UAAA;EARI;EAUb,WAAA,OAAkB,IAAA,EAAM,CAAA,KAAM,CAAA;AAAA;AAAA,UAGf,kBAAA;EAHA;EAKf,IAAA,EAAM,SAAA;EALY;EAOlB,OAAA;EAP+B;EAS/B,OAAA;EANe;EAQf,KAAA;;EAEA,OAAA;EARA;;;;;;EAeA,eAAA,EAAiB,eAAA;AAAA;;;AAiEnB;;;;;;;;;;;;;;;;;iBAAgB,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,kBAAA"}