@databricks/appkit-ui 0.21.0 → 0.23.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 (77) hide show
  1. package/CLAUDE.md +11 -0
  2. package/NOTICE.md +1 -0
  3. package/README.md +3 -20
  4. package/dist/cli/commands/generate-types.js +15 -13
  5. package/dist/cli/commands/generate-types.js.map +1 -1
  6. package/dist/cli/commands/setup.js +2 -2
  7. package/dist/cli/commands/setup.js.map +1 -1
  8. package/dist/js/config.d.ts +24 -0
  9. package/dist/js/config.d.ts.map +1 -0
  10. package/dist/js/config.js +49 -0
  11. package/dist/js/config.js.map +1 -0
  12. package/dist/js/index.d.ts +2 -1
  13. package/dist/js/index.js +2 -1
  14. package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
  15. package/dist/react/genie/genie-chat-message-list.js +5 -4
  16. package/dist/react/genie/genie-chat-message-list.js.map +1 -1
  17. package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
  18. package/dist/react/genie/genie-chat-message.js +7 -6
  19. package/dist/react/genie/genie-chat-message.js.map +1 -1
  20. package/dist/react/genie/genie-query-visualization.d.ts.map +1 -1
  21. package/dist/react/genie/genie-query-visualization.js +4 -3
  22. package/dist/react/genie/genie-query-visualization.js.map +1 -1
  23. package/dist/react/genie/index.d.ts +1 -1
  24. package/dist/react/genie/types.d.ts +2 -1
  25. package/dist/react/genie/types.d.ts.map +1 -1
  26. package/dist/react/genie/types.js +6 -0
  27. package/dist/react/genie/types.js.map +1 -0
  28. package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
  29. package/dist/react/genie/use-genie-chat.js +60 -23
  30. package/dist/react/genie/use-genie-chat.js.map +1 -1
  31. package/dist/react/hooks/index.d.ts +5 -2
  32. package/dist/react/hooks/index.js +3 -0
  33. package/dist/react/hooks/types.d.ts +30 -1
  34. package/dist/react/hooks/types.d.ts.map +1 -1
  35. package/dist/react/hooks/use-plugin-config.d.ts +25 -0
  36. package/dist/react/hooks/use-plugin-config.d.ts.map +1 -0
  37. package/dist/react/hooks/use-plugin-config.js +32 -0
  38. package/dist/react/hooks/use-plugin-config.js.map +1 -0
  39. package/dist/react/hooks/use-serving-invoke.d.ts +30 -0
  40. package/dist/react/hooks/use-serving-invoke.d.ts.map +1 -0
  41. package/dist/react/hooks/use-serving-invoke.js +82 -0
  42. package/dist/react/hooks/use-serving-invoke.js.map +1 -0
  43. package/dist/react/hooks/use-serving-stream.d.ts +35 -0
  44. package/dist/react/hooks/use-serving-stream.d.ts.map +1 -0
  45. package/dist/react/hooks/use-serving-stream.js +101 -0
  46. package/dist/react/hooks/use-serving-stream.js.map +1 -0
  47. package/dist/react/index.d.ts +6 -3
  48. package/dist/react/index.js +4 -1
  49. package/dist/shared/src/index.d.ts +1 -1
  50. package/dist/shared/src/plugin.d.ts +12 -1
  51. package/dist/shared/src/plugin.d.ts.map +1 -0
  52. package/docs/api/appkit/Class.Plugin.md +83 -20
  53. package/docs/api/appkit/Function.appKitServingTypesPlugin.md +24 -0
  54. package/docs/api/appkit/Function.extractServingEndpoints.md +22 -0
  55. package/docs/api/appkit/Function.findServerFile.md +20 -0
  56. package/docs/api/appkit/Interface.EndpointConfig.md +23 -0
  57. package/docs/api/appkit/Interface.ServingEndpointEntry.md +30 -0
  58. package/docs/api/appkit/Interface.ServingEndpointRegistry.md +3 -0
  59. package/docs/api/appkit/TypeAlias.ExecutionResult.md +36 -0
  60. package/docs/api/appkit/TypeAlias.ServingFactory.md +15 -0
  61. package/docs/api/appkit.md +39 -31
  62. package/docs/app-management.md +1 -1
  63. package/docs/architecture.md +1 -1
  64. package/docs/development/ai-assisted-development.md +2 -2
  65. package/docs/development/local-development.md +1 -1
  66. package/docs/development/remote-bridge.md +1 -1
  67. package/docs/development/templates.md +93 -0
  68. package/docs/development.md +1 -1
  69. package/docs/faq.md +66 -0
  70. package/docs/plugins/caching.md +3 -1
  71. package/docs/plugins/execution-context.md +1 -1
  72. package/docs/plugins/lakebase.md +1 -1
  73. package/docs/plugins/serving.md +223 -0
  74. package/docs.md +2 -2
  75. package/llms.txt +11 -0
  76. package/package.json +60 -58
  77. package/sbom.cdx.json +1 -0
package/CLAUDE.md CHANGED
@@ -27,6 +27,7 @@ npx @databricks/appkit docs <query>
27
27
  - [Configuration](./docs/configuration.md): This guide covers environment variables and configuration options for AppKit applications.
28
28
  - [Core principles](./docs/core-principles.md): Learn about the fundamental concepts and principles behind AppKit.
29
29
  - [Development](./docs/development.md): AppKit provides multiple development workflows to suit different needs: local development with hot reload, AI-assisted development with Agent Skills, and remote tunneling to deployed backends.
30
+ - [FAQ](./docs/faq.md): Integrations
30
31
  - [Plugins](./docs/plugins.md): Plugins are modular extensions that add capabilities to your AppKit application. They follow a defined lifecycle and have access to shared services like caching, telemetry, and streaming.
31
32
 
32
33
  ## Development
@@ -36,6 +37,7 @@ npx @databricks/appkit docs <query>
36
37
  - [Local development](./docs/development/local-development.md): Once your app is bootstrapped according to the Manual quick start guide, you can start the development server with hot reload for both UI and backend code.
37
38
  - [Project setup](./docs/development/project-setup.md): This guide covers the recommended project structure and scaffolding for AppKit applications.
38
39
  - [Remote Bridge](./docs/development/remote-bridge.md): Remote bridge allows you to develop against a deployed backend while keeping your UI and queries local. This is useful for testing against production data or debugging deployed backend code without redeploying your app.
40
+ - [Templates](./docs/development/templates.md): AppKit uses a template system powered by the Databricks CLI's databricks apps init command. Templates define the project structure, and .tmpl files are processed with Go's text/template engine to generate customized output.
39
41
  - [Type generation](./docs/development/type-generation.md): AppKit can automatically generate TypeScript types for your SQL queries, providing end-to-end type safety from database to UI.
40
42
 
41
43
  ## Plugins
@@ -49,6 +51,7 @@ npx @databricks/appkit docs <query>
49
51
  - [Lakebase plugin](./docs/plugins/lakebase.md): Provides a PostgreSQL connection pool for Databricks Lakebase Autoscaling with automatic OAuth token refresh.
50
52
  - [Plugin management](./docs/plugins/plugin-management.md): AppKit includes a CLI for managing plugins. All commands are available under npx @databricks/appkit plugin.
51
53
  - [Server plugin](./docs/plugins/server.md): Provides HTTP server capabilities with development and production modes.
54
+ - [Serving plugin](./docs/plugins/serving.md): Provides an authenticated proxy to Databricks Model Serving endpoints, with invoke and streaming support.
52
55
 
53
56
  ## appkit API reference [collapsed]
54
57
 
@@ -66,9 +69,12 @@ npx @databricks/appkit docs <query>
66
69
  - [Class: ValidationError](./docs/api/appkit/Class.ValidationError.md): Error thrown when input validation fails.
67
70
  - [Enumeration: RequestedClaimsPermissionSet](./docs/api/appkit/Enumeration.RequestedClaimsPermissionSet.md): Permission set for Unity Catalog table access
68
71
  - [Enumeration: ResourceType](./docs/api/appkit/Enumeration.ResourceType.md): Resource types from schema $defs.resourceType.enum
72
+ - [Function: appKitServingTypesPlugin()](./docs/api/appkit/Function.appKitServingTypesPlugin.md): Vite plugin to generate TypeScript types for AppKit serving endpoints.
69
73
  - [Function: appKitTypesPlugin()](./docs/api/appkit/Function.appKitTypesPlugin.md): Vite plugin to generate types for AppKit queries.
70
74
  - [Function: createApp()](./docs/api/appkit/Function.createApp.md): Bootstraps AppKit with the provided configuration.
