@prismiq/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{CustomSQLEditor-DYeId0Gp.d.ts → ChatBubble-ARocmvZD.d.cts} +48 -4
  2. package/dist/{CustomSQLEditor-BXB4rf1q.d.cts → ChatBubble-BN_CjIpk.d.ts} +48 -4
  3. package/dist/{DashboardDialog-B3vYC5Gs.d.ts → DashboardDialog-UhUGXx2h.d.ts} +6 -4
  4. package/dist/{DashboardDialog-LHmrtNQU.d.cts → DashboardDialog-Z-HypxmG.d.cts} +6 -4
  5. package/dist/{accessibility-2yy5yqRR.d.cts → accessibility-Bu2mNtaB.d.cts} +1 -1
  6. package/dist/{accessibility-2yy5yqRR.d.ts → accessibility-Bu2mNtaB.d.ts} +1 -1
  7. package/dist/charts/index.cjs +27 -27
  8. package/dist/charts/index.d.cts +2 -2
  9. package/dist/charts/index.d.ts +2 -2
  10. package/dist/charts/index.js +2 -2
  11. package/dist/{chunk-NK7HKX2J.cjs → chunk-73TPDGXB.cjs} +7 -7
  12. package/dist/{chunk-NK7HKX2J.cjs.map → chunk-73TPDGXB.cjs.map} +1 -1
  13. package/dist/{chunk-FEABEF3J.cjs → chunk-FKXCINUF.cjs} +551 -299
  14. package/dist/chunk-FKXCINUF.cjs.map +1 -0
  15. package/dist/{chunk-2H5WTH4K.js → chunk-FQ23KG6G.js} +3 -3
  16. package/dist/{chunk-2H5WTH4K.js.map → chunk-FQ23KG6G.js.map} +1 -1
  17. package/dist/{chunk-UPYINBZU.js → chunk-GELI7MDZ.js} +982 -51
  18. package/dist/chunk-GELI7MDZ.js.map +1 -0
  19. package/dist/{chunk-WWTT2OJ5.js → chunk-HKZFEXT6.js} +27 -9
  20. package/dist/chunk-HKZFEXT6.js.map +1 -0
  21. package/dist/{chunk-MOAEEF5P.js → chunk-JBJ5LEAG.js} +362 -110
  22. package/dist/chunk-JBJ5LEAG.js.map +1 -0
  23. package/dist/{chunk-4AVL6GQK.cjs → chunk-KXB2IZI2.cjs} +36 -9
  24. package/dist/chunk-KXB2IZI2.cjs.map +1 -0
  25. package/dist/{chunk-EX74SI67.js → chunk-LBE6GIBC.js} +36 -9
  26. package/dist/chunk-LBE6GIBC.js.map +1 -0
  27. package/dist/{chunk-NY6TZLST.cjs → chunk-PG7QBH3G.cjs} +988 -53
  28. package/dist/chunk-PG7QBH3G.cjs.map +1 -0
  29. package/dist/{chunk-MDXGGZSW.cjs → chunk-ZYVN6XAZ.cjs} +35 -37
  30. package/dist/chunk-ZYVN6XAZ.cjs.map +1 -0
  31. package/dist/components/index.cjs +63 -55
  32. package/dist/components/index.d.cts +2 -2
  33. package/dist/components/index.d.ts +2 -2
  34. package/dist/components/index.js +2 -2
  35. package/dist/dashboard/index.cjs +36 -36
  36. package/dist/dashboard/index.d.cts +7 -5
  37. package/dist/dashboard/index.d.ts +7 -5
  38. package/dist/dashboard/index.js +4 -4
  39. package/dist/export/index.cjs +7 -7
  40. package/dist/export/index.d.cts +6 -4
  41. package/dist/export/index.d.ts +6 -4
  42. package/dist/export/index.js +1 -1
  43. package/dist/{index-C-Qcuu4Y.d.cts → index-B8DelfpL.d.cts} +2 -2
  44. package/dist/{index-rPc7ijt8.d.ts → index-RbfYPQD_.d.ts} +2 -2
  45. package/dist/index.cjs +150 -134
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +97 -9
  48. package/dist/index.d.ts +97 -9
  49. package/dist/index.js +7 -7
  50. package/dist/index.js.map +1 -1
  51. package/dist/{types-WrCbOeAV.d.cts → types-ccB9Ps3k.d.cts} +59 -1
  52. package/dist/{types-WrCbOeAV.d.ts → types-ccB9Ps3k.d.ts} +59 -1
  53. package/dist/utils/index.cjs +15 -15
  54. package/dist/utils/index.d.cts +5 -21
  55. package/dist/utils/index.d.ts +5 -21
  56. package/dist/utils/index.js +1 -1
  57. package/package.json +3 -7
  58. package/dist/chunk-4AVL6GQK.cjs.map +0 -1
  59. package/dist/chunk-EX74SI67.js.map +0 -1
  60. package/dist/chunk-FEABEF3J.cjs.map +0 -1
  61. package/dist/chunk-MDXGGZSW.cjs.map +0 -1
  62. package/dist/chunk-MOAEEF5P.js.map +0 -1
  63. package/dist/chunk-NY6TZLST.cjs.map +0 -1
  64. package/dist/chunk-UPYINBZU.js.map +0 -1
  65. package/dist/chunk-WWTT2OJ5.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkLMTG3LRC_cjs = require('./chunk-LMTG3LRC.cjs');
4
- var chunk4AVL6GQK_cjs = require('./chunk-4AVL6GQK.cjs');
4
+ var chunkKXB2IZI2_cjs = require('./chunk-KXB2IZI2.cjs');
5
5
  var react = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var reactDom = require('react-dom');
