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