71
75
  - [Function: createLakebasePool()](./docs/api/appkit/Function.createLakebasePool.md): Create a Lakebase pool with appkit's logger integration.
76
+ - [Function: extractServingEndpoints()](./docs/api/appkit/Function.extractServingEndpoints.md): Extract serving endpoint config from a server file by AST-parsing it.
77
+ - [Function: findServerFile()](./docs/api/appkit/Function.findServerFile.md): Find the server entry file by checking candidate paths in order.
72
78
  - [Function: generateDatabaseCredential()](./docs/api/appkit/Function.generateDatabaseCredential.md): Generate OAuth credentials for Postgres database connection using the proper Postgres API.
73
79
  - [Function: getExecutionContext()](./docs/api/appkit/Function.getExecutionContext.md): Get the current execution context.
74
80
  - [Function: getLakebaseOrmConfig()](./docs/api/appkit/Function.getLakebaseOrmConfig.md): Get Lakebase connection configuration for ORMs that don't accept pg.Pool directly.
@@ -81,6 +87,7 @@ npx @databricks/appkit docs <query>
81
87
  - [Interface: BasePluginConfig](./docs/api/appkit/Interface.BasePluginConfig.md): Base configuration interface for AppKit plugins
82
88
  - [Interface: CacheConfig](./docs/api/appkit/Interface.CacheConfig.md): Configuration for the CacheInterceptor. Controls TTL, size limits, storage backend, and probabilistic cleanup.
83
89
  - [Interface: DatabaseCredential](./docs/api/appkit/Interface.DatabaseCredential.md): Database credentials with OAuth token for Postgres connection
90
+ - [Interface: EndpointConfig](./docs/api/appkit/Interface.EndpointConfig.md): Properties
84
91
  - [Interface: GenerateDatabaseCredentialRequest](./docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md): Request parameters for generating database OAuth credentials
85
92
  - [Interface: ITelemetry](./docs/api/appkit/Interface.ITelemetry.md): Plugin-facing interface for OpenTelemetry instrumentation.
86
93
  - [Interface: LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md): Configuration for creating a Lakebase connection pool
@@ -90,13 +97,17 @@ npx @databricks/appkit docs <query>
90
97
  - [Interface: ResourceEntry](./docs/api/appkit/Interface.ResourceEntry.md): Internal representation of a resource in the registry.
91
98
  - [Interface: ResourceFieldEntry](./docs/api/appkit/Interface.ResourceFieldEntry.md): Defines a single field for a resource. Each field has its own environment variable and optional description. Single-value types use one key (e.g. id); multi-value types (database, secret) use multiple (e.g. instancename, databasename or scope, key).
92
99
  - [Interface: ResourceRequirement](./docs/api/appkit/Interface.ResourceRequirement.md): Declares a resource requirement for a plugin.
100
+ - [Interface: ServingEndpointEntry](./docs/api/appkit/Interface.ServingEndpointEntry.md): Shape of a single registry entry.
101
+ - [Interface: ServingEndpointRegistry](./docs/api/appkit/Interface.ServingEndpointRegistry.md): Registry interface for serving endpoint type generation.
93
102
  - [Interface: StreamExecutionSettings](./docs/api/appkit/Interface.StreamExecutionSettings.md): Execution settings for streaming endpoints. Extends PluginExecutionSettings with SSE stream configuration.
94
103
  - [Interface: TelemetryConfig](./docs/api/appkit/Interface.TelemetryConfig.md): OpenTelemetry configuration for AppKit applications
95
104
  - [Interface: ValidationResult](./docs/api/appkit/Interface.ValidationResult.md): Result of validating all registered resources against the environment.
96
105
  - [Type Alias: ConfigSchema](./docs/api/appkit/TypeAlias.ConfigSchema.md): Configuration schema definition for plugin config.
106
+ - [Type Alias: ExecutionResult<T>](./docs/api/appkit/TypeAlias.ExecutionResult.md): Discriminated union for plugin execution results.
97
107
  - [Type Alias: IAppRouter](./docs/api/appkit/TypeAlias.IAppRouter.md): Express router type for plugin route registration
98
108
  - [Type Alias: PluginData<T, U, N>](./docs/api/appkit/TypeAlias.PluginData.md): Tuple of plugin class, config, and name. Created by toPlugin() and passed to createApp().
99
109
  - [Type Alias: ResourcePermission](./docs/api/appkit/TypeAlias.ResourcePermission.md): Union of all possible permission levels across all resource types.
110
+ - [Type Alias: ServingFactory](./docs/api/appkit/TypeAlias.ServingFactory.md): Factory function returned by AppKit.serving.
100
111
  - [Type Alias: ToPlugin()<T, U, N>](./docs/api/appkit/TypeAlias.ToPlugin.md): Factory function type returned by toPlugin(). Accepts optional config and returns a PluginData tuple.
101
112
  - [Variable: sql](./docs/api/appkit/Variable.sql.md): SQL helper namespace
102
113
 