@@ -1179,7 +1179,7 @@ var Dialog = react.forwardRef(function Dialog2({
1179
1179
  style,
1180
1180
  ...props
1181
1181
  }, _ref) {
1182
- const { containerRef } = chunk4AVL6GQK_cjs.useFocusTrap({
1182
+ const { containerRef } = chunkKXB2IZI2_cjs.useFocusTrap({
1183
1183
  active: open,
1184
1184
  onEscape: closeOnEscape ? onClose : void 0
1185
1185
  });
@@ -2327,6 +2327,9 @@ var PrismiqClient = class {
2327
2327
  }
2328
2328
  /**
2329
2329
  * Make an authenticated request to the API.
2330
+ *
2331
+ * @param path - API path (starting with /)
2332
+ * @param options - Fetch options including signal for cancellation
2330
2333
  */
2331
2334
  async request(path, options = {}) {
2332
2335
  const url = `${this.endpoint}${path}`;
@@ -2472,6 +2475,17 @@ var PrismiqClient = class {
2472
2475
  const result = await this.request(path);
2473
2476
  return result.values;
2474
2477
  }
2478
+ /**
2479
+ * Get data source metadata including display names and descriptions.
2480
+ *
2481
+ * Returns metadata for all exposed tables/views that can be used
2482
+ * to show user-friendly names in the UI instead of raw table names.
2483
+ *
2484
+ * @returns Array of data source metadata.
2485
+ */
2486
+ async getDataSources() {
2487
+ return this.request("/data-sources");
2488
+ }
2475
2489
  // ============================================================================
2476
2490
  // Query Methods
2477
2491
  // ============================================================================
@@ -2505,12 +2519,14 @@ var PrismiqClient = class {
2505
2519
  *
2506
2520
  * @param query - The query definition to execute.
2507
2521
  * @param bypassCache - If true, bypass cache and re-execute query.
2522
+ * @param signal - Optional AbortSignal for cancellation.
2508
2523
  * @returns The query result with all rows and cache metadata.
2509
2524
  */
2510
- async executeQuery(query, bypassCache = false) {
2525
+ async executeQuery(query, bypassCache = false, signal) {
2511
2526
  return this.request("/query/execute", {
2512
2527
  method: "POST",
2513
- body: JSON.stringify({ query, bypass_cache: bypassCache })
2528
+ body: JSON.stringify({ query, bypass_cache: bypassCache }),
2529
+ signal
2514
2530
  });
2515
2531
  }
2516
2532
  /**
@@ -2827,6 +2843,83 @@ var PrismiqClient = class {
2827
2843
  })
2828
2844
  });
2829
2845
  }
2846
+ // ============================================================================
2847
+ // LLM Methods
2848
+ // ============================================================================
2849
+ /**
2850
+ * Get the LLM agent status.
2851
+ *
2852
+ * @returns LLM status including enabled state, provider, and model.
2853
+ */
2854
+ async getLLMStatus() {
2855
+ return this.request("/llm/status");
2856
+ }
2857
+ /**
2858
+ * Stream a chat response from the LLM agent via SSE.
2859
+ *
2860
+ * @param message - User's message.
2861
+ * @param history - Previous conversation messages.
2862
+ * @param currentSql - Current SQL in the editor (for context).
2863
+ * @param signal - Optional AbortSignal for cancellation.
2864
+ * @yields StreamChunk objects as the response is generated.
2865
+ */
2866
+ async *streamChat(message, history, currentSql, signal) {
2867
+ const url = `${this.endpoint}/llm/chat`;
2868
+ const headers = {
2869
+ "Content-Type": "application/json",
2870
+ "X-Tenant-ID": this.tenantId
2871
+ };
2872
+ if (this.userId) headers["X-User-ID"] = this.userId;
2873
+ if (this.schemaName) headers["X-Schema-Name"] = this.schemaName;
2874
+ if (this.customHeaders) Object.assign(headers, this.customHeaders);
2875
+ if (this.getToken) {
2876
+ const token = await this.getToken();
2877
+ headers["Authorization"] = `Bearer ${token}`;
2878
+ }
2879
+ const response = await fetch(url, {
2880
+ method: "POST",
2881
+ headers,
2882
+ body: JSON.stringify({
2883
+ message,
2884
+ history,
2885
+ current_sql: currentSql
2886
+ }),
2887
+ signal
2888
+ });
2889
+ if (!response.ok) {
2890
+ throw new PrismiqError(
2891
+ `LLM chat failed: ${response.status} ${response.statusText}`,
2892
+ response.status
2893
+ );
2894
+ }
2895
+ const reader = response.body?.getReader();
2896
+ if (!reader) return;
2897
+ const decoder = new TextDecoder();
2898
+ let buffer = "";
2899
+ try {
2900
+ while (true) {
2901
+ const { done, value } = await reader.read();
2902
+ if (done) break;
2903
+ buffer += decoder.decode(value, { stream: true });
2904
+ const lines = buffer.split("\n");
2905
+ buffer = lines.pop() ?? "";
2906
+ for (const line of lines) {
2907
+ if (line.startsWith("data: ")) {
2908
+ const data = line.slice(6).trim();
2909
+ if (data) {
2910
+ try {
2911
+ const chunk = JSON.parse(data);
2912
+ yield chunk;
2913
+ } catch {
2914
+ }
2915
+ }
2916
+ }
2917
+ }
2918
+ }
2919
+ } finally {
2920
+ reader.releaseLock();
2921
+ }
2922
+ }
2830
2923
  };
2831
2924
  var AnalyticsContext = react.createContext(null);
2832
2925
  var CallbacksContext = react.createContext({});
@@ -2863,6 +2956,7 @@ function AnalyticsProvider({
2863
2956
  }
2864
2957
  const client = clientRef.current;
2865
2958
  const [schema, setSchema] = react.useState(null);
2959
+ const [dataSources, setDataSources] = react.useState([]);
2866
2960
  const [isLoading, setIsLoading] = react.useState(true);
2867
2961
  const [error, setError] = react.useState(null);
2868
2962
  const hasFetchedSchemaRef = react.useRef(false);
@@ -2880,8 +2974,13 @@ function AnalyticsProvider({
2880
2974
  setIsLoading(true);
2881
2975
  setError(null);
2882
2976
  try {
2883
- const fetchedSchema = await client.getSchema();
2977
+ const [fetchedSchema, fetchedDataSources] = await Promise.all([
2978
+ client.getSchema(),
2979
+ client.getDataSources().catch(() => [])
2980
+ // Non-critical, fallback to empty
2981
+ ]);
2884
2982
  setSchema(fetchedSchema);
2983
+ setDataSources(fetchedDataSources);
2885
2984
  onSchemaLoadRef.current?.(fetchedSchema);
2886
2985
  } catch (err) {
2887
2986
  const schemaError = err instanceof Error ? err : new Error(String(err));
@@ -2903,6 +3002,7 @@ function AnalyticsProvider({
2903
3002
  () => ({
2904
3003
  client,
2905
3004
  schema,
3005
+ dataSources,
2906
3006
  isLoading,
2907
3007
  error,
2908
3008
  refetchSchema,
@@ -2910,7 +3010,7 @@ function AnalyticsProvider({
2910
3010
  userId,
2911
3011
  schemaName
2912
3012
  }),
2913
- [client, schema, isLoading, error, refetchSchema, tenantId, userId, schemaName]
3013
+ [client, schema, dataSources, isLoading, error, refetchSchema, tenantId, userId, schemaName]
2914
3014
  );
2915
3015
  const callbacks = react.useMemo(
2916
3016
  () => ({
@@ -2931,22 +3031,44 @@ function useAnalytics() {
2931
3031
  return context;
2932
3032
  }
2933
3033
  function useSchema() {
2934
- const { schema, isLoading, error } = useAnalytics();
3034
+ const { schema, dataSources, isLoading, error } = useAnalytics();
2935
3035
  const tables = react.useMemo(() => schema?.tables ?? [], [schema]);
2936
3036
  const relationships = react.useMemo(() => schema?.relationships ?? [], [schema]);
3037
+ const dataSourceMap = react.useMemo(() => {
3038
+ const map = /* @__PURE__ */ new Map();
3039
+ for (const ds of dataSources) {
3040
+ map.set(ds.table, ds);
3041
+ }
3042
+ return map;
3043
+ }, [dataSources]);
2937
3044
  const getTable = react.useCallback(
2938
3045
  (name) => {
2939
3046
  return tables.find((table) => table.name === name);
2940
3047
  },
2941
3048
  [tables]
2942
3049
  );
3050
+ const getDisplayName = react.useCallback(
3051
+ (tableName) => {
3052
+ return dataSourceMap.get(tableName)?.title ?? tableName;
3053
+ },
3054
+ [dataSourceMap]
3055
+ );
3056
+ const getDescription = react.useCallback(
3057
+ (tableName) => {
3058
+ return dataSourceMap.get(tableName)?.subtitle ?? "";
3059
+ },
3060
+ [dataSourceMap]
3061
+ );
2943
3062
  return {
2944
3063
  schema,
2945
3064
  tables,
2946
3065
  relationships,
3066
+ dataSources,
2947
3067
  isLoading,
2948
3068
  error,
2949
- getTable
3069
+ getTable,
3070
+ getDisplayName,
3071
+ getDescription
2950
3072
  };
2951
3073
  }
2952
3074
  function queryEquals(a, b) {
@@ -3855,6 +3977,140 @@ function CrossFilterProvider({
3855
3977
  function useCrossFilterOptional() {
3856
3978
  return react.useContext(CrossFilterContext);
3857
3979
  }
3980
+
3981
+ // src/hooks/useLLMStatus.ts
3982
+ function useLLMStatus() {
3983
+ const { client } = useAnalytics();
3984
+ const [status, setStatus] = react.useState({ enabled: false });
3985
+ const [isLoading, setIsLoading] = react.useState(true);
3986
+ const [error, setError] = react.useState(null);
3987
+ react.useEffect(() => {
3988
+ if (!client) {
3989
+ setIsLoading(false);
3990
+ return;
3991
+ }
3992
+ let cancelled = false;
3993
+ client.getLLMStatus().then((result) => {
3994
+ if (!cancelled) {
3995
+ setStatus(result);
3996
+ setError(null);
3997
+ }
3998
+ }).catch((err) => {
3999
+ if (!cancelled) {
4000
+ setStatus({ enabled: false });
4001
+ setError(err instanceof Error ? err : new Error(String(err)));
4002
+ }
4003
+ }).finally(() => {
4004
+ if (!cancelled) setIsLoading(false);
4005
+ });
4006
+ return () => {
4007
+ cancelled = true;
4008
+ };
4009
+ }, [client]);
4010
+ return {
4011
+ enabled: status.enabled,
4012
+ provider: status.provider,
4013
+ model: status.model,
4014
+ isLoading,
4015
+ error
4016
+ };
4017
+ }
4018
+ function useLLMChat() {
4019
+ const { client } = useAnalytics();
4020
+ const [messages, setMessages] = react.useState([]);
4021
+ const [isStreaming, setIsStreaming] = react.useState(false);
4022
+ const [streamingContent, setStreamingContent] = react.useState("");
4023
+ const [suggestedSql, setSuggestedSql] = react.useState(null);
4024
+ const [error, setError] = react.useState(null);
4025
+ const abortRef = react.useRef(null);
4026
+ const isStreamingRef = react.useRef(false);
4027
+ const messagesRef = react.useRef([]);
4028
+ messagesRef.current = messages;
4029
+ const sendMessage = react.useCallback(
4030
+ async (message, currentSql) => {
4031
+ if (!client || isStreamingRef.current) return;
4032
+ abortRef.current?.abort();
4033
+ const controller = new AbortController();
4034
+ abortRef.current = controller;
4035
+ const userMsg = { role: "user", content: message };
4036
+ setMessages((prev) => [...prev, userMsg]);
4037
+ isStreamingRef.current = true;
4038
+ setIsStreaming(true);
4039
+ setStreamingContent("");
4040
+ setSuggestedSql(null);
4041
+ setError(null);
4042
+ let accumulatedText = "";
4043
+ let lastSql = null;
4044
+ try {
4045
+ const history = messagesRef.current.map((m) => ({
4046
+ role: m.role,
4047
+ content: m.content
4048
+ }));
4049
+ for await (const chunk of client.streamChat(
4050
+ message,
4051
+ history,
4052
+ currentSql,
4053
+ controller.signal
4054
+ )) {
4055
+ if (controller.signal.aborted) break;
4056
+ switch (chunk.type) {
4057
+ case "text":
4058
+ accumulatedText += chunk.content ?? "";
4059
+ setStreamingContent(accumulatedText);
4060
+ break;
4061
+ case "sql":
4062
+ lastSql = chunk.content ?? null;
4063
+ setSuggestedSql(lastSql);
4064
+ break;
4065
+ case "error":
4066
+ setError(chunk.content ?? "Unknown error");
4067
+ break;
4068
+ case "done":
4069
+ break;
4070
+ }
4071
+ }
4072
+ } catch (err) {
4073
+ if (err instanceof Error && err.name !== "AbortError") {
4074
+ setError(err.message);
4075
+ }
4076
+ } finally {
4077
+ isStreamingRef.current = false;
4078
+ setIsStreaming(false);
4079
+ if (accumulatedText) {
4080
+ const assistantMsg = {
4081
+ role: "assistant",
4082
+ content: accumulatedText
4083
+ };
4084
+ setMessages((prev) => [...prev, assistantMsg]);
4085
+ }
4086
+ setStreamingContent("");
4087
+ }
4088
+ },
4089
+ [client]
4090
+ );
4091
+ react.useEffect(() => {
4092
+ return () => {
4093
+ abortRef.current?.abort();
4094
+ };
4095
+ }, []);
4096
+ const clearHistory = react.useCallback(() => {
4097
+ abortRef.current?.abort();
4098
+ setMessages([]);
4099
+ setStreamingContent("");
4100
+ setSuggestedSql(null);
4101
+ setError(null);
4102
+ setIsStreaming(false);
4103
+ }, []);
4104
+ return {
4105
+ messages,
4106
+ isStreaming,
4107
+ streamingContent,
4108
+ suggestedSql,
4109
+ sendMessage,
4110
+ clearHistory,
4111
+ error
4112
+ };
4113
+ }
3858
4114
  var nodeStyles = {
3859
4115
  display: "flex",
3860
4116
  alignItems: "center",
@@ -4317,6 +4573,7 @@ function SchemaExplorer({
4317
4573
  selectedColumns = [],
4318
4574
  searchable = true,
4319
4575
  collapsible = true,
4576
+ headerAction,
4320
4577
  className,
4321
4578
  style
4322
4579
  }) {
@@ -4352,10 +4609,12 @@ function SchemaExplorer({
4352
4609
  style: { ...containerStyles4, ...style },
4353
4610
  role: "tree",
4354
4611
  "aria-label": "Database schema",
4612
+ "data-testid": "schema-explorer-root",
4355
4613
  children: [
4356
4614
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headerStyles2, children: [
4357
4615
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "table", size: 16, style: { color: "var(--prismiq-color-primary)" } }),
4358
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: titleStyles2, children: "Schema Explorer" })
4616
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...titleStyles2, flex: 1 }, children: "Schema Explorer" }),
4617
+ headerAction
4359
4618
  ] }),
4360
4619
  searchable && /* @__PURE__ */ jsxRuntime.jsx("div", { style: searchContainerStyles, children: /* @__PURE__ */ jsxRuntime.jsx(
4361
4620
  Input,
@@ -4364,7 +4623,8 @@ function SchemaExplorer({
4364
4623
  placeholder: "Search tables and columns...",
4365
4624
  value: searchQuery,
4366
4625
  onChange: (e) => setSearchQuery(e.target.value),
4367
- style: { width: "100%" }
4626
+ style: { width: "100%" },
4627
+ "data-testid": "schema-explorer-search"
4368
4628
  }
4369
4629
  ) }),
4370
4630
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: treeContainerStyles, children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingSkeleton, {}) : error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: errorStyles2, children: [
@@ -4835,6 +5095,29 @@ var containerStyles7 = {
4835
5095
  gap: "var(--prismiq-spacing-xs)",
4836
5096
  flex: 1
4837
5097
  };
5098
+ var comboboxContainerStyles = {
5099
+ position: "relative",
5100
+ flex: 1
5101
+ };
5102
+ var dropdownStyles2 = {
5103
+ position: "fixed",
5104
+ backgroundColor: "var(--prismiq-color-background)",
5105
+ border: "1px solid var(--prismiq-color-border)",
5106
+ borderRadius: "var(--prismiq-radius-md)",
5107
+ boxShadow: "var(--prismiq-shadow-md)",
5108
+ zIndex: 1e3,
5109
+ maxHeight: "200px",
5110
+ overflow: "auto"
5111
+ };
5112
+ var optionStyles2 = {
5113
+ padding: "var(--prismiq-spacing-sm) var(--prismiq-spacing-md)",
5114
+ cursor: "pointer",
5115
+ fontSize: "var(--prismiq-font-size-sm)",
5116
+ transition: "background-color 0.1s"
5117
+ };
5118
+ var optionHoverStyles2 = {
5119
+ backgroundColor: "var(--prismiq-color-surface-hover)"
5120
+ };
4838
5121
  function parseValue(value, dataType) {
4839
5122
  if (!value) return void 0;
4840
5123
  const type = dataType?.toLowerCase() ?? "";
@@ -4865,15 +5148,270 @@ function getInputType(dataType) {
4865
5148
  }
4866
5149
  return "text";
4867
5150
  }
5151
+ function isMultiValueOperator(op) {
5152
+ return op === "in_" || op === "not_in" || op === "in_or_null";
5153
+ }
5154
+ var tagContainerStyles = {
5155
+ display: "flex",
5156
+ flexWrap: "wrap",
5157
+ alignItems: "center",
5158
+ gap: "4px",
5159
+ padding: "4px 8px",
5160
+ border: "1px solid var(--prismiq-color-border)",
5161
+ borderRadius: "var(--prismiq-radius-sm)",
5162
+ backgroundColor: "var(--prismiq-color-background)",
5163
+ minHeight: "32px",
5164
+ cursor: "text",
5165
+ flex: 1
5166
+ };
5167
+ var tagStyles = {
5168
+ display: "inline-flex",
5169
+ alignItems: "center",
5170
+ gap: "4px",
5171
+ padding: "1px 6px",
5172
+ backgroundColor: "var(--prismiq-color-surface)",
5173
+ border: "1px solid var(--prismiq-color-border)",
5174
+ borderRadius: "var(--prismiq-radius-sm)",
5175
+ fontSize: "var(--prismiq-font-size-sm)",
5176
+ lineHeight: "20px",
5177
+ whiteSpace: "nowrap"
5178
+ };
5179
+ var tagRemoveStyles = {
5180
+ display: "inline-flex",
5181
+ alignItems: "center",
5182
+ justifyContent: "center",
5183
+ width: "14px",
5184
+ height: "14px",
5185
+ border: "none",
5186
+ background: "none",
5187
+ cursor: "pointer",
5188
+ padding: 0,
5189
+ fontSize: "12px",
5190
+ lineHeight: 1,
5191
+ color: "var(--prismiq-color-text-muted)",
5192
+ borderRadius: "50%"
5193
+ };
5194
+ var tagInputStyles = {
5195
+ border: "none",
5196
+ outline: "none",
5197
+ background: "none",
5198
+ flex: 1,
5199
+ minWidth: "80px",
5200
+ fontSize: "var(--prismiq-font-size-sm)",
5201
+ padding: "2px 0",
5202
+ color: "var(--prismiq-color-text)"
5203
+ };
5204
+ var multiOptionCheckStyles = {
5205
+ marginRight: "6px",
5206
+ color: "var(--prismiq-color-primary)",
5207
+ fontWeight: 700,
5208
+ fontSize: "12px"
5209
+ };
4868
5210
  function FilterValueInput({
4869
5211
  operator,
4870
5212
  value,
4871
5213
  onChange,
4872
5214
  dataType,
4873
5215
  disabled = false,
4874
- className
5216
+ className,
5217
+ tableName,
5218
+ columnName
4875
5219
  }) {
5220
+ const { client } = useAnalytics();
4876
5221
  const inputType = getInputType(dataType);
5222
+ const isMulti = isMultiValueOperator(operator);
5223
+ const [sampleValues, setSampleValues] = react.useState([]);
5224
+ const [isLoadingValues, setIsLoadingValues] = react.useState(false);
5225
+ const fetchedRef = react.useRef(null);
5226
+ const fetchSeqRef = react.useRef(0);
5227
+ const [isDropdownOpen, setIsDropdownOpen] = react.useState(false);
5228
+ const [highlightedIndex, setHighlightedIndex] = react.useState(-1);
5229
+ const [dropdownPosition, setDropdownPosition] = react.useState({ top: 0, left: 0, width: 0 });
5230
+ const inputRef = react.useRef(null);
5231
+ const dropdownRef = react.useRef(null);
5232
+ const containerRef = react.useRef(null);
5233
+ const [multiInputText, setMultiInputText] = react.useState("");
5234
+ react.useEffect(() => {
5235
+ setMultiInputText("");
5236
+ }, [operator]);
5237
+ react.useEffect(() => {
5238
+ if (!tableName || !columnName || !client) {
5239
+ setSampleValues([]);
5240
+ setIsLoadingValues(false);
5241
+ fetchedRef.current = null;
5242
+ return;
5243
+ }
5244
+ const fetchKey = `${tableName}.${columnName}`;
5245
+ if (fetchedRef.current === fetchKey) return;
5246
+ const fetchSeq = ++fetchSeqRef.current;
5247
+ const fetchSamples = async () => {
5248
+ setIsLoadingValues(true);
5249
+ try {
5250
+ const values = await client.getColumnSample(tableName, columnName, 100);
5251
+ const stringValues = values.filter((v) => v !== null && v !== void 0).map((v) => String(v));
5252
+ if (fetchSeqRef.current !== fetchSeq) return;
5253
+ setSampleValues(stringValues);
5254
+ fetchedRef.current = fetchKey;
5255
+ } catch (err) {
5256
+ if (fetchSeqRef.current !== fetchSeq) return;
5257
+ console.error("Failed to fetch sample values:", err);
5258
+ setSampleValues([]);
5259
+ fetchedRef.current = null;
5260
+ } finally {
5261
+ if (fetchSeqRef.current === fetchSeq) {
5262
+ setIsLoadingValues(false);
5263
+ }
5264
+ }
5265
+ };
5266
+ fetchSamples();
5267
+ }, [client, tableName, columnName]);
5268
+ const selectedValues = isMulti && Array.isArray(value) ? value.filter((v) => v !== null && v !== void 0).map((v) => String(v)) : [];
5269
+ const currentValueStr = isMulti ? multiInputText : formatValue(value);
5270
+ const filteredOptions = isMulti ? sampleValues.filter(
5271
+ (v) => v.toLowerCase().includes(multiInputText.toLowerCase()) && !selectedValues.includes(v)
5272
+ ) : sampleValues.filter(
5273
+ (v) => v.toLowerCase().includes(currentValueStr.toLowerCase())
5274
+ );
5275
+ const updateDropdownPosition = react.useCallback(() => {
5276
+ const el = isMulti ? containerRef.current : inputRef.current;
5277
+ if (el) {
5278
+ const rect = el.getBoundingClientRect();
5279
+ setDropdownPosition({
5280
+ top: rect.bottom + 4,
5281
+ left: rect.left,
5282
+ width: rect.width
5283
+ });
5284
+ }
5285
+ }, [isMulti]);
5286
+ react.useEffect(() => {
5287
+ const handleClickOutside = (event) => {
5288
+ const target = event.target;
5289
+ const isInsideContainer = containerRef.current?.contains(target);
5290
+ const isInsideDropdown = dropdownRef.current?.contains(target);
5291
+ if (!isInsideContainer && !isInsideDropdown) {
5292
+ setIsDropdownOpen(false);
5293
+ }
5294
+ };
5295
+ document.addEventListener("mousedown", handleClickOutside);
5296
+ return () => document.removeEventListener("mousedown", handleClickOutside);
5297
+ }, []);
5298
+ const handleInputFocus = react.useCallback(() => {
5299
+ if (sampleValues.length > 0) {
5300
+ updateDropdownPosition();
5301
+ setIsDropdownOpen(true);
5302
+ setHighlightedIndex(-1);
5303
+ }
5304
+ }, [sampleValues.length, updateDropdownPosition]);
5305
+ const handleOptionSelect = react.useCallback(
5306
+ (optionValue) => {
5307
+ onChange(parseValue(optionValue, dataType));
5308
+ setIsDropdownOpen(false);
5309
+ inputRef.current?.blur();
5310
+ },
5311
+ [onChange, dataType]
5312
+ );
5313
+ const addMultiValue = react.useCallback(
5314
+ (val) => {
5315
+ const trimmed = val.trim();
5316
+ if (!trimmed) return;
5317
+ if (selectedValues.includes(trimmed)) return;
5318
+ const newValues = [...selectedValues, trimmed].map((v) => parseValue(v, dataType));
5319
+ onChange(newValues);
5320
+ setMultiInputText("");
5321
+ },
5322
+ [selectedValues, onChange, dataType]
5323
+ );
5324
+ const removeMultiValue = react.useCallback(
5325
+ (val) => {
5326
+ const newValues = selectedValues.filter((v) => v !== val).map((v) => parseValue(v, dataType));
5327
+ onChange(newValues.length > 0 ? newValues : []);
5328
+ },
5329
+ [selectedValues, onChange, dataType]
5330
+ );
5331
+ const handleMultiOptionSelect = react.useCallback(
5332
+ (optionValue) => {
5333
+ if (selectedValues.includes(optionValue)) {
5334
+ removeMultiValue(optionValue);
5335
+ } else {
5336
+ addMultiValue(optionValue);
5337
+ }
5338
+ setMultiInputText("");
5339
+ updateDropdownPosition();
5340
+ inputRef.current?.focus();
5341
+ },
5342
+ [selectedValues, addMultiValue, removeMultiValue, updateDropdownPosition]
5343
+ );
5344
+ const handleMultiInputKeyDown = react.useCallback(
5345
+ (e) => {
5346
+ if (e.key === "Backspace" && multiInputText === "" && selectedValues.length > 0) {
5347
+ const lastVal = selectedValues[selectedValues.length - 1];
5348
+ if (lastVal !== void 0) removeMultiValue(lastVal);
5349
+ return;
5350
+ }
5351
+ if (isDropdownOpen && filteredOptions.length > 0) {
5352
+ switch (e.key) {
5353
+ case "ArrowDown":
5354
+ e.preventDefault();
5355
+ setHighlightedIndex((prev) => Math.min(prev + 1, filteredOptions.length - 1));
5356
+ return;
5357
+ case "ArrowUp":
5358
+ e.preventDefault();
5359
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
5360
+ return;
5361
+ case "Enter":
5362
+ e.preventDefault();
5363
+ if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
5364
+ handleMultiOptionSelect(filteredOptions[highlightedIndex]);
5365
+ } else if (multiInputText.trim()) {
5366
+ addMultiValue(multiInputText);
5367
+ }
5368
+ return;
5369
+ case "Escape":
5370
+ setIsDropdownOpen(false);
5371
+ return;
5372
+ }
5373
+ }
5374
+ if (e.key === "," || e.key === "Enter") {
5375
+ e.preventDefault();
5376
+ addMultiValue(multiInputText);
5377
+ }
5378
+ },
5379
+ [
5380
+ multiInputText,
5381
+ selectedValues,
5382
+ isDropdownOpen,
5383
+ filteredOptions,
5384
+ highlightedIndex,
5385
+ addMultiValue,
5386
+ removeMultiValue,
5387
+ handleMultiOptionSelect
5388
+ ]
5389
+ );
5390
+ const handleSingleKeyDown = react.useCallback(
5391
+ (e) => {
5392
+ if (!isDropdownOpen || filteredOptions.length === 0) return;
5393
+ switch (e.key) {
5394
+ case "ArrowDown":
5395
+ e.preventDefault();
5396
+ setHighlightedIndex((prev) => Math.min(prev + 1, filteredOptions.length - 1));
5397
+ break;
5398
+ case "ArrowUp":
5399
+ e.preventDefault();
5400
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
5401
+ break;
5402
+ case "Enter":
5403
+ e.preventDefault();
5404
+ if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
5405
+ handleOptionSelect(filteredOptions[highlightedIndex]);
5406
+ }
5407
+ break;
5408
+ case "Escape":
5409
+ setIsDropdownOpen(false);
5410
+ break;
5411
+ }
5412
+ },
5413
+ [isDropdownOpen, filteredOptions, highlightedIndex, handleOptionSelect]
5414
+ );
4877
5415
  if (operator === "is_null" || operator === "is_not_null") {
4878
5416
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, {});
4879
5417
  }
@@ -4913,36 +5451,163 @@ function FilterValueInput({
4913
5451
  )
4914
5452
  ] });
4915
5453
  }
4916
- if (operator === "in_" || operator === "not_in" || operator === "in_or_null") {
4917
- const handleMultiChange = (e) => {
4918
- const values = e.target.value.split(",").map((v) => v.trim()).filter(Boolean).map((v) => parseValue(v, dataType));
4919
- onChange(values);
4920
- };
4921
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: containerStyles7, children: /* @__PURE__ */ jsxRuntime.jsx(
5454
+ if (isMulti) {
5455
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: containerStyles7, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, style: comboboxContainerStyles, children: [
5456
+ /* @__PURE__ */ jsxRuntime.jsxs(
5457
+ "div",
5458
+ {
5459
+ "data-testid": "filter-tag-container",
5460
+ style: tagContainerStyles,
5461
+ onClick: () => inputRef.current?.focus(),
5462
+ children: [
5463
+ selectedValues.map((val) => /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-testid": `filter-tag-${val}`, style: tagStyles, children: [
5464
+ val,
5465
+ /* @__PURE__ */ jsxRuntime.jsx(
5466
+ "button",
5467
+ {
5468
+ type: "button",
5469
+ "data-testid": `filter-tag-remove-${val}`,
5470
+ style: tagRemoveStyles,
5471
+ onClick: (e) => {
5472
+ e.stopPropagation();
5473
+ removeMultiValue(val);
5474
+ },
5475
+ tabIndex: -1,
5476
+ children: "\xD7"
5477
+ }
5478
+ )
5479
+ ] }, val)),
5480
+ /* @__PURE__ */ jsxRuntime.jsx(
5481
+ "input",
5482
+ {
5483
+ ref: inputRef,
5484
+ "data-testid": "filter-multi-input",
5485
+ type: "text",
5486
+ placeholder: selectedValues.length === 0 ? isLoadingValues ? "Loading..." : "Type or select values" : "",
5487
+ value: multiInputText,
5488
+ disabled: disabled || isLoadingValues,
5489
+ onChange: (e) => {
5490
+ setMultiInputText(e.target.value);
5491
+ if (sampleValues.length > 0) {
5492
+ updateDropdownPosition();
5493
+ setIsDropdownOpen(true);
5494
+ setHighlightedIndex(-1);
5495
+ }
5496
+ },
5497
+ onFocus: handleInputFocus,
5498
+ onKeyDown: handleMultiInputKeyDown,
5499
+ style: tagInputStyles
5500
+ }
5501
+ )
5502
+ ]
5503
+ }
5504
+ ),
5505
+ selectedValues.length === 0 && !multiInputText && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": "filter-multi-hint", style: {
5506
+ fontSize: "11px",
5507
+ color: "var(--prismiq-color-text-muted)",
5508
+ marginTop: "2px",
5509
+ paddingLeft: "2px"
5510
+ }, children: [
5511
+ "Press ",
5512
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { style: { padding: "0 3px", border: "1px solid var(--prismiq-color-border)", borderRadius: "3px", fontSize: "10px" }, children: "," }),
5513
+ " or ",
5514
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { style: { padding: "0 3px", border: "1px solid var(--prismiq-color-border)", borderRadius: "3px", fontSize: "10px" }, children: "Enter" }),
5515
+ " to add values"
5516
+ ] }),
5517
+ isDropdownOpen && filteredOptions.length > 0 && typeof document !== "undefined" && reactDom.createPortal(
5518
+ /* @__PURE__ */ jsxRuntime.jsx(
5519
+ "div",
5520
+ {
5521
+ ref: dropdownRef,
5522
+ "data-testid": "filter-dropdown",
5523
+ style: {
5524
+ ...dropdownStyles2,
5525
+ top: dropdownPosition.top,
5526
+ left: dropdownPosition.left,
5527
+ width: dropdownPosition.width
5528
+ },
5529
+ children: filteredOptions.map((optionValue, index) => {
5530
+ const isSelected = selectedValues.includes(optionValue);
5531
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5532
+ "div",
5533
+ {
5534
+ "data-testid": `filter-option-${index}`,
5535
+ onClick: () => handleMultiOptionSelect(optionValue),
5536
+ onMouseEnter: () => setHighlightedIndex(index),
5537
+ style: {
5538
+ ...optionStyles2,
5539
+ ...index === highlightedIndex ? optionHoverStyles2 : {},
5540
+ ...isSelected ? { backgroundColor: "var(--prismiq-color-surface)", fontWeight: 500 } : {}
5541
+ },
5542
+ children: [
5543
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: multiOptionCheckStyles, children: isSelected ? "\u2713" : "\u2003" }),
5544
+ optionValue
5545
+ ]
5546
+ },
5547
+ `${optionValue}-${index}`
5548
+ );
5549
+ })
5550
+ }
5551
+ ),
5552
+ document.body
5553
+ )
5554
+ ] }) });
5555
+ }
5556
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: containerStyles7, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, style: comboboxContainerStyles, children: [
5557
+ /* @__PURE__ */ jsxRuntime.jsx(
4922
5558
  Input,
4923
5559
  {
5560
+ ref: inputRef,
5561
+ "data-testid": "filter-single-input",
4924
5562
  inputSize: "sm",
4925
- type: "text",
4926
- placeholder: "value1, value2, ...",
4927
- value: formatValue(value),
4928
- disabled,
4929
- onChange: handleMultiChange,
4930
- style: { flex: 1 }
5563
+ type: inputType,
5564
+ placeholder: isLoadingValues ? "Loading..." : "Type or select value",
5565
+ value: currentValueStr,
5566
+ disabled: disabled || isLoadingValues,
5567
+ onChange: (e) => {
5568
+ onChange(parseValue(e.target.value, dataType));
5569
+ if (sampleValues.length > 0) {
5570
+ updateDropdownPosition();
5571
+ setIsDropdownOpen(true);
5572
+ }
5573
+ },
5574
+ onFocus: handleInputFocus,
5575
+ onKeyDown: handleSingleKeyDown,
5576
+ style: { width: "100%" }
4931
5577
  }
4932
- ) });
4933
- }
4934
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: containerStyles7, children: /* @__PURE__ */ jsxRuntime.jsx(
4935
- Input,
4936
- {
4937
- inputSize: "sm",
4938
- type: inputType,
4939
- placeholder: "Value",
4940
- value: formatValue(value),
4941
- disabled,
4942
- onChange: (e) => onChange(parseValue(e.target.value, dataType)),
4943
- style: { flex: 1 }
4944
- }
4945
- ) });
5578
+ ),
5579
+ isDropdownOpen && filteredOptions.length > 0 && typeof document !== "undefined" && reactDom.createPortal(
5580
+ /* @__PURE__ */ jsxRuntime.jsx(
5581
+ "div",
5582
+ {
5583
+ ref: dropdownRef,
5584
+ "data-testid": "filter-dropdown",
5585
+ style: {
5586
+ ...dropdownStyles2,
5587
+ top: dropdownPosition.top,
5588
+ left: dropdownPosition.left,
5589
+ width: dropdownPosition.width
5590
+ },
5591
+ children: filteredOptions.map((optionValue, index) => /* @__PURE__ */ jsxRuntime.jsx(
5592
+ "div",
5593
+ {
5594
+ "data-testid": `filter-option-${index}`,
5595
+ onClick: () => handleOptionSelect(optionValue),
5596
+ onMouseEnter: () => setHighlightedIndex(index),
5597
+ style: {
5598
+ ...optionStyles2,
5599
+ ...index === highlightedIndex ? optionHoverStyles2 : {},
5600
+ ...optionValue === currentValueStr ? { backgroundColor: "var(--prismiq-color-surface)", fontWeight: 500 } : {}
5601
+ },
5602
+ children: optionValue
5603
+ },
5604
+ `${optionValue}-${index}`
5605
+ ))
5606
+ }
5607
+ ),
5608
+ document.body
5609
+ )
5610
+ ] }) });
4946
5611
  }
