@databricks/appkit-ui 0.42.0 → 0.43.1

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 (34) hide show
  1. package/dist/cli/commands/generate-types.js +5 -0
  2. package/dist/cli/commands/generate-types.js.map +1 -1
  3. package/dist/js/index.js +0 -2
  4. package/dist/react/charts/base.d.ts +11 -1
  5. package/dist/react/charts/base.d.ts.map +1 -1
  6. package/dist/react/charts/base.js +23 -2
  7. package/dist/react/charts/base.js.map +1 -1
  8. package/dist/react/charts/normalize.js +0 -1
  9. package/dist/react/charts/normalize.js.map +1 -1
  10. package/dist/react/genie/use-genie-chat.js +0 -1
  11. package/dist/react/genie/use-genie-chat.js.map +1 -1
  12. package/dist/react/hooks/use-agent-chat.js +0 -1
  13. package/dist/react/hooks/use-agent-chat.js.map +1 -1
  14. package/dist/react/hooks/use-analytics-query.js +0 -1
  15. package/dist/react/hooks/use-analytics-query.js.map +1 -1
  16. package/dist/react/hooks/use-plugin-config.js +0 -1
  17. package/dist/react/hooks/use-plugin-config.js.map +1 -1
  18. package/dist/react/hooks/use-serving-stream.js +0 -1
  19. package/dist/react/hooks/use-serving-stream.js.map +1 -1
  20. package/dist/react/index.js +0 -6
  21. package/dist/react/table/table-wrapper.js +0 -1
  22. package/dist/react/table/table-wrapper.js.map +1 -1
  23. package/docs/development/type-generation.md +28 -2
  24. package/package.json +4 -1
  25. package/sbom.cdx.json +1 -1
  26. package/dist/js/arrow/index.js +0 -3
  27. package/dist/react/charts/index.js +0 -19
  28. package/dist/react/file-browser/index.js +0 -6
  29. package/dist/react/genie/index.js +0 -8
  30. package/dist/react/hooks/index.js +0 -9
  31. package/dist/react/table/index.js +0 -1
  32. package/dist/react/ui/index.js +0 -53
  33. package/dist/shared/src/index.js +0 -2
  34. package/dist/shared/src/sql/index.js +0 -1
@@ -42,6 +42,11 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
42
42
  mode
43
43
  });
44
44
  console.log(`Generated query types: ${resolvedOutFile}`);
45
+ const metricConfig = path.join(queryFolder, "metric-views.json");
46
+ if (fs.existsSync(metricConfig)) {
47
+ const typesDir = path.dirname(resolvedOutFile);
48
+ console.log(`Generated metric types: ${path.join(typesDir, "metric-views.d.ts")}`);
49
+ }
45
50
  }
46
51
  } else console.error("Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.");
47
52
  const servingOutFile = path.join(process.cwd(), "shared/appkit-types/serving.d.ts");