package/NOTICE.md CHANGED
@@ -58,6 +58,7 @@ This Software contains code from the following open source projects:
58
58
  | [clsx](https://www.npmjs.com/package/clsx) | 2.1.1 | MIT | https://github.com/lukeed/clsx#readme |
59
59
  | [cmdk](https://www.npmjs.com/package/cmdk) | 1.1.1 | MIT | https://github.com/pacocoursey/cmdk#readme |
60
60
  | [commander](https://www.npmjs.com/package/commander) | 2.20.3, 5.1.0, 7.2.0, 8.3.0, 10.0.1, 12.1.0 | MIT | https://github.com/tj/commander.js#readme |
61
+ | [dompurify](https://www.npmjs.com/package/dompurify) | 3.3.3 | (MPL-2.0 OR Apache-2.0) | https://github.com/cure53/DOMPurify |
61
62
  | [dotenv](https://www.npmjs.com/package/dotenv) | 16.6.1 | BSD-2-Clause | https://github.com/motdotla/dotenv#readme |
62
63
  | [echarts](https://www.npmjs.com/package/echarts) | 6.0.0 | Apache-2.0 | https://echarts.apache.org |
63
64
  | [echarts-for-react](https://www.npmjs.com/package/echarts-for-react) | 3.0.5 | MIT | https://github.com/hustcc/echarts-for-react |
package/README.md CHANGED
@@ -2,17 +2,6 @@
2
2
 
3
3
  Build Databricks Apps faster with our brand-new Node.js + React SDK. Built for humans and AI.
4
4
 
5
- > [!WARNING]
6
- > PREVIEW - NOT FOR PRODUCTION USE
7
-
8
- > **This SDK is in preview and is subject to change without notice.**
9
- >
10
- > - ❌ **Do NOT use in production environments**
11
- > - ⚠️ **Breaking changes may occur at any time**
12
- > - 🔬 **APIs are experimental and unstable**
13
- > - 📝 **Use for development and testing only**
14
- >
15
-
16
5
  ## Introduction
17
6
 
18
7
  AppKit is a TypeScript SDK for building production-ready Databricks applications with a plugin-based architecture. It provides opinionated defaults, built-in observability, and seamless integration with Databricks services.
@@ -29,16 +18,10 @@ AppKit simplifies building data applications on Databricks by providing:
29
18
 
30
19
  AppKit's power comes from its plugin system. Each plugin adds a focused capability to your app with minimal configuration.
31
20
 
32
- ### Available now
33
-
34
- - **Analytics Plugin** — Query your Lakehouse data directly from your app. Define SQL queries as files, execute them against Databricks SQL Warehouses, and get automatic caching, parameterization, and on-behalf-of user execution out of the box. Perfect for building apps that surface insights from your Lakehouse.
21
+ - **Analytics Plugin** — Query your Lakehouse data directly from your app. Define SQL queries as files, execute them against Databricks SQL Warehouses, and get automatic caching, parameterization, and on-behalf-of user execution out of the box.
35
22
  - **Genie Plugin** — Conversational AI interface powered by Databricks AI/BI Genie. Let users ask natural language questions against your data and get answers with automatic chart inference and visualization.
36
-
37
- ### Coming soon
38
-
39
- - **Files Plugin** — Browse, upload, and manage files in Unity Catalog Volumes
40
- - **Lakebase Plugin** — OLTP database operations with automatic OAuth token management
41
- - ...and this is just the beginning.
23
+ - **Files Plugin** — Browse, upload, and manage files in Unity Catalog Volumes. Supports multiple volumes, content type validation, and on-behalf-of user access.
24
+ - **Lakebase Plugin** — OLTP database operations against Databricks Lakebase with automatic OAuth token management. Returns a standard `pg.Pool` compatible with Prisma, Drizzle, TypeORM, and other ORMs.
42
25
 
43
26
  > Missing a plugin? [Open an issue](https://github.com/databricks/appkit/issues/new) and tell us what you need — community input directly shapes the roadmap.
44
27
 
@@ -8,21 +8,23 @@ import { Command } from "commander";
8
8
  */
9
9
  async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
10
10
  try {
11
- const resolvedWarehouseId = warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;
12
- if (!resolvedWarehouseId) process.exit(0);
13
- const { generateFromEntryPoint } = await import("@databricks/appkit/type-generator");
14
11
  const resolvedRootDir = rootDir || process.cwd();
15
- const resolvedOutFile = outFile || path.join(process.cwd(), "client/src/appKitTypes.d.ts");
16
- const queryFolder = path.join(resolvedRootDir, "config/queries");
17
- if (!fs.existsSync(queryFolder)) {
18
- console.warn(`Warning: No queries found at ${queryFolder}. Skipping type generation.`);
19
- return;
12
+ const noCache = options?.noCache || false;
13
+ const typeGen = await import("@databricks/appkit/type-generator");
14
+ const resolvedWarehouseId = warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;
15
+ if (resolvedWarehouseId) {
16
+ const resolvedOutFile = outFile || path.join(process.cwd(), "client/src/appKitTypes.d.ts");
17
+ const queryFolder = path.join(resolvedRootDir, "config/queries");
18
+ if (fs.existsSync(queryFolder)) await typeGen.generateFromEntryPoint({
19
+ queryFolder,
20
+ outFile: resolvedOutFile,
21
+ warehouseId: resolvedWarehouseId,
22
+ noCache
23
+ });
20
24
  }
21
- await generateFromEntryPoint({
22
- queryFolder,
23
- outFile: resolvedOutFile,
24
- warehouseId: resolvedWarehouseId,
25
- noCache: options?.noCache || false
25
+ await typeGen.generateServingTypes({
26
+ outFile: path.join(process.cwd(), "client/src/appKitServingTypes.d.ts"),
27
+ noCache
26
28
  });
27
29
  } catch (error) {
28
30
  if (error instanceof Error && error.message.includes("Cannot find module")) {
@@ -1 +1 @@
1
- {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\n/**\n * Generate types command implementation\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: { noCache?: boolean },\n) {\n try {\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (!resolvedWarehouseId) {\n process.exit(0);\n }\n\n // Try to import the type generator from @databricks/appkit\n const { generateFromEntryPoint } = await import(\n \"@databricks/appkit/type-generator\"\n );\n\n const resolvedRootDir = rootDir || process.cwd();\n const resolvedOutFile =\n outFile || path.join(process.cwd(), \"client/src/appKitTypes.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (!fs.existsSync(queryFolder)) {\n console.warn(\n `Warning: No queries found at ${queryFolder}. Skipping type generation.`,\n );\n return;\n }\n\n await generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache: options?.noCache || false,\n });\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n throw error;\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"client/src/appKitTypes.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .action(runGenerateTypes);\n"],"mappings":";;;;;;;;AAOA,eAAe,iBACb,SACA,SACA,aACA,SACA;AACA,KAAI;EACF,MAAM,sBACJ,eAAe,QAAQ,IAAI;AAE7B,MAAI,CAAC,oBACH,SAAQ,KAAK,EAAE;EAIjB,MAAM,EAAE,2BAA2B,MAAM,OACvC;EAGF,MAAM,kBAAkB,WAAW,QAAQ,KAAK;EAChD,MAAM,kBACJ,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,8BAA8B;EAEpE,MAAM,cAAc,KAAK,KAAK,iBAAiB,iBAAiB;AAChE,MAAI,CAAC,GAAG,WAAW,YAAY,EAAE;AAC/B,WAAQ,KACN,gCAAgC,YAAY,6BAC7C;AACD;;AAGF,QAAM,uBAAuB;GAC3B;GACA,SAAS;GACT,aAAa;GACb,SAAS,SAAS,WAAW;GAC9B,CAAC;UACK,OAAO;AACd,MACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,qBAAqB,EAC5C;AACA,WAAQ,MACN,+EACD;AACD,WAAQ,MAAM,yDAAyD;AACvE,WAAQ,KAAK,EAAE;;AAEjB,QAAM;;;AAIV,MAAa,uBAAuB,IAAI,QAAQ,iBAAiB,CAC9D,YAAY,6CAA6C,CACzD,SAAS,aAAa,iCAAiC,QAAQ,KAAK,CAAC,CACrE,SACC,aACA,oBACA,KAAK,KAAK,QAAQ,KAAK,EAAE,8BAA8B,CACxD,CACA,SAAS,iBAAiB,0BAA0B,CACpD,OAAO,cAAc,sCAAsC,CAC3D,OAAO,iBAAiB"}
1
+ {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\n/**\n * Generate types command implementation\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: { noCache?: boolean },\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile || path.join(process.cwd(), \"client/src/appKitTypes.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (fs.existsSync(queryFolder)) {\n await typeGen.generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n });\n }\n }\n\n // Generate serving endpoint types (no warehouse required)\n await typeGen.generateServingTypes({\n outFile: path.join(process.cwd(), \"client/src/appKitServingTypes.d.ts\"),\n noCache,\n });\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n throw error;\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"client/src/appKitTypes.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .action(runGenerateTypes);\n"],"mappings":";;;;;;;;AAOA,eAAe,iBACb,SACA,SACA,aACA,SACA;AACA,KAAI;EACF,MAAM,kBAAkB,WAAW,QAAQ,KAAK;EAChD,MAAM,UAAU,SAAS,WAAW;EAEpC,MAAM,UAAU,MAAM,OAAO;EAG7B,MAAM,sBACJ,eAAe,QAAQ,IAAI;AAE7B,MAAI,qBAAqB;GACvB,MAAM,kBACJ,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,8BAA8B;GAEpE,MAAM,cAAc,KAAK,KAAK,iBAAiB,iBAAiB;AAChE,OAAI,GAAG,WAAW,YAAY,CAC5B,OAAM,QAAQ,uBAAuB;IACnC;IACA,SAAS;IACT,aAAa;IACb;IACD,CAAC;;AAKN,QAAM,QAAQ,qBAAqB;GACjC,SAAS,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC;GACvE;GACD,CAAC;UACK,OAAO;AACd,MACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,qBAAqB,EAC5C;AACA,WAAQ,MACN,+EACD;AACD,WAAQ,MAAM,yDAAyD;AACvE,WAAQ,KAAK,EAAE;;AAEjB,QAAM;;;AAIV,MAAa,uBAAuB,IAAI,QAAQ,iBAAiB,CAC9D,YAAY,6CAA6C,CACzD,SAAS,aAAa,iCAAiC,QAAQ,KAAK,CAAC,CACrE,SACC,aACA,oBACA,KAAK,KAAK,QAAQ,KAAK,EAAE,8BAA8B,CACxD,CACA,SAAS,iBAAiB,0BAA0B,CACpD,OAAO,cAAc,sCAAsC,CAC3D,OAAO,iBAAiB"}
@@ -43,7 +43,7 @@ ${packages.map((pkg) => {
43
43
  For enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:
44
44
 
45
45
  \`\`\`bash
46
- databricks experimental aitools skills install
46
+ databricks experimental aitools install
47
47
  \`\`\`
48
48
  ${SECTION_END}`;
49
49
  }
@@ -68,7 +68,7 @@ ${packages.map((pkg) => {
68
68
  For enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:
69
69
 
70
70
  \`\`\`bash
71
- databricks experimental aitools skills install
71
+ databricks experimental aitools install
72
72
  \`\`\`
73
73
  ${SECTION_END}
74
74
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools skills install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools skills install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nMake sure you've installed at least one of:\");\n PACKAGES.forEach((pkg) => {\n console.log(` - ${pkg.name}`);\n });\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .action(runSetup);\n"],"mappings":";;;;;AAIA,MAAM,WAAW,CACf;CAAE,MAAM;CAAsB,aAAa;CAAe,EAC1D;CACE,MAAM;CACN,aAAa;CACd,CACF;AAED,MAAM,gBAAgB;AACtB,MAAM,cAAc;;;;AAKpB,SAAS,wBAAwB;CAC/B,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,YAAY,EAAE;AAEpB,MAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,cAAc,KAAK,KACvB,KACA,gBACA,IAAI,MACJ,eACD;AACD,MAAI,GAAG,WAAW,YAAY,CAC5B,WAAU,KAAK,IAAI;;AAIvB,QAAO;;;;;AAMT,SAAS,gBAAgB,UAA2B;AAQlD,QAAO,GAAG,cAAc;;;;;EAPV,SACX,KAAK,QAAQ;EACZ,MAAM,UAAU,kBAAkB,IAAI,KAAK;AAC3C,SAAO,OAAO,IAAI,KAAK,MAAM,IAAI,YAAY,MAAM,QAAQ,IAAI,QAAQ;GACvE,CACD,KAAK,KAAK,CAOP;;;;;;;;;EASN;;;;;AAMF,SAAS,mBAAmB,UAA2B;AAQrD,QAAO;;EAEP,cAAc;;;;;EATA,SACX,KAAK,QAAQ;EACZ,MAAM,UAAU,kBAAkB,IAAI,KAAK;AAC3C,SAAO,OAAO,IAAI,KAAK,MAAM,IAAI,YAAY,MAAM,QAAQ,IAAI,QAAQ;GACvE,CACD,KAAK,KAAK,CASP;;;;;;;;;EASN,YAAY;;;;;;AAOd,SAAS,cAAc,iBAAyB,UAA2B;CACzE,MAAM,aAAa,gBAAgB,SAAS;CAG5C,MAAM,aAAa,gBAAgB,QAAQ,cAAc;CACzD,MAAM,WAAW,gBAAgB,QAAQ,YAAY;AAErD,KAAI,eAAe,MAAM,aAAa,IAAI;EAExC,MAAM,SAAS,gBAAgB,UAAU,GAAG,WAAW;EACvD,MAAM,QAAQ,gBAAgB,UAAU,WAAW,GAAmB;AACtE,SAAO,SAAS,aAAa;;AAI/B,QAAO,GAAG,gBAAgB,SAAS,CAAC,MAAM,WAAW;;;;;AAMvD,SAAS,SAAS,SAA8B;CAC9C,MAAM,cAAc,QAAQ;CAG5B,MAAM,YAAY,uBAAuB;AAEzC,KAAI,UAAU,WAAW,GAAG;AAC1B,UAAQ,IAAI,wDAAwD;AACpE,UAAQ,IAAI,gDAAgD;AAC5D,WAAS,SAAS,QAAQ;AACxB,WAAQ,IAAI,OAAO,IAAI,OAAO;IAC9B;AACF,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,qBAAqB;AACjC,WAAU,SAAS,QAAQ;AACzB,UAAQ,IAAI,OAAO,IAAI,OAAO;GAC9B;CAEF,MAAM,aAAa,KAAK,KAAK,QAAQ,KAAK,EAAE,YAAY;CACxD,MAAM,kBAAkB,GAAG,WAAW,WAAW,GAC7C,GAAG,aAAa,YAAY,QAAQ,GACpC;CAEJ,IAAI;CACJ,IAAI;AAEJ,KAAI,iBAAiB;AACnB,iBAAe,cAAc,iBAAiB,UAAU;AACxD,WAAS,gBAAgB,SAAS,cAAc,GAAG,YAAY;QAC1D;AACL,iBAAe,mBAAmB,UAAU;AAC5C,WAAS;;AAGX,KAAI,aAAa;AACf,KAAG,cAAc,YAAY,aAAa;AAC1C,UAAQ,IAAI,OAAO,OAAO,YAAY;AACtC,UAAQ,IAAI,WAAW,aAAa;QAC/B;AACL,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,IAAI,+BAA+B;AAE3C,MAAI,gBACF,SAAQ,IACN,aACE,gBAAgB,SAAS,cAAc,GACnC,wBACA,YACL,oBACF;AAGH,UAAQ,IAAI,+BAA+B;AAC3C,UAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;AAC3B,UAAQ,IAAI,gBAAgB,UAAU,CAAC;AACvC,UAAQ,IAAI,IAAI,OAAO,GAAG,CAAC;;;AAI/B,MAAa,eAAe,IAAI,QAAQ,QAAQ,CAC7C,YAAY,iDAAiD,CAC7D,OAAO,eAAe,uDAAuD,CAC7E,OAAO,SAAS"}
1
+ {"version":3,"file":"setup.js","names":[],"sources":["../../../src/cli/commands/setup.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\nconst PACKAGES = [\n { name: \"@databricks/appkit\", description: \"Backend SDK\" },\n {\n name: \"@databricks/appkit-ui\",\n description: \"UI Integration, Charts, Tables, SSE, and more.\",\n },\n];\n\nconst SECTION_START = \"<!-- appkit-instructions-start -->\";\nconst SECTION_END = \"<!-- appkit-instructions-end -->\";\n\n/**\n * Find which AppKit packages are installed by checking for package.json\n */\nfunction findInstalledPackages() {\n const cwd = process.cwd();\n const installed = [];\n\n for (const pkg of PACKAGES) {\n const packagePath = path.join(\n cwd,\n \"node_modules\",\n pkg.name,\n \"package.json\",\n );\n if (fs.existsSync(packagePath)) {\n installed.push(pkg);\n }\n }\n\n return installed;\n}\n\n/**\n * Generate the AppKit section content\n */\nfunction generateSection(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools install\n\\`\\`\\`\n${SECTION_END}`;\n}\n\n/**\n * Generate standalone CLAUDE.md content (when no existing file)\n */\nfunction generateStandalone(packages: typeof PACKAGES) {\n const links = packages\n .map((pkg) => {\n const docPath = `./node_modules/${pkg.name}/CLAUDE.md`;\n return `- **${pkg.name}** (${pkg.description}): [${docPath}](${docPath})`;\n })\n .join(\"\\n\");\n\n return `# AI Assistant Instructions\n\n${SECTION_START}\n## Databricks AppKit\n\nThis project uses Databricks AppKit packages. For AI assistant guidance on using these packages, refer to:\n\n${links}\n\n### Databricks Skills\n\nFor enhanced AI assistance with Databricks CLI operations, authentication, data exploration, and app development, install the Databricks skills:\n\n\\`\\`\\`bash\ndatabricks experimental aitools install\n\\`\\`\\`\n${SECTION_END}\n`;\n}\n\n/**\n * Update existing content with AppKit section\n */\nfunction updateContent(existingContent: string, packages: typeof PACKAGES) {\n const newSection = generateSection(packages);\n\n // Check if AppKit section already exists\n const startIndex = existingContent.indexOf(SECTION_START);\n const endIndex = existingContent.indexOf(SECTION_END);\n\n if (startIndex !== -1 && endIndex !== -1) {\n // Replace existing section\n const before = existingContent.substring(0, startIndex);\n const after = existingContent.substring(endIndex + SECTION_END.length);\n return before + newSection + after;\n }\n\n // Append section to end\n return `${existingContent.trimEnd()}\\n\\n${newSection}\\n`;\n}\n\n/**\n * Setup command implementation\n */\nfunction runSetup(options: { write?: boolean }) {\n const shouldWrite = options.write;\n\n // Find installed packages\n const installed = findInstalledPackages();\n\n if (installed.length === 0) {\n console.log(\"No @databricks/appkit packages found in node_modules.\");\n console.log(\"\\nMake sure you've installed at least one of:\");\n PACKAGES.forEach((pkg) => {\n console.log(` - ${pkg.name}`);\n });\n process.exit(1);\n }\n\n console.log(\"Detected packages:\");\n installed.forEach((pkg) => {\n console.log(` ✓ ${pkg.name}`);\n });\n\n const claudePath = path.join(process.cwd(), \"CLAUDE.md\");\n const existingContent = fs.existsSync(claudePath)\n ? fs.readFileSync(claudePath, \"utf-8\")\n : null;\n\n let finalContent: string;\n let action: string;\n\n if (existingContent) {\n finalContent = updateContent(existingContent, installed);\n action = existingContent.includes(SECTION_START) ? \"Updated\" : \"Added to\";\n } else {\n finalContent = generateStandalone(installed);\n action = \"Created\";\n }\n\n if (shouldWrite) {\n fs.writeFileSync(claudePath, finalContent);\n console.log(`\\n✓ ${action} CLAUDE.md`);\n console.log(` Path: ${claudePath}`);\n } else {\n console.log(\"\\nTo create/update CLAUDE.md, run:\");\n console.log(\" npx appkit setup --write\\n\");\n\n if (existingContent) {\n console.log(\n `This will ${\n existingContent.includes(SECTION_START)\n ? \"update the existing\"\n : \"add a new\"\n } AppKit section.\\n`,\n );\n }\n\n console.log(\"Preview of AppKit section:\\n\");\n console.log(\"─\".repeat(50));\n console.log(generateSection(installed));\n console.log(\"─\".repeat(50));\n }\n}\n\nexport const setupCommand = new Command(\"setup\")\n .description(\"Setup CLAUDE.md with AppKit package references\")\n .option(\"-w, --write\", \"Create or update CLAUDE.md file in current directory\")\n .action(runSetup);\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { PluginClientConfigs, PluginEndpoints } from "../shared/src/plugin.js";
2
+ import "../shared/src/index.js";
3
+
4
+ //#region src/js/config.d.ts
5
+ interface AppKitClientConfig {
6
+ appName: string;
7
+ queries: Record<string, string>;
8
+ endpoints: PluginEndpoints;
9
+ plugins: PluginClientConfigs;
10
+ }
11
+ declare global {
12
+ interface Window {
13
+ __appkit__?: AppKitClientConfig;
14
+ }
15
+ }
16
+ /**
17
+ * @internal Reset the module-scoped config cache. Test utility only.
18
+ */
19
+ declare function _resetConfigCache(): void;
20
+ declare function getClientConfig(): AppKitClientConfig;
21
+ declare function getPluginClientConfig<T = Record<string, unknown>>(pluginName: string): T;
22
+ //#endregion
23
+ export { AppKitClientConfig, _resetConfigCache, getClientConfig, getPluginClientConfig };
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","names":[],"sources":["../../src/js/config.ts"],"mappings":";;;;UAEiB,kBAAA;EACf,OAAA;EACA,OAAA,EAAS,MAAA;EACT,SAAA,EAAW,eAAA;EACX,OAAA,EAAS,mBAAA;AAAA;AAAA,QAGH,MAAA;EAAA,UACI,MAAA;IACR,UAAA,GAAa,kBAAA;EAAA;AAAA;;;;iBAkDD,iBAAA,CAAA;AAAA,iBAIA,eAAA,CAAA,GAAmB,kBAAA;AAAA,iBAenB,qBAAA,KAA0B,MAAA,kBAAA,CACxC,UAAA,WACC,CAAA"}
@@ -0,0 +1,49 @@
1
+ //#region src/js/config.ts
2
+ const APPKIT_CONFIG_SCRIPT_ID = "__appkit__";
3
+ const EMPTY_CONFIG = Object.freeze({
4
+ appName: "",
5
+ queries: Object.freeze({}),
6
+ endpoints: Object.freeze({}),
7
+ plugins: Object.freeze({})
8
+ });
9
+ function normalizeClientConfig(config) {
10
+ if (!config || typeof config !== "object" || Array.isArray(config)) return EMPTY_CONFIG;
11
+ const normalized = config;
12
+ return {
13
+ appName: normalized.appName ?? EMPTY_CONFIG.appName,
14
+ queries: normalized.queries ?? EMPTY_CONFIG.queries,
15
+ endpoints: normalized.endpoints ?? EMPTY_CONFIG.endpoints,
16
+ plugins: normalized.plugins ?? EMPTY_CONFIG.plugins
17
+ };
18
+ }
19
+ function readClientConfigFromDom() {
20
+ if (typeof document === "undefined") return;
21
+ const configScript = document.getElementById(APPKIT_CONFIG_SCRIPT_ID);
22
+ if (!configScript?.textContent) return;
23
+ try {
24
+ return normalizeClientConfig(JSON.parse(configScript.textContent));
25
+ } catch (error) {
26
+ console.warn("[appkit] Failed to parse config from DOM:", error);
27
+ return;
28
+ }
29
+ }
30
+ let _cache;
31
+ /**
32
+ * @internal Reset the module-scoped config cache. Test utility only.
33
+ */
34
+ function _resetConfigCache() {
35
+ _cache = void 0;
36
+ }
37
+ function getClientConfig() {
38
+ if (typeof window === "undefined") return EMPTY_CONFIG;
39
+ if (!_cache) _cache = readClientConfigFromDom() ?? normalizeClientConfig(window.__appkit__);
40
+ return _cache;
41
+ }
42
+ const EMPTY_PLUGIN_CONFIG = Object.freeze({});
43
+ function getPluginClientConfig(pluginName) {
44
+ return getClientConfig().plugins[pluginName] ?? EMPTY_PLUGIN_CONFIG;
45
+ }
46
+
47
+ //#endregion
48
+ export { _resetConfigCache, getClientConfig, getPluginClientConfig };
49
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../src/js/config.ts"],"sourcesContent":["import type { PluginClientConfigs, PluginEndpoints } from \"shared\";\n\nexport interface AppKitClientConfig {\n appName: string;\n queries: Record<string, string>;\n endpoints: PluginEndpoints;\n plugins: PluginClientConfigs;\n}\n\ndeclare global {\n interface Window {\n __appkit__?: AppKitClientConfig;\n }\n}\n\nconst APPKIT_CONFIG_SCRIPT_ID = \"__appkit__\";\nconst EMPTY_CONFIG: AppKitClientConfig = Object.freeze({\n appName: \"\",\n queries: Object.freeze({}),\n endpoints: Object.freeze({}),\n plugins: Object.freeze({}),\n});\n\nfunction normalizeClientConfig(config: unknown): AppKitClientConfig {\n if (!config || typeof config !== \"object\" || Array.isArray(config)) {\n return EMPTY_CONFIG;\n }\n\n const normalized = config as Partial<AppKitClientConfig>;\n\n return {\n appName: normalized.appName ?? EMPTY_CONFIG.appName,\n queries: normalized.queries ?? EMPTY_CONFIG.queries,\n endpoints: normalized.endpoints ?? EMPTY_CONFIG.endpoints,\n plugins: normalized.plugins ?? EMPTY_CONFIG.plugins,\n };\n}\n\nfunction readClientConfigFromDom(): AppKitClientConfig | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n\n const configScript = document.getElementById(APPKIT_CONFIG_SCRIPT_ID);\n if (!configScript?.textContent) {\n return undefined;\n }\n\n try {\n return normalizeClientConfig(JSON.parse(configScript.textContent));\n } catch (error) {\n console.warn(\"[appkit] Failed to parse config from DOM:\", error);\n return undefined;\n }\n}\n\nlet _cache: AppKitClientConfig | undefined;\n\n/**\n * @internal Reset the module-scoped config cache. Test utility only.\n */\nexport function _resetConfigCache(): void {\n _cache = undefined;\n}\n\nexport function getClientConfig(): AppKitClientConfig {\n if (typeof window === \"undefined\") {\n return EMPTY_CONFIG;\n }\n\n if (!_cache) {\n _cache =\n readClientConfigFromDom() ?? normalizeClientConfig(window.__appkit__);\n }\n\n return _cache;\n}\n\nconst EMPTY_PLUGIN_CONFIG = Object.freeze({});\n\nexport function getPluginClientConfig<T = Record<string, unknown>>(\n pluginName: string,\n): T {\n return (getClientConfig().plugins[pluginName] ?? EMPTY_PLUGIN_CONFIG) as T;\n}\n"],"mappings":";AAeA,MAAM,0BAA0B;AAChC,MAAM,eAAmC,OAAO,OAAO;CACrD,SAAS;CACT,SAAS,OAAO,OAAO,EAAE,CAAC;CAC1B,WAAW,OAAO,OAAO,EAAE,CAAC;CAC5B,SAAS,OAAO,OAAO,EAAE,CAAC;CAC3B,CAAC;AAEF,SAAS,sBAAsB,QAAqC;AAClE,KAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,CAChE,QAAO;CAGT,MAAM,aAAa;AAEnB,QAAO;EACL,SAAS,WAAW,WAAW,aAAa;EAC5C,SAAS,WAAW,WAAW,aAAa;EAC5C,WAAW,WAAW,aAAa,aAAa;EAChD,SAAS,WAAW,WAAW,aAAa;EAC7C;;AAGH,SAAS,0BAA0D;AACjE,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,eAAe,SAAS,eAAe,wBAAwB;AACrE,KAAI,CAAC,cAAc,YACjB;AAGF,KAAI;AACF,SAAO,sBAAsB,KAAK,MAAM,aAAa,YAAY,CAAC;UAC3D,OAAO;AACd,UAAQ,KAAK,6CAA6C,MAAM;AAChE;;;AAIJ,IAAI;;;;AAKJ,SAAgB,oBAA0B;AACxC,UAAS;;AAGX,SAAgB,kBAAsC;AACpD,KAAI,OAAO,WAAW,YACpB,QAAO;AAGT,KAAI,CAAC,OACH,UACE,yBAAyB,IAAI,sBAAsB,OAAO,WAAW;AAGzE,QAAO;;AAGT,MAAM,sBAAsB,OAAO,OAAO,EAAE,CAAC;AAE7C,SAAgB,sBACd,YACG;AACH,QAAQ,iBAAiB,CAAC,QAAQ,eAAe"}
@@ -5,6 +5,7 @@ import { DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS } from
5
5
  import { ArrowClient, DetectedFields, Field, Table } from "./arrow/arrow-client.js";
6
6
  import { getArrowModule, initializeTypeIdSets } from "./arrow/lazy-arrow.js";
7
7
  import "./arrow/index.js";
8
+ import { AppKitClientConfig, _resetConfigCache, getClientConfig, getPluginClientConfig } from "./config.js";
8
9
  import { ConnectSSEOptions, SSEMessage } from "./sse/types.js";
9
10
  import { connectSSE } from "./sse/connect-sse.js";
10
- export { ArrowClient, ConnectSSEOptions, DATE_FIELD_PATTERNS, DetectedFields, Field, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, type SQLBinaryMarker, type SQLBooleanMarker, type SQLDateMarker, type SQLNumberMarker, type SQLStringMarker, type SQLTimestampMarker, type SQLTypeMarker, SSEMessage, Table, connectSSE, getArrowModule, initializeTypeIdSets, isSQLTypeMarker, sql };
11
+ export { AppKitClientConfig, ArrowClient, ConnectSSEOptions, DATE_FIELD_PATTERNS, DetectedFields, Field, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, type SQLBinaryMarker, type SQLBooleanMarker, type SQLDateMarker, type SQLNumberMarker, type SQLStringMarker, type SQLTimestampMarker, type SQLTypeMarker, SSEMessage, Table, _resetConfigCache, connectSSE, getArrowModule, getClientConfig, getPluginClientConfig, initializeTypeIdSets, isSQLTypeMarker, sql };
package/dist/js/index.js CHANGED
@@ -3,6 +3,7 @@ import { DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS } from
3
3
  import { getArrowModule, initializeTypeIdSets } from "./arrow/lazy-arrow.js";
4
4
  import { ArrowClient } from "./arrow/arrow-client.js";
5
5
  import "./arrow/index.js";
6
+ import { _resetConfigCache, getClientConfig, getPluginClientConfig } from "./config.js";
6
7
  import { connectSSE } from "./sse/connect-sse.js";
7
8
 
8
- export { ArrowClient, DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, connectSSE, getArrowModule, initializeTypeIdSets, isSQLTypeMarker, sql };
9
+ export { ArrowClient, DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS, _resetConfigCache, connectSSE, getArrowModule, getClientConfig, getPluginClientConfig, initializeTypeIdSets, isSQLTypeMarker, sql };
@@ -1 +1 @@
1
- {"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"mappings":";;;;UAQU,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":";;;;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"}
@@ -3,6 +3,7 @@ import { Skeleton } from "../ui/skeleton.js";
3
3
  import { ScrollArea } from "../ui/scroll-area.js";
4
4
  import { Spinner } from "../ui/spinner.js";
5
5
  import { GenieChatMessage } from "./genie-chat-message.js";
6
+ import { TERMINAL_STATUSES } from "./types.js";
6
7
  import { useEffect, useLayoutEffect, useRef } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
8
9
 
@@ -94,12 +95,12 @@ function GenieChatMessageList({ messages, status, className, hasPreviousPage = f
94
95
  const sentinelRef = useLoadOlderOnScroll(scrollRef, hasPreviousPage && status !== "loading-older", onFetchPreviousPage);
95
96
  useScrollManagement(scrollRef, messages, status);
96
97
  const lastMessage = messages[messages.length - 1];
97
- const showStreamingIndicator = status === "streaming" && lastMessage?.role === "assistant" && lastMessage.id === "";
98
+ const showStreamingIndicator = status === "streaming" && lastMessage?.role === "assistant" && !lastMessage.content && !TERMINAL_STATUSES.has(lastMessage.status);
98
99
  return /* @__PURE__ */ jsx(ScrollArea, {
99
100
  ref: scrollRef,
100
- className: cn("flex-1 min-h-0 p-4", className),
101
+ className: cn("flex-1 min-h-0 p-4 [&_[data-slot=scroll-area-viewport]>div]:!block", className),
101
102
  children: /* @__PURE__ */ jsxs("div", {
102
- className: "flex flex-col gap-4",
103
+ className: "flex flex-col gap-4 min-w-0",
103
104
  children: [
104
105
  hasPreviousPage && /* @__PURE__ */ jsx("div", {
105
106
  ref: sentinelRef,
@@ -120,7 +121,7 @@ function GenieChatMessageList({ messages, status, className, hasPreviousPage = f
120
121
  /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-2/3 self-end" })
121
122
  ]
122
123
  }),
123
- messages.filter((msg) => msg.role !== "assistant" || msg.id !== "" || msg.content).map((msg) => /* @__PURE__ */ jsx(GenieChatMessage, { message: msg }, msg.id)),
124
+ messages.filter((msg) => msg.role !== "assistant" || msg.content || msg.id !== "" && TERMINAL_STATUSES.has(msg.status)).map((msg) => /* @__PURE__ */ jsx(GenieChatMessage, { message: msg }, msg.id)),
124
125
  showStreamingIndicator && /* @__PURE__ */ jsxs("div", {
125
126
  className: "flex items-center gap-2 text-sm text-muted-foreground px-11",
126
127
  children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", { children: formatStatus(lastMessage.status) })]
@@ -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 type { GenieChatStatus, GenieMessageItem } 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.id === \"\";\n\n return (\n <ScrollArea ref={scrollRef} className={cn(\"flex-1 min-h-0 p-4\", className)}>\n <div className=\"flex flex-col gap-4\">\n {hasPreviousPage && <div ref={sentinelRef} className=\"h-px\" />}\n\n {status === \"loading-older\" && (\n <div className=\"flex items-center justify-center gap-2 py-2\">\n <Spinner className=\"h-3 w-3\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading older messages...\n </span>\n </div>\n )}\n\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages\n .filter(\n (msg) => msg.role !== \"assistant\" || msg.id !== \"\" || msg.content,\n )\n .map((msg) => (\n <GenieChatMessage key={msg.id} message={msg} />\n ))}\n\n {showStreamingIndicator && (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(lastMessage.status)}</span>\n </div>\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":"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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\";\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 +1 @@
1
- {"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;UA0BU,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":";;;;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"}
@@ -4,19 +4,20 @@ import { Avatar, AvatarFallback } from "../ui/avatar.js";
4
4
  import { GenieQueryVisualization } from "./genie-query-visualization.js";
5
5
  import { useMemo } from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
+ import DOMPurify from "dompurify";
7
8
  import { marked } from "marked";
8
9
 
9
10
  //#region src/react/genie/genie-chat-message.tsx
10
11
  /**
11
12
  * Using `marked` instead of `react-markdown` because `react-markdown` depends on
12
13
  * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.
13
- * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.
14
+ * Output is sanitized with DOMPurify before being passed to `dangerouslySetInnerHTML`.
14
15
  */
15
16
  marked.setOptions({
16
17
  breaks: true,
17
18
  gfm: true
18
19
  });
19
- const markdownStyles = cn("text-sm", "[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0", "[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto", "[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded", "[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1", "[&_table]:border-collapse [&_th]:border [&_td]:border", "[&_th]:border-border [&_td]:border-border", "[&_a]:underline");
20
+ const markdownStyles = cn("text-sm break-words", "[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0", "[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto", "[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded", "[&_table]:text-xs [&_table]:block [&_table]:overflow-x-auto [&_table]:max-w-full", "[&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1", "[&_table]:border-collapse [&_th]:border [&_td]:border", "[&_th]:border-border [&_td]:border-border", "[&_a]:underline");
20
21
  function isQueryAttachment(att) {
21
22
  return !!(att.query?.title || att.query?.query);
22
23
  }
@@ -24,7 +25,7 @@ function isQueryAttachment(att) {
24
25
  function GenieChatMessage({ message, className }) {
25
26
  const isUser = message.role === "user";
26
27
  const queryAttachments = message.attachments.filter(isQueryAttachment);
27
- const html = useMemo(() => message.content ? marked.parse(message.content) : "", [message.content]);
28
+ const html = useMemo(() => message.content ? DOMPurify.sanitize(marked.parse(message.content)) : "", [message.content]);
28
29
  return /* @__PURE__ */ jsxs("div", {
29
30
  className: cn("flex gap-3", isUser ? "flex-row-reverse" : "flex-row", className),
30
31
  children: [/* @__PURE__ */ jsx(Avatar, {
@@ -34,9 +35,9 @@ function GenieChatMessage({ message, className }) {
34
35
  children: isUser ? "You" : "AI"
35
36
  })
36
37
  }), /* @__PURE__ */ jsxs("div", {
37
- className: cn("flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden", isUser ? "items-end" : "items-start"),
38
+ className: "flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden",
38
39
  children: [/* @__PURE__ */ jsxs(Card, {
39
- className: cn("px-4 py-3 max-w-full overflow-hidden", isUser ? "bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30" : "bg-muted"),
40
+ className: cn("w-full px-4 py-3 overflow-hidden", isUser ? "bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30" : "bg-muted"),
40
41
  children: [html && /* @__PURE__ */ jsx("div", {
41
42
  className: markdownStyles,
42
43
  dangerouslySetInnerHTML: { __html: html }
@@ -67,7 +68,7 @@ function GenieChatMessage({ message, className }) {
67
68
  })]
68
69
  })] })
69
70
  }), queryResult != null && /* @__PURE__ */ jsx(Card, {
70
- className: "px-4 py-3 overflow-hidden",
71
+ className: "w-full px-4 py-3 overflow-hidden",
71
72
  children: /* @__PURE__ */ jsx(GenieQueryVisualization, { data: queryResult })
72
73
  })]
73
74
  }, key);
@@ -1 +1 @@
1
- {"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import { marked } from \"marked\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar, AvatarFallback } from \"../ui/avatar\";\nimport { Card } from \"../ui/card\";\nimport { GenieQueryVisualization } from \"./genie-query-visualization\";\nimport type { GenieAttachmentResponse, GenieMessageItem } from \"./types\";\n\n/**\n * Using `marked` instead of `react-markdown` because `react-markdown` depends on\n * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.\n * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm\",\n \"[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0\",\n \"[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto\",\n \"[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded\",\n \"[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1\",\n \"[&_table]:border-collapse [&_th]:border [&_td]:border\",\n \"[&_th]:border-border [&_td]:border-border\",\n \"[&_a]:underline\",\n);\n\ninterface GenieChatMessageProps {\n /** The message object to render */\n message: GenieMessageItem;\n /** Additional CSS class */\n className?: string;\n}\n\nfunction isQueryAttachment(att: GenieAttachmentResponse): boolean {\n return !!(att.query?.title || att.query?.query);\n}\n\n/** Renders a single Genie message bubble with optional expandable SQL query attachments. */\nexport function GenieChatMessage({\n message,\n className,\n}: GenieChatMessageProps) {\n const isUser = message.role === \"user\";\n const queryAttachments = message.attachments.filter(isQueryAttachment);\n const html = useMemo(\n () => (message.content ? (marked.parse(message.content) as string) : \"\"),\n [message.content],\n );\n\n return (\n <div\n className={cn(\n \"flex gap-3\",\n isUser ? \"flex-row-reverse\" : \"flex-row\",\n className,\n )}\n >\n <Avatar className=\"h-8 w-8 shrink-0 mt-1\">\n <AvatarFallback\n className={cn(\n \"text-xs font-medium\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {isUser ? \"You\" : \"AI\"}\n </AvatarFallback>\n </Avatar>\n\n <div\n className={cn(\n \"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\",\n isUser ? \"items-end\" : \"items-start\",\n )}\n >\n <Card\n className={cn(\n \"px-4 py-3 max-w-full overflow-hidden\",\n isUser\n ? \"bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30\"\n : \"bg-muted\",\n )}\n >\n {html && (\n <div\n className={markdownStyles}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {message.error && (\n <p className=\"text-sm text-destructive mt-1\">{message.error}</p>\n )}\n </Card>\n\n {queryAttachments.length > 0 && (\n <div className=\"flex flex-col gap-2 w-full min-w-0\">\n {queryAttachments.map((att) => {\n const key = att.attachmentId ?? \"query\";\n const queryResult = att.attachmentId\n ? message.queryResults.get(att.attachmentId)\n : undefined;\n\n return (\n <div key={key} className=\"flex flex-col gap-2\">\n <Card className=\"px-4 py-3 text-xs overflow-hidden shadow-none\">\n <details>\n <summary className=\"cursor-pointer select-none font-medium\">\n {att.query?.title ?? \"SQL Query\"}\n </summary>\n <div className=\"mt-2 flex flex-col gap-1\">\n {att.query?.description && (\n <span className=\"text-muted-foreground\">\n {att.query.description}\n </span>\n )}\n {att.query?.query && (\n <pre className=\"mt-1 p-2 rounded bg-background text-[11px] whitespace-pre-wrap break-all\">\n {att.query.query}\n </pre>\n )}\n </div>\n </details>\n </Card>\n {queryResult != null && (\n <Card className=\"px-4 py-3 overflow-hidden\">\n <GenieQueryVisualization data={queryResult} />\n </Card>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,OAAO,WAAW;CAAE,QAAQ;CAAM,KAAK;CAAM,CAAC;AAE9C,MAAM,iBAAiB,GACrB,WACA,kDACA,gGACA,6EACA,qEACA,yDACA,6CACA,kBACD;AASD,SAAS,kBAAkB,KAAuC;AAChE,QAAO,CAAC,EAAE,IAAI,OAAO,SAAS,IAAI,OAAO;;;AAI3C,SAAgB,iBAAiB,EAC/B,SACA,aACwB;CACxB,MAAM,SAAS,QAAQ,SAAS;CAChC,MAAM,mBAAmB,QAAQ,YAAY,OAAO,kBAAkB;CACtE,MAAM,OAAO,cACJ,QAAQ,UAAW,OAAO,MAAM,QAAQ,QAAQ,GAAc,IACrE,CAAC,QAAQ,QAAQ,CAClB;AAED,QACE,qBAAC;EACC,WAAW,GACT,cACA,SAAS,qBAAqB,YAC9B,UACD;aAED,oBAAC;GAAO,WAAU;aAChB,oBAAC;IACC,WAAW,GACT,uBACA,SAAS,uCAAuC,WACjD;cAEA,SAAS,QAAQ;KACH;IACV,EAET,qBAAC;GACC,WAAW,GACT,2DACA,SAAS,cAAc,cACxB;cAED,qBAAC;IACC,WAAW,GACT,wCACA,SACI,yHACA,WACL;eAEA,QACC,oBAAC;KACC,WAAW;KACX,yBAAyB,EAAE,QAAQ,MAAM;MACzC,EAGH,QAAQ,SACP,oBAAC;KAAE,WAAU;eAAiC,QAAQ;MAAU;KAE7D,EAEN,iBAAiB,SAAS,KACzB,oBAAC;IAAI,WAAU;cACZ,iBAAiB,KAAK,QAAQ;KAC7B,MAAM,MAAM,IAAI,gBAAgB;KAChC,MAAM,cAAc,IAAI,eACpB,QAAQ,aAAa,IAAI,IAAI,aAAa,GAC1C;AAEJ,YACE,qBAAC;MAAc,WAAU;iBACvB,oBAAC;OAAK,WAAU;iBACd,qBAAC,wBACC,oBAAC;QAAQ,WAAU;kBAChB,IAAI,OAAO,SAAS;SACb,EACV,qBAAC;QAAI,WAAU;mBACZ,IAAI,OAAO,eACV,oBAAC;SAAK,WAAU;mBACb,IAAI,MAAM;UACN,EAER,IAAI,OAAO,SACV,oBAAC;SAAI,WAAU;mBACZ,IAAI,MAAM;UACP;SAEJ,IACE;QACL,EACN,eAAe,QACd,oBAAC;OAAK,WAAU;iBACd,oBAAC,2BAAwB,MAAM,cAAe;QACzC;QAvBD,IAyBJ;MAER;KACE;IAEJ;GACF"}
1
+ {"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import DOMPurify from \"dompurify\";\nimport { marked } from \"marked\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar, AvatarFallback } from \"../ui/avatar\";\nimport { Card } from \"../ui/card\";\nimport { GenieQueryVisualization } from \"./genie-query-visualization\";\nimport type { GenieAttachmentResponse, GenieMessageItem } from \"./types\";\n\n/**\n * Using `marked` instead of `react-markdown` because `react-markdown` depends on\n * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.\n * Output is sanitized with DOMPurify before being passed to `dangerouslySetInnerHTML`.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm break-words\",\n \"[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0\",\n \"[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto\",\n \"[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded\",\n \"[&_table]:text-xs [&_table]:block [&_table]:overflow-x-auto [&_table]:max-w-full\",\n \"[&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1\",\n \"[&_table]:border-collapse [&_th]:border [&_td]:border\",\n \"[&_th]:border-border [&_td]:border-border\",\n \"[&_a]:underline\",\n);\n\ninterface GenieChatMessageProps {\n /** The message object to render */\n message: GenieMessageItem;\n /** Additional CSS class */\n className?: string;\n}\n\nfunction isQueryAttachment(att: GenieAttachmentResponse): boolean {\n return !!(att.query?.title || att.query?.query);\n}\n\n/** Renders a single Genie message bubble with optional expandable SQL query attachments. */\nexport function GenieChatMessage({\n message,\n className,\n}: GenieChatMessageProps) {\n const isUser = message.role === \"user\";\n const queryAttachments = message.attachments.filter(isQueryAttachment);\n const html = useMemo(\n () =>\n message.content\n ? DOMPurify.sanitize(marked.parse(message.content) as string)\n : \"\",\n [message.content],\n );\n\n return (\n <div\n className={cn(\n \"flex gap-3\",\n isUser ? \"flex-row-reverse\" : \"flex-row\",\n className,\n )}\n >\n <Avatar className=\"h-8 w-8 shrink-0 mt-1\">\n <AvatarFallback\n className={cn(\n \"text-xs font-medium\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {isUser ? \"You\" : \"AI\"}\n </AvatarFallback>\n </Avatar>\n\n <div className=\"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\">\n <Card\n className={cn(\n \"w-full px-4 py-3 overflow-hidden\",\n isUser\n ? \"bg-primary text-primary-foreground [&_*::selection]:bg-primary-foreground/30 [&::selection]:bg-primary-foreground/30\"\n : \"bg-muted\",\n )}\n >\n {html && (\n <div\n className={markdownStyles}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {message.error && (\n <p className=\"text-sm text-destructive mt-1\">{message.error}</p>\n )}\n </Card>\n\n {queryAttachments.length > 0 && (\n <div className=\"flex flex-col gap-2 w-full min-w-0\">\n {queryAttachments.map((att) => {\n const key = att.attachmentId ?? \"query\";\n const queryResult = att.attachmentId\n ? message.queryResults.get(att.attachmentId)\n : undefined;\n\n return (\n <div key={key} className=\"flex flex-col gap-2\">\n <Card className=\"px-4 py-3 text-xs overflow-hidden shadow-none\">\n <details>\n <summary className=\"cursor-pointer select-none font-medium\">\n {att.query?.title ?? \"SQL Query\"}\n </summary>\n <div className=\"mt-2 flex flex-col gap-1\">\n {att.query?.description && (\n <span className=\"text-muted-foreground\">\n {att.query.description}\n </span>\n )}\n {att.query?.query && (\n <pre className=\"mt-1 p-2 rounded bg-background text-[11px] whitespace-pre-wrap break-all\">\n {att.query.query}\n </pre>\n )}\n </div>\n </details>\n </Card>\n {queryResult != null && (\n <Card className=\"w-full px-4 py-3 overflow-hidden\">\n <GenieQueryVisualization data={queryResult} />\n </Card>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,OAAO,WAAW;CAAE,QAAQ;CAAM,KAAK;CAAM,CAAC;AAE9C,MAAM,iBAAiB,GACrB,uBACA,kDACA,gGACA,6EACA,oFACA,mDACA,yDACA,6CACA,kBACD;AASD,SAAS,kBAAkB,KAAuC;AAChE,QAAO,CAAC,EAAE,IAAI,OAAO,SAAS,IAAI,OAAO;;;AAI3C,SAAgB,iBAAiB,EAC/B,SACA,aACwB;CACxB,MAAM,SAAS,QAAQ,SAAS;CAChC,MAAM,mBAAmB,QAAQ,YAAY,OAAO,kBAAkB;CACtE,MAAM,OAAO,cAET,QAAQ,UACJ,UAAU,SAAS,OAAO,MAAM,QAAQ,QAAQ,CAAW,GAC3D,IACN,CAAC,QAAQ,QAAQ,CAClB;AAED,QACE,qBAAC;EACC,WAAW,GACT,cACA,SAAS,qBAAqB,YAC9B,UACD;aAED,oBAAC;GAAO,WAAU;aAChB,oBAAC;IACC,WAAW,GACT,uBACA,SAAS,uCAAuC,WACjD;cAEA,SAAS,QAAQ;KACH;IACV,EAET,qBAAC;GAAI,WAAU;cACb,qBAAC;IACC,WAAW,GACT,oCACA,SACI,yHACA,WACL;eAEA,QACC,oBAAC;KACC,WAAW;KACX,yBAAyB,EAAE,QAAQ,MAAM;MACzC,EAGH,QAAQ,SACP,oBAAC;KAAE,WAAU;eAAiC,QAAQ;MAAU;KAE7D,EAEN,iBAAiB,SAAS,KACzB,oBAAC;IAAI,WAAU;cACZ,iBAAiB,KAAK,QAAQ;KAC7B,MAAM,MAAM,IAAI,gBAAgB;KAChC,MAAM,cAAc,IAAI,eACpB,QAAQ,aAAa,IAAI,IAAI,aAAa,GAC1C;AAEJ,YACE,qBAAC;MAAc,WAAU;iBACvB,oBAAC;OAAK,WAAU;iBACd,qBAAC,wBACC,oBAAC;QAAQ,WAAU;kBAChB,IAAI,OAAO,SAAS;SACb,EACV,qBAAC;QAAI,WAAU;mBACZ,IAAI,OAAO,eACV,oBAAC;SAAK,WAAU;mBACb,IAAI,MAAM;UACN,EAER,IAAI,OAAO,SACV,oBAAC;SAAI,WAAU;mBACZ,IAAI,MAAM;UACP;SAEJ,IACE;QACL,EACN,eAAe,QACd,oBAAC;OAAK,WAAU;iBACd,oBAAC,2BAAwB,MAAM,cAAe;QACzC;QAvBD,IAyBJ;MAER;KACE;IAEJ;GACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"genie-query-visualization.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"mappings":";;;;;UA4CU,4BAAA;;EAER,IAAA,EAAM,sBAAA;;EAEN,SAAA;AAAA;;;;;;;;iBAUc,uBAAA,CAAA;EACd,IAAA;EACA;AAAA,GACC,4BAAA,GAA4B,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"genie-query-visualization.d.ts","names":[],"sources":["../../../src/react/genie/genie-query-visualization.tsx"],"mappings":";;;;;UA6CU,4BAAA;;EAER,IAAA,EAAM,sBAAA;;EAEN,SAAA;AAAA;;;;;;;;iBAUc,uBAAA,CAAA;EACd,IAAA;EACA;AAAA,GACC,4BAAA,GAA4B,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1,5 +1,6 @@
1
1
  import { BaseChart } from "../charts/base.js";
2
2
  import { ChartErrorBoundary } from "../charts/chart-error-boundary.js";
3
+ import { cn } from "../lib/utils.js";
3
4
  import { Button } from "../ui/button.js";
4
5
  import { getCompatibleChartTypes, inferChartType } from "./genie-chart-inference.js";
5
6
  import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
@@ -63,12 +64,12 @@ function GenieQueryVisualization({ data, className }) {
63
64
  })]
64
65
  });
65
66
  if (!inference || !activeChartType) return /* @__PURE__ */ jsx("div", {
66
- className,
67
+ className: cn("min-w-0", className),
67
68
  children: dataTable
68
69
  });
69
70
  return /* @__PURE__ */ jsxs(Tabs, {
70
71
  defaultValue: "chart",
71
- className,
72
+ className: cn("min-w-0", className),
72
73
  children: [/* @__PURE__ */ jsxs("div", {
73
74
  className: "flex items-center justify-between",
74
75
  children: [/* @__PURE__ */ jsxs(TabsList, { children: [/* @__PURE__ */ jsx(TabsTrigger, {
@@ -98,7 +99,7 @@ function GenieQueryVisualization({ data, className }) {
98
99
  })]
99
100
  })] })]
100
101
  }), /* @__PURE__ */ jsxs("div", {
101
- className: "grid [&>*]:col-start-1 [&>*]:row-start-1",
102
+ className: "grid min-w-0 [&>*]:col-start-1 [&>*]:row-start-1 [&>*]:min-w-0",
102
103
  children: [/* @__PURE__ */ jsx(TabsContent, {
103
104
  value: "chart",
104
105
  forceMount: true,