4947
5612
  var rowStyles = {
4948
5613
  display: "flex",
@@ -5035,13 +5700,16 @@ function FilterRow({
5035
5700
  });
5036
5701
  return options;
5037
5702
  }, [tables, schema]);
5703
+ const currentTable = react.useMemo(
5704
+ () => tables.find((t) => t.id === filter.table_id),
5705
+ [tables, filter.table_id]
5706
+ );
5038
5707
  const currentColumnSchema = react.useMemo(() => {
5039
- const table = tables.find((t) => t.id === filter.table_id);
5040
- if (!table) return void 0;
5041
- const tableSchema = schema.tables.find((t) => t.name === table.name);
5708
+ if (!currentTable) return void 0;
5709
+ const tableSchema = schema.tables.find((t) => t.name === currentTable.name);
5042
5710
  if (!tableSchema) return void 0;
5043
5711
  return tableSchema.columns.find((c) => c.name === filter.column);
5044
- }, [tables, schema, filter.table_id, filter.column]);
5712
+ }, [currentTable, schema, filter.column]);
5045
5713
  const operatorOptions = react.useMemo(
5046
5714
  () => getOperatorsForType(currentColumnSchema?.data_type),
5047
5715
  [currentColumnSchema]
@@ -5102,7 +5770,9 @@ function FilterRow({
5102
5770
  operator: filter.operator,
5103
5771
  value: filter.value,
5104
5772
  onChange: handleValueChange,
5105
- dataType: currentColumnSchema?.data_type
5773
+ dataType: currentColumnSchema?.data_type,
5774
+ tableName: currentTable?.name,
5775
+ columnName: filter.column
5106
5776
  }
5107
5777
  ) }),