@@ -1 +1 @@
1
- {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\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 ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.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 mode,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\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 // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\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(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\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 ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.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 mode,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n\n const metricConfig = path.join(queryFolder, \"metric-views.json\");\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.d.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\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 // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\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(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
package/dist/js/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import { isSQLTypeMarker, sql } from "../shared/src/sql/helpers.js";
2
- import "../shared/src/index.js";
3
2
  import { DATE_FIELD_PATTERNS, METADATA_DATE_PATTERNS, NAME_FIELD_PATTERNS } from "./constants.js";
4
3
  import { getArrowModule, initializeTypeIdSets } from "./arrow/lazy-arrow.js";
5
4
  import { ArrowClient } from "./arrow/arrow-client.js";
6
- import "./arrow/index.js";
7
5
  import { _resetConfigCache, getClientConfig, getPluginClientConfig } from "./config.js";
8
6
  import { connectSSE } from "./sse/connect-sse.js";
9
7
 
@@ -50,7 +50,17 @@ interface BaseChartProps {
50
50
  min?: number;
51
51
  /** Max value for heatmap color scale */
52
52
  max?: number;
53
- /** Additional ECharts options to merge */
53
+ /**
54
+ * Additional ECharts options to merge.
55
+ *
56
+ * Only the built-in feature set is registered by this package, so options
57
+ * referencing extra ECharts features (`dataZoom`, `toolbox`, `markLine`,
58
+ * `markArea`, `graphic`, `dataset`, top-level `axisPointer`, ...) require
59
+ * registering them in your app via `import { use } from "echarts/core"`.
60
+ * This only works when your `echarts` resolves to the same module
61
+ * instance/version as this package's (the registry is a singleton;
62
+ * duplicate echarts copies won't share registrations).
63
+ */
54
64
  options?: Record<string, unknown>;
55
65
  /** Additional CSS classes */
56
66
  className?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","names":[],"sources":["../../../src/react/charts/base.tsx"],"mappings":";;;;UA0CiB,cAAA;;EAEf,IAAA,EAAM,SAAA;EAFS;EAIf,SAAA,EAAW,SAAA;;EAEX,IAAA;EAFW;EAIX,IAAA;EAee;EAbf,WAAA,GAAc,WAAA;EAuCE;EArChB,MAAA;EAVA;EAYA,KAAA;EAVA;EAYA,UAAA;EAVA;;;;;;EAiBA,YAAA,GAAe,iBAAA;EAAf;EAEA,MAAA;EAAA;EAEA,UAAA;EAEA;EAAA,MAAA;EAIA;EAFA,OAAA;EAMA;EAJA,UAAA;EAQA;EANA,QAAA;EAUA;EARA,WAAA;EAYA;EAVA,UAAA;EAYA;EAVA,aAAA;EAUS;EART,QAAA;EAmBuB;EAjBvB,GAAA;EAkBA;EAhBA,GAAA;EAkBA;EAhBA,OAAA,GAAU,MAAA;EAkBV;EAhBA,SAAA;AAAA;;;;;iBAWc,SAAA,CAAA;EACd,IAAA;EACA,SAAA;EACA,IAAA;EACA,IAAA;EACA,WAAA;EACA,MAAA;EACA,KAAA;EACA,UAAA;EACA,YAAA;EACA,MAAA,EAAQ,YAAA;EACR,UAAA;EACA,MAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA;EACA,WAAA;EACA,UAAA;EACA,aAAA;EACA,QAAA;EACA,GAAA;EACA,GAAA;EACA,OAAA,EAAS,aAAA;EACT;AAAA,GACC,cAAA,GAAc,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"base.d.ts","names":[],"sources":["../../../src/react/charts/base.tsx"],"mappings":";;;;UA4GiB,cAAA;;EAEf,IAAA,EAAM,SAAA;EAFS;EAIf,SAAA,EAAW,SAAA;;EAEX,IAAA;EAFW;EAIX,IAAA;EAee;EAbf,WAAA,GAAc,WAAA;EAiDE;EA/ChB,MAAA;EAVA;EAYA,KAAA;EAVA;EAYA,UAAA;EAVA;;;;;;EAiBA,YAAA,GAAe,iBAAA;EAAf;EAEA,MAAA;EAAA;EAEA,UAAA;EAEA;EAAA,MAAA;EAIA;EAFA,OAAA;EAMA;EAJA,UAAA;EAQA;EANA,QAAA;EAUA;EARA,WAAA;EAsBA;EApBA,UAAA;EAsBA;EApBA,aAAA;EAoBS;EAlBT,QAAA;EA6BuB;EA3BvB,GAAA;EA4BA;EA1BA,GAAA;EA4BA;;;;;;;;;;;EAhBA,OAAA,GAAU,MAAA;EA4BV;EA1BA,SAAA;AAAA;;;;;iBAWc,SAAA,CAAA;EACd,IAAA;EACA,SAAA;EACA,IAAA;EACA,IAAA;EACA,WAAA;EACA,MAAA;EACA,KAAA;EACA,UAAA;EACA,YAAA;EACA,MAAA,EAAQ,YAAA;EACR,UAAA;EACA,MAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA;EACA,WAAA;EACA,UAAA;EACA,aAAA;EACA,QAAA;EACA,GAAA;EACA,GAAA;EACA,OAAA,EAAS,aAAA;EACT;AAAA,GACC,cAAA,GAAc,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1,11 +1,31 @@
1
1
  import { normalizeChartData, normalizeHeatmapData } from "./normalize.js";
2
2
  import { buildCartesianOption, buildHeatmapOption, buildHorizontalBarOption, buildPieOption, buildRadarOption } from "./options.js";
3
3
  import { useChartUITokens, useThemeColors } from "./theme.js";
4
- import ReactECharts from "echarts-for-react";
4
+ import { BarChart, HeatmapChart, LineChart, PieChart, RadarChart, ScatterChart } from "echarts/charts";
5
+ import { GridComponent, LegendComponent, TitleComponent, TooltipComponent, VisualMapComponent } from "echarts/components";
6
+ import * as echarts from "echarts/core";
7
+ import { LegacyGridContainLabel } from "echarts/features";
8
+ import { CanvasRenderer } from "echarts/renderers";
9
+ import ReactEChartsCore from "echarts-for-react/esm/core";
5
10
  import { useCallback, useMemo, useRef } from "react";
6
11
  import { jsx } from "react/jsx-runtime";
7
12
 
8
13
  //#region src/react/charts/base.tsx
14
+ echarts.use([
15
+ LineChart,
16
+ BarChart,
17
+ PieChart,
18
+ ScatterChart,
19
+ HeatmapChart,
20
+ RadarChart,
21
+ TitleComponent,
22
+ TooltipComponent,
23
+ LegendComponent,
24
+ GridComponent,
25
+ VisualMapComponent,
26
+ LegacyGridContainLabel,
27
+ CanvasRenderer
28
+ ]);
9
29
  /**
10
30
  * Determines the appropriate color palette for a chart type.
11
31
  * - Heatmaps use sequential (low → high intensity)
@@ -111,8 +131,9 @@ function BaseChart({ data, chartType, xKey, yKey, orientation, height = 300, tit
111
131
  className: "flex items-center justify-center h-full text-muted-foreground",
112
132
  children: "No data"
113
133
  });
114
- return /* @__PURE__ */ jsx(ReactECharts, {
134
+ return /* @__PURE__ */ jsx(ReactEChartsCore, {
115
135
  ref: chartRefCallback,
136
+ echarts,
116
137
  option,
117
138
  style: { height },
118
139
  className,
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":[],"sources":["../../../src/react/charts/base.tsx"],"sourcesContent":["import type { ECharts } from \"echarts\";\nimport ReactECharts from \"echarts-for-react\";\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { normalizeChartData, normalizeHeatmapData } from \"./normalize\";\nimport {\n buildCartesianOption,\n buildHeatmapOption,\n buildHorizontalBarOption,\n buildPieOption,\n buildRadarOption,\n type OptionBuilderContext,\n} from \"./options\";\nimport { useChartUITokens, useThemeColors } from \"./theme\";\nimport type {\n ChartColorPalette,\n ChartData,\n ChartType,\n Orientation,\n} from \"./types\";\n\n// ============================================================================\n// Palette Selection\n// ============================================================================\n\n/**\n * Determines the appropriate color palette for a chart type.\n * - Heatmaps use sequential (low → high intensity)\n * - All other charts use categorical (distinct categories)\n */\nfunction getDefaultPalette(chartType: ChartType): ChartColorPalette {\n switch (chartType) {\n case \"heatmap\":\n return \"sequential\";\n default:\n return \"categorical\";\n }\n}\n\n// ============================================================================\n// Component Props\n// ============================================================================\n\nexport interface BaseChartProps {\n /** Chart data (Arrow Table or JSON array) - format is auto-detected */\n data: ChartData;\n /** Chart type */\n chartType: ChartType;\n /** X-axis field key (auto-detected from schema if not provided) */\n xKey?: string;\n /** Y-axis field key(s) (auto-detected from schema if not provided) */\n yKey?: string | string[];\n /** Chart orientation @default \"vertical\" */\n orientation?: Orientation;\n /** Chart height in pixels @default 300 */\n height?: number;\n /** Chart title */\n title?: string;\n /** Show legend @default true */\n showLegend?: boolean;\n /**\n * Color palette to use. Auto-selected based on chart type if not specified.\n * - \"categorical\": Distinct colors for different categories (bar, pie, line)\n * - \"sequential\": Gradient for magnitude (heatmap)\n * - \"diverging\": Two-tone for positive/negative (correlation)\n */\n colorPalette?: ChartColorPalette;\n /** Custom colors (overrides colorPalette) */\n colors?: string[];\n /** Show data point symbols (line/area charts) @default false */\n showSymbol?: boolean;\n /** Smooth line curves (line/area charts) @default true */\n smooth?: boolean;\n /** Stack series @default false */\n stacked?: boolean;\n /** Symbol size for scatter charts @default 8 */\n symbolSize?: number;\n /** Show area fill for radar charts @default true */\n showArea?: boolean;\n /** Inner radius for pie/donut (0-100) @default 0 */\n innerRadius?: number;\n /** Show labels on pie/donut slices @default true */\n showLabels?: boolean;\n /** Label position for pie/donut @default \"outside\" */\n labelPosition?: \"outside\" | \"inside\" | \"center\";\n /** Y-axis field key for heatmap (the row dimension) */\n yAxisKey?: string;\n /** Min value for heatmap color scale */\n min?: number;\n /** Max value for heatmap color scale */\n max?: number;\n /** Additional ECharts options to merge */\n options?: Record<string, unknown>;\n /** Additional CSS classes */\n className?: string;\n}\n\n// ============================================================================\n// Base Chart Component\n// ============================================================================\n\n/**\n * Base chart component that handles both Arrow and JSON data.\n * Renders using ECharts for consistent output across both formats.\n */\nexport function BaseChart({\n data,\n chartType,\n xKey,\n yKey,\n orientation,\n height = 300,\n title,\n showLegend = true,\n colorPalette,\n colors: customColors,\n showSymbol = false,\n smooth = true,\n stacked = false,\n symbolSize = 8,\n showArea = true,\n innerRadius = 0,\n showLabels = true,\n labelPosition = \"outside\",\n yAxisKey,\n min,\n max,\n options: customOptions,\n className,\n}: BaseChartProps) {\n // Determine the appropriate color palette based on chart type\n const resolvedPalette = colorPalette ?? getDefaultPalette(chartType);\n const themeColors = useThemeColors(resolvedPalette);\n const colors = customColors ?? themeColors;\n\n const ui = useChartUITokens();\n\n // Store ECharts instance directly to avoid stale ref issues on unmount\n const echartsInstanceRef = useRef<ECharts | null>(null);\n\n // Callback ref pattern: captures the ECharts instance when ReactECharts mounts\n // This ensures we always have a stable reference to the actual instance\n const chartRefCallback = useCallback((node: ReactECharts | null) => {\n // Dispose previous instance if component is being replaced\n if (\n echartsInstanceRef.current &&\n !echartsInstanceRef.current.isDisposed()\n ) {\n echartsInstanceRef.current.dispose();\n }\n\n // Store the new instance\n if (node) {\n echartsInstanceRef.current = node.getEchartsInstance();\n } else {\n // Component unmounting - dispose the stored instance\n if (\n echartsInstanceRef.current &&\n !echartsInstanceRef.current.isDisposed()\n ) {\n echartsInstanceRef.current.dispose();\n }\n echartsInstanceRef.current = null;\n }\n }, []);\n\n // Memoize data normalization\n const normalized = useMemo(\n () =>\n chartType === \"heatmap\"\n ? normalizeHeatmapData(data, xKey, yAxisKey, yKey)\n : normalizeChartData(data, xKey, yKey, orientation),\n [data, xKey, yKey, yAxisKey, orientation, chartType],\n );\n\n // Memoize option building\n const option = useMemo(() => {\n const { xData, yFields, xField, chartType: detectedChartType } = normalized;\n\n if (xData.length === 0) return null;\n\n // Determine chart mode first (needed to handle yDataMap)\n const isHeatmap = chartType === \"heatmap\";\n\n // Heatmaps use heatmapData instead of yDataMap\n // For other charts, yDataMap is required\n const yDataMap = \"yDataMap\" in normalized ? normalized.yDataMap : {};\n\n const baseCtx: OptionBuilderContext = {\n xData,\n yDataMap,\n yFields,\n colors,\n title,\n showLegend,\n xField,\n ui,\n };\n const isPie = chartType === \"pie\" || chartType === \"donut\";\n const isRadar = chartType === \"radar\";\n const isHorizontal =\n !isPie &&\n !isRadar &&\n !isHeatmap &&\n (orientation === \"horizontal\" ||\n (detectedChartType === \"categorical\" &&\n !orientation &&\n chartType === \"bar\"));\n const isTimeSeries =\n detectedChartType === \"timeseries\" &&\n !isHorizontal &&\n !isRadar &&\n !isHeatmap;\n\n // Build option based on chart type\n let opt: Record<string, unknown>;\n\n if (isHeatmap && \"yAxisData\" in normalized && \"heatmapData\" in normalized) {\n const heatmapNorm = normalized as {\n yAxisData: (string | number)[];\n heatmapData: [number, number, number][];\n min: number;\n max: number;\n } & typeof normalized;\n opt = buildHeatmapOption({\n ...baseCtx,\n yAxisData: heatmapNorm.yAxisData,\n heatmapData: heatmapNorm.heatmapData,\n min: min ?? heatmapNorm.min,\n max: max ?? heatmapNorm.max,\n showLabels,\n });\n } else if (isRadar) {\n opt = buildRadarOption(baseCtx, showArea);\n } else if (isPie) {\n opt = buildPieOption(\n baseCtx,\n chartType as \"pie\" | \"donut\",\n innerRadius,\n showLabels,\n labelPosition,\n );\n } else if (isHorizontal) {\n opt = buildHorizontalBarOption(baseCtx, stacked);\n } else {\n opt = buildCartesianOption({\n ...baseCtx,\n chartType,\n isTimeSeries,\n stacked,\n smooth,\n showSymbol,\n symbolSize,\n });\n }\n\n // Merge custom options\n return customOptions ? { ...opt, ...customOptions } : opt;\n }, [\n normalized,\n colors,\n ui,\n title,\n showLegend,\n chartType,\n orientation,\n innerRadius,\n showLabels,\n labelPosition,\n stacked,\n smooth,\n showSymbol,\n symbolSize,\n showArea,\n min,\n max,\n customOptions,\n ]);\n\n if (!option) {\n return (\n <div className=\"flex items-center justify-center h-full text-muted-foreground\">\n No data\n </div>\n );\n }\n\n return (\n <ReactECharts\n ref={chartRefCallback}\n option={option}\n style={{ height }}\n className={className}\n opts={{ renderer: \"canvas\" }}\n notMerge={false}\n lazyUpdate={true}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA6BA,SAAS,kBAAkB,WAAyC;AAClE,SAAQ,WAAR;EACE,KAAK,UACH,QAAO;EACT,QACE,QAAO;;;;;;;AAsEb,SAAgB,UAAU,EACxB,MACA,WACA,MACA,MACA,aACA,SAAS,KACT,OACA,aAAa,MACb,cACA,QAAQ,cACR,aAAa,OACb,SAAS,MACT,UAAU,OACV,aAAa,GACb,WAAW,MACX,cAAc,GACd,aAAa,MACb,gBAAgB,WAChB,UACA,KACA,KACA,SAAS,eACT,aACiB;CAGjB,MAAM,cAAc,eADI,gBAAgB,kBAAkB,UAAU,CACjB;CACnD,MAAM,SAAS,gBAAgB;CAE/B,MAAM,KAAK,kBAAkB;CAG7B,MAAM,qBAAqB,OAAuB,KAAK;CAIvD,MAAM,mBAAmB,aAAa,SAA8B;AAElE,MACE,mBAAmB,WACnB,CAAC,mBAAmB,QAAQ,YAAY,CAExC,oBAAmB,QAAQ,SAAS;AAItC,MAAI,KACF,oBAAmB,UAAU,KAAK,oBAAoB;OACjD;AAEL,OACE,mBAAmB,WACnB,CAAC,mBAAmB,QAAQ,YAAY,CAExC,oBAAmB,QAAQ,SAAS;AAEtC,sBAAmB,UAAU;;IAE9B,EAAE,CAAC;CAGN,MAAM,aAAa,cAEf,cAAc,YACV,qBAAqB,MAAM,MAAM,UAAU,KAAK,GAChD,mBAAmB,MAAM,MAAM,MAAM,YAAY,EACvD;EAAC;EAAM;EAAM;EAAM;EAAU;EAAa;EAAU,CACrD;CAGD,MAAM,SAAS,cAAc;EAC3B,MAAM,EAAE,OAAO,SAAS,QAAQ,WAAW,sBAAsB;AAEjE,MAAI,MAAM,WAAW,EAAG,QAAO;EAG/B,MAAM,YAAY,cAAc;EAMhC,MAAM,UAAgC;GACpC;GACA,UAJe,cAAc,aAAa,WAAW,WAAW,EAAE;GAKlE;GACA;GACA;GACA;GACA;GACA;GACD;EACD,MAAM,QAAQ,cAAc,SAAS,cAAc;EACnD,MAAM,UAAU,cAAc;EAC9B,MAAM,eACJ,CAAC,SACD,CAAC,WACD,CAAC,cACA,gBAAgB,gBACd,sBAAsB,iBACrB,CAAC,eACD,cAAc;EACpB,MAAM,eACJ,sBAAsB,gBACtB,CAAC,gBACD,CAAC,WACD,CAAC;EAGH,IAAI;AAEJ,MAAI,aAAa,eAAe,cAAc,iBAAiB,YAAY;GACzE,MAAM,cAAc;AAMpB,SAAM,mBAAmB;IACvB,GAAG;IACH,WAAW,YAAY;IACvB,aAAa,YAAY;IACzB,KAAK,OAAO,YAAY;IACxB,KAAK,OAAO,YAAY;IACxB;IACD,CAAC;aACO,QACT,OAAM,iBAAiB,SAAS,SAAS;WAChC,MACT,OAAM,eACJ,SACA,WACA,aACA,YACA,cACD;WACQ,aACT,OAAM,yBAAyB,SAAS,QAAQ;MAEhD,OAAM,qBAAqB;GACzB,GAAG;GACH;GACA;GACA;GACA;GACA;GACA;GACD,CAAC;AAIJ,SAAO,gBAAgB;GAAE,GAAG;GAAK,GAAG;GAAe,GAAG;IACrD;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;AAEF,KAAI,CAAC,OACH,QACE,oBAAC;EAAI,WAAU;YAAgE;GAEzE;AAIV,QACE,oBAAC;EACC,KAAK;EACG;EACR,OAAO,EAAE,QAAQ;EACN;EACX,MAAM,EAAE,UAAU,UAAU;EAC5B,UAAU;EACV,YAAY;GACZ"}
1
+ {"version":3,"file":"base.js","names":[],"sources":["../../../src/react/charts/base.tsx"],"sourcesContent":["// Type-only import: erased at compile time, does not pull the full echarts\n// bundle in. The `ECharts` type is used for the stored ECharts instance ref.\nimport type { ECharts } from \"echarts\";\nimport {\n BarChart,\n HeatmapChart,\n LineChart,\n PieChart,\n RadarChart,\n ScatterChart,\n} from \"echarts/charts\";\nimport {\n GridComponent,\n LegendComponent,\n TitleComponent,\n TooltipComponent,\n VisualMapComponent,\n} from \"echarts/components\";\nimport * as echarts from \"echarts/core\";\nimport { LegacyGridContainLabel } from \"echarts/features\";\nimport { CanvasRenderer } from \"echarts/renderers\";\n// Import the ESM core build, not `echarts-for-react/lib/core` (CJS). The package\n// has no `exports` map, so `/lib/core` resolves to the literal CommonJS file\n// whose default export is `module.exports`; under ESM default-import interop a\n// consumer's bundler hands back the whole `{ default, __esModule }` object rather\n// than the component, so `<ReactEChartsCore/>` renders an object and React throws\n// \"Element type is invalid\". `esm/core` is real ESM with a true `export default`.\nimport ReactEChartsCore from \"echarts-for-react/esm/core\";\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { normalizeChartData, normalizeHeatmapData } from \"./normalize\";\nimport {\n buildCartesianOption,\n buildHeatmapOption,\n buildHorizontalBarOption,\n buildPieOption,\n buildRadarOption,\n type OptionBuilderContext,\n} from \"./options\";\nimport { useChartUITokens, useThemeColors } from \"./theme\";\nimport type {\n ChartColorPalette,\n ChartData,\n ChartType,\n Orientation,\n} from \"./types\";\n\n// ============================================================================\n// ECharts Registration (modular imports for tree-shaking)\n// ============================================================================\n// Only the chart types and components used by the option builders in\n// `options.ts` are registered. Importing from `echarts/core` instead of the\n// full `echarts` entry keeps unused chart types (graph, sankey, gauge, ...)\n// out of consumer bundles.\n//\n// If you add a new chart type or use a new ECharts feature (e.g. dataZoom,\n// toolbox, markLine), register it here. Consumers passing custom `options`\n// that need extra features can register them in their own app via\n// `import { use } from \"echarts/core\"`.\n//\n// Note: tooltip-driven axisPointer is bundled with `TooltipComponent`, so no\n// explicit AxisPointerComponent registration is needed.\n//\n// This `use()` call must stay co-located in the same module as `BaseChart`:\n// `package.json#sideEffects` declares JS modules side-effect free, so moving\n// registration to a separate import-for-side-effect module would let bundlers\n// drop it during tree-shaking.\necharts.use([\n // Series types used by the option builders\n LineChart, // line + area charts (area = line with areaStyle)\n BarChart, // bar + horizontal bar charts\n PieChart, // pie + donut charts\n ScatterChart,\n HeatmapChart,\n RadarChart,\n // Components referenced by built options\n TitleComponent, // `title`\n TooltipComponent, // `tooltip`\n LegendComponent, // `legend`\n GridComponent, // `grid` / `xAxis` / `yAxis`\n VisualMapComponent, // `visualMap` (heatmap color scale)\n // Features\n LegacyGridContainLabel, // `grid.containLabel` (cartesian option builder)\n // Renderer (BaseChart always renders with `renderer: \"canvas\"`)\n CanvasRenderer,\n]);\n\n// ============================================================================\n// Palette Selection\n// ============================================================================\n\n/**\n * Determines the appropriate color palette for a chart type.\n * - Heatmaps use sequential (low → high intensity)\n * - All other charts use categorical (distinct categories)\n */\nfunction getDefaultPalette(chartType: ChartType): ChartColorPalette {\n switch (chartType) {\n case \"heatmap\":\n return \"sequential\";\n default:\n return \"categorical\";\n }\n}\n\n// ============================================================================\n// Component Props\n// ============================================================================\n\nexport interface BaseChartProps {\n /** Chart data (Arrow Table or JSON array) - format is auto-detected */\n data: ChartData;\n /** Chart type */\n chartType: ChartType;\n /** X-axis field key (auto-detected from schema if not provided) */\n xKey?: string;\n /** Y-axis field key(s) (auto-detected from schema if not provided) */\n yKey?: string | string[];\n /** Chart orientation @default \"vertical\" */\n orientation?: Orientation;\n /** Chart height in pixels @default 300 */\n height?: number;\n /** Chart title */\n title?: string;\n /** Show legend @default true */\n showLegend?: boolean;\n /**\n * Color palette to use. Auto-selected based on chart type if not specified.\n * - \"categorical\": Distinct colors for different categories (bar, pie, line)\n * - \"sequential\": Gradient for magnitude (heatmap)\n * - \"diverging\": Two-tone for positive/negative (correlation)\n */\n colorPalette?: ChartColorPalette;\n /** Custom colors (overrides colorPalette) */\n colors?: string[];\n /** Show data point symbols (line/area charts) @default false */\n showSymbol?: boolean;\n /** Smooth line curves (line/area charts) @default true */\n smooth?: boolean;\n /** Stack series @default false */\n stacked?: boolean;\n /** Symbol size for scatter charts @default 8 */\n symbolSize?: number;\n /** Show area fill for radar charts @default true */\n showArea?: boolean;\n /** Inner radius for pie/donut (0-100) @default 0 */\n innerRadius?: number;\n /** Show labels on pie/donut slices @default true */\n showLabels?: boolean;\n /** Label position for pie/donut @default \"outside\" */\n labelPosition?: \"outside\" | \"inside\" | \"center\";\n /** Y-axis field key for heatmap (the row dimension) */\n yAxisKey?: string;\n /** Min value for heatmap color scale */\n min?: number;\n /** Max value for heatmap color scale */\n max?: number;\n /**\n * Additional ECharts options to merge.\n *\n * Only the built-in feature set is registered by this package, so options\n * referencing extra ECharts features (`dataZoom`, `toolbox`, `markLine`,\n * `markArea`, `graphic`, `dataset`, top-level `axisPointer`, ...) require\n * registering them in your app via `import { use } from \"echarts/core\"`.\n * This only works when your `echarts` resolves to the same module\n * instance/version as this package's (the registry is a singleton;\n * duplicate echarts copies won't share registrations).\n */\n options?: Record<string, unknown>;\n /** Additional CSS classes */\n className?: string;\n}\n\n// ============================================================================\n// Base Chart Component\n// ============================================================================\n\n/**\n * Base chart component that handles both Arrow and JSON data.\n * Renders using ECharts for consistent output across both formats.\n */\nexport function BaseChart({\n data,\n chartType,\n xKey,\n yKey,\n orientation,\n height = 300,\n title,\n showLegend = true,\n colorPalette,\n colors: customColors,\n showSymbol = false,\n smooth = true,\n stacked = false,\n symbolSize = 8,\n showArea = true,\n innerRadius = 0,\n showLabels = true,\n labelPosition = \"outside\",\n yAxisKey,\n min,\n max,\n options: customOptions,\n className,\n}: BaseChartProps) {\n // Determine the appropriate color palette based on chart type\n const resolvedPalette = colorPalette ?? getDefaultPalette(chartType);\n const themeColors = useThemeColors(resolvedPalette);\n const colors = customColors ?? themeColors;\n\n const ui = useChartUITokens();\n\n // Store ECharts instance directly to avoid stale ref issues on unmount\n const echartsInstanceRef = useRef<ECharts | null>(null);\n\n // Callback ref pattern: captures the ECharts instance when ReactECharts mounts\n // This ensures we always have a stable reference to the actual instance\n const chartRefCallback = useCallback((node: ReactEChartsCore | null) => {\n // Dispose previous instance if component is being replaced\n if (\n echartsInstanceRef.current &&\n !echartsInstanceRef.current.isDisposed()\n ) {\n echartsInstanceRef.current.dispose();\n }\n\n // Store the new instance\n if (node) {\n echartsInstanceRef.current = node.getEchartsInstance();\n } else {\n // Component unmounting - dispose the stored instance\n if (\n echartsInstanceRef.current &&\n !echartsInstanceRef.current.isDisposed()\n ) {\n echartsInstanceRef.current.dispose();\n }\n echartsInstanceRef.current = null;\n }\n }, []);\n\n // Memoize data normalization\n const normalized = useMemo(\n () =>\n chartType === \"heatmap\"\n ? normalizeHeatmapData(data, xKey, yAxisKey, yKey)\n : normalizeChartData(data, xKey, yKey, orientation),\n [data, xKey, yKey, yAxisKey, orientation, chartType],\n );\n\n // Memoize option building\n const option = useMemo(() => {\n const { xData, yFields, xField, chartType: detectedChartType } = normalized;\n\n if (xData.length === 0) return null;\n\n // Determine chart mode first (needed to handle yDataMap)\n const isHeatmap = chartType === \"heatmap\";\n\n // Heatmaps use heatmapData instead of yDataMap\n // For other charts, yDataMap is required\n const yDataMap = \"yDataMap\" in normalized ? normalized.yDataMap : {};\n\n const baseCtx: OptionBuilderContext = {\n xData,\n yDataMap,\n yFields,\n colors,\n title,\n showLegend,\n xField,\n ui,\n };\n const isPie = chartType === \"pie\" || chartType === \"donut\";\n const isRadar = chartType === \"radar\";\n const isHorizontal =\n !isPie &&\n !isRadar &&\n !isHeatmap &&\n (orientation === \"horizontal\" ||\n (detectedChartType === \"categorical\" &&\n !orientation &&\n chartType === \"bar\"));\n const isTimeSeries =\n detectedChartType === \"timeseries\" &&\n !isHorizontal &&\n !isRadar &&\n !isHeatmap;\n\n // Build option based on chart type\n let opt: Record<string, unknown>;\n\n if (isHeatmap && \"yAxisData\" in normalized && \"heatmapData\" in normalized) {\n const heatmapNorm = normalized as {\n yAxisData: (string | number)[];\n heatmapData: [number, number, number][];\n min: number;\n max: number;\n } & typeof normalized;\n opt = buildHeatmapOption({\n ...baseCtx,\n yAxisData: heatmapNorm.yAxisData,\n heatmapData: heatmapNorm.heatmapData,\n min: min ?? heatmapNorm.min,\n max: max ?? heatmapNorm.max,\n showLabels,\n });\n } else if (isRadar) {\n opt = buildRadarOption(baseCtx, showArea);\n } else if (isPie) {\n opt = buildPieOption(\n baseCtx,\n chartType as \"pie\" | \"donut\",\n innerRadius,\n showLabels,\n labelPosition,\n );\n } else if (isHorizontal) {\n opt = buildHorizontalBarOption(baseCtx, stacked);\n } else {\n opt = buildCartesianOption({\n ...baseCtx,\n chartType,\n isTimeSeries,\n stacked,\n smooth,\n showSymbol,\n symbolSize,\n });\n }\n\n // Merge custom options\n return customOptions ? { ...opt, ...customOptions } : opt;\n }, [\n normalized,\n colors,\n ui,\n title,\n showLegend,\n chartType,\n orientation,\n innerRadius,\n showLabels,\n labelPosition,\n stacked,\n smooth,\n showSymbol,\n symbolSize,\n showArea,\n min,\n max,\n customOptions,\n ]);\n\n if (!option) {\n return (\n <div className=\"flex items-center justify-center h-full text-muted-foreground\">\n No data\n </div>\n );\n }\n\n return (\n <ReactEChartsCore\n ref={chartRefCallback}\n echarts={echarts}\n option={option}\n style={{ height }}\n className={className}\n opts={{ renderer: \"canvas\" }}\n notMerge={false}\n lazyUpdate={true}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAkEA,QAAQ,IAAI;CAEV;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CAEA;CACD,CAAC;;;;;;AAWF,SAAS,kBAAkB,WAAyC;AAClE,SAAQ,WAAR;EACE,KAAK,UACH,QAAO;EACT,QACE,QAAO;;;;;;;AAgFb,SAAgB,UAAU,EACxB,MACA,WACA,MACA,MACA,aACA,SAAS,KACT,OACA,aAAa,MACb,cACA,QAAQ,cACR,aAAa,OACb,SAAS,MACT,UAAU,OACV,aAAa,GACb,WAAW,MACX,cAAc,GACd,aAAa,MACb,gBAAgB,WAChB,UACA,KACA,KACA,SAAS,eACT,aACiB;CAGjB,MAAM,cAAc,eADI,gBAAgB,kBAAkB,UAAU,CACjB;CACnD,MAAM,SAAS,gBAAgB;CAE/B,MAAM,KAAK,kBAAkB;CAG7B,MAAM,qBAAqB,OAAuB,KAAK;CAIvD,MAAM,mBAAmB,aAAa,SAAkC;AAEtE,MACE,mBAAmB,WACnB,CAAC,mBAAmB,QAAQ,YAAY,CAExC,oBAAmB,QAAQ,SAAS;AAItC,MAAI,KACF,oBAAmB,UAAU,KAAK,oBAAoB;OACjD;AAEL,OACE,mBAAmB,WACnB,CAAC,mBAAmB,QAAQ,YAAY,CAExC,oBAAmB,QAAQ,SAAS;AAEtC,sBAAmB,UAAU;;IAE9B,EAAE,CAAC;CAGN,MAAM,aAAa,cAEf,cAAc,YACV,qBAAqB,MAAM,MAAM,UAAU,KAAK,GAChD,mBAAmB,MAAM,MAAM,MAAM,YAAY,EACvD;EAAC;EAAM;EAAM;EAAM;EAAU;EAAa;EAAU,CACrD;CAGD,MAAM,SAAS,cAAc;EAC3B,MAAM,EAAE,OAAO,SAAS,QAAQ,WAAW,sBAAsB;AAEjE,MAAI,MAAM,WAAW,EAAG,QAAO;EAG/B,MAAM,YAAY,cAAc;EAMhC,MAAM,UAAgC;GACpC;GACA,UAJe,cAAc,aAAa,WAAW,WAAW,EAAE;GAKlE;GACA;GACA;GACA;GACA;GACA;GACD;EACD,MAAM,QAAQ,cAAc,SAAS,cAAc;EACnD,MAAM,UAAU,cAAc;EAC9B,MAAM,eACJ,CAAC,SACD,CAAC,WACD,CAAC,cACA,gBAAgB,gBACd,sBAAsB,iBACrB,CAAC,eACD,cAAc;EACpB,MAAM,eACJ,sBAAsB,gBACtB,CAAC,gBACD,CAAC,WACD,CAAC;EAGH,IAAI;AAEJ,MAAI,aAAa,eAAe,cAAc,iBAAiB,YAAY;GACzE,MAAM,cAAc;AAMpB,SAAM,mBAAmB;IACvB,GAAG;IACH,WAAW,YAAY;IACvB,aAAa,YAAY;IACzB,KAAK,OAAO,YAAY;IACxB,KAAK,OAAO,YAAY;IACxB;IACD,CAAC;aACO,QACT,OAAM,iBAAiB,SAAS,SAAS;WAChC,MACT,OAAM,eACJ,SACA,WACA,aACA,YACA,cACD;WACQ,aACT,OAAM,yBAAyB,SAAS,QAAQ;MAEhD,OAAM,qBAAqB;GACzB,GAAG;GACH;GACA;GACA;GACA;GACA;GACA;GACD,CAAC;AAIJ,SAAO,gBAAgB;GAAE,GAAG;GAAK,GAAG;GAAe,GAAG;IACrD;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;AAEF,KAAI,CAAC,OACH,QACE,oBAAC;EAAI,WAAU;YAAgE;GAEzE;AAIV,QACE,oBAAC;EACC,KAAK;EACI;EACD;EACR,OAAO,EAAE,QAAQ;EACN;EACX,MAAM,EAAE,UAAU,UAAU;EAC5B,UAAU;EACV,YAAY;GACZ"}
@@ -1,6 +1,5 @@
1
1
  import { DATE_FIELD_PATTERNS, NAME_FIELD_PATTERNS } from "../../js/constants.js";
2
2
  import { ArrowClient } from "../../js/arrow/arrow-client.js";
3
- import "../../js/index.js";
4
3
  import { isArrowTable } from "./types.js";
5
4
  import { sortNumericAscending, sortTimeSeriesAscending, toChartArray } from "./utils.js";
6
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"normalize.js","names":[],"sources":["../../../src/react/charts/normalize.ts"],"sourcesContent":["import type { Table } from \"apache-arrow\";\nimport { ArrowClient } from \"@/js\";\nimport { DATE_FIELD_PATTERNS, NAME_FIELD_PATTERNS } from \"./constants\";\nimport type {\n ChartData,\n NormalizedChartData,\n NormalizedChartDataBase,\n Orientation,\n} from \"./types\";\nimport { isArrowTable } from \"./types\";\nimport {\n sortNumericAscending,\n sortTimeSeriesAscending,\n toChartArray,\n} from \"./utils\";\n\n// ============================================================================\n// Type Detection Helpers\n// ============================================================================\n\n/**\n * Checks if a value looks like an ISO date string\n */\nfunction isDateString(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n return /^\\d{4}-\\d{2}-\\d{2}(T|$)/.test(value);\n}\n\n/**\n * Checks if a value is numeric (number or numeric string)\n */\nfunction isNumericValue(value: unknown): boolean {\n if (typeof value === \"number\") return true;\n if (typeof value === \"bigint\") return true;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\" || isDateString(trimmed)) return false;\n const parsed = Number(trimmed);\n return !Number.isNaN(parsed) && Number.isFinite(parsed);\n }\n return false;\n}\n\n/**\n * Checks if a value looks like a category/label (non-numeric string)\n */\nfunction isCategoryValue(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n const trimmed = value.trim();\n if (trimmed === \"\") return false;\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(trimmed)) return false;\n const parsed = Number(trimmed);\n return Number.isNaN(parsed) || !Number.isFinite(parsed);\n}\n\n// ============================================================================\n// Field Detection\n// ============================================================================\n\n/**\n * Detects fields from JSON data for charting\n */\nfunction detectFieldsFromJson(\n data: Record<string, unknown>[],\n orientation?: Orientation,\n): {\n xField: string;\n yFields: string[];\n chartType: \"timeseries\" | \"categorical\";\n} {\n if (!data || data.length === 0) {\n return { xField: \"x\", yFields: [\"y\"], chartType: \"categorical\" };\n }\n\n const firstRow = data[0];\n const keys = Object.keys(firstRow);\n\n // Detect date fields by key name OR by value being a date string\n const dateFields = keys.filter((key) => {\n const value = firstRow[key];\n const keyMatchesDatePattern = DATE_FIELD_PATTERNS.some((p) =>\n key.toLowerCase().includes(p),\n );\n const valueIsDateString = isDateString(value);\n return keyMatchesDatePattern || valueIsDateString;\n });\n\n // Detect name/category fields by pattern AND value type\n let nameFields = keys.filter((key) => {\n const value = firstRow[key];\n return (\n isCategoryValue(value) &&\n !isDateString(value) &&\n NAME_FIELD_PATTERNS.some((p) => key.toLowerCase().includes(p))\n );\n });\n\n // Fallback: any string field that isn't a date or ID\n if (nameFields.length === 0) {\n nameFields = keys.filter((key) => {\n const value = firstRow[key];\n return (\n isCategoryValue(value) &&\n !isDateString(value) &&\n !dateFields.includes(key) &&\n !key.toLowerCase().endsWith(\"_id\")\n );\n });\n }\n\n // Detect numeric fields\n const numericFields = keys.filter((key) => {\n const value = firstRow[key];\n return isNumericValue(value) && !dateFields.includes(key);\n });\n\n const isHorizontal = orientation === \"horizontal\";\n\n if (isHorizontal || (nameFields.length > 0 && dateFields.length === 0)) {\n const xField = nameFields[0] || dateFields[0] || keys[0];\n const yFields =\n numericFields.length > 0\n ? numericFields\n : keys.filter((k) => k !== xField);\n return { xField, yFields, chartType: \"categorical\" };\n }\n\n const xField = dateFields[0] || nameFields[0] || keys[0];\n const yFields =\n numericFields.length > 0 ? numericFields : keys.filter((k) => k !== xField);\n return {\n xField,\n yFields,\n chartType: dateFields.length > 0 ? \"timeseries\" : \"categorical\",\n };\n}\n\n// ============================================================================\n// Value Conversion\n// ============================================================================\n\n/**\n * Converts a JSON value to a chart-compatible value.\n */\nfunction jsonValueToChartValue(\n value: unknown,\n isYValue: boolean,\n isDateField: boolean,\n): string | number {\n if (value === null || value === undefined) {\n return isYValue ? 0 : \"\";\n }\n if (typeof value === \"number\") {\n return value;\n }\n if (typeof value === \"bigint\") {\n return Number(value);\n }\n if (typeof value === \"string\") {\n if (isDateField && isDateString(value)) {\n const timestamp = new Date(value).getTime();\n if (!Number.isNaN(timestamp)) {\n return timestamp;\n }\n }\n if (isYValue) {\n const trimmed = value.trim();\n const parsed = Number(trimmed);\n if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {\n return parsed;\n }\n }\n return value;\n }\n return String(value);\n}\n\n// ============================================================================\n// Data Extraction\n// ============================================================================\n\n/**\n * Extracts chart data from JSON array\n */\nfunction extractFromJson(\n data: Record<string, unknown>[],\n xField: string,\n yFields: string[],\n): {\n xData: (string | number)[];\n yDataMap: Record<string, (string | number)[]>;\n} {\n const xData: (string | number)[] = [];\n const yDataMap: Record<string, (string | number)[]> = {};\n\n for (const field of yFields) {\n yDataMap[field] = [];\n }\n\n const xIsDateField = data.length > 0 && isDateString(data[0][xField]);\n\n for (const row of data) {\n xData.push(jsonValueToChartValue(row[xField], false, xIsDateField));\n for (const field of yFields) {\n yDataMap[field].push(jsonValueToChartValue(row[field], true, false));\n }\n }\n\n return { xData, yDataMap };\n}\n\n// ============================================================================\n// Main Normalization Function\n// ============================================================================\n\n/**\n * Normalizes chart data from either Arrow or JSON format.\n * Converts BigInt and Date values to chart-compatible types.\n */\nexport function normalizeChartData(\n data: ChartData,\n xKey?: string,\n yKey?: string | string[],\n orientation?: Orientation,\n): NormalizedChartData {\n if (isArrowTable(data)) {\n const table = data as Table;\n const detected = ArrowClient.detectFieldsFromArrow(table, orientation);\n const resolvedXKey = xKey ?? detected.xField;\n const resolvedYKeys = yKey\n ? Array.isArray(yKey)\n ? yKey\n : [yKey]\n : detected.yFields;\n\n const { xData: rawXData, yDataMap: rawYDataMap } =\n ArrowClient.extractChartData(table, resolvedXKey, resolvedYKeys);\n\n let xData = toChartArray(rawXData);\n let yDataMap: Record<string, (string | number)[]> = {};\n for (const key of resolvedYKeys) {\n yDataMap[key] = toChartArray(rawYDataMap[key] ?? []);\n }\n\n if (detected.chartType === \"timeseries\") {\n ({ xData, yDataMap } = sortTimeSeriesAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n } else if (xData.length > 0 && xData.every(isNumericValue)) {\n ({ xData, yDataMap } = sortNumericAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n }\n\n return {\n xData,\n yDataMap,\n xField: resolvedXKey,\n yFields: resolvedYKeys,\n chartType: detected.chartType,\n };\n }\n\n // JSON Array\n const jsonData = data as Record<string, unknown>[];\n const detected = detectFieldsFromJson(jsonData, orientation);\n const resolvedXKey = xKey ?? detected.xField;\n const resolvedYKeys = yKey\n ? Array.isArray(yKey)\n ? yKey\n : [yKey]\n : detected.yFields;\n\n const { xData: rawXData, yDataMap: rawYDataMap } = extractFromJson(\n jsonData,\n resolvedXKey,\n resolvedYKeys,\n );\n\n let xData = toChartArray(rawXData);\n let yDataMap: Record<string, (string | number)[]> = {};\n for (const key of resolvedYKeys) {\n yDataMap[key] = toChartArray(rawYDataMap[key] ?? []);\n }\n\n if (detected.chartType === \"timeseries\") {\n ({ xData, yDataMap } = sortTimeSeriesAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n } else if (xData.length > 0 && xData.every(isNumericValue)) {\n ({ xData, yDataMap } = sortNumericAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n }\n\n return {\n xData,\n yDataMap,\n xField: resolvedXKey,\n yFields: resolvedYKeys,\n chartType: detected.chartType,\n };\n}\n\n// ============================================================================\n// Heatmap Data Normalization\n// ============================================================================\n\n/**\n * Normalized data for heatmap charts.\n * Extends base (not NormalizedChartData) because heatmaps don't use yDataMap.\n * Instead, they use heatmapData which contains [xIndex, yIndex, value] tuples.\n */\nexport interface NormalizedHeatmapData extends NormalizedChartDataBase {\n /** Y-axis categories (rows) */\n yAxisData: (string | number)[];\n /** Heatmap data as [xIndex, yIndex, value] tuples */\n heatmapData: [number, number, number][];\n /** Min value in the data */\n min: number;\n /** Max value in the data */\n max: number;\n}\n\n/**\n * Normalizes data specifically for heatmap charts.\n * Expects data in format: `{ xKey: string, yAxisKey: string, valueKey: number }`\n *\n * @param data - Raw data (Arrow Table or JSON array)\n * @param xKey - Field key for X-axis (columns)\n * @param yAxisKey - Field key for Y-axis (rows)\n * @param valueKey - Field key for the cell values\n */\nexport function normalizeHeatmapData(\n data: ChartData,\n xKey?: string,\n yAxisKey?: string,\n valueKey?: string | string[],\n): NormalizedHeatmapData {\n // First, get the standard normalization\n const jsonData = isArrowTable(data)\n ? extractJsonFromArrow(data)\n : (data as Record<string, unknown>[]);\n\n if (jsonData.length === 0) {\n return {\n xData: [],\n xField: xKey ?? \"x\",\n yFields: [],\n chartType: \"categorical\",\n yAxisData: [],\n heatmapData: [],\n min: 0,\n max: 0,\n };\n }\n\n // Detect fields if not provided\n const keys = Object.keys(jsonData[0]);\n const resolvedXKey = xKey ?? keys[0];\n const resolvedYAxisKey = yAxisKey ?? keys[1];\n const resolvedValueKey = valueKey\n ? Array.isArray(valueKey)\n ? valueKey[0]\n : valueKey\n : keys[2];\n\n // Extract unique X and Y categories\n const xSet = new Set<string | number>();\n const ySet = new Set<string | number>();\n\n for (const row of jsonData) {\n const xVal = jsonValueToChartValue(row[resolvedXKey], false, false);\n const yVal = jsonValueToChartValue(row[resolvedYAxisKey], false, false);\n xSet.add(xVal);\n ySet.add(yVal);\n }\n\n const xData = Array.from(xSet);\n const yAxisData = Array.from(ySet);\n\n // Create index maps for fast lookup\n const xIndexMap = new Map<string | number, number>();\n const yIndexMap = new Map<string | number, number>();\n xData.forEach((v, i) => {\n xIndexMap.set(v, i);\n });\n yAxisData.forEach((v, i) => {\n yIndexMap.set(v, i);\n });\n\n // Build heatmap data and track min/max\n const heatmapData: [number, number, number][] = [];\n let min = Number.POSITIVE_INFINITY;\n let max = Number.NEGATIVE_INFINITY;\n\n for (const row of jsonData) {\n const xVal = jsonValueToChartValue(row[resolvedXKey], false, false);\n const yVal = jsonValueToChartValue(row[resolvedYAxisKey], false, false);\n const value = jsonValueToChartValue(row[resolvedValueKey], true, false);\n\n const xIdx = xIndexMap.get(xVal);\n const yIdx = yIndexMap.get(yVal);\n const numValue = typeof value === \"number\" ? value : 0;\n\n if (xIdx !== undefined && yIdx !== undefined) {\n heatmapData.push([xIdx, yIdx, numValue]);\n min = Math.min(min, numValue);\n max = Math.max(max, numValue);\n }\n }\n\n // Handle edge case where no valid data was found\n if (heatmapData.length === 0) {\n min = 0;\n max = 0;\n }\n\n return {\n xData,\n xField: resolvedXKey,\n yFields: [resolvedValueKey],\n chartType: \"categorical\",\n yAxisData,\n heatmapData,\n min,\n max,\n };\n}\n\n/**\n * Helper to extract JSON array from Arrow table for heatmap processing.\n */\nfunction extractJsonFromArrow(table: Table): Record<string, unknown>[] {\n const result: Record<string, unknown>[] = [];\n const fields = table.schema.fields.map((f) => f.name);\n\n for (let i = 0; i < table.numRows; i++) {\n const row: Record<string, unknown> = {};\n for (const field of fields) {\n const col = table.getChild(field);\n row[field] = col?.get(i);\n }\n result.push(row);\n }\n\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"normalize.js","names":[],"sources":["../../../src/react/charts/normalize.ts"],"sourcesContent":["import type { Table } from \"apache-arrow\";\nimport { ArrowClient } from \"@/js\";\nimport { DATE_FIELD_PATTERNS, NAME_FIELD_PATTERNS } from \"./constants\";\nimport type {\n ChartData,\n NormalizedChartData,\n NormalizedChartDataBase,\n Orientation,\n} from \"./types\";\nimport { isArrowTable } from \"./types\";\nimport {\n sortNumericAscending,\n sortTimeSeriesAscending,\n toChartArray,\n} from \"./utils\";\n\n// ============================================================================\n// Type Detection Helpers\n// ============================================================================\n\n/**\n * Checks if a value looks like an ISO date string\n */\nfunction isDateString(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n return /^\\d{4}-\\d{2}-\\d{2}(T|$)/.test(value);\n}\n\n/**\n * Checks if a value is numeric (number or numeric string)\n */\nfunction isNumericValue(value: unknown): boolean {\n if (typeof value === \"number\") return true;\n if (typeof value === \"bigint\") return true;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\" || isDateString(trimmed)) return false;\n const parsed = Number(trimmed);\n return !Number.isNaN(parsed) && Number.isFinite(parsed);\n }\n return false;\n}\n\n/**\n * Checks if a value looks like a category/label (non-numeric string)\n */\nfunction isCategoryValue(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n const trimmed = value.trim();\n if (trimmed === \"\") return false;\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(trimmed)) return false;\n const parsed = Number(trimmed);\n return Number.isNaN(parsed) || !Number.isFinite(parsed);\n}\n\n// ============================================================================\n// Field Detection\n// ============================================================================\n\n/**\n * Detects fields from JSON data for charting\n */\nfunction detectFieldsFromJson(\n data: Record<string, unknown>[],\n orientation?: Orientation,\n): {\n xField: string;\n yFields: string[];\n chartType: \"timeseries\" | \"categorical\";\n} {\n if (!data || data.length === 0) {\n return { xField: \"x\", yFields: [\"y\"], chartType: \"categorical\" };\n }\n\n const firstRow = data[0];\n const keys = Object.keys(firstRow);\n\n // Detect date fields by key name OR by value being a date string\n const dateFields = keys.filter((key) => {\n const value = firstRow[key];\n const keyMatchesDatePattern = DATE_FIELD_PATTERNS.some((p) =>\n key.toLowerCase().includes(p),\n );\n const valueIsDateString = isDateString(value);\n return keyMatchesDatePattern || valueIsDateString;\n });\n\n // Detect name/category fields by pattern AND value type\n let nameFields = keys.filter((key) => {\n const value = firstRow[key];\n return (\n isCategoryValue(value) &&\n !isDateString(value) &&\n NAME_FIELD_PATTERNS.some((p) => key.toLowerCase().includes(p))\n );\n });\n\n // Fallback: any string field that isn't a date or ID\n if (nameFields.length === 0) {\n nameFields = keys.filter((key) => {\n const value = firstRow[key];\n return (\n isCategoryValue(value) &&\n !isDateString(value) &&\n !dateFields.includes(key) &&\n !key.toLowerCase().endsWith(\"_id\")\n );\n });\n }\n\n // Detect numeric fields\n const numericFields = keys.filter((key) => {\n const value = firstRow[key];\n return isNumericValue(value) && !dateFields.includes(key);\n });\n\n const isHorizontal = orientation === \"horizontal\";\n\n if (isHorizontal || (nameFields.length > 0 && dateFields.length === 0)) {\n const xField = nameFields[0] || dateFields[0] || keys[0];\n const yFields =\n numericFields.length > 0\n ? numericFields\n : keys.filter((k) => k !== xField);\n return { xField, yFields, chartType: \"categorical\" };\n }\n\n const xField = dateFields[0] || nameFields[0] || keys[0];\n const yFields =\n numericFields.length > 0 ? numericFields : keys.filter((k) => k !== xField);\n return {\n xField,\n yFields,\n chartType: dateFields.length > 0 ? \"timeseries\" : \"categorical\",\n };\n}\n\n// ============================================================================\n// Value Conversion\n// ============================================================================\n\n/**\n * Converts a JSON value to a chart-compatible value.\n */\nfunction jsonValueToChartValue(\n value: unknown,\n isYValue: boolean,\n isDateField: boolean,\n): string | number {\n if (value === null || value === undefined) {\n return isYValue ? 0 : \"\";\n }\n if (typeof value === \"number\") {\n return value;\n }\n if (typeof value === \"bigint\") {\n return Number(value);\n }\n if (typeof value === \"string\") {\n if (isDateField && isDateString(value)) {\n const timestamp = new Date(value).getTime();\n if (!Number.isNaN(timestamp)) {\n return timestamp;\n }\n }\n if (isYValue) {\n const trimmed = value.trim();\n const parsed = Number(trimmed);\n if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {\n return parsed;\n }\n }\n return value;\n }\n return String(value);\n}\n\n// ============================================================================\n// Data Extraction\n// ============================================================================\n\n/**\n * Extracts chart data from JSON array\n */\nfunction extractFromJson(\n data: Record<string, unknown>[],\n xField: string,\n yFields: string[],\n): {\n xData: (string | number)[];\n yDataMap: Record<string, (string | number)[]>;\n} {\n const xData: (string | number)[] = [];\n const yDataMap: Record<string, (string | number)[]> = {};\n\n for (const field of yFields) {\n yDataMap[field] = [];\n }\n\n const xIsDateField = data.length > 0 && isDateString(data[0][xField]);\n\n for (const row of data) {\n xData.push(jsonValueToChartValue(row[xField], false, xIsDateField));\n for (const field of yFields) {\n yDataMap[field].push(jsonValueToChartValue(row[field], true, false));\n }\n }\n\n return { xData, yDataMap };\n}\n\n// ============================================================================\n// Main Normalization Function\n// ============================================================================\n\n/**\n * Normalizes chart data from either Arrow or JSON format.\n * Converts BigInt and Date values to chart-compatible types.\n */\nexport function normalizeChartData(\n data: ChartData,\n xKey?: string,\n yKey?: string | string[],\n orientation?: Orientation,\n): NormalizedChartData {\n if (isArrowTable(data)) {\n const table = data as Table;\n const detected = ArrowClient.detectFieldsFromArrow(table, orientation);\n const resolvedXKey = xKey ?? detected.xField;\n const resolvedYKeys = yKey\n ? Array.isArray(yKey)\n ? yKey\n : [yKey]\n : detected.yFields;\n\n const { xData: rawXData, yDataMap: rawYDataMap } =\n ArrowClient.extractChartData(table, resolvedXKey, resolvedYKeys);\n\n let xData = toChartArray(rawXData);\n let yDataMap: Record<string, (string | number)[]> = {};\n for (const key of resolvedYKeys) {\n yDataMap[key] = toChartArray(rawYDataMap[key] ?? []);\n }\n\n if (detected.chartType === \"timeseries\") {\n ({ xData, yDataMap } = sortTimeSeriesAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n } else if (xData.length > 0 && xData.every(isNumericValue)) {\n ({ xData, yDataMap } = sortNumericAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n }\n\n return {\n xData,\n yDataMap,\n xField: resolvedXKey,\n yFields: resolvedYKeys,\n chartType: detected.chartType,\n };\n }\n\n // JSON Array\n const jsonData = data as Record<string, unknown>[];\n const detected = detectFieldsFromJson(jsonData, orientation);\n const resolvedXKey = xKey ?? detected.xField;\n const resolvedYKeys = yKey\n ? Array.isArray(yKey)\n ? yKey\n : [yKey]\n : detected.yFields;\n\n const { xData: rawXData, yDataMap: rawYDataMap } = extractFromJson(\n jsonData,\n resolvedXKey,\n resolvedYKeys,\n );\n\n let xData = toChartArray(rawXData);\n let yDataMap: Record<string, (string | number)[]> = {};\n for (const key of resolvedYKeys) {\n yDataMap[key] = toChartArray(rawYDataMap[key] ?? []);\n }\n\n if (detected.chartType === \"timeseries\") {\n ({ xData, yDataMap } = sortTimeSeriesAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n } else if (xData.length > 0 && xData.every(isNumericValue)) {\n ({ xData, yDataMap } = sortNumericAscending(\n xData,\n yDataMap,\n resolvedYKeys,\n ));\n }\n\n return {\n xData,\n yDataMap,\n xField: resolvedXKey,\n yFields: resolvedYKeys,\n chartType: detected.chartType,\n };\n}\n\n// ============================================================================\n// Heatmap Data Normalization\n// ============================================================================\n\n/**\n * Normalized data for heatmap charts.\n * Extends base (not NormalizedChartData) because heatmaps don't use yDataMap.\n * Instead, they use heatmapData which contains [xIndex, yIndex, value] tuples.\n */\nexport interface NormalizedHeatmapData extends NormalizedChartDataBase {\n /** Y-axis categories (rows) */\n yAxisData: (string | number)[];\n /** Heatmap data as [xIndex, yIndex, value] tuples */\n heatmapData: [number, number, number][];\n /** Min value in the data */\n min: number;\n /** Max value in the data */\n max: number;\n}\n\n/**\n * Normalizes data specifically for heatmap charts.\n * Expects data in format: `{ xKey: string, yAxisKey: string, valueKey: number }`\n *\n * @param data - Raw data (Arrow Table or JSON array)\n * @param xKey - Field key for X-axis (columns)\n * @param yAxisKey - Field key for Y-axis (rows)\n * @param valueKey - Field key for the cell values\n */\nexport function normalizeHeatmapData(\n data: ChartData,\n xKey?: string,\n yAxisKey?: string,\n valueKey?: string | string[],\n): NormalizedHeatmapData {\n // First, get the standard normalization\n const jsonData = isArrowTable(data)\n ? extractJsonFromArrow(data)\n : (data as Record<string, unknown>[]);\n\n if (jsonData.length === 0) {\n return {\n xData: [],\n xField: xKey ?? \"x\",\n yFields: [],\n chartType: \"categorical\",\n yAxisData: [],\n heatmapData: [],\n min: 0,\n max: 0,\n };\n }\n\n // Detect fields if not provided\n const keys = Object.keys(jsonData[0]);\n const resolvedXKey = xKey ?? keys[0];\n const resolvedYAxisKey = yAxisKey ?? keys[1];\n const resolvedValueKey = valueKey\n ? Array.isArray(valueKey)\n ? valueKey[0]\n : valueKey\n : keys[2];\n\n // Extract unique X and Y categories\n const xSet = new Set<string | number>();\n const ySet = new Set<string | number>();\n\n for (const row of jsonData) {\n const xVal = jsonValueToChartValue(row[resolvedXKey], false, false);\n const yVal = jsonValueToChartValue(row[resolvedYAxisKey], false, false);\n xSet.add(xVal);\n ySet.add(yVal);\n }\n\n const xData = Array.from(xSet);\n const yAxisData = Array.from(ySet);\n\n // Create index maps for fast lookup\n const xIndexMap = new Map<string | number, number>();\n const yIndexMap = new Map<string | number, number>();\n xData.forEach((v, i) => {\n xIndexMap.set(v, i);\n });\n yAxisData.forEach((v, i) => {\n yIndexMap.set(v, i);\n });\n\n // Build heatmap data and track min/max\n const heatmapData: [number, number, number][] = [];\n let min = Number.POSITIVE_INFINITY;\n let max = Number.NEGATIVE_INFINITY;\n\n for (const row of jsonData) {\n const xVal = jsonValueToChartValue(row[resolvedXKey], false, false);\n const yVal = jsonValueToChartValue(row[resolvedYAxisKey], false, false);\n const value = jsonValueToChartValue(row[resolvedValueKey], true, false);\n\n const xIdx = xIndexMap.get(xVal);\n const yIdx = yIndexMap.get(yVal);\n const numValue = typeof value === \"number\" ? value : 0;\n\n if (xIdx !== undefined && yIdx !== undefined) {\n heatmapData.push([xIdx, yIdx, numValue]);\n min = Math.min(min, numValue);\n max = Math.max(max, numValue);\n }\n }\n\n // Handle edge case where no valid data was found\n if (heatmapData.length === 0) {\n min = 0;\n max = 0;\n }\n\n return {\n xData,\n xField: resolvedXKey,\n yFields: [resolvedValueKey],\n chartType: \"categorical\",\n yAxisData,\n heatmapData,\n min,\n max,\n };\n}\n\n/**\n * Helper to extract JSON array from Arrow table for heatmap processing.\n */\nfunction extractJsonFromArrow(table: Table): Record<string, unknown>[] {\n const result: Record<string, unknown>[] = [];\n const fields = table.schema.fields.map((f) => f.name);\n\n for (let i = 0; i < table.numRows; i++) {\n const row: Record<string, unknown> = {};\n for (const field of fields) {\n const col = table.getChild(field);\n row[field] = col?.get(i);\n }\n result.push(row);\n }\n\n return result;\n}\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { connectSSE } from "../../js/sse/connect-sse.js";
2
- import "../../js/index.js";
3
2
  import { TERMINAL_STATUSES } from "./types.js";
4
3
  import { useCallback, useEffect, useRef, useState } from "react";
5
4
 
@@ -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":";;;;;;AAYA,SAAS,YAAY,MAA6B;AAChD,QAAO,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAAC,IAAI,KAAK;;AAG9D,SAAS,YAAY,MAAc,OAAqB;CACtD,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,aAAa,IAAI,MAAM,MAAM;AACjC,QAAO,QAAQ,aAAa,EAAE,EAAE,IAAI,IAAI,UAAU,CAAC;;AAGrD,SAAS,eAAe,MAAoB;CAC1C,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,aAAa,OAAO,KAAK;AAC7B,QAAO,QAAQ,aAAa,EAAE,EAAE,IAAI,IAAI,UAAU,CAAC;;;;;;;AAQrD,SAAS,wBAAwB,KAAmC;CAClE,MAAM,aAAa,IAAI,eAAe,EAAE,EACrC,KAAK,QAAQ,IAAI,MAAM,QAAQ,CAC/B,OAAO,QAAQ;AAClB,QAAO,UAAU,SAAS,IAAI,UAAU,KAAK,OAAO,GAAG,IAAI;;AAG7D,SAAS,aACP,KACA,WAAW,IACO;AAClB,QAAO;EACL,IAAI,GAAG,IAAI,YAAY;EACvB,MAAM;EACN,SAAS,IAAI;EACb,QAAQ,IAAI;EACZ,aAAa,EAAE;EACf,8BAAc,IAAI,KAAK;EACxB;;AAGH,SAAS,kBAAkB,KAA6C;AACtE,QAAO;EACL,IAAI,IAAI;EACR,MAAM;EACN,SAAS,wBAAwB,IAAI;EACrC,QAAQ,IAAI;EACZ,aAAa,IAAI,eAAe,EAAE;EAClC,8BAAc,IAAI,KAAK;EACvB,OAAO,IAAI;EACZ;;;;;;;;;;AAWH,SAAS,qBAAqB,KAA+C;CAC3E,MAAM,kBAAkB,IAAI,aAAa,UAAU,KAAK;AAExD,KAAI,CAAC,kBAAkB,kBAAkB,IAAI,IAAI,OAAO,CACtD,QAAO,CAAC,aAAa,IAAI,CAAC;AAE5B,KAAI,CAAC,eACH,QAAO,CACL,aAAa,KAAK,QAAQ,EAC1B;EACE,IAAI,IAAI;EACR,MAAM;EACN,SAAS;EACT,QAAQ,IAAI;EACZ,aAAa,EAAE;EACf,8BAAc,IAAI,KAAK;EACxB,CACF;AAEH,QAAO,CAAC,aAAa,KAAK,QAAQ,EAAE,kBAAkB,IAAI,CAAC;;;;;;AAO7D,SAAS,sBACP,UACA,OACA,QACA,SAO6B;CAC7B,MAAM,SAAS,IAAI,gBAAgB,EACjC,WAAW,OAAO,YAAY,EAC/B,CAAC;AACF,KAAI,QAAQ,UACV,QAAO,IAAI,aAAa,QAAQ,UAAU;CAG5C,MAAM,QAA4B,EAAE;AACpC,QAAO,WAAW;EAChB,KAAK,GAAG,SAAS,GAAG,mBAAmB,MAAM,CAAC,iBAAiB,mBAAmB,OAAO,CAAC,GAAG;EAC7F,QAAQ,QAAQ;EAChB,WAAW,OAAO,YAAY;AAC5B,OAAI;IACF,MAAM,QAAQ,KAAK,MAAM,QAAQ,KAAK;AACtC,YAAQ,MAAM,MAAd;KACE,KAAK;AACH,YAAM,KAAK,GAAG,qBAAqB,MAAM,QAAQ,CAAC;AAClD;KACF,KAAK;AACH,WAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;OAC1C,MAAM,OAAO,MAAM;AACnB,WACE,KAAK,YAAY,MACd,MAAM,EAAE,iBAAiB,MAAM,aACjC,EACD;AACA,aAAK,aAAa,IAAI,MAAM,cAAc,MAAM,KAAK;AACrD;;;AAGJ;KACF,KAAK;AACH,cAAQ,mBAAmB,MAAM,cAAc;AAC/C;KACF,KAAK;AACH,cAAQ,UAAU,MAAM,MAAM;AAC9B;;WAEE;;EAIV,UAAU,QAAQ,QAAQ,oBAAoB,IAAI;EACnD,CAAC,CAAC,WAAW,MAAM;;;AAItB,MAAM,4BAA4B;;;;;;;;;;AAWlC,SAAgB,aAAa,SAAkD;CAC7E,MAAM,EACJ,OACA,WAAW,cACX,eAAe,MACf,eAAe,qBACb;CAEJ,MAAM,CAAC,UAAU,eAAe,SAA6B,EAAE,CAAC;CAChE,MAAM,CAAC,QAAQ,aAAa,SAA0B,OAAO;CAC7D,MAAM,CAAC,gBAAgB,qBAAqB,SAAwB,KAAK;CACzE,MAAM,CAAC,OAAO,YAAY,SAAwB,KAAK;CACvD,MAAM,CAAC,eAAe,oBAAoB,SAAwB,KAAK;CAEvE,MAAM,kBAAkB,kBAAkB;CAC1C,MAAM,yBAAyB,WAAW;CAE1C,MAAM,qBAAqB,OAA+B,KAAK;CAC/D,MAAM,qBAAqB,OAA+B,KAAK;CAC/D,MAAM,oBAAoB,OAAsB,KAAK;CACrD,MAAM,mBAAmB,OAAsB,KAAK;CACpD,MAAM,oBAAoB,OAAO,MAAM;CACvC,MAAM,wBAAwB,aACtB,GACP;AAED,iBAAgB;AACd,oBAAkB,UAAU;AAC5B,mBAAiB,UAAU;IAC1B,CAAC,gBAAgB,cAAc,CAAC;AA4EnC,uBAAsB,UAzEK,aACxB,UAA4B;AAC3B,UAAQ,MAAM,MAAd;GACE,KAAK;AACH,sBAAkB,MAAM,eAAe;AACvC,QAAI,aACF,aAAY,cAAc,MAAM,eAAe;AAEjD;GAGF,KAAK;AACH,iBAAa,SAAS;KACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,SAAI,MAAM,SAAS,YACjB,QAAO,CAAC,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;MAAE,GAAG;MAAM,QAAQ,MAAM;MAAQ,CAAC;AAElE,YAAO;MACP;AACF;GAGF,KAAK,kBAAkB;IACrB,MAAM,OAAO,kBAAkB,MAAM,QAAQ;AAC7C,iBAAa,SAAS;KACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,SAAI,CAAC,QAAQ,KAAK,SAAS,YAAa,QAAO;AAE/C,SAAI,KAAK,OAAO,MAAM,QAAQ,aAAa,KAAK,OAAO,GACrD,QAAO,CAAC,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE,KAAK;AAGrC,YAAO;MACP;AACF;;GAGF,KAAK;AACH,iBAAa,SAAS;AAEpB,UAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK;MACzC,MAAM,MAAM,KAAK;AACjB,UACE,IAAI,YAAY,MACb,MAAM,EAAE,iBAAiB,MAAM,aACjC,EACD;OACA,MAAM,UAAU,KAAK,OAAO;AAC5B,eAAQ,KAAK;QACX,GAAG;QACH,cAAc,IAAI,IAAI,IAAI,aAAa,CAAC,IACtC,MAAM,cACN,MAAM,KACP;QACF;AACD,cAAO;;;AAGX,YAAO;MACP;AACF;GAGF,KAAK;AACH,aAAS,MAAM,MAAM;AACrB,cAAU,QAAQ;AAClB;;IAIN,CAAC,cAAc,aAAa,CAC7B;CAID,MAAM,cAAc,aACjB,YAAoB;EACnB,MAAM,UAAU,QAAQ,MAAM;AAC9B,MAAI,CAAC,QAAS;AAEd,qBAAmB,SAAS,OAAO;AACnC,qBAAmB,SAAS,OAAO;AACnC,WAAS,KAAK;AACd,YAAU,YAAY;EAEtB,MAAM,cAAgC;GACpC,IAAI,OAAO,YAAY;GACvB,MAAM;GACN,SAAS;GACT,QAAQ;GACR,aAAa,EAAE;GACf,8BAAc,IAAI,KAAK;GACxB;EAED,MAAM,uBAAyC;GAC7C,IAAI;GACJ,MAAM;GACN,SAAS;GACT,QAAQ;GACR,aAAa,EAAE;GACf,8BAAc,IAAI,KAAK;GACxB;AAED,eAAa,SAAS;GAAC,GAAG;GAAM;GAAa;GAAqB,CAAC;EAEnE,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,qBAAmB,UAAU;EAE7B,MAAM,YAAY,OAAO,YAAY;AAErC,aAAW;GACT,KAAK,GAAG,SAAS,GAAG,mBAAmB,MAAM,CAAC,sBAAsB,mBAAmB,UAAU;GACjG,SAAS;IACP,SAAS;IACT,gBAAgB,kBAAkB,WAAW;IAC9C;GACD,QAAQ,gBAAgB;GACxB,WAAW,OAAO,YAAY;AAC5B,QAAI;AACF,2BAAsB,QACpB,KAAK,MAAM,QAAQ,KAAK,CACzB;YACK;;GAIV,UAAU,QAAQ;AAChB,QAAI,gBAAgB,OAAO,QAAS;AACpC,aACE,eAAe,QACX,IAAI,UACJ,sCACL;AACD,cAAU,QAAQ;AAClB,iBAAa,SAAS;KACpB,MAAM,OAAO,KAAK,KAAK,SAAS;AAChC,YAAO,MAAM,SAAS,eAAe,KAAK,OAAO,KAC7C,KAAK,MAAM,GAAG,GAAG,GACjB;MACJ;;GAEL,CAAC,CACC,WAAW;AACV,OAAI,CAAC,gBAAgB,OAAO,QAC1B,YAAW,SAAU,SAAS,UAAU,UAAU,OAAQ;IAE5D,CACD,YAAY;AACX,OAAI,gBAAgB,OAAO,QAAS;AACpC,YAAS,sCAAsC;AAC/C,aAAU,QAAQ;IAClB;IAEN,CAAC,OAAO,SAAS,CAClB;;CAGD,MAAM,YAAY,aAEd,eACA,QACA,YACG;AACH,gBAAc,SAAS,OAAO;EAC9B,MAAM,kBAAkB,IAAI,iBAAiB;AAC7C,gBAAc,UAAU;AAqBxB,SAAO;GAAE,SAnBO,sBAAsB,UAAU,OAAO,QAAQ;IAC7D,WAAW,SAAS;IACpB,QAAQ,gBAAgB;IACxB,kBAAkB;IAClB,UAAU,QAAQ;AAChB,cAAS,IAAI;AACb,eAAU,QAAQ;;IAEpB,oBAAoB,QAAQ;AAC1B,SAAI,gBAAgB,OAAO,QAAS;AACpC,cACE,eAAe,QACX,IAAI,UACH,SAAS,gBAAgB,2BAC/B;AACD,eAAU,QAAQ;;IAErB,CAAC;GAEgB;GAAiB;IAErC,CAAC,OAAO,SAAS,CAClB;CAED,MAAM,qBAAqB,aAEvB,QACA,WACA,0BACG;AACH,YAAU,YAAY;EAEtB,MAAM,YAAY,OAAO,YAAY;AAKrC,aAAW;GACT,KAJA,GAAG,SAAS,GAAG,mBAAmB,MAAM,CAAC,iBAAiB,mBAAmB,OAAO,aACvE,mBAAmB,UAAU,CAAC,aAAa,mBAAmB,UAAU;GAIrF,QAAQ,sBAAsB;GAC9B,WAAW,OAAO,YAAY;AAC5B,QAAI;AACF,2BAAsB,QACpB,KAAK,MAAM,QAAQ,KAAK,CACzB;YACK;;GAIV,UAAU,QAAQ;AAChB,QAAI,sBAAsB,OAAO,QAAS;AAC1C,aACE,eAAe,QACX,IAAI,UACJ,kCACL;AACD,cAAU,QAAQ;;GAErB,CAAC,CACC,WAAW;AACV,OAAI,CAAC,sBAAsB,OAAO,QAChC,YAAW,SAAU,SAAS,UAAU,UAAU,OAAQ;IAE5D,CACD,YAAY;AACX,OAAI,sBAAsB,OAAO,QAAS;AAC1C,YAAS,kCAAkC;AAC3C,aAAU,QAAQ;IAClB;IAEN,CAAC,OAAO,SAAS,CAClB;CAED,MAAM,cAAc,aACjB,WAAmB;AAClB,qBAAmB,SAAS,OAAO;AACnC,YAAU,kBAAkB;AAC5B,WAAS,KAAK;AACd,cAAY,EAAE,CAAC;AACf,oBAAkB,OAAO;EAEzB,MAAM,EAAE,SAAS,oBAAoB,UACnC,oBACA,QACA,EAAE,cAAc,wCAAwC,CACzD;AACD,UAAQ,MAAM,UAAU;AACtB,OAAI,gBAAgB,OAAO,QAAS;AACpC,eAAY,MAAM;GAElB,MAAM,WAAW,MAAM,MAAM,SAAS;AACtC,OACE,UAAU,SAAS,eACnB,CAAC,kBAAkB,IAAI,SAAS,OAAO,CAEvC,oBAAmB,QAAQ,SAAS,IAAI,gBAAgB;OAExD,YAAW,SAAU,SAAS,UAAU,UAAU,OAAQ;IAE5D;IAEJ,CAAC,WAAW,mBAAmB,CAChC;CAED,MAAM,oBAAoB,kBAAkB;AAC1C,MACE,CAAC,iBAAiB,WAClB,CAAC,kBAAkB,WACnB,kBAAkB,QAElB;AAEF,oBAAkB,UAAU;AAC5B,YAAU,gBAAgB;AAC1B,WAAS,KAAK;EAEd,MAAM,YAAY,KAAK,KAAK;EAC5B,MAAM,EAAE,SAAS,oBAAoB,UACnC,oBACA,kBAAkB,SAClB;GACE,WAAW,iBAAiB;GAC5B,cAAc;GACf,CACF;AACD,UACG,KAAK,OAAO,UAAU;AACrB,OAAI,gBAAgB,OAAO,QAAS;GACpC,MAAM,UAAU,KAAK,KAAK,GAAG;AAC7B,OAAI,UAAU,0BACZ,OAAM,IAAI,SAAS,MACjB,WAAW,GAAG,4BAA4B,QAAQ,CACnD;AAEH,OAAI,gBAAgB,OAAO,QAAS;AACpC,OAAI,MAAM,SAAS,EACjB,cAAa,SAAS,CAAC,GAAG,OAAO,GAAG,KAAK,CAAC;AAE5C,cAAW,YACT,YAAY,kBAAkB,SAAS,QACxC;IACD,CACD,cAAc;AACb,qBAAkB,UAAU;IAC5B;IACH,CAAC,UAAU,CAAC;CAEf,MAAM,QAAQ,kBAAkB;AAC9B,qBAAmB,SAAS,OAAO;AACnC,qBAAmB,SAAS,OAAO;AACnC,cAAY,EAAE,CAAC;AACf,oBAAkB,KAAK;AACvB,WAAS,KAAK;AACd,YAAU,OAAO;AACjB,mBAAiB,KAAK;AACtB,MAAI,aACF,gBAAe,aAAa;IAE7B,CAAC,cAAc,aAAa,CAAC;AAEhC,iBAAgB;AACd,MAAI,CAAC,aAAc;EACnB,MAAM,aAAa,YAAY,aAAa;AAC5C,MAAI,WACF,aAAY,WAAW;AAEzB,eAAa;AACX,sBAAmB,SAAS,OAAO;AACnC,sBAAmB,SAAS,OAAO;;IAEpC;EAAC;EAAc;EAAc;EAAY,CAAC;AAE7C,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
1
+ {"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,5 +1,4 @@
1
1
  import { connectSSE } from "../../js/sse/connect-sse.js";
2
- import "../../js/index.js";
3
2
  import { useCallback, useEffect, useRef, useState } from "react";
4
3
 
5
4
  //#region src/react/hooks/use-agent-chat.ts
@@ -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\";\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,6 +1,5 @@
1
1
  import { ArrowClient } from "../../js/arrow/arrow-client.js";
2
2
  import { connectSSE } from "../../js/sse/connect-sse.js";
3
- import "../../js/index.js";
4
3
  import { useAnalyticsWarehousePublisher } from "./use-analytics-warehouse-status.js";
5
4
  import { useQueryHMR } from "./use-query-hmr.js";
6
5
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";