5108
5778
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -7480,7 +8150,7 @@ function TimeSeriesConfig({
7480
8150
  setError("No date column available. Add a date column to the query first.");
7481
8151
  return;
7482
8152
  }
7483
- const parsed = chunk4AVL6GQK_cjs.parseColumnRef(currentDateColumn, "t1");
8153
+ const parsed = chunkKXB2IZI2_cjs.parseColumnRef(currentDateColumn, "t1");
7484
8154
  if (!parsed) {
7485
8155
  setError("Invalid date column reference. Please select a valid date column.");
7486
8156
  return;
@@ -7494,7 +8164,7 @@ function TimeSeriesConfig({
7494
8164
  };
7495
8165
  const handleDateColumnChange = (value) => {
7496
8166
  if (!config || !value) return;
7497
- const parsed = chunk4AVL6GQK_cjs.parseColumnRef(value, config.table_id);
8167
+ const parsed = chunkKXB2IZI2_cjs.parseColumnRef(value, config.table_id);
7498
8168
  if (!parsed) {
7499
8169
  setError("Invalid column reference. Please select a valid date column.");
7500
8170
  return;
@@ -8418,7 +9088,14 @@ function CustomSQLEditor({
8418
9088
  style
8419
9089
  }) {
8420
9090
  const [sql, setSql] = react.useState(initialSql);
9091
+ const prevInitialSqlRef = react.useRef(initialSql);
8421
9092
  const [isFocused, setIsFocused] = react.useState(false);
9093
+ react.useEffect(() => {
9094
+ if (initialSql !== prevInitialSqlRef.current) {
9095
+ prevInitialSqlRef.current = initialSql;
9096
+ setSql(initialSql);
9097
+ }
9098
+ }, [initialSql]);
8422
9099
  const [executeEnabled, setExecuteEnabled] = react.useState(false);
8423
9100
  const [lastExecutedSql, setLastExecutedSql] = react.useState(null);
8424
9101
  const {
@@ -8464,7 +9141,7 @@ function CustomSQLEditor({
8464
9141
  ...buttonStyles,
8465
9142
  ...canExecute ? {} : buttonDisabledStyles
8466
9143
  };
8467
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...containerStyles17, ...style }, children: [
9144
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...containerStyles17, ...style }, "data-testid": "custom-sql-editor", children: [
8468
9145
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: editorWrapperStyles, children: [
8469
9146
  /* @__PURE__ */ jsxRuntime.jsx(
8470
9147
  "textarea",
@@ -8478,7 +9155,8 @@ function CustomSQLEditor({
8478
9155
  style: mergedTextareaStyles,
8479
9156
  spellCheck: false,
8480
9157
  autoComplete: "off",
8481
- autoCapitalize: "off"
9158
+ autoCapitalize: "off",
9159
+ "data-testid": "custom-sql-textarea"
8482
9160
  }
8483
9161
  ),
8484
9162
  validation && !validation.valid && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: validationErrorStyles, children: [
@@ -8502,6 +9180,7 @@ function CustomSQLEditor({
8502
9180
  disabled: !canExecute,
8503
9181
  style: mergedButtonStyles,
8504
9182
  type: "button",
9183
+ "data-testid": "custom-sql-run-button",
8505
9184
  children: [
8506
9185
  isLoading ? "Executing..." : "Run Query",
8507
9186
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", opacity: 0.7 }, children: "(Cmd+Enter)" })
@@ -8543,13 +9222,14 @@ function TableSelector({
8543
9222
  className
8544
9223
  }) {
8545
9224
  const { theme } = chunkLMTG3LRC_cjs.useTheme();
9225
+ const { getDisplayName } = useSchema();
8546
9226
  const availableTableOptions = react.useMemo(() => {
8547
9227
  const selectedNames = new Set(tables.map((t) => t.name));
8548
9228
  return schema.tables.filter((t) => !selectedNames.has(t.name)).map((t) => ({
8549
9229
  value: t.name,
8550
- label: `${t.name} (${t.columns.length} cols)`
9230
+ label: getDisplayName(t.name)
8551
9231
  }));
8552
- }, [schema.tables, tables]);
9232
+ }, [schema.tables, tables, getDisplayName]);
8553
9233
  const suggestedTables = react.useMemo(() => {
8554
9234
  if (!showRelationships || tables.length === 0) return [];
8555
9235
  const selectedNames = new Set(tables.map((t) => t.name));
@@ -8651,7 +9331,7 @@ function TableSelector({
8651
9331
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: containerStyle, children: [
8652
9332
  tables.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: selectedTablesStyle, children: tables.map((table, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: tableChipStyle, children: [
8653
9333
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "table", size: 14 }),
8654
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: table.name }),
9334
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: getDisplayName(table.name) }),
8655
9335
  index === 0 && /* @__PURE__ */ jsxRuntime.jsx(Badge, { size: "sm", variant: "default", children: "primary" }),
8656
9336
  tables.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
8657
9337
  "button",
@@ -8659,7 +9339,7 @@ function TableSelector({
8659
9339
  type: "button",
8660
9340
  style: removeButtonStyle,
8661
9341
  onClick: () => handleRemoveTable(table.id),
8662
- "aria-label": `Remove ${table.name}`,
9342
+ "aria-label": `Remove ${getDisplayName(table.name)}`,
8663
9343
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "x", size: 12 })
8664
9344
  }
8665
9345
  )
@@ -8693,7 +9373,7 @@ function TableSelector({
8693
9373
  title: suggestion.relationship,
8694
9374
  children: [
8695
9375
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "plus", size: 10 }),
8696
- suggestion.table
9376
+ getDisplayName(suggestion.table)
8697
9377
  ]
8698
9378
  },
8699
9379
  suggestion.table
@@ -8702,6 +9382,257 @@ function TableSelector({
8702
9382
  ] })
8703
9383
  ] });
8704
9384
  }
9385
+ function parseContent(content) {
9386
+ const startToken = "```sql";
9387
+ const endToken = "```";
9388
+ const parts = [];
9389
+ let cursor = 0;
9390
+ while (cursor < content.length) {
9391
+ const start = content.indexOf(startToken, cursor);
9392
+ if (start === -1) break;
9393
+ if (start > cursor) {
9394
+ parts.push({ type: "text", value: content.slice(cursor, start) });
9395
+ }
9396
+ const sqlStart = content.indexOf("\n", start + startToken.length);
9397
+ if (sqlStart === -1) break;
9398
+ const end = content.indexOf(endToken, sqlStart + 1);
9399
+ if (end === -1) break;
9400
+ const sql = content.slice(sqlStart + 1, end).trim();
9401
+ if (sql) {
9402
+ parts.push({ type: "sql", value: sql });
9403
+ }
9404
+ cursor = end + endToken.length;
9405
+ }
9406
+ if (cursor < content.length) {
9407
+ parts.push({ type: "text", value: content.slice(cursor) });
9408
+ }
9409
+ return parts;
9410
+ }
9411
+ function ChatBubble({ message, onApplySql }) {
9412
+ const { theme } = chunkLMTG3LRC_cjs.useTheme();
9413
+ const isUser = message.role === "user";
9414
+ const parts = react.useMemo(() => parseContent(message.content), [message.content]);
9415
+ const bubbleStyle = {
9416
+ maxWidth: "85%",
9417
+ padding: `${theme.spacing.sm} ${theme.spacing.md}`,
9418
+ borderRadius: theme.radius.md,
9419
+ fontSize: theme.fontSizes.sm,
9420
+ lineHeight: 1.5,
9421
+ whiteSpace: "pre-wrap",
9422
+ wordBreak: "break-word",
9423
+ alignSelf: isUser ? "flex-end" : "flex-start",
9424
+ backgroundColor: isUser ? theme.colors.primary : theme.colors.surface,
9425
+ color: isUser ? "#fff" : theme.colors.text,
9426
+ border: isUser ? "none" : `1px solid ${theme.colors.border}`
9427
+ };
9428
+ const sqlBlockStyle = {
9429
+ backgroundColor: isUser ? "rgba(0,0,0,0.2)" : theme.colors.background,
9430
+ borderRadius: theme.radius.sm,
9431
+ padding: theme.spacing.sm,
9432
+ margin: `${theme.spacing.xs} 0`,
9433
+ fontFamily: theme.fonts.mono,
9434
+ fontSize: theme.fontSizes.xs,
9435
+ overflow: "auto",
9436
+ position: "relative"
9437
+ };
9438
+ const applyBtnContainerStyle = {
9439
+ display: "flex",
9440
+ justifyContent: "flex-end",
9441
+ marginTop: theme.spacing.xs
9442
+ };
9443
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: bubbleStyle, children: parts.map((part, i) => {
9444
+ if (part.type === "sql") {
9445
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-testid": `chat-sql-${i}`, children: [
9446
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { style: sqlBlockStyle, children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: part.value }) }),
9447
+ onApplySql && /* @__PURE__ */ jsxRuntime.jsx("div", { style: applyBtnContainerStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
9448
+ Button,
9449
+ {
9450
+ variant: "ghost",
9451
+ size: "sm",
9452
+ onClick: () => onApplySql(part.value),
9453
+ "data-testid": `apply-sql-btn-${i}`,
9454
+ children: "Apply to Editor"
9455
+ }
9456
+ ) })
9457
+ ] }, i);
9458
+ }
9459
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: part.value }, i);
9460
+ }) });
9461
+ }
9462
+ function ChatPanel({ currentSql, onApplySql }) {
9463
+ const { theme } = chunkLMTG3LRC_cjs.useTheme();
9464
+ const {
9465
+ messages,
9466
+ isStreaming,
9467
+ streamingContent,
9468
+ suggestedSql,
9469
+ sendMessage,
9470
+ clearHistory,
9471
+ error
9472
+ } = useLLMChat();
9473
+ const [input, setInput] = react.useState("");
9474
+ const messagesEndRef = react.useRef(null);
9475
+ react.useEffect(() => {
9476
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
9477
+ }, [messages, streamingContent]);
9478
+ const handleSend = react.useCallback(() => {
9479
+ const trimmed = input.trim();
9480
+ if (!trimmed) return;
9481
+ setInput("");
9482
+ void sendMessage(trimmed, currentSql);
9483
+ }, [input, currentSql, sendMessage]);
9484
+ const handleKeyDown = react.useCallback(
9485
+ (e) => {
9486
+ if (e.key === "Enter" && !e.shiftKey) {
9487
+ e.preventDefault();
9488
+ handleSend();
9489
+ }
9490
+ },
9491
+ [handleSend]
9492
+ );
9493
+ const containerStyle = {
9494
+ display: "flex",
9495
+ flexDirection: "column",
9496
+ height: "100%",
9497
+ borderLeft: `1px solid ${theme.colors.border}`,
9498
+ backgroundColor: theme.colors.background
9499
+ };
9500
+ const headerStyle = {
9501
+ display: "flex",
9502
+ alignItems: "center",
9503
+ justifyContent: "space-between",
9504
+ padding: `${theme.spacing.sm} ${theme.spacing.md}`,
9505
+ borderBottom: `1px solid ${theme.colors.border}`,
9506
+ flexShrink: 0
9507
+ };
9508
+ const headerTitleStyle = {
9509
+ display: "flex",
9510
+ alignItems: "center",
9511
+ gap: theme.spacing.xs,
9512
+ fontSize: theme.fontSizes.sm,
9513
+ fontWeight: 600,
9514
+ color: theme.colors.text
9515
+ };
9516
+ const messagesStyle = {
9517
+ flex: 1,
9518
+ overflow: "auto",
9519
+ padding: theme.spacing.md,
9520
+ display: "flex",
9521
+ flexDirection: "column",
9522
+ gap: theme.spacing.md
9523
+ };
9524
+ const streamingStyle = {
9525
+ alignSelf: "flex-start",
9526
+ maxWidth: "85%",
9527
+ padding: `${theme.spacing.sm} ${theme.spacing.md}`,
9528
+ borderRadius: theme.radius.md,
9529
+ fontSize: theme.fontSizes.sm,
9530
+ lineHeight: 1.5,
9531
+ whiteSpace: "pre-wrap",
9532
+ wordBreak: "break-word",
9533
+ backgroundColor: theme.colors.surface,
9534
+ color: theme.colors.text,
9535
+ border: `1px solid ${theme.colors.border}`
9536
+ };
9537
+ const suggestedSqlStyle = {
9538
+ padding: theme.spacing.sm,
9539
+ borderTop: `1px solid ${theme.colors.border}`,
9540
+ display: "flex",
9541
+ alignItems: "center",
9542
+ justifyContent: "center",
9543
+ flexShrink: 0
9544
+ };
9545
+ const inputAreaStyle = {
9546
+ display: "flex",
9547
+ gap: theme.spacing.sm,
9548
+ padding: theme.spacing.sm,
9549
+ borderTop: `1px solid ${theme.colors.border}`,
9550
+ flexShrink: 0
9551
+ };
9552
+ const textareaStyle = {
9553
+ flex: 1,
9554
+ resize: "none",
9555
+ border: `1px solid ${theme.colors.border}`,
9556
+ borderRadius: theme.radius.sm,
9557
+ padding: theme.spacing.sm,
9558
+ fontSize: theme.fontSizes.sm,
9559
+ fontFamily: theme.fonts.sans,
9560
+ backgroundColor: theme.colors.surface,
9561
+ color: theme.colors.text,
9562
+ outline: "none",
9563
+ minHeight: "36px",
9564
+ maxHeight: "120px"
9565
+ };
9566
+ const emptyStyle = {
9567
+ flex: 1,
9568
+ display: "flex",
9569
+ alignItems: "center",
9570
+ justifyContent: "center",
9571
+ textAlign: "center",
9572
+ padding: theme.spacing.lg,
9573
+ color: theme.colors.textMuted,
9574
+ fontSize: theme.fontSizes.sm
9575
+ };
9576
+ const errorStyle = {
9577
+ padding: theme.spacing.sm,
9578
+ margin: `0 ${theme.spacing.md}`,
9579
+ borderRadius: theme.radius.sm,
9580
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
9581
+ color: "#ef4444",
9582
+ fontSize: theme.fontSizes.xs,
9583
+ flexShrink: 0
9584
+ };
9585
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: containerStyle, className: "prismiq-chat-panel", "data-testid": "chat-panel-root", children: [
9586
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headerStyle, children: [
9587
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headerTitleStyle, children: [
9588
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "edit", size: 16 }),
9589
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "SQL Assistant" })
9590
+ ] }),
9591
+ messages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", onClick: clearHistory, "data-testid": "chat-clear", children: "Clear" })
9592
+ ] }),
9593
+ messages.length === 0 && !isStreaming ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: emptyStyle, "data-testid": "chat-empty", children: [
9594
+ "Ask me to help write SQL queries.",
9595
+ "\n",
9596
+ "I can see your database schema and validate queries."
9597
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: messagesStyle, "data-testid": "chat-messages", children: [
9598
+ messages.map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx(ChatBubble, { message: msg, onApplySql }, i)),
9599
+ isStreaming && streamingContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: streamingStyle, "data-testid": "chat-streaming", children: [
9600
+ streamingContent,
9601
+ "\u258D"
9602
+ ] }),
9603
+ isStreaming && !streamingContent && /* @__PURE__ */ jsxRuntime.jsx("div", { style: streamingStyle, "data-testid": "chat-streaming", children: "Thinking..." }),
9604
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: messagesEndRef })
9605
+ ] }),
9606
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: errorStyle, "data-testid": "chat-error", children: error }),
9607
+ suggestedSql && !isStreaming && /* @__PURE__ */ jsxRuntime.jsx("div", { style: suggestedSqlStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "primary", size: "sm", onClick: () => onApplySql(suggestedSql), "data-testid": "chat-apply-sql", children: "Apply SQL to Editor" }) }),
9608
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: inputAreaStyle, children: [
9609
+ /* @__PURE__ */ jsxRuntime.jsx(
9610
+ "textarea",
9611
+ {
9612
+ style: textareaStyle,
9613
+ value: input,
9614
+ onChange: (e) => setInput(e.target.value),
9615
+ onKeyDown: handleKeyDown,
9616
+ placeholder: "Ask about your data...",
9617
+ rows: 1,
9618
+ disabled: isStreaming,
9619
+ "data-testid": "chat-input"
9620
+ }
9621
+ ),
9622
+ /* @__PURE__ */ jsxRuntime.jsx(
9623
+ Button,
9624
+ {
9625
+ variant: "primary",
9626
+ size: "sm",
9627
+ onClick: handleSend,
9628
+ disabled: isStreaming || !input.trim(),
9629
+ "data-testid": "chat-send",
9630
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "play", size: 16 })
9631
+ }
9632
+ )
9633
+ ] })
9634
+ ] });
9635
+ }
8705
9636
 
8706
9637
  exports.AggregationPicker = AggregationPicker;
8707
9638
  exports.AnalyticsProvider = AnalyticsProvider;
@@ -8709,6 +9640,8 @@ exports.AutoSaveIndicator = AutoSaveIndicator;
8709
9640
  exports.Badge = Badge;
8710
9641
  exports.Button = Button;
8711
9642
  exports.CalculatedFieldBuilder = CalculatedFieldBuilder;
9643
+ exports.ChatBubble = ChatBubble;
9644
+ exports.ChatPanel = ChatPanel;
8712
9645
  exports.Checkbox = Checkbox;
8713
9646
  exports.CollapsibleSection = CollapsibleSection;
8714
9647
  exports.ColorPaletteSelector = ColorPaletteSelector;
@@ -8772,10 +9705,12 @@ exports.useDashboardMutations = useDashboardMutations;
8772
9705
  exports.useDashboardPinStatus = useDashboardPinStatus;
8773
9706
  exports.useDashboards = useDashboards;
8774
9707
  exports.useDebouncedLayoutSave = useDebouncedLayoutSave;
9708
+ exports.useLLMChat = useLLMChat;
9709
+ exports.useLLMStatus = useLLMStatus;
8775
9710
  exports.usePinMutations = usePinMutations;
8776
9711
  exports.usePinnedDashboards = usePinnedDashboards;
8777
9712
  exports.useQuery = useQuery;
8778
9713
  exports.useSavedQueries = useSavedQueries;
8779
9714
  exports.useSchema = useSchema;
8780
- //# sourceMappingURL=chunk-NY6TZLST.cjs.map
8781
- //# sourceMappingURL=chunk-NY6TZLST.cjs.map
9715
+ //# sourceMappingURL=chunk-PG7QBH3G.cjs.map
9716
+ //# sourceMappingURL=chunk-PG7QBH3G.cjs.map