@trilogy-data/trilogy-studio-components 0.1.26 → 0.1.27

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 (138) hide show
  1. package/dist/AssetAutoImporter-OHphsfW9.js +403 -0
  2. package/dist/{ChatCreatorModal-CL-jBnc3.js → ChatCreatorModal-CuXha6Dg.js} +3 -3
  3. package/dist/{ChatModelSelect-Do-VRUFc.js → ChatModelSelect-Cz7axWjP.js} +1 -1
  4. package/dist/{CodeBlock-PT4no49I.js → CodeBlock-BBsaC6hX.js} +4 -4
  5. package/dist/{CommunityModels-Cmro0hV3.js → CommunityModels-CIQQhO-y.js} +7 -7
  6. package/dist/{ConnectionErrorPopup-FSs7U-Yl.js → ConnectionErrorPopup-7vpqgF0s.js} +1 -1
  7. package/dist/{ConnectionView-COnTTuA4.js → ConnectionView-7Jj_Ggdi.js} +49 -49
  8. package/dist/{CreateEditorFromDatasourcePopup-snOh1d5h.js → CreateEditorFromDatasourcePopup-CrVHUWFl.js} +4 -4
  9. package/dist/{Dashboard-BH9ZwMVJ.js → Dashboard-DxSi9W6Q.js} +276 -280
  10. package/dist/{DashboardImportSelector-DQSvu9T-.js → DashboardImportSelector-kuDRDYi8.js} +2 -2
  11. package/dist/{DashboardMobile-BUPtdN2V.js → DashboardMobile-CZEgTb0R.js} +39 -39
  12. package/dist/{DrilldownPane-CBs5ZojS.js → DrilldownPane-BheRHJMs.js} +5 -7
  13. package/dist/{Editor-DQsYpkwl.js → Editor-D6tdutTm.js} +105 -103
  14. package/dist/{ErrorMessage-XMn_08gJ.js → ErrorMessage-D5JeF3Iz.js} +4 -4
  15. package/dist/{GlobalChatPanel-DhnOaige.js → GlobalChatPanel-BXZh0sdD.js} +20 -21
  16. package/dist/{IDE-HLa8vybk.js → IDE-T8PmyAQ_.js} +200 -203
  17. package/dist/{JobsView-B97HBoIE.js → JobsView-Ca97MiSd.js} +13 -14
  18. package/dist/{LLMChat-CxJcPLEG.js → LLMChat-Cch33Yd_.js} +3 -3
  19. package/dist/{LLMView-C1MUf0c2.js → LLMView-BDb6wD74.js} +23 -23
  20. package/dist/{LoadingView-s1uznGwG.js → LoadingView-B-xFwd2N.js} +2 -2
  21. package/dist/{MarkdownRenderer-BNDuEyG-.js → MarkdownRenderer-BWvdx0k3.js} +531 -430
  22. package/dist/{MobileIDE-BpqXoaMR.js → MobileIDE-DpdEQEqc.js} +179 -182
  23. package/dist/{ModelCard-BhN25jaV.js → ModelCard-DkAMP4X9.js} +12 -13
  24. package/dist/{ModelView-DUDA_PtZ.js → ModelView-DomyhN8g.js} +7 -7
  25. package/dist/PageLoading-BAvKGo9h.js +5937 -0
  26. package/dist/{ResultComponent-CAn_r3BL.js → ResultComponent-zoOSoRLe.js} +8 -8
  27. package/dist/{Results-BJtlwebY.js → Results-dq-SlJbU.js} +17 -19
  28. package/dist/{StatusIcon-BylPx2EO.js → StatusIcon-AbxNvJ6p.js} +2 -2
  29. package/dist/{StoreTokenModal-zCSYhfrw.js → StoreTokenModal-CM8WXdu1.js} +2 -2
  30. package/dist/{SymbolsPane-KYSwN9Sv.js → SymbolsPane-DfaxeaPS.js} +6 -3
  31. package/dist/{TrilogyEmbedProvider-WA_dlv9e.js → TrilogyEmbedProvider-D3h0gYnX.js} +9 -9
  32. package/dist/{TutorialPage-NRcee6Dx.js → TutorialPage-BtEJG7cT.js} +27 -28
  33. package/dist/{VegaLiteChart-D1slfOUY.js → VegaLiteChart-BVyAab-o.js} +12131 -10947
  34. package/dist/{WelcomePage-D5mU2x1L.js → WelcomePage-BoqzYBLr.js} +5 -5
  35. package/dist/{embedTheme-Bub46ILG.js → _plugin-vue_export-helper-f9Rw-Il4.js} +14 -6
  36. package/dist/assets/{css.worker-9aDGcEUy.js → css.worker-niORQtzO.js} +14 -14
  37. package/dist/assets/{html.worker-D2LKDpnG.js → html.worker-CO5LvJpF.js} +17 -17
  38. package/dist/assets/json.worker-BezHzWUV.js +58 -0
  39. package/dist/assets/{ts.worker-Bz0-EaKK.js → ts.worker-CYhxGg0r.js} +107 -107
  40. package/dist/{results-DTgAKr5j.js → base-DuKnVD-A.js} +216 -45
  41. package/dist/components/FeedbackBanner.vue.d.ts +4 -0
  42. package/dist/components/FeedbackBanner.vue.d.ts.map +1 -0
  43. package/dist/components/dashboard/DashboardCreatorIcon.vue.d.ts +54 -0
  44. package/dist/components/dashboard/DashboardCreatorIcon.vue.d.ts.map +1 -0
  45. package/dist/components/dashboard/DashboardProvenance.vue.d.ts +30 -0
  46. package/dist/components/dashboard/DashboardProvenance.vue.d.ts.map +1 -0
  47. package/dist/components/dashboard/ReportLayout.vue.d.ts +19 -0
  48. package/dist/components/dashboard/ReportLayout.vue.d.ts.map +1 -0
  49. package/dist/components/editor/Editor.vue.d.ts +3 -3
  50. package/dist/components/editor/EditorCreatorIcon.vue.d.ts +62 -0
  51. package/dist/components/editor/EditorCreatorIcon.vue.d.ts.map +1 -0
  52. package/dist/components/editor/ValidationComponent.vue.d.ts +50 -0
  53. package/dist/components/editor/ValidationComponent.vue.d.ts.map +1 -0
  54. package/dist/components/model/ModelCard.vue.d.ts +3 -3
  55. package/dist/components/sidebar/LLMConnectionList.vue.d.ts.map +1 -1
  56. package/dist/components/sidebar/Sidebar.vue.d.ts.map +1 -1
  57. package/dist/components/tutorial/TutorialPrompt.vue.d.ts +3 -3
  58. package/dist/components/tutorial/TutorialPrompt.vue.d.ts.map +1 -1
  59. package/dist/{remoteWorker-Dkv3TJ0j.js → connections-CWH3zwH2.js} +4544 -4126
  60. package/dist/connections.js +3 -4
  61. package/dist/{cssMode-D4Rzr1Cu.js → cssMode-zpO2ap2B.js} +5 -6
  62. package/dist/dashboard.js +28 -31
  63. package/dist/{dist-CXoL8kXK.js → dist-1z5KURzS.js} +55 -50
  64. package/dist/{dist-DKkuvbPb.js → dist-Bv3D2p5A.js} +20 -18
  65. package/dist/{editor.main-Cl8i7rHY.js → editor.main-BQh6DxDB.js} +33 -33
  66. package/dist/{editorRefinementRuntime-DoP9P5J2.js → editorStore-CAlMJftt.js} +1692 -575
  67. package/dist/embed.js +2 -2
  68. package/dist/{freemarker2-fW5NBILf.js → freemarker2-DQBqlEXc.js} +1 -1
  69. package/dist/{globalChatRuntime-DWZxeS57.js → globalChatRuntime-CyS7vH51.js} +16 -16
  70. package/dist/{handlebars-dG4w8UUY.js → handlebars-tgYxaM70.js} +1 -1
  71. package/dist/{helpers-BLQAgX3C.js → helpers-BbVlA3Pf.js} +34 -35
  72. package/dist/{html-1Dq70BdU.js → html-jJahD3Mi.js} +1 -1
  73. package/dist/{htmlMode--IQmLBeh.js → htmlMode-BA1-8l2e.js} +5 -6
  74. package/dist/{javascript-CTzBM4sF.js → javascript-Cypfo7va.js} +1 -1
  75. package/dist/{jsonMode-DzeJjIEH.js → jsonMode-xJye8PWS.js} +25 -25
  76. package/dist/{liquid-DlHThiki.js → liquid-BGDrv5da.js} +1 -1
  77. package/dist/llm-ClU-2rgk.js +18493 -0
  78. package/dist/llm.js +2 -3
  79. package/dist/{localStorage-D9uyOFEO.js → localStorage-CGQ1iJXt.js} +40 -43
  80. package/dist/{lspLanguageFeatures-Cdfs3EKq.js → lspLanguageFeatures-V3e_p9oN.js} +32 -32
  81. package/dist/{mdx-fn3g4PH5.js → mdx-BUR1aImW.js} +1 -1
  82. package/dist/monaco.js +3 -3
  83. package/dist/{python-CCVj0pFt.js → python-YLkl0pc6.js} +1 -1
  84. package/dist/{razor-Cj3flYnk.js → razor-BP8TRMDY.js} +1 -1
  85. package/dist/{reconcile-Dyf4UZmj.js → reconcile-DXIvGwBk.js} +1 -1
  86. package/dist/{remoteStoreSync-Z-yjlYtI.js → remoteStoreSync-CuQBWpWf.js} +3 -3
  87. package/dist/{rolldown-runtime-Dqa2HsxW.js → rolldown-runtime-B0aSnxlc.js} +2 -2
  88. package/dist/static/test_import.vue.d.ts +4 -0
  89. package/dist/static/test_import.vue.d.ts.map +1 -0
  90. package/dist/stores-LlyiwSbK.js +2604 -0
  91. package/dist/stores.js +4 -7
  92. package/dist/trilogy-studio-components.css +1 -1
  93. package/dist/{tsMode-GI4xMYAl.js → tsMode-BH9HS_sH.js} +64 -66
  94. package/dist/{typescript-CPibXi3J.js → typescript-BM-ynfk9.js} +1 -1
  95. package/dist/{useChatWithTools-B0HZ_DVh.js → useChatWithTools-m3Jn_SV-.js} +11 -11
  96. package/dist/{useDashboard-CdSnPe_6.js → useDashboard-BOAuvVEw.js} +1014 -1022
  97. package/dist/{tabulator_esm-CxLvGDp8.js → useGlobalChatPanel-DuP1q7_f.js} +1729 -711
  98. package/dist/{useResizableDialog-B0ePSts4.js → useResizableDialog-Cs90WJdF.js} +2 -2
  99. package/dist/views/ModelView.vue.d.ts +6 -6
  100. package/dist/views/ModelView.vue.d.ts.map +1 -1
  101. package/dist/views/TutorialPage.vue.d.ts +9 -9
  102. package/dist/views/TutorialPage.vue.d.ts.map +1 -1
  103. package/dist/views.js +192 -194
  104. package/dist/{editor.api2-ClD5pp8H.js → workers-B9Eybl-M.js} +16252 -15926
  105. package/dist/{xml-BNyphceI.js → xml-M5WFPSpA.js} +1 -1
  106. package/dist/{yaml-DE5Vz0PH.js → yaml-CEeFwjz9.js} +1 -1
  107. package/package.json +29 -20
  108. package/dist/AssetAutoImporter-CBzvBaOt.js +0 -405
  109. package/dist/DashboardCreatorInline-CDU7kLy3.js +0 -129
  110. package/dist/DataTable-DoRDynP1.js +0 -365
  111. package/dist/EditorCreatorInline-D6_y1H_h.js +0 -125
  112. package/dist/LoadingButton-OH3frQuO.js +0 -138
  113. package/dist/ModalDialog-D1EOG4rl.js +0 -42
  114. package/dist/ModelCreator-CCdC7EXt.js +0 -253
  115. package/dist/PageLoading-BzniJM0C.js +0 -2669
  116. package/dist/Tooltip-Dihir4zs.js +0 -103
  117. package/dist/_plugin-vue_export-helper-B3ysoDQm.js +0 -8
  118. package/dist/assets/json.worker-CJ5MmO5Q.js +0 -58
  119. package/dist/base-C8IuZWqY.js +0 -164
  120. package/dist/connectionHistoryStorage-DKJHiKJY.js +0 -111
  121. package/dist/connectionHistoryStore-DQeBvStZ.js +0 -61
  122. package/dist/connections-CJSb1TZl.js +0 -229
  123. package/dist/constants-DgFEYN2b.js +0 -7
  124. package/dist/dashboardStore-ppM6nGks.js +0 -641
  125. package/dist/demoSetup-mwa-RDC6.js +0 -2942
  126. package/dist/editorRefinementToolExecutor-CVY3FA7k.js +0 -4624
  127. package/dist/editorStore-BhhmXbi-.js +0 -1019
  128. package/dist/formatting-BbJ5ICgh.js +0 -6
  129. package/dist/llm-BBOVWb-F.js +0 -11967
  130. package/dist/llmStore-DJM2JhzY.js +0 -153
  131. package/dist/monaco.contribution-ikesdolO.js +0 -128
  132. package/dist/stores-BT9hTLny.js +0 -1708
  133. package/dist/trilogy-BroD2Qmp.js +0 -4
  134. package/dist/urlStore-52HG6lsX.js +0 -55
  135. package/dist/useGlobalChatPanel-D_0luajQ.js +0 -85
  136. package/dist/useIsMobile-CNpUQAvk.js +0 -8
  137. package/dist/usePrismHighlight-DVyDiDAL.js +0 -183
  138. package/dist/workers-Br7ThUKL.js +0 -47
@@ -1,4624 +0,0 @@
1
- import { a as e, n as t, t as n } from "./results-DTgAKr5j.js";
2
- import { t as r } from "./formatting-BbJ5ICgh.js";
3
- import { computed as i, ref as a, toValue as o } from "vue";
4
- import { defineStore as s } from "pinia";
5
- //#region models/model.ts
6
- var c = class e {
7
- address;
8
- name;
9
- namespace;
10
- datatype;
11
- purpose;
12
- description;
13
- lineage;
14
- keys;
15
- constructor(e, t, n, r, i, a, o = [], s = []) {
16
- this.address = e, this.name = t, this.namespace = n, this.datatype = r, this.purpose = i, this.description = a || null, this.lineage = o, this.keys = s;
17
- }
18
- toJSON() {
19
- return {
20
- address: this.address,
21
- name: this.name,
22
- namespace: this.namespace,
23
- datatype: this.datatype,
24
- purpose: this.purpose,
25
- description: this.description,
26
- lineage: this.lineage,
27
- keys: this.keys
28
- };
29
- }
30
- static fromJSON(t) {
31
- return new e(t.address, t.name, t.namespace, t.datatype, t.purpose, t.description, t.lineage, t.keys);
32
- }
33
- }, l = class e {
34
- name;
35
- address;
36
- concepts;
37
- grain;
38
- constructor(e, t, n, r) {
39
- this.name = e, this.address = t, this.concepts = n, this.grain = r;
40
- }
41
- toJSON() {
42
- return {
43
- name: this.name,
44
- address: this.address,
45
- concepts: this.concepts.map((e) => e.toJSON()),
46
- grain: this.grain.map((e) => e.toJSON())
47
- };
48
- }
49
- static fromJSON(t) {
50
- return new e(t.name, t.address, t.concepts.map((e) => c.fromJSON(e)), t.grain.map((e) => c.fromJSON(e)));
51
- }
52
- }, u = class e {
53
- editor;
54
- alias;
55
- concepts;
56
- datasources;
57
- constructor(t, n, r, i) {
58
- this.editor = t, this.alias = e.normalizeAlias(n), this.concepts = r || [], this.datasources = i || [];
59
- }
60
- static normalizeAlias(e) {
61
- return e.replace(/\.(preql|sql|py)$/i, "");
62
- }
63
- toJSON() {
64
- return {
65
- editor: this.editor,
66
- alias: this.alias,
67
- concepts: this.concepts.map((e) => e.toJSON()),
68
- datasources: this.datasources.map((e) => e.toJSON())
69
- };
70
- }
71
- static fromJSON(t) {
72
- return new e(t.editor, t.alias, (t.concepts || []).map((e) => new c(e.address, e.name, e.namespace, e.datatype, e.purpose, e.description, e.lineage, e.keys || [])), (t.datasources || []).map((e) => new l(e.name, e.address, (e.concepts || []).map((e) => new c(e.address, e.name, e.namespace, e.datatype, e.purpose, e.description, e.lineage, e.keys || [])), (e.grain || []).map((e) => new c(e.address, e.name, e.namespace, e.datatype, e.purpose, e.description, e.lineage, e.keys || [])))));
73
- }
74
- }, d = class e {
75
- name;
76
- storage;
77
- sources;
78
- parseError = null;
79
- changed = !0;
80
- description = null;
81
- deleted = !1;
82
- constructor({ name: e, sources: t, storage: n, description: r }) {
83
- this.name = e, this.sources = t, this.storage = n, this.changed = !0, this.description = r || null;
84
- }
85
- delete() {
86
- this.deleted = !0, this.changed = !0;
87
- }
88
- setParseResults(e) {
89
- this.changed = !0, this.parseError = null;
90
- for (let t of this.sources) {
91
- let n = e.sources.find((e) => e.alias === t.alias);
92
- n ? (t.concepts = n.concepts, t.datasources = n.datasources) : this.parseError = `No parse results found for source ${t.alias}`;
93
- }
94
- }
95
- setSources(e) {
96
- this.sources = e, this.changed = !0;
97
- }
98
- addModelSource(e) {
99
- let t = this.sources.find((t) => t.alias === e.alias);
100
- if (t) {
101
- if (t.editor === e.editor) return !0;
102
- this.sources = this.sources.filter((t) => t.alias !== e.alias);
103
- }
104
- this.sources.push(e), this.changed = !0;
105
- }
106
- addModelSourceSimple(e, t) {
107
- this.addModelSource(new u(e, t, [], []));
108
- }
109
- removeModelSourceSimple(e) {
110
- this.sources = this.sources.filter((t) => t.editor !== e), this.changed = !0;
111
- }
112
- updateModelSourceName(e, t) {
113
- let n = this.sources.find((t) => t.editor === e);
114
- n && (n.alias = u.normalizeAlias(t), this.changed = !0);
115
- }
116
- setParseError(e) {
117
- this.parseError = e, this.changed = !0;
118
- }
119
- getEditorByName(e) {
120
- for (let t of this.sources) if (t.alias === e) return t.editor;
121
- return null;
122
- }
123
- static fromJSON(t) {
124
- let n = new e({
125
- name: t.name,
126
- storage: t.storage,
127
- description: t.description,
128
- sources: t.sources.map((e) => u.fromJSON(e))
129
- });
130
- return n.changed = !1, n;
131
- }
132
- toJSON() {
133
- return {
134
- name: this.name,
135
- storage: this.storage,
136
- description: this.description,
137
- sources: this.sources.map((e) => e.toJSON())
138
- };
139
- }
140
- }, f = s("analytics", {
141
- state: () => ({
142
- isEnabled: !0,
143
- sendTelemetry: !0
144
- }),
145
- actions: {
146
- log(e, t, n = !0) {
147
- if (this.isEnabled) {
148
- if (!this.sendTelemetry) {
149
- console.debug("Telemetry disabled. Would have sent:", {
150
- path: e,
151
- title: t,
152
- isEvent: n
153
- });
154
- return;
155
- }
156
- try {
157
- window.goatcounter && (console.debug("Sending telemetry event:", {
158
- path: e,
159
- title: t,
160
- isEvent: n
161
- }), window.goatcounter.count({
162
- path: e,
163
- title: t,
164
- event: n
165
- }));
166
- } catch (e) {
167
- console.warn("Telemetry send failed:", e);
168
- }
169
- }
170
- },
171
- setEnabled(e) {
172
- this.isEnabled = e;
173
- },
174
- setSendTelemetry(e) {
175
- this.sendTelemetry = e;
176
- },
177
- init({ enabled: e = !0, sendTelemetry: t = !0 }) {
178
- this.isEnabled = e, this.sendTelemetry = t;
179
- }
180
- }
181
- }), p = "userSettings", m = s("userSettings", {
182
- state: () => ({
183
- settings: {
184
- theme: "",
185
- trilogyResolver: "",
186
- telemetryEnabled: null,
187
- tipsRead: [],
188
- skipAllTips: !1
189
- },
190
- defaults: {
191
- theme: "dark",
192
- trilogyResolver: "https://trilogy-service.fly.dev",
193
- telemetryEnabled: !0,
194
- tipsRead: [],
195
- skipAllTips: !1
196
- },
197
- isLoading: !1,
198
- hasChanges: !1,
199
- hasLoaded: !1
200
- }),
201
- getters: {
202
- getSettings: (e) => e.settings,
203
- getDefaults: (e) => e.defaults,
204
- hasUnsavedChanges: (e) => e.hasChanges
205
- },
206
- actions: {
207
- updateSetting(e, t) {
208
- this.settings[e] = t, e === "theme" ? this.toggleTheme() : e === "telemetryEnabled" && f().setEnabled(t === !0), this.hasChanges = !0;
209
- },
210
- getUnreadTips(e) {
211
- return this.settings.skipAllTips ? [] : (this.settings.tipsRead || (this.settings.tipsRead = []), e.filter((e) => !this.settings.tipsRead.includes(e.id)));
212
- },
213
- clearDismissedTips() {
214
- this.settings.tipsRead = [], this.hasChanges = !0, this.saveSettings(), this.settings.skipAllTips = !1;
215
- },
216
- markTipRead(e) {
217
- this.settings.tipsRead.includes(e) || (this.settings.tipsRead.push(e), this.hasChanges = !0, this.saveSettings());
218
- },
219
- clearReadTips() {
220
- this.settings.tipsRead = [], this.hasChanges = !0;
221
- },
222
- toggleTheme() {
223
- document.documentElement.classList.remove("dark-theme", "light-theme"), this.settings.theme === "light" ? document.documentElement.classList.add("light-theme") : document.documentElement.classList.add("dark-theme");
224
- },
225
- setHasChanges(e) {
226
- this.hasChanges = e;
227
- },
228
- updateSettings(e) {
229
- this.settings = {
230
- ...this.settings,
231
- ...e
232
- }, this.hasChanges = !0;
233
- },
234
- resetToDefaults() {
235
- let e = this.settings.theme;
236
- this.settings = { ...this.defaults }, this.settings.theme = e, this.hasChanges = !0;
237
- },
238
- async saveSettings() {
239
- this.isLoading = !0;
240
- try {
241
- return localStorage.setItem(p, JSON.stringify(this.settings)), this.hasChanges = !1, !0;
242
- } catch (e) {
243
- return console.error("Failed to save settings:", e), !1;
244
- } finally {
245
- this.isLoading = !1;
246
- }
247
- },
248
- loadSettings() {
249
- this.isLoading = !0;
250
- try {
251
- let e = localStorage.getItem(p);
252
- if (e) {
253
- this.settings = {
254
- ...this.settings,
255
- ...JSON.parse(e)
256
- };
257
- let t = f();
258
- this.settings.telemetryEnabled === !1 && t.setEnabled(this.settings.telemetryEnabled), this.settings.tipsRead || (this.settings.tipsRead = []), "disableEditorHints" in this.settings && delete this.settings.disableEditorHints;
259
- }
260
- } catch (e) {
261
- console.error("Failed to load settings:", e);
262
- } finally {
263
- this.isLoading = !1, this.hasLoaded = !0;
264
- }
265
- }
266
- }
267
- }), h = /* @__PURE__ */ new Set(), g = /* @__PURE__ */ new Map(), _ = {
268
- query: "select 1 as resolver_warmup;",
269
- dialect: "duckdb",
270
- full_model: {
271
- name: "",
272
- sources: []
273
- },
274
- imports: [],
275
- extra_filters: [],
276
- parameters: {},
277
- current_filename: "__resolver_warmup__.preql"
278
- }, v = class {
279
- capacity;
280
- cache;
281
- keyOrder;
282
- constructor(e = 100) {
283
- this.capacity = e, this.cache = /* @__PURE__ */ new Map(), this.keyOrder = [];
284
- }
285
- get(e) {
286
- if (this.cache.has(e)) return this.keyOrder = this.keyOrder.filter((t) => t !== e), this.keyOrder.push(e), this.cache.get(e);
287
- }
288
- set(e, t) {
289
- if (this.cache.has(e)) this.keyOrder = this.keyOrder.filter((t) => t !== e);
290
- else if (this.keyOrder.length >= this.capacity) {
291
- let e = this.keyOrder.shift();
292
- e && this.cache.delete(e);
293
- }
294
- this.keyOrder.push(e), this.cache.set(e, t);
295
- }
296
- clear() {
297
- this.cache.clear(), this.keyOrder = [];
298
- }
299
- size() {
300
- return this.cache.size;
301
- }
302
- }, y = class e {
303
- settingStore;
304
- validateCache;
305
- formatCache;
306
- queryCache;
307
- batchQueryCache;
308
- modelCache;
309
- constructor(e, t = 100) {
310
- this.settingStore = e, this.validateCache = new v(t), this.formatCache = new v(t), this.queryCache = new v(t), this.batchQueryCache = new v(t), this.modelCache = new v(t);
311
- }
312
- createHash(e) {
313
- let t = JSON.stringify(e), n = 5381;
314
- for (let e = 0; e < t.length; e++) n = n * 33 ^ t.charCodeAt(e);
315
- return (n >>> 0).toString(16);
316
- }
317
- createQueryCacheKey(e, t, n, r, i, a, o, s = null, c = null) {
318
- let l = {
319
- query: e,
320
- dialect: t,
321
- full_model: {
322
- name: "",
323
- sources: n || []
324
- },
325
- imports: r || [],
326
- extra_filters: i || [],
327
- parameters: a || {},
328
- current_filename: o || null,
329
- files: s && s.length > 0 ? [...s].sort() : [],
330
- working_path: c || null
331
- };
332
- return this.createHash(l);
333
- }
334
- getErrorMessage(e) {
335
- let t = "An error occurred.";
336
- return e instanceof Error && (t = e.message), e.response && e.response.data && (t = e.response.data.detail), t;
337
- }
338
- buildResolverUrl(e) {
339
- return `${this.settingStore.settings.trilogyResolver.replace(/\/+$/, "")}/${e}`;
340
- }
341
- static RETRY_STATUSES = /* @__PURE__ */ new Set([503]);
342
- static MAX_RETRIES = 3;
343
- static MAX_RETRY_DELAY_MS = 5e3;
344
- delay(e) {
345
- return new Promise((t) => setTimeout(t, e));
346
- }
347
- retryDelayMs(t, n) {
348
- let r = Number(t.headers?.get?.("retry-after")), i = Number.isFinite(r) && r > 0 ? r * 1e3 : 500 * 2 ** n;
349
- return Math.min(i, e.MAX_RETRY_DELAY_MS) * (.75 + Math.random() * .5);
350
- }
351
- async fetchWithErrorHandling(t, n) {
352
- try {
353
- let r = await fetch(t, n);
354
- for (let i = 0; !r.ok && e.RETRY_STATUSES.has(r.status) && i < e.MAX_RETRIES; i++) await this.delay(this.retryDelayMs(r, i)), r = await fetch(t, n);
355
- if (!r.ok) {
356
- let e = await r.json().catch(() => ({}));
357
- throw {
358
- message: `HTTP error ${r.status}`,
359
- response: { data: e }
360
- };
361
- }
362
- return { data: await r.json() };
363
- } catch (e) {
364
- throw console.log(e), Error(this.getErrorMessage(e));
365
- }
366
- }
367
- async validate_query(e, t = null, n = null, r = null, i = null, a = null, o = null, s = null, c = null) {
368
- let l = {
369
- query: e,
370
- sources: t || [],
371
- imports: n || [],
372
- extra_filters: r || [],
373
- extra_content: i || {},
374
- current_filename: a || null,
375
- parameters: o || {},
376
- files: s || null,
377
- working_path: c || null
378
- }, u = this.createHash(l), d = this.validateCache.get(u);
379
- if (d) return d;
380
- let f = await this.fetchWithErrorHandling(this.buildResolverUrl("validate_query"), {
381
- method: "POST",
382
- headers: { "Content-Type": "application/json" },
383
- body: JSON.stringify(l)
384
- });
385
- return this.validateCache.set(u, f), f;
386
- }
387
- async drilldown_query(e, t, n, r, i, a, o = null, s = null, c = null, l = null, u = null, d = null, f = null) {
388
- console.log("TrilogyResolver.drilldown_query called"), console.log(a);
389
- let p = {
390
- query: e,
391
- dialect: t,
392
- type: n,
393
- drilldown_remove: r,
394
- drilldown_add: i,
395
- drilldown_filter: a,
396
- full_model: {
397
- name: "",
398
- sources: o || []
399
- },
400
- imports: s || [],
401
- extra_filters: c || [],
402
- parameters: l || {},
403
- current_filename: u || null,
404
- files: d || null,
405
- working_path: f || null
406
- };
407
- return await this.fetchWithErrorHandling(this.buildResolverUrl("drilldown_query"), {
408
- method: "POST",
409
- headers: { "Content-Type": "application/json" },
410
- body: JSON.stringify(p)
411
- });
412
- }
413
- async format_query(e, t, n, r = null, i = null, a = null, o = null, s = null, c = null, l = null) {
414
- if (n === "sql") return { data: { text: e } };
415
- let u = {
416
- query: e,
417
- dialect: t,
418
- full_model: {
419
- name: "",
420
- sources: r || []
421
- },
422
- imports: i || [],
423
- extra_filters: a || [],
424
- parameters: o || {},
425
- current_filename: s || null,
426
- files: c || null,
427
- working_path: l || null
428
- }, d = this.createHash(u), f = this.formatCache.get(d);
429
- if (f) return f;
430
- let p = await this.fetchWithErrorHandling(this.buildResolverUrl("format_query"), {
431
- method: "POST",
432
- headers: { "Content-Type": "application/json" },
433
- body: JSON.stringify(u)
434
- });
435
- return this.formatCache.set(d, p), p;
436
- }
437
- async resolve_query(e, t, n, r = null, i = null, a = null, o = null, s = null, c = null, l = null) {
438
- if (n === "sql") return { data: {
439
- generated_sql: e,
440
- columns: [],
441
- error: null
442
- } };
443
- let u = this.createQueryCacheKey(e, t, r, i, a, o, s, c, l), d = this.queryCache.get(u);
444
- if (d) return d;
445
- let f = {
446
- query: e,
447
- dialect: t,
448
- full_model: {
449
- name: "",
450
- sources: r || []
451
- },
452
- imports: i || [],
453
- extra_filters: a || [],
454
- parameters: o || {},
455
- current_filename: s || null,
456
- files: c || null,
457
- working_path: l || null
458
- }, p = await this.fetchWithErrorHandling(this.buildResolverUrl("generate_query"), {
459
- method: "POST",
460
- headers: { "Content-Type": "application/json" },
461
- body: JSON.stringify(f)
462
- });
463
- return this.queryCache.set(u, p), p;
464
- }
465
- async resolve_queries_batch(e, t, n = null, r = null, i = null, a = null, o = null, s = null) {
466
- let c = {
467
- queries: e,
468
- dialect: t,
469
- full_model: {
470
- name: "",
471
- sources: n || []
472
- },
473
- imports: r || [],
474
- extra_filters: i || [],
475
- parameters: a || {},
476
- files: o && o.length > 0 ? [...o].sort() : [],
477
- working_path: s || null
478
- }, l = this.createHash(c), u = this.batchQueryCache.get(l);
479
- if (u) return u;
480
- let d = [], f = [];
481
- if (e.forEach((e, c) => {
482
- let l = [...i || [], ...e.extra_filters || []], u = {
483
- ...a || {},
484
- ...e.parameters || {}
485
- }, p = this.createQueryCacheKey(e.query, t, n, r, l.length > 0 ? l : null, Object.keys(u).length > 0 ? u : null, null, o, s), m = this.queryCache.get(p);
486
- m ? d.push({
487
- index: c,
488
- result: {
489
- ...m.data,
490
- label: e.label
491
- }
492
- }) : f.push({
493
- index: c,
494
- component: e
495
- });
496
- }), f.length === 0) {
497
- let e = { data: { queries: d.sort((e, t) => e.index - t.index).map((e) => e.result) } };
498
- return this.batchQueryCache.set(l, e), e;
499
- }
500
- let p = {
501
- queries: f.map((e) => e.component),
502
- dialect: t,
503
- full_model: {
504
- name: "",
505
- sources: n || []
506
- },
507
- imports: r || [],
508
- extra_filters: i || [],
509
- parameters: a || {},
510
- files: o || null,
511
- working_path: s || null
512
- };
513
- (await this.fetchWithErrorHandling(this.buildResolverUrl("generate_queries"), {
514
- method: "POST",
515
- headers: { "Content-Type": "application/json" },
516
- body: JSON.stringify(p)
517
- })).data.queries.forEach((e, c) => {
518
- let l = f[c].index, u = f[c].component, p = [...i || [], ...u.extra_filters || []], m = {
519
- ...a || {},
520
- ...u.parameters || {}
521
- }, h = this.createQueryCacheKey(u.query, t, n, r, p.length > 0 ? p : null, Object.keys(m).length > 0 ? m : null, null, o, s);
522
- this.queryCache.set(h, { data: e }), d.push({
523
- index: l,
524
- result: {
525
- ...e,
526
- label: u.label
527
- }
528
- });
529
- });
530
- let m = { data: { queries: d.sort((e, t) => e.index - t.index).map((e) => e.result) } };
531
- return this.batchQueryCache.set(l, m), m;
532
- }
533
- async resolveModel(e, t) {
534
- let n = {
535
- name: e,
536
- sources: t
537
- }, r = this.createHash(n), i = this.modelCache.get(r);
538
- if (i) return i;
539
- let a = await this.fetchWithErrorHandling(this.buildResolverUrl("parse_model"), {
540
- method: "POST",
541
- headers: { "Content-Type": "application/json" },
542
- body: JSON.stringify(n)
543
- }), o = d.fromJSON(a.data);
544
- return this.modelCache.set(r, o), o;
545
- }
546
- async warmResolver() {
547
- let e = this.settingStore.settings.trilogyResolver.replace(/\/+$/, "");
548
- if (!e || h.has(e)) return;
549
- let t = g.get(e);
550
- if (t) return t;
551
- let n = this.fetchWithErrorHandling(`${e}/generate_query`, {
552
- method: "POST",
553
- headers: { "Content-Type": "application/json" },
554
- body: JSON.stringify(_)
555
- }).then(() => {
556
- h.add(e);
557
- }).catch((e) => {
558
- console.warn("Resolver warm-up request failed:", e);
559
- }).finally(() => {
560
- g.delete(e);
561
- });
562
- return g.set(e, n), n;
563
- }
564
- clearAllCaches() {
565
- this.validateCache.clear(), this.formatCache.clear(), this.queryCache.clear(), this.batchQueryCache.clear(), this.modelCache.clear();
566
- }
567
- getCacheStats() {
568
- return {
569
- validate: this.validateCache.size(),
570
- format: this.formatCache.size(),
571
- query: this.queryCache.size(),
572
- batchQuery: this.batchQueryCache.size(),
573
- model: this.modelCache.size()
574
- };
575
- }
576
- }, ee = "<style>\n /* Every color comes from the theme contract, so the widget follows the\n host's light/dark mode instead of pinning one of them. */\n .widget { padding: 12px 14px; color: var(--widget-text); }\n .row {\n display: flex; align-items: center; gap: 10px;\n padding: 6px 8px; border-radius: 8px; cursor: pointer;\n }\n .row:hover { background: rgba(var(--widget-accent-rgb), 0.08); }\n .row.selected { background: rgba(var(--widget-accent-rgb), 0.18); }\n .label { flex: 0 0 30%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .bar { height: 10px; border-radius: 999px; background: var(--widget-accent); min-width: 2px; }\n .value {\n margin-left: auto;\n font-variant-numeric: tabular-nums;\n color: var(--widget-text-muted);\n }\n .empty { color: var(--widget-text-muted); padding: 12px 14px; }\n</style>\n\n<div class=\"widget\" id=\"root\"></div>\n\n<script>\n var root = document.getElementById('root');\n var selected = null;\n\n trilogy.subscribe(function (state) {\n if (state.status === 'loading' && !state.rows.length) return;\n if (!state.rows.length) {\n root.innerHTML = '<div class=\"empty\">No rows.</div>';\n return;\n }\n\n // Convention: first column is the dimension, second is the measure.\n var dim = state.columns[0] && state.columns[0].name;\n var measure = state.columns[1] && state.columns[1].name;\n if (!dim || !measure) {\n root.innerHTML = '<div class=\"empty\">Needs a dimension and a measure column.</div>';\n return;\n }\n\n var max = 0;\n state.rows.forEach(function (row) {\n var n = Number(row[measure]);\n if (isFinite(n) && n > max) max = n;\n });\n\n root.innerHTML = '';\n state.rows.slice(0, 25).forEach(function (row) {\n var value = Number(row[measure]) || 0;\n var label = String(row[dim]);\n\n var el = document.createElement('div');\n el.className = 'row' + (selected === label ? ' selected' : '');\n\n var name = document.createElement('div');\n name.className = 'label';\n name.textContent = label;\n\n var bar = document.createElement('div');\n bar.className = 'bar';\n bar.style.width = (max ? (value / max) * 55 : 0) + '%';\n\n var num = document.createElement('div');\n num.className = 'value';\n num.textContent = value.toLocaleString();\n\n el.appendChild(name);\n el.appendChild(bar);\n el.appendChild(num);\n\n el.addEventListener('click', function () {\n if (selected === label) {\n selected = null;\n trilogy.filters.clear();\n } else {\n selected = label;\n trilogy.filters.eq(dim, row[dim]);\n }\n });\n\n root.appendChild(el);\n });\n });\n\n trilogy.ready();\n<\/script>", b = [
577
- {
578
- value: "default",
579
- label: "Default",
580
- hint: "Rounded cards, tinted headers, a hairline ring. The standard look."
581
- },
582
- {
583
- value: "flat",
584
- label: "Flat",
585
- hint: "Square corners, no shadow, untinted headers. Reads as one surface."
586
- },
587
- {
588
- value: "paper",
589
- label: "Paper",
590
- hint: "Borderless cards floating on the canvas with roomy spacing. Best for reports and presentations."
591
- },
592
- {
593
- value: "dense",
594
- label: "Dense",
595
- hint: "Tight gutters and short headers, to fit more panels on one screen."
596
- }
597
- ], te = [
598
- {
599
- value: "square",
600
- label: "Square",
601
- hint: "No rounding."
602
- },
603
- {
604
- value: "soft",
605
- label: "Soft",
606
- hint: "Lightly rounded."
607
- },
608
- {
609
- value: "round",
610
- label: "Round",
611
- hint: "Fully rounded cards."
612
- }
613
- ], ne = [
614
- {
615
- value: "compact",
616
- label: "Compact",
617
- hint: "Tight gutters and padding."
618
- },
619
- {
620
- value: "comfortable",
621
- label: "Comfortable",
622
- hint: "Balanced spacing."
623
- },
624
- {
625
- value: "spacious",
626
- label: "Spacious",
627
- hint: "Generous gutters and padding."
628
- }
629
- ], re = [
630
- {
631
- value: "none",
632
- label: "None",
633
- hint: "Cards sit flat on the canvas."
634
- },
635
- {
636
- value: "subtle",
637
- label: "Subtle",
638
- hint: "A faint drop shadow."
639
- },
640
- {
641
- value: "raised",
642
- label: "Raised",
643
- hint: "Cards clearly float."
644
- }
645
- ], ie = b.map((e) => e.value), ae = te.map((e) => e.value), oe = ne.map((e) => e.value), se = re.map((e) => e.value), ce = [
646
- "canvasBackground",
647
- "cardBackground",
648
- "headerBackground",
649
- "borderColor",
650
- "textColor",
651
- "accentColor"
652
- ], le = [
653
- {
654
- key: "canvasBackground",
655
- label: "Canvas",
656
- hint: "The page behind the cards.",
657
- placeholder: {
658
- light: "#f6f8fb",
659
- dark: "#0d0f13"
660
- }
661
- },
662
- {
663
- key: "cardBackground",
664
- label: "Card",
665
- hint: "Fill of each panel.",
666
- placeholder: {
667
- light: "#ffffff",
668
- dark: "#121417"
669
- }
670
- },
671
- {
672
- key: "headerBackground",
673
- label: "Card header",
674
- hint: "Title strip at the top of each panel.",
675
- placeholder: {
676
- light: "#f6f8fb",
677
- dark: "#111318"
678
- }
679
- },
680
- {
681
- key: "borderColor",
682
- label: "Border",
683
- hint: "Card ring and header rule.",
684
- placeholder: {
685
- light: "#d6dde6",
686
- dark: "#2a2f37"
687
- }
688
- },
689
- {
690
- key: "textColor",
691
- label: "Text",
692
- hint: "Body and title text.",
693
- placeholder: {
694
- light: "#1f2733",
695
- dark: "#e6e9ef"
696
- }
697
- },
698
- {
699
- key: "accentColor",
700
- label: "Accent",
701
- hint: "Links, chips, active controls, and chart highlights.",
702
- placeholder: {
703
- light: "#2563eb",
704
- dark: "#60a5fa"
705
- }
706
- }
707
- ], ue = "var(--dashboard-card-bg, var(--trilogy-embed-dashboard-background, var(--dashboard-background, #ffffff)))", de = "var(--dashboard-card-border-color, var(--trilogy-embed-border, var(--border-color, var(--border, #d6dde6))))", fe = {
708
- square: {
709
- card: "0px",
710
- control: "0px",
711
- chip: "4px"
712
- },
713
- soft: {
714
- card: "8px",
715
- control: "6px",
716
- chip: "999px"
717
- },
718
- round: {
719
- card: "14px",
720
- control: "10px",
721
- chip: "999px"
722
- }
723
- }, pe = {
724
- compact: {
725
- gutter: 6,
726
- rowHeight: 30,
727
- canvasPadding: "8px 10px 16px",
728
- headerPadding: "2px 8px 2px",
729
- headerMinHeight: "24px",
730
- mobileGap: "8px",
731
- mobileCanvasPadding: "4px 6px",
732
- mobileHeaderPadding: "2px 8px 2px",
733
- mobileHeaderMinHeight: "22px"
734
- },
735
- comfortable: {
736
- gutter: 10,
737
- rowHeight: 30,
738
- canvasPadding: "16px 18px 24px",
739
- headerPadding: "4px 12px 3px",
740
- headerMinHeight: "27px",
741
- mobileGap: "15px",
742
- mobileCanvasPadding: "5px 10px",
743
- mobileHeaderPadding: "3px 10px 2px",
744
- mobileHeaderMinHeight: "25px"
745
- },
746
- spacious: {
747
- gutter: 16,
748
- rowHeight: 32,
749
- canvasPadding: "24px 28px 32px",
750
- headerPadding: "7px 16px 6px",
751
- headerMinHeight: "34px",
752
- mobileGap: "22px",
753
- mobileCanvasPadding: "10px 14px",
754
- mobileHeaderPadding: "6px 14px 5px",
755
- mobileHeaderMinHeight: "32px"
756
- }
757
- }, me = {
758
- none: {
759
- light: "none",
760
- dark: "none"
761
- },
762
- subtle: {
763
- light: "0 1px 2px rgba(15, 23, 42, 0.08)",
764
- dark: "0 1px 2px rgba(2, 6, 23, 0.35)"
765
- },
766
- raised: {
767
- light: "0 8px 20px rgba(15, 23, 42, 0.12)",
768
- dark: "0 8px 20px rgba(2, 6, 23, 0.5)"
769
- }
770
- }, he = {
771
- default: {
772
- corners: "round",
773
- density: "comfortable",
774
- elevation: "subtle",
775
- mobileCards: !1,
776
- borderWidth: "1px",
777
- headerTint: !0
778
- },
779
- flat: {
780
- corners: "square",
781
- density: "comfortable",
782
- elevation: "none",
783
- mobileCards: !1,
784
- borderWidth: "1px",
785
- headerTint: !1
786
- },
787
- paper: {
788
- corners: "round",
789
- density: "spacious",
790
- elevation: "raised",
791
- mobileCards: !0,
792
- borderWidth: "0px",
793
- headerTint: !0
794
- },
795
- dense: {
796
- corners: "soft",
797
- density: "compact",
798
- elevation: "subtle",
799
- mobileCards: !1,
800
- borderWidth: "1px",
801
- headerTint: !0
802
- }
803
- }, ge = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i, _e = /^(?:rgb|rgba|hsl|hsla)\(\s*[0-9a-z.,%/\s-]+\)$/i, ve = /^[a-z]+$/i;
804
- function ye(e) {
805
- if (typeof e != "string") return !1;
806
- let t = e.trim();
807
- return !t || t.length > 64 || /[;{}\\<>]|\/\*|url\(|var\(|expression|@import/i.test(t) ? !1 : ge.test(t) || _e.test(t) || ve.test(t);
808
- }
809
- function be(e) {
810
- let t = e.trim(), n = t.match(/^#([0-9a-f]{3,8})$/i)?.[1];
811
- if (n) {
812
- let e = (e) => e.length === 3 || e.length === 4 ? e.slice(0, 3).split("").map((e) => e + e).join("") : e.slice(0, 6);
813
- if (n.length === 5 || n.length === 7) return null;
814
- let t = e(n), r = parseInt(t, 16);
815
- return Number.isNaN(r) ? null : `${r >> 16 & 255}, ${r >> 8 & 255}, ${r & 255}`;
816
- }
817
- let r = t.match(/^rgba?\(\s*([0-9.]+)[\s,]+([0-9.]+)[\s,]+([0-9.]+)/i);
818
- if (r) {
819
- let e = [
820
- r[1],
821
- r[2],
822
- r[3]
823
- ].map((e) => Math.round(Number(e)));
824
- return e.some((e) => !Number.isFinite(e) || e < 0 || e > 255) ? null : e.join(", ");
825
- }
826
- return null;
827
- }
828
- function x(e) {
829
- if (!e || typeof e != "object" || Array.isArray(e)) return;
830
- let t = e, n = {}, r = (e, t) => typeof e == "string" && t.includes(e) ? e : void 0, i = r(t.preset, ie);
831
- i && (n.preset = i);
832
- let a = r(t.corners, ae);
833
- a && (n.corners = a);
834
- let o = r(t.density, oe);
835
- o && (n.density = o);
836
- let s = r(t.elevation, se);
837
- s && (n.elevation = s), typeof t.mobileCards == "boolean" && (n.mobileCards = t.mobileCards);
838
- for (let e of ce) ye(t[e]) && (n[e] = t[e].trim());
839
- return Object.keys(n).length > 0 ? n : void 0;
840
- }
841
- function xe(e) {
842
- if (!e || Object.keys(e).length === 0) return "default (inherits the app light/dark theme)";
843
- let t = [`preset ${e.preset || "default"}`];
844
- e.corners && t.push(`corners ${e.corners}`), e.density && t.push(`density ${e.density}`), e.elevation && t.push(`elevation ${e.elevation}`), e.mobileCards !== void 0 && t.push(e.mobileCards ? "cards kept on mobile" : "cards flattened on mobile");
845
- let n = ce.filter((t) => e[t]).map((t) => `${t}=${e[t]}`);
846
- return t.push(n.length > 0 ? `colors: ${n.join(", ")}` : "colors inherited"), t.join(", ");
847
- }
848
- function Se(e) {
849
- let { mode: t } = e, n = e.theme || {}, r = he[n.preset || "default"], i = fe[n.corners || r.corners], a = pe[n.density || r.density], o = n.elevation || r.elevation, s = n.mobileCards ?? r.mobileCards, c = me[o][t], l = `inset 0 0 0 ${r.borderWidth} ${de}` + (c === "none" ? "" : `, ${c}`), u = {
850
- "--dashboard-card-radius": i.card,
851
- "--dashboard-control-radius": i.control,
852
- "--dashboard-chip-radius": i.chip,
853
- "--dashboard-card-border-width": r.borderWidth,
854
- "--dashboard-header-border-width": r.borderWidth,
855
- "--dashboard-card-shadow": c,
856
- "--dashboard-canvas-padding": a.canvasPadding,
857
- "--dashboard-header-padding": a.headerPadding,
858
- "--dashboard-header-min-height": a.headerMinHeight,
859
- "--dashboard-gutter": `${a.gutter}px`,
860
- "--dashboard-card-radius-mobile": s ? i.card : "0px",
861
- "--dashboard-card-bg-mobile": s ? ue : "transparent",
862
- "--dashboard-card-shadow-mobile": s ? l : "none",
863
- "--dashboard-header-border-width-mobile": s ? r.borderWidth : "0px",
864
- "--dashboard-mobile-item-bg": s ? "transparent" : "var(--result-window-bg, transparent)",
865
- "--dashboard-mobile-gap": a.mobileGap,
866
- "--dashboard-mobile-canvas-padding": a.mobileCanvasPadding,
867
- "--dashboard-header-padding-mobile": a.mobileHeaderPadding,
868
- "--dashboard-header-min-height-mobile": a.mobileHeaderMinHeight
869
- };
870
- if (!r.headerTint && !n.headerBackground && (u["--dashboard-header-bg"] = ue), n.cardBackground && (u["--dashboard-card-bg"] = n.cardBackground), n.headerBackground && (u["--dashboard-header-bg"] = n.headerBackground), n.canvasBackground && (u["--dashboard-canvas-bg"] = n.canvasBackground), n.borderColor && (u["--dashboard-card-border-color"] = n.borderColor), n.textColor && (u["--text-color"] = n.textColor), n.accentColor) {
871
- u["--special-text"] = n.accentColor;
872
- let e = be(n.accentColor);
873
- e && (u["--special-text-rgb"] = e);
874
- }
875
- return {
876
- vars: u,
877
- gridMargin: [a.gutter, a.gutter],
878
- rowHeight: a.rowHeight
879
- };
880
- }
881
- function Ce(e, t) {
882
- if (!t) return e;
883
- let n = {};
884
- for (let [r, i] of Object.entries(e)) r in t || (n[r] = i);
885
- return n;
886
- }
887
- //#endregion
888
- //#region dashboards/conditions.ts
889
- function we(e) {
890
- return e.replace(/[^a-zA-Z0-9_]/g, "_");
891
- }
892
- function Te(e) {
893
- let t = 0;
894
- for (let n = 0; n < e.length; n++) t = Math.imul(31, t) + e.charCodeAt(n) | 0;
895
- return Math.abs(t).toString(36).slice(0, 5);
896
- }
897
- function S(e) {
898
- return e instanceof Date ? `${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}` : typeof e == "object" && e && typeof e.toISODate == "function" ? e.toISODate() : e;
899
- }
900
- function Ee(e, t, n = "") {
901
- let r = we(e) + "_" + Te(e) + n;
902
- switch (t.op) {
903
- case "eq": {
904
- let n = S(t.value);
905
- return {
906
- filterString: `${e} = :${r}`,
907
- parameters: { [`:${r}`]: n }
908
- };
909
- }
910
- case "range": {
911
- let n = S(t.value[0]), i = S(t.value[1]);
912
- return {
913
- filterString: `${e} between :${r}_min and :${r}_max`,
914
- parameters: {
915
- [`:${r}_min`]: n,
916
- [`:${r}_max`]: i
917
- }
918
- };
919
- }
920
- case "in": {
921
- let n = {};
922
- return {
923
- filterString: `${e} in (${t.value.map((e, t) => (n[`:${r}_${t}`] = S(e), `:${r}_${t}`)).join(", ")})`,
924
- parameters: n
925
- };
926
- }
927
- case "is_null": return {
928
- filterString: `${e} IS NULL`,
929
- parameters: {}
930
- };
931
- }
932
- }
933
- function De(e) {
934
- if (Array.isArray(e)) {
935
- let t = {};
936
- return e.forEach((e) => {
937
- Object.entries(e).forEach(([e, n]) => {
938
- t[e] || (t[e] = []), t[e].push(n);
939
- });
940
- }), Object.entries(t).map(([e, t]) => t.length > 1 ? `(${t.map((t) => C(e, t)).join(" OR ")})` : C(e, t[0])).join(" AND ");
941
- }
942
- return Object.keys(e).length === 0 ? "" : Object.entries(e).map(([e, t]) => C(e, t)).join(" AND ");
943
- }
944
- function Oe(e) {
945
- if (e instanceof Date) return `'${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}'::date`;
946
- if (e === null) return "NULL";
947
- if (typeof e == "string") return `'${e.replace(/'/g, "!`")}'`;
948
- if (typeof e == "boolean") return e ? "TRUE" : "FALSE";
949
- if (typeof e == "number") return e.toString();
950
- }
951
- function C(e, t) {
952
- return t === null ? `${e} IS NULL` : typeof t == "string" ? `${e}='''${t.replace(/'/g, "!`")}'''` : Array.isArray(t) ? `${e} between ${Oe(t[0])} and ${Oe(t[t.length - 1])}` : typeof t == "number" || typeof t == "boolean" ? `${e}=${t}` : t === void 0 ? `${e} IS NULL` : `${e}='${JSON.stringify(t).replace(/'/g, "!`")}'`;
953
- }
954
- //#endregion
955
- //#region dashboards/crossFilters.ts
956
- function ke(e = []) {
957
- let t = /* @__PURE__ */ new Set();
958
- for (let n of e) n?.label && t.add(n.label);
959
- return Array.from(t);
960
- }
961
- function w(e) {
962
- return { ...e };
963
- }
964
- function T(e) {
965
- return { ...e };
966
- }
967
- function Ae(e, t) {
968
- if (e.op !== t.op) return !1;
969
- switch (e.op) {
970
- case "is_null": return !0;
971
- case "eq": return e.value === t.value;
972
- case "range": {
973
- let n = t;
974
- return e.value[0] === n.value[0] && e.value[1] === n.value[1];
975
- }
976
- case "in": {
977
- let n = t;
978
- return e.value.length === n.value.length && e.value.every((e, t) => e === n.value[t]);
979
- }
980
- }
981
- }
982
- function E(e, t) {
983
- let n = Object.keys(e).sort(), r = Object.keys(t).sort();
984
- return n.length === r.length && n.every((n, i) => n === r[i] && Ae(e[n], t[n]));
985
- }
986
- function je(e, t) {
987
- return Array.isArray(e) && Array.isArray(t) ? e.length === t.length && e.every((e, n) => e === t[n]) : e === t;
988
- }
989
- function Me(e, t) {
990
- let n = Object.keys(e).sort(), r = Object.keys(t).sort();
991
- return n.length === r.length && n.every((n, i) => n === r[i] && je(e[n], t[n]));
992
- }
993
- function Ne(e, t, n, r) {
994
- let i = e.filter((e) => e.source !== t), a = e.filter((e) => e.source === t);
995
- if (r === "remove") return i;
996
- if (r === "add") return [...i, {
997
- source: t,
998
- value: T(n)
999
- }];
1000
- let o = a.some((e) => Me(e.value, n)) ? a.filter((e) => !Me(e.value, n)) : [...a, {
1001
- source: t,
1002
- value: T(n)
1003
- }];
1004
- return [...i, ...o];
1005
- }
1006
- function Pe(e, t, n) {
1007
- if (t.size === 0) return n && e.startsWith("local.") ? e.replace(/^local\./, "") : e;
1008
- if (t.has(e)) return e;
1009
- if (e.startsWith("local.")) {
1010
- let r = e.replace(/^local\./, "");
1011
- if (t.has(r)) return n ? r : e;
1012
- }
1013
- return null;
1014
- }
1015
- function Fe(e, t = [], n = {}) {
1016
- let r = new Set(t), i = n.normalizeLocalFields ?? !1;
1017
- return Object.entries(e).reduce((e, [t, n]) => {
1018
- if (typeof n != "object" || !n || ![
1019
- "eq",
1020
- "range",
1021
- "in",
1022
- "is_null"
1023
- ].includes(n.op)) return e;
1024
- let a = Pe(t, r, i);
1025
- return a && (e[a] = n), e;
1026
- }, {});
1027
- }
1028
- function D(e) {
1029
- if (!e.length) return {
1030
- filterStrings: [],
1031
- parameters: {}
1032
- };
1033
- let t = {};
1034
- for (let n of e) for (let [e, r] of Object.entries(n.value)) t[e] || (t[e] = []), t[e].push(r);
1035
- let n = [], r = {};
1036
- for (let [e, i] of Object.entries(t)) if (i.length > 1) {
1037
- let t = [];
1038
- for (let n = 0; n < i.length; n++) {
1039
- let { filterString: a, parameters: o } = Ee(e, i[n], `_or${n}`);
1040
- t.push(a), r = {
1041
- ...r,
1042
- ...o
1043
- };
1044
- }
1045
- n.push(`(${t.join(" OR ")})`);
1046
- } else {
1047
- let { filterString: t, parameters: a } = Ee(e, i[0]);
1048
- n.push(t), r = {
1049
- ...r,
1050
- ...a
1051
- };
1052
- }
1053
- return {
1054
- filterStrings: n,
1055
- parameters: r
1056
- };
1057
- }
1058
- function O(e) {
1059
- let t = e.conceptFilters || [], n = (e.filters || []).filter((e) => e.source !== "cross"), { filterStrings: r, parameters: i } = D(t), a = r.join(" AND ");
1060
- a && n.push({
1061
- source: "cross",
1062
- value: a,
1063
- parameters: Object.keys(i).length ? i : void 0
1064
- });
1065
- let o = JSON.stringify(n) !== JSON.stringify(e.filters || []);
1066
- return e.filters = n, o;
1067
- }
1068
- function Ie(e, t, n, r) {
1069
- let i = e.filter((e) => e.source !== t), a = e.filter((e) => e.source === t);
1070
- if (r === "remove") return i;
1071
- if (r === "add") return [...i, {
1072
- source: t,
1073
- value: w(n)
1074
- }];
1075
- let o = a.some((e) => E(e.value, n)) ? a.filter((e) => !E(e.value, n)) : [...a, {
1076
- source: t,
1077
- value: w(n)
1078
- }];
1079
- return [...i, ...o];
1080
- }
1081
- function Le(e, t, n, r, i) {
1082
- let a = [];
1083
- return Object.entries(e).forEach(([e, o]) => {
1084
- if (e === t) {
1085
- let e = Ne(o.chartFilters || [], t, r, i);
1086
- JSON.stringify(e) !== JSON.stringify(o.chartFilters || []) && (o.chartFilters = e);
1087
- return;
1088
- }
1089
- if (!o.allowCrossFilter) return;
1090
- let s = Ie(o.conceptFilters || [], t, n, i), c = JSON.stringify(s) !== JSON.stringify(o.conceptFilters || []);
1091
- c && (o.conceptFilters = s);
1092
- let l = O(o);
1093
- (c || l) && a.push(e);
1094
- }), a;
1095
- }
1096
- function Re(e, t, n) {
1097
- let r = e[t];
1098
- if (!r) return !1;
1099
- let i = (r.conceptFilters || []).filter((e) => e.source !== n), a = JSON.stringify(i) !== JSON.stringify(r.conceptFilters || []);
1100
- a && (r.conceptFilters = i);
1101
- let o = O(r);
1102
- return a || o;
1103
- }
1104
- function ze(e, t) {
1105
- let n = [];
1106
- return Object.entries(e).forEach(([e, r]) => {
1107
- if (e === t) {
1108
- r.chartFilters = [];
1109
- return;
1110
- }
1111
- let i = (r.conceptFilters || []).filter((e) => e.source !== t), a = JSON.stringify(i) !== JSON.stringify(r.conceptFilters || []);
1112
- a && (r.conceptFilters = i);
1113
- let o = O(r);
1114
- (a || o) && n.push(e);
1115
- }), n;
1116
- }
1117
- function Be(e) {
1118
- let t = [];
1119
- return Object.entries(e).forEach(([e, n]) => {
1120
- let r = !!n.conceptFilters?.length || !!n.chartFilters?.length || !!n.filters?.length;
1121
- n.conceptFilters = [], n.chartFilters = [], n.filters = [], r && t.push(e);
1122
- }), t;
1123
- }
1124
- function Ve(e = {}) {
1125
- let t = /* @__PURE__ */ new Map(), n = () => {
1126
- let t = e.validFields ? o(e.validFields) : [];
1127
- return new Set(t);
1128
- }, r = (t) => {
1129
- let r = Fe(t.filters, n(), { normalizeLocalFields: e.normalizeLocalFields ?? !0 });
1130
- return {
1131
- source: t.source,
1132
- filters: r,
1133
- chart: T(t.chart || {}),
1134
- append: t.append
1135
- };
1136
- };
1137
- return {
1138
- getSelections() {
1139
- return Array.from(t.values()).flat().map((e) => ({
1140
- source: e.source,
1141
- filters: w(e.filters),
1142
- chart: T(e.chart || {}),
1143
- append: e.append
1144
- }));
1145
- },
1146
- getSelectionSources() {
1147
- return Array.from(t.keys());
1148
- },
1149
- applyDimensionClick(e, n) {
1150
- let i = r(e), a = n ?? (e.append ? "append" : "add");
1151
- if (Object.keys(i.filters).length === 0) return a !== "append" && t.delete(e.source), {};
1152
- let o = t.get(e.source) || [];
1153
- if (a === "remove") return t.delete(e.source), i.filters;
1154
- if (a === "add") return t.set(e.source, [i]), i.filters;
1155
- let s = o.some((e) => E(e.filters, i.filters)) ? o.filter((e) => !E(e.filters, i.filters)) : [...o, i];
1156
- return s.length === 0 ? t.delete(e.source) : t.set(e.source, s), i.filters;
1157
- },
1158
- clearSource(e) {
1159
- t.delete(e);
1160
- },
1161
- clearAll() {
1162
- t.clear();
1163
- },
1164
- hasSelectionFrom(e) {
1165
- return t.has(e);
1166
- },
1167
- getSqlFilterInputsFor(e) {
1168
- return Array.from(t.entries()).filter(([t]) => t !== e).flatMap(([e, t]) => t.map((t) => ({
1169
- source: e,
1170
- value: w(t.filters)
1171
- })));
1172
- },
1173
- getChartSelectionsFor(e) {
1174
- return (t.get(e) || []).map((e) => T(e.chart || {}));
1175
- },
1176
- getFilterExpressionFor(e) {
1177
- let { filterStrings: t } = D(this.getSqlFilterInputsFor(e));
1178
- return t.join(" AND ");
1179
- },
1180
- getSqlFiltersFor(e, t = []) {
1181
- let n = this.getFilterExpressionFor(e);
1182
- return n ? [...t, n] : [...t];
1183
- },
1184
- getSqlFilterLikesFor(e, t = []) {
1185
- let n = t.map((e) => ({
1186
- source: "base",
1187
- value: e
1188
- })), { filterStrings: r, parameters: i } = D(this.getSqlFilterInputsFor(e)), a = r.join(" AND ");
1189
- return a && n.push({
1190
- source: "cross",
1191
- value: a,
1192
- parameters: Object.keys(i).length ? i : void 0
1193
- }), n;
1194
- },
1195
- getSqlParametersFor(e) {
1196
- let { parameters: t } = D(this.getSqlFilterInputsFor(e));
1197
- return t;
1198
- }
1199
- };
1200
- }
1201
- function He(e = {}) {
1202
- let t = a(0), n = Ve(o(e)), r = () => {
1203
- t.value += 1;
1204
- };
1205
- return {
1206
- version: i(() => t.value),
1207
- applyDimensionClick(e, t) {
1208
- let i = n.applyDimensionClick(e, t);
1209
- return r(), i;
1210
- },
1211
- clearSource(e) {
1212
- n.clearSource(e), r();
1213
- },
1214
- clearAll() {
1215
- n.clearAll(), r();
1216
- },
1217
- hasSelectionFrom(e) {
1218
- return t.value, n.hasSelectionFrom(e);
1219
- },
1220
- getSelections() {
1221
- return t.value, n.getSelections();
1222
- },
1223
- getSelectionSources() {
1224
- return t.value, n.getSelectionSources();
1225
- },
1226
- getSqlFilterInputsFor(e) {
1227
- return t.value, n.getSqlFilterInputsFor(e);
1228
- },
1229
- getChartSelectionsFor(e) {
1230
- return t.value, n.getChartSelectionsFor(e);
1231
- },
1232
- getFilterExpressionFor(e) {
1233
- return t.value, n.getFilterExpressionFor(e);
1234
- },
1235
- getSqlFiltersFor(e, r = []) {
1236
- return t.value, n.getSqlFiltersFor(e, r);
1237
- },
1238
- getSqlFilterLikesFor(e, r = []) {
1239
- return t.value, n.getSqlFilterLikesFor(e, r);
1240
- },
1241
- getSqlParametersFor(e) {
1242
- return t.value, n.getSqlParametersFor(e);
1243
- }
1244
- };
1245
- }
1246
- //#endregion
1247
- //#region dashboards/base.ts
1248
- var k = {
1249
- CHART: "chart",
1250
- MARKDOWN: "markdown",
1251
- TABLE: "table",
1252
- FILTER: "filter",
1253
- SECTION_HEADER: "section-header",
1254
- MEMO: "memo",
1255
- CLAIM: "claim",
1256
- APPENDIX_HEADER: "appendix-header",
1257
- FREEFORM: "freeform"
1258
- };
1259
- function Ue(e) {
1260
- return !!e && typeof e == "object" && "markdown" in e;
1261
- }
1262
- function We(e) {
1263
- return !!e && typeof e == "object" && "headline" in e && "verdict" in e && "confidence" in e;
1264
- }
1265
- function Ge(e) {
1266
- return !!e && typeof e == "object" && "claim" in e && !("headline" in e);
1267
- }
1268
- function Ke(e) {
1269
- return !!e && typeof e == "object" && typeof e.html == "string";
1270
- }
1271
- function qe(e) {
1272
- let t = e.content;
1273
- if (typeof t == "string") return e.type === k.CHART || e.type === k.TABLE ? t : void 0;
1274
- if (Ke(t) || We(t) || Ge(t) || Ue(t)) return t.query || void 0;
1275
- }
1276
- var Je = class t {
1277
- id;
1278
- name;
1279
- storage;
1280
- connection;
1281
- connectionId;
1282
- layout = [];
1283
- gridItems = {};
1284
- nextId = 0;
1285
- createdAt;
1286
- updatedAt;
1287
- filter = null;
1288
- imports = [];
1289
- version;
1290
- state = "editing";
1291
- description = "";
1292
- changed = !1;
1293
- deleted = !1;
1294
- layoutType = "grid";
1295
- theme;
1296
- parentDashboardId;
1297
- investigationName;
1298
- investigationCreatedFrom;
1299
- children;
1300
- chatId;
1301
- constructor({ id: e, name: t, storage: n = "local", connection: r, connectionId: i = "", layout: a = [], gridItems: o = {}, nextId: s = 0, createdAt: c, updatedAt: l, filter: u = null, imports: d = [], version: f = 1, state: p = "editing", description: m = "", layoutType: h = "grid", theme: g, parentDashboardId: _ = null, investigationName: v, investigationCreatedFrom: y, children: ee = [], chatId: b = null }) {
1302
- this.id = e, this.name = t, this.storage = n, this.connection = r, this.connectionId = i, this.layout = a, this.gridItems = o, this.nextId = s, this.createdAt = c || /* @__PURE__ */ new Date(), this.updatedAt = l || /* @__PURE__ */ new Date(), this.filter = u, this.imports = d, this.version = f, this.state = p, this.description = m, this.changed = !1, this.layoutType = h, this.theme = x(g), this.parentDashboardId = _, this.investigationName = v, this.investigationCreatedFrom = y, this.children = ee, this.chatId = b;
1303
- }
1304
- setChatId(e) {
1305
- this.chatId !== e && (this.chatId = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1306
- }
1307
- get isInvestigation() {
1308
- return !!this.parentDashboardId;
1309
- }
1310
- delete() {
1311
- this.deleted = !0, this.changed = !0;
1312
- }
1313
- updateItemLayoutDimensions(e, t = null, n = null, r = null, i = null) {
1314
- let a = this.layout.find((t) => t.i === e);
1315
- a ? (t !== null && (a.x = t), n !== null && (a.y = n), r !== null && (a.w = r), i !== null && (a.h = i), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0) : console.warn(`Layout item with ID "${e}" does not exist in the dashboard.`);
1316
- }
1317
- addItem(e, t = 0, n = 0, r = 4, i = null, a = null, o) {
1318
- let s = this.nextId.toString(), c = 10, l = r;
1319
- e === k.MARKDOWN ? c = 3 : e === k.SECTION_HEADER || e === k.APPENDIX_HEADER ? c = 1 : e === k.MEMO ? (c = 8, l = 20) : e === k.CLAIM ? (c = 4, l = 20) : e === k.FREEFORM && (c = 10);
1320
- let u = n;
1321
- u = this.layout.reduce((e, t) => Math.max(e, t.y + t.h), 0), this.layout.push({
1322
- x: t,
1323
- y: u,
1324
- w: l,
1325
- h: i || c,
1326
- i: s,
1327
- static: !1
1328
- });
1329
- let d = `Note ${s}`;
1330
- e === k.CHART ? d = `Chart ${s}` : e === k.TABLE ? d = `Table ${s}` : e === k.SECTION_HEADER ? d = "### Section Header" : e === k.MEMO ? d = "Executive Memo" : e === k.CLAIM ? d = `Claim ${s}` : e === k.APPENDIX_HEADER ? d = "### Appendix" : e === k.FREEFORM && (d = `Widget ${s}`);
1331
- let f = o || d, p = "# Markdown Cell\nEnter your markdown content here.";
1332
- e === k.CHART ? p = "SELECT unnest([1,2,3,4]) as value, 'example' as dim" : e === k.TABLE ? p = "SELECT [1,2,3,4] as value, 'example' as dim" : e === k.SECTION_HEADER || e === k.APPENDIX_HEADER ? p = "" : e === k.MEMO ? p = {
1333
- headline: "",
1334
- verdict: "inconclusive",
1335
- magnitude: "",
1336
- cause: "",
1337
- action: "",
1338
- confidence: "medium"
1339
- } : e === k.CLAIM ? p = { claim: "" } : e === k.FREEFORM && (p = {
1340
- html: ee,
1341
- query: "SELECT unnest(['alpha','beta','gamma']) as dim, random() * 100 as value"
1342
- });
1343
- let m = a ?? p;
1344
- return this.gridItems[s] = {
1345
- type: e,
1346
- content: m,
1347
- name: f,
1348
- width: 0,
1349
- height: 0,
1350
- allowCrossFilter: !(e === k.SECTION_HEADER || e === k.APPENDIX_HEADER || e === k.MEMO)
1351
- }, this.nextId++, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, s;
1352
- }
1353
- setLayoutType(e) {
1354
- this.layoutType !== e && (this.layoutType = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1355
- }
1356
- setTheme(e) {
1357
- let t = e === null ? void 0 : x({
1358
- ...this.theme,
1359
- ...e
1360
- });
1361
- JSON.stringify(t ?? null) !== JSON.stringify(this.theme ?? null) && (this.theme = t, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1362
- }
1363
- copyItem(e) {
1364
- if (!this.gridItems[e]) throw Error(`Item with ID "${e}" does not exist in the dashboard.`);
1365
- let t = this.layout.find((t) => t.i === e);
1366
- if (!t) throw Error(`Layout item with ID "${e}" does not exist in the dashboard.`);
1367
- let n = this.nextId.toString();
1368
- return this.gridItems[n] = { ...this.gridItems[e] }, this.layout.push({
1369
- ...t,
1370
- i: n
1371
- }), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, this.nextId++, n;
1372
- }
1373
- updateItemContent(e, t) {
1374
- this.gridItems[e] && (this.gridItems[e] = {
1375
- ...this.gridItems[e],
1376
- content: t
1377
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1378
- }
1379
- updateItemCrossFilterEligibility(e, t) {
1380
- this.gridItems[e] ? (this.gridItems[e] = {
1381
- ...this.gridItems[e],
1382
- allowCrossFilter: t
1383
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0) : console.warn(`Item with ID "${e}" does not exist in dashboard "${this.id}". Cannot update cross-filter eligibility.`);
1384
- }
1385
- updateItemLoading(e, t) {
1386
- this.gridItems[e] ? (this.gridItems[e] = {
1387
- ...this.gridItems[e],
1388
- loading: t
1389
- }, t && (this.gridItems[e].loadStartTime = Date.now())) : console.warn(`Item with ID "${e}" does not exist in dashboard "${this.id}". Cannot update loading state.`);
1390
- }
1391
- updateItemError(e, t) {
1392
- this.gridItems[e] ? this.gridItems[e] = {
1393
- ...this.gridItems[e],
1394
- error: t,
1395
- loading: !1,
1396
- loadStartTime: null
1397
- } : console.warn(`Item with ID "${e}" does not exist in dashboard "${this.id}". Cannot update error.`);
1398
- }
1399
- updateItemDrilldown(e, t) {
1400
- this.gridItems[e] && (this.gridItems[e] = {
1401
- ...this.gridItems[e],
1402
- drilldown: t
1403
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1404
- }
1405
- updateItemDrilldownChartConfig(e, t) {
1406
- this.gridItems[e] && (this.gridItems[e] = {
1407
- ...this.gridItems[e],
1408
- drilldownChartConfig: t
1409
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1410
- }
1411
- updateItemResults(e, t) {
1412
- if (this.gridItems[e]) {
1413
- this.gridItems[e] = {
1414
- ...this.gridItems[e],
1415
- results: t,
1416
- loading: !1,
1417
- error: "",
1418
- loadStartTime: null
1419
- };
1420
- let n = this.gridItems[e];
1421
- if (n.chartFilters && n.chartFilters.length > 0) {
1422
- if (!(n.chartFilters?.every((e) => e?.value && typeof e.value == "object" ? Object.values(e.value).every((e) => typeof e == "string") : !1) ?? !1)) {
1423
- console.warn(`Cross filter values in item ${e} are not static strings, skipping cross filter check.`);
1424
- return;
1425
- }
1426
- if (!this.gridItems[e].chartFilters?.some((e) => Object.keys(e.value).some((n) => t?.data.some((t) => t[n] === e.value[n])))) {
1427
- let t = this.gridItems[e].chartFilters?.map((e) => e.value);
1428
- console.log("Removing cross filter that was sourced from", n.name, "and is no longer present in the results: ", t), this.removeItemCrossFilterSource(e);
1429
- }
1430
- }
1431
- }
1432
- }
1433
- updateItemType(e, t) {
1434
- this.gridItems[e] && (this.gridItems[e] = {
1435
- ...this.gridItems[e],
1436
- type: t
1437
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1438
- }
1439
- updateItemFilters(e) {
1440
- let t = this.gridItems[e];
1441
- t && O(t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1442
- }
1443
- removeItemCrossFilter(e, t) {
1444
- Re(this.gridItems, e, t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1445
- }
1446
- removeItemCrossFilterSource(e) {
1447
- let t = !!this.gridItems[e]?.chartFilters?.length, n = ze(this.gridItems, e);
1448
- return (n.length > 0 || t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0), n;
1449
- }
1450
- removeAllFilters() {
1451
- let e = Be(this.gridItems);
1452
- for (let e in this.gridItems) this.gridItems[e].parameters = {};
1453
- return e.length > 0 && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0), e;
1454
- }
1455
- updateItemCrossFilters(e, t, n, r) {
1456
- let i = Le(this.gridItems, e, t, n, r);
1457
- return this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, i;
1458
- }
1459
- updateItemName(e, t) {
1460
- this.gridItems[e] && (this.gridItems[e] = {
1461
- ...this.gridItems[e],
1462
- name: t
1463
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1464
- }
1465
- updateItemChartConfig(e, t) {
1466
- this.gridItems[e] && (this.gridItems[e] = {
1467
- ...this.gridItems[e],
1468
- chartConfig: t
1469
- }, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1470
- }
1471
- updateItemMultipleProperties(e, t) {
1472
- if (!this.gridItems[e]) throw Error(`Item with ID "${e}" does not exist in the dashboard.`);
1473
- let n = this.gridItems[e], r = !1;
1474
- if (t.name !== void 0 && t.name !== n.name && (n.name = t.name, r = !0), t.content !== void 0 && t.content !== n.content && (n.content = t.content, r = !0), t.type !== void 0 && t.type !== n.type && (n.type = t.type, r = !0), t.allowCrossFilter !== void 0 && t.allowCrossFilter !== n.allowCrossFilter && (n.allowCrossFilter = t.allowCrossFilter, r = !0), t.width !== void 0 && t.width !== n.width && (n.width = t.width, r = !0), t.height !== void 0 && t.height !== n.height && (n.height = t.height, r = !0), t.chartConfig !== void 0 && JSON.stringify(n.chartConfig) !== JSON.stringify(t.chartConfig) && (n.chartConfig = t.chartConfig, r = !0), t.drilldown !== void 0 && t.drilldown !== n.drilldown && (n.drilldown = t.drilldown, r = !0), t.drilldownChartConfig !== void 0 && JSON.stringify(n.drilldownChartConfig) !== JSON.stringify(t.drilldownChartConfig) && (n.drilldownChartConfig = t.drilldownChartConfig, r = !0), t.conceptFilters !== void 0 && JSON.stringify(n.conceptFilters) !== JSON.stringify(t.conceptFilters) && (n.conceptFilters = t.conceptFilters, r = !0), t.chartFilters !== void 0 && JSON.stringify(n.chartFilters) !== JSON.stringify(t.chartFilters) && (n.chartFilters = t.chartFilters, r = !0), t.filters !== void 0 && JSON.stringify(n.filters) !== JSON.stringify(t.filters) && (n.filters = t.filters, r = !0), t.parameters !== void 0 && JSON.stringify(n.parameters) !== JSON.stringify(t.parameters) && (n.parameters = t.parameters, r = !0), t.results !== void 0 && t.results !== n.results && (n.results = t.results, n.loading = !1, n.error = null, n.loadStartTime = null, r = !0), t.loading !== void 0 && t.loading !== n.loading && (n.loading = t.loading, t.loading && (n.loadStartTime = Date.now()), r = !0), t.error !== void 0 && t.error !== n.error && (n.error = t.error, n.error && (n.loading = !1, n.loadStartTime = null), r = !0), t.layoutDimensions !== void 0) {
1475
- let n = this.layout.find((t) => t.i === e);
1476
- if (n) {
1477
- let e = !1;
1478
- t.layoutDimensions.x !== void 0 && t.layoutDimensions.x !== null && t.layoutDimensions.x !== n.x && (n.x = t.layoutDimensions.x, e = !0), t.layoutDimensions.y !== void 0 && t.layoutDimensions.y !== null && t.layoutDimensions.y !== n.y && (n.y = t.layoutDimensions.y, e = !0), t.layoutDimensions.w !== void 0 && t.layoutDimensions.w !== null && t.layoutDimensions.w !== n.w && (n.w = t.layoutDimensions.w, e = !0), t.layoutDimensions.h !== void 0 && t.layoutDimensions.h !== null && t.layoutDimensions.h !== n.h && (n.h = t.layoutDimensions.h, e = !0), e && (r = !0);
1479
- }
1480
- }
1481
- r && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
1482
- }
1483
- updateLayout(e) {
1484
- this.layout = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
1485
- }
1486
- updateItemDimensions(e, t, n) {
1487
- if (this.gridItems[e]) {
1488
- if (this.gridItems[e].width === t && this.gridItems[e].height === n) return;
1489
- this.gridItems[e] = {
1490
- ...this.gridItems[e],
1491
- width: t,
1492
- height: n
1493
- }, this.updatedAt = /* @__PURE__ */ new Date();
1494
- }
1495
- }
1496
- removeItem(e) {
1497
- this.layout = this.layout.filter((t) => t.i !== e), this.gridItems[e] && delete this.gridItems[e], this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
1498
- }
1499
- clearItems() {
1500
- this.layout = [], this.gridItems = {}, this.nextId = 0, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
1501
- }
1502
- serialize() {
1503
- let e = {};
1504
- for (let [t, n] of Object.entries(this.gridItems)) {
1505
- let { results: r, loading: i, error: a, loadStartTime: o, ...s } = n;
1506
- e[t] = s;
1507
- }
1508
- return {
1509
- id: this.id,
1510
- name: this.name,
1511
- storage: this.storage,
1512
- connection: this.connection,
1513
- connectionId: this.connectionId,
1514
- layout: this.layout,
1515
- gridItems: e,
1516
- nextId: this.nextId,
1517
- createdAt: this.createdAt,
1518
- updatedAt: this.updatedAt,
1519
- filter: this.filter,
1520
- imports: this.imports,
1521
- version: this.version,
1522
- state: this.state,
1523
- description: this.description,
1524
- layoutType: this.layoutType,
1525
- theme: this.theme,
1526
- parentDashboardId: this.parentDashboardId,
1527
- investigationName: this.investigationName,
1528
- investigationCreatedFrom: this.investigationCreatedFrom,
1529
- children: this.children,
1530
- chatId: this.chatId ?? null
1531
- };
1532
- }
1533
- static fromSerialized(n) {
1534
- let r = {};
1535
- for (let [t, i] of Object.entries(n.gridItems)) r[t] = {
1536
- ...i,
1537
- results: null,
1538
- loading: !1,
1539
- error: null,
1540
- loadStartTime: null,
1541
- allowCrossFilter: i.allowCrossFilter !== !1,
1542
- chartConfig: e(i.chartConfig),
1543
- drilldownChartConfig: e(i.drilldownChartConfig)
1544
- };
1545
- return new t({
1546
- ...n,
1547
- createdAt: new Date(n.createdAt),
1548
- updatedAt: new Date(n.updatedAt),
1549
- gridItems: r,
1550
- layoutType: n.layoutType || "grid",
1551
- theme: x(n.theme),
1552
- parentDashboardId: n.parentDashboardId || null,
1553
- investigationName: n.investigationName,
1554
- investigationCreatedFrom: n.investigationCreatedFrom ? {
1555
- ...n.investigationCreatedFrom,
1556
- timestamp: new Date(n.investigationCreatedFrom.timestamp)
1557
- } : void 0,
1558
- children: n.children || []
1559
- });
1560
- }
1561
- }, Ye = .5, Xe = 1e4, Ze = 2e5, Qe = [
1562
- {
1563
- value: "demo",
1564
- label: "Demo (limited messages)"
1565
- },
1566
- {
1567
- value: "anthropic",
1568
- label: "Anthropic"
1569
- },
1570
- {
1571
- value: "google",
1572
- label: "Google"
1573
- },
1574
- {
1575
- value: "openai",
1576
- label: "OpenAI"
1577
- },
1578
- {
1579
- value: "openrouter",
1580
- label: "OpenRouter"
1581
- },
1582
- {
1583
- value: "deepseek",
1584
- label: "DeepSeek"
1585
- }
1586
- ], $e = Object.fromEntries(Qe.map((e) => [e.value, e.label])), et = {
1587
- anthropic: "sk-ant-...",
1588
- openai: "sk-...",
1589
- google: "AIza...",
1590
- openrouter: "sk-or-...",
1591
- deepseek: "sk-..."
1592
- }, A = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.\n\n\nFunctions (the `aggregate` family doubles as the Aggregate Functions list):\naggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)\nstring: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)\ndate/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()\n date_part(<arg1>, <arg2>) e.g. date_part('2020-01-01'::date, year)\n date_truncate(<arg1>, <arg2>) e.g. date_trunc('2020-01-01'::date, month)\n date_add(<arg1>, <arg2>, <arg3>) e.g. date_add('2020-01-01'::date, month, 1)\n date_sub(<arg1>, <arg2>, <arg3>) e.g. date_sub('2020-01-01'::date, day, 1)\n date_diff(<arg1>, <arg2>, <arg3>) e.g. date_diff('2020-01-01'::date, '2020-01-02'::date, day)\n current_timestamp() e.g. now()\narray/map/struct: unnest|array_distinct|array_sum|map_keys|map_values(<arg1>) ; struct|array(<arg1>, <arg2>, ..., <argN>) ; array_sort|array_to_string(<arg1>, <arg2>) ; array_transform|array_filter|generate_array(<arg1>, <arg2>, <arg3>)\nmath: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)\nconditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)\ngeo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)\nother: is_not_distinct(<arg1>, <arg2>)\n\nSome types carry trait metadata that adds meaning: 'latitude', 'longitude' and 'currency' are all 'float' with extra semantics.\n\nAdditional syntax examples:\nThese less-common patterns have complete, copy-pasteable examples. Do NOT guess\nthe syntax — print the full example on demand with\n`trilogy agent-info syntax example <name>`:\n- `python-datasource` - run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix\n- `query-structure` - the exact clause order of a query, and how to define a rowset\n- `chart` - drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`\n- `filtered-aggregate` - aggregating only the rows matching a condition, and counting rows correctly\n- `row-level-where-vs-having` - choosing between WHERE and HAVING for a threshold condition\n- `nested-aggregate-group-average` - the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark\n- `pivot-columns` - turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate\n- `case-when` - `case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it\n- `window-period-over-period` - `lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`\n- `union-stack-channels` - stacking several sources/channels together as rows\n- `except-intersect-setops` - set difference or co-occurrence across sources\n- `scoped-join` - blending a second fact model or rowset into one query\n- `existence-anti-join` - keeping or excluding rows by whether a key matches another model\n- `set-intersect-difference` - count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom\n- `dedup-before-aggregate` - a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute\n- `rollup` - subtotals and a grand total in one pass, and labeling the levels\n- `rank-over-rollup` - rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level\n- `staged-membership` - filtering a fine-grain query by a set computed at a coarser grain\n- `correlated-exists-via-grouped-counts` - translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`", tt = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.", nt = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], rt = [
1593
- "aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)",
1594
- "string: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)",
1595
- "date/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()",
1596
- " date_part(<arg1>, <arg2>) e.g. date_part('2020-01-01'::date, year)",
1597
- " date_truncate(<arg1>, <arg2>) e.g. date_trunc('2020-01-01'::date, month)",
1598
- " date_add(<arg1>, <arg2>, <arg3>) e.g. date_add('2020-01-01'::date, month, 1)",
1599
- " date_sub(<arg1>, <arg2>, <arg3>) e.g. date_sub('2020-01-01'::date, day, 1)",
1600
- " date_diff(<arg1>, <arg2>, <arg3>) e.g. date_diff('2020-01-01'::date, '2020-01-02'::date, day)",
1601
- " current_timestamp() e.g. now()",
1602
- "array/map/struct: unnest|array_distinct|array_sum|map_keys|map_values(<arg1>) ; struct|array(<arg1>, <arg2>, ..., <argN>) ; array_sort|array_to_string(<arg1>, <arg2>) ; array_transform|array_filter|generate_array(<arg1>, <arg2>, <arg3>)",
1603
- "math: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)",
1604
- "conditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)",
1605
- "geo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)",
1606
- "other: is_not_distinct(<arg1>, <arg2>)"
1607
- ], it = [
1608
- "string",
1609
- "bytes",
1610
- "bool",
1611
- "map",
1612
- "number",
1613
- "float",
1614
- "double",
1615
- "numeric",
1616
- "int",
1617
- "bigint",
1618
- "date",
1619
- "datetime",
1620
- "timestamp",
1621
- "array",
1622
- "date_part",
1623
- "struct",
1624
- "geography",
1625
- "null",
1626
- "unix_seconds",
1627
- "unknown",
1628
- "any"
1629
- ], at = [
1630
- {
1631
- name: "case-when",
1632
- title: "Conditional values with case (relabel codes / bucket a measure)",
1633
- summary: "`case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it",
1634
- body: "# `case when <cond> then <val> ... else <val> end` builds a derived value.\n# Assign it to an `auto` concept, then group/filter by it like any other.\n# Use `else null` when unmatched rows should drop out of downstream grouping.\nimport iris as iris;\n\nauto petal_size <- case\n when iris.petal_length < 2.0 then 'small'\n when iris.petal_length < 5.0 then 'medium'\n else 'large'\nend;\n\nselect\n petal_size,\n count(iris.petal_length) as flower_count,\n avg(iris.petal_width) as avg_petal_width,\norder by flower_count desc;\n"
1635
- },
1636
- {
1637
- name: "chart",
1638
- title: "Chart statement - layers, roles, settings and reference lines",
1639
- summary: "drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`",
1640
- body: "# `chart` is a top-level statement, like `select` - it wraps a query AND says\n# how to draw it. It renders wherever results render: a trilogy code block in a\n# `trilogy render` report, a studio cell, terminal output, and\n# `copy into png|svg|html|pdf '<path>' from chart ...` for a standalone image.\nimport enrollments as enroll;\n\n# --- 1. Minimal form --------------------------------------------------------\n# `layer <type> ( <role> <- <expr> [as <name>], ... )`. The bindings ARE the\n# select: grouping is automatic by the non-aggregated ones, exactly as in a\n# query. NEVER write a separate `select` for a chart.\nchart layer bar ( x_axis <- enroll.department, y_axis <- sum(enroll.credits) as credits );\n\n# A COMPUTED binding must be aliased: `y_axis <- sum(enroll.credits) as credits`.\n# A bare concept reference needs no alias (`x_axis <- enroll.department`) but may\n# take one as a display label. `y_axis <- sum(...)` with no `as` is a parse error.\n#\n# GRAIN: every binding is a select column, so binding a FINER concept to any\n# role (color, group, annotation, ...) refines the grain and splits the marks -\n# exactly as adding that column to a select would. Aggregate annotations at the\n# mark's own grain (`count(enroll.id) as ...`) instead of binding a detail field.\n\n# --- 2. Every part of the statement -----------------------------------------\n# Components may appear in any order after `chart`; at least one `layer` is\n# required, and multiple `layer`s compose into one overlaid chart.\nchart\n set show_title # title from the value-axis label\n set hide_legend\n set scale_y: log # scale_x | scale_y : linear | log | sqrt\n layer bar (\n x_axis <- enroll.department,\n y_axis <- sum(enroll.credits) as credits,\n color <- enroll.completed, # one legend series per value\n annotation <- count(enroll.id) as enrollments # text label on each mark\n )\n order by credits desc # category order follows ORDER BY\n limit 10 # (ascending when there is no ORDER BY)\n place hline at 5 as target; # reference rule; `place vline at ...`\n # too. `at` takes a literal; `as <label>`\n # is optional\n\n# --- 3. `from <select>` - when the bindings cannot express the query ---------\n# Bindings alone have no `where` / `having` / hidden columns. Add `from` plus a\n# COMPLETE select, which carries its own clauses in the usual order:\nchart layer line (\n x_axis <- enroll.year,\n y_axis <- completions,\n color <- enroll.department\n )\n from\n where enroll.year >= 2016\n select\n enroll.year,\n enroll.department,\n count(enroll.id ? enroll.completed = true) as completions\n order by enroll.year asc;\n# With `from select`, EVERY binding must be a bare reference to one of the\n# select's outputs - put transformations inside the select - and layer-level\n# `order by` / `limit` are rejected: they belong to the select.\n\n# --- 4. Types ---------------------------------------------------------------\n# bar vertical bars: x_axis = category, y_axis = measure\n# barh horizontal bars: x_axis = MEASURE, y_axis = category (roles swap)\n# line x_axis = the ordered axis (date/year), y_axis = measure\n# point scatter; `size <- <measure>` scales the marks\n# area line, filled\n# headline big KPI number - needs only `x_axis <- <measure> as <name>`\n# donut the numeric binding sizes the slices; the other positional one -\n# or `color` - names them. No annotations.\n# heatmap x_axis and y_axis are BOTH categories, `color` is the measure\n# boxplot x_axis = category, y_axis = the distribution being summarized\n# `treemap` parses but no renderer implements it; the error names what does.\nchart layer barh ( x_axis <- sum(enroll.credits) as credits, y_axis <- enroll.department );\nchart layer headline ( x_axis <- count(enroll.student_id) as students );\nchart layer donut ( x_axis <- enroll.department, y_axis <- sum(enroll.credits) as credits );\nchart layer heatmap (\n x_axis <- enroll.year,\n y_axis <- enroll.department,\n color <- sum(enroll.credits) as credits\n);\n# A boxplot summarizes RAW rows, so its select has to carry the fact's row grain\n# - hidden with `--` - or output dedup collapses the distribution to the set of\n# distinct values.\nchart layer boxplot ( x_axis <- enroll.department, y_axis <- enroll.credits )\n from select --enroll.id, enroll.department, enroll.credits;\n\n# --- 5. Roles ---------------------------------------------------------------\n# x_axis, y_axis position\n# color one series per value, WITH a legend\n# size mark size (point)\n# group side-by-side bars, or a per-series split on\n# line/point/area - no legend\n# x_trellis / y_trellis small multiples (columns / rows)\n# annotation per-mark text label\n# geo reserved - parses, then raises\n# Each role may be bound at most once per layer; an unknown role name is a parse\n# error. Trellis roles cannot combine with multiple layers, placements, or\n# annotations (Vega-Lite forbids facets inside layered charts).\nchart layer bar (\n x_axis <- enroll.year,\n y_axis <- sum(enroll.credits) as credits,\n group <- enroll.department # side-by-side bars, no legend\n);\n\n# EXPLICIT SERIES COLORS: select a `string::hex` column (the trait comes from\n# `import std.color;`) alongside a `color` binding and each color value takes the\n# hex code found on its rows (missing -> gray); bind the hex column itself to\n# `color` to use the codes directly.\n"
1641
- },
1642
- {
1643
- name: "correlated-exists-via-grouped-counts",
1644
- title: "Correlated EXISTS / NOT-EXISTS over rows of the SAME model",
1645
- summary: "translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`",
1646
- body: "# A self-referential EXISTS / NOT-EXISTS (e.g. \"the ONLY enrollee who completed\n# a course that had more than one enrollee\") becomes TWO filtered counts pinned\n# to the correlation grain, compared in `where`:\n# count(k) by <grain> > 1 # EXISTS another row in the group\n# count(k ? cond) by <grain> = 1 # NOT EXISTS another row matching cond\n# (exactly one matches: the row itself)\n# An aggregate condition in `where` groups by the SELECT grain unless pinned.\n# When the correlation grain DIFFERS from the select grain - as here\n# (correlate by course, select per student) - you MUST pin it with `by <grain>`;\n# a bare `count(...) > 0` would group by the select grain and give wrong rows.\n# SCOPE: the pin fixes only the GROUPS. A `where` aggregate always computes\n# over ALL rows of the model; the other `where` predicates never narrow its\n# input rows - exactly right for whole-table EXISTS checks like this one. If\n# the aggregate should see only already-filtered rows, use `having` or stage\n# it with `then where` (a stage's aggregates see only rows passing earlier\n# stages).\nimport enrollments as enroll;\n\nauto enrollees_per_course <- count(enroll.student_id) by enroll.course;\nauto completers_per_course <- count(enroll.student_id ? enroll.completed = true) by enroll.course;\n\nwhere\n enroll.completed = true\n and enrollees_per_course > 1 # the course had at least one OTHER enrollee\n and completers_per_course = 1 # and no OTHER enrollee completed it\nselect\n enroll.student_id,\n count(enroll.course) as sole_completions\norder by sole_completions desc\nlimit 100;\n"
1647
- },
1648
- {
1649
- name: "dedup-before-aggregate",
1650
- title: "Deduplicate identical rows before aggregating (UNION DISTINCT)",
1651
- summary: "a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute",
1652
- body: "# `distinct` and UNION are not available. To collapse identical tuples to one\n# row BEFORE aggregating (so duplicates don't double-count), materialize the\n# tuple in a `rowset <- select <cols>`: a rowset select groups at its own grain,\n# emitting one row per distinct tuple. Then aggregate over the rowset's columns.\n# - ALIAS every rowset column you reference downstream with `as`. Without `as`,\n# the column keeps its FULL source path under the rowset name - `select\n# sales.item.brand_id` is reachable as `rs.sales.item.brand_id`, NOT\n# `rs.brand_id`. `as brand_id` makes it `rs.brand_id`.\nimport enrollments as enroll;\n\n# Collapse repeat (student, course) enrollments to one row each (a student who\n# took a course in multiple years counts once).\nrowset unique_sc <- select\n enroll.student_id as sid,\n enroll.course as course,\n;\n\n# Count over the deduplicated rows - identical (student, course) pairs counted once.\nselect\n unique_sc.course,\n count(unique_sc.sid) as distinct_students,\norder by unique_sc.course asc;\n"
1653
- },
1654
- {
1655
- name: "except-intersect-setops",
1656
- title: "Set difference / co-occurrence across sources with except(...) / intersect(...)",
1657
- summary: "set difference or co-occurrence across sources",
1658
- body: "# `except(...)` and `intersect(...)` are relational table-valued functions with the\n# SAME shape as `union(...)`: TWO OR MORE inline self-contained select arms, matched\n# by COLUMN POSITION to the trailing `-> (...)` output list. They are SQL SET\n# operators, which differ from the union stack in three ways:\n# - the output is DEDUPLICATED (distinct rows);\n# - whole rows are compared with NULL-SAFE equality (NULL matches NULL - the\n# same identity semantics as `in` / `not in` membership);\n# - for `except(...)` ARM ORDER MATTERS: the FIRST arm is the base population and\n# every later arm is subtracted from it, left to right.\n# Use `except` for \"combinations in A that never appear in B (or C)\" and\n# `intersect` for \"combinations present in every source\".\nimport enrollments as enroll;\n\n# Distinct (course, year) combinations Biology offered that Chemistry did not.\nwith bio_only as except(\n (where enroll.department = 'Biology'\n select enroll.course as course, enroll.year as year),\n (where enroll.department = 'Chemistry'\n select enroll.course as course, enroll.year as year)\n) -> (course, year);\n\nselect\n bio_only.course,\n bio_only.year,\norder by bio_only.course asc nulls first, bio_only.year asc nulls first\nlimit 100;\n\n# Counting the surviving combinations is the idiomatic anti-membership count\n# (e.g. \"customers who bought in store but never online\"):\n# from except((...store combos...), (...web combos...)) -> (last, first, day)\n# select count(day) as combos;\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Same arm rules as `union(...)`: inline arms only, positional binding, equal\n# column count/order/types per arm; reference outputs as `<rowset>.<output>`.\n# - `except((a), (b), (c))` means (a EXCEPT b) EXCEPT c - subtract left to right.\n# `intersect(...)` keeps rows present in EVERY arm (order does not matter).\n# - Outputs are DISTINCT rows: aggregate them directly (`count(x)`) for \"how many\n# unique combinations\" - no extra dedup step needed.\n"
1659
- },
1660
- {
1661
- name: "existence-anti-join",
1662
- title: "Existence / anti-join across models",
1663
- summary: "keeping or excluding rows by whether a key matches another model",
1664
- body: "# `in` / `not in` against another model's key is the semi-join / anti-join\n# idiom - no manual JOIN. Use it instead of `count(...) is null` hacks.\n# Membership is IDENTITY matching and total: NULL matches NULL, the result is\n# never NULL, and `not in` is the EXACT complement of `in` - a NULL in the set\n# does not empty an anti-join (no SQL NOT-IN footgun), and NULL-keyed rows land\n# in exactly one of the two sides. For strict SQL parity, filter NULLs\n# explicitly (`where key is not null and key in other.key`).\nimport students as students;\nimport enrollments as enroll;\n\n# semi-join: majors with at least one student enrolled in 'Biology 101'\nauto bio_students <- enroll.student_id ? enroll.course = 'Biology 101';\nwhere students.id in bio_students\nselect students.major, count(students.id) as enrolled_students;\n\n# anti-join: students who never enrolled in ANY course\nwhere students.id not in enroll.student_id\nselect students.major, count(students.id) as never_enrolled;\n\n# NULLABLE FLAG: a flag that is true only when a match exists and NULL otherwise\n# (e.g. `enroll.completed` set only for graded rows) needs `is null` for the\n# \"never matched\" case - `not flag` evaluates NULL→dropped, losing those rows.\nwhere enroll.completed is null\nselect enroll.student_id;\n\n# COMPOSITE (multi-column) membership: to test a MULTI-PART key against a set,\n# wrap both sides in matching tuples - `(a, b) in (set.a, set.b)`. This matches\n# ROW-WISE (the whole tuple must appear as one row of the set), so it is NOT the\n# same as `a in set.a and b in set.b`, which matches the parts independently and\n# lets unrelated cross-pairs through. Build the set as a `rowset` selecting the\n# key columns, then test the tuple with `in` / `not in`. Prefer this over\n# concatenating the parts into a single string key.\nrowset multi_course_pairs <- select\n enroll.department as dept,\n enroll.course as course,\nhaving count_distinct(enroll.year) > 1; # (dept, course) offered in >1 year\n\nwhere (enroll.department, enroll.course)\n in (multi_course_pairs.dept, multi_course_pairs.course)\nselect enroll.student_id, count(enroll.id) as enrollments;\n"
1665
- },
1666
- {
1667
- name: "filtered-aggregate",
1668
- title: "Filtered aggregate - aggregate only the rows matching a condition",
1669
- summary: "aggregating only the rows matching a condition, and counting rows correctly",
1670
- body: "# `?` is a local filter - a filter that applies only to the prior expression-\n# several differently-filtered measures can share one select. Add `by <grain>`\n# to pin an aggregate to a grain other than the select's.\nimport iris as iris;\n\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\n avg(iris.petal_length ? iris.petal_width > 1.5) as avg_petal_wide_only,\n count(iris.petal_length ? iris.sepal_length > 6.0) as long_sepal_count,\norder by iris.species asc;\n\n# COUNTING ROWS: `count(k ? cond)` counts DISTINCT values of k - `count` is\n# distinct-by-argument. To count matching ROWS, count the model's unique grain /\n# row key (often named `row_key`), NOT a non-unique sub-key. e.g. counting line\n# items by `count(line_number ? cond)` collapses to the number of distinct line\n# numbers (1..7) and undercounts - use `count(row_key ? cond)`.\n\n# COMPOSITE GRAIN: when SEVERAL keys together identify the thing you are counting,\n# name them with `grain(...)` instead of picking one:\n# count(grain(sales.order_id, sales.item_id)) # order+item lines\n# count_distinct(grain(cust.first_name, cust.last_name, sales.date))\n# `grain(...)` is NEVER NULL, so a combination with a missing member (a customer\n# with no last name) still counts - whereas `count(last_name)` silently drops it.\n# It composes with `?` like any expression: count(grain(a, b) ? cond).\n"
1671
- },
1672
- {
1673
- name: "nested-aggregate-group-average",
1674
- title: "Compare a per-entity total to the group average of those totals",
1675
- summary: "the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark",
1676
- body: "# Compare each entity's total to the GROUP AVERAGE of those per-entity totals\n# (the \"above 1.2x the group norm\" pattern). Give EACH grain its own `by`: the\n# inner total is per (entity, group); the outer average re-aggregates those\n# totals up to the group. \n# since the aggregates are in the having, both are aggregates *after* the id filter is applied\nimport enrollments as enroll;\n\nauto student_dept_credits <- sum(enroll.credits) by enroll.student_id, enroll.department;\nauto dept_avg_credits <- avg(student_dept_credits) by enroll.department;\n\nwhere enroll.student_id > 103\nselect\n enroll.student_id,\nhaving student_dept_credits > 1.2 * dept_avg_credits;\n\n# BENCHMARK OVER FULL POPULATION + FILTERED OUTPUT: if the department average must span\n# EVERY student but the output should contain only target students (id > 103), put the aggregate\n# comparison in WHERE alongside the output-only filter. Aggregate conditions in WHERE do\n# not filter one another's inputs, so `student_id > 103` does not narrow the department\n# benchmark. Do NOT put the comparison in HAVING after the output filter; that computes a\n# target-students-only department average.\nwhere\n student_dept_credits > 1.2 * dept_avg_credits\n and enroll.student_id > 103\nselect\n enroll.student_id,\n enroll.department,\n student_dept_credits,\n;\n\n# SCOPED INPUT POPULATION: when the totals and their group average must use only\n# rows from a specific period, bind that condition inside the inner aggregate.\n# A separate output-only filter still belongs in the final WHERE and does not\n# narrow the rows feeding either aggregate.\nauto student_dept_credits_2024 <- sum(enroll.credits ? enroll.year = 2024)\n by enroll.student_id, enroll.department;\nauto dept_avg_credits_2024 <- avg(student_dept_credits_2024) by enroll.department;\n\nwhere\n student_dept_credits_2024 > 1.2 * dept_avg_credits_2024\n and enroll.student_id > 103\nselect\n enroll.student_id,\n enroll.department,\n student_dept_credits_2024,\n;\n\n# HIDDEN GROUP KEY: if the requested output is one row per (student, department)\n# but displays only student_id, keep department in the select with the `--`\n# PREFIX. Otherwise identical displayed student rows are deduplicated and the\n# required per-department row grain is lost.\nwhere student_dept_credits_2024 > 1.2 * dept_avg_credits_2024\nselect\n enroll.student_id,\n --enroll.department,\n;\n\n# For more complex boundaries, compute the full-population benchmark in a separate rowset\n# with no output filter, then apply the subset filter only in the final selection.\n"
1677
- },
1678
- {
1679
- name: "pivot-columns",
1680
- title: "Pivot dimension values into columns",
1681
- summary: "turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate",
1682
- body: "# A `def` macro expands inline at each `@call`. Pair it with a filtered\n# aggregate to project one column per category value (a pivot). With no row\n# dimension in the select this yields a single summary row.\nimport iris as iris;\n\ndef species_avg(s) -> avg(iris.petal_length ? iris.species = s);\n\nselect\n @species_avg('setosa') as setosa_avg_petal,\n @species_avg('versicolor') as versicolor_avg_petal,\n @species_avg('virginica') as virginica_avg_petal,\n;\n"
1683
- },
1684
- {
1685
- name: "python-datasource",
1686
- title: "Python script datasource - Arrow IPC producer plus Trilogy model",
1687
- summary: "run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix",
1688
- body: "# Python datasources require `enable_python_datasources = true` under\n# `[engine.config]` in trilogy.toml. Create these TWO files side by side.\n#\n# File 1: fibonacci.py\n# ---------------------------------------------------------------------------\n# #!/usr/bin/env -S uv run\n# # /// script\n# # requires-python = \">=3.11\"\n# # dependencies = [\"pyarrow>=16\", \"pytrilogy\"]\n# # ///\n#\n# from trilogy.io import run\n#\n#\n# def fibonacci():\n# values = [0, 1]\n# while len(values) < 20:\n# values.append(values[-1] + values[-2])\n# return [{\"index\": i, \"value\": v} for i, v in enumerate(values)]\n#\n#\n# if __name__ == \"__main__\":\n# raise SystemExit(run(fibonacci))\n#\n# `run` converts whatever you return -- a pyarrow table, a pandas/polars frame,\n# a list of dicts, or an iterator of those -- and gives the script a CLI\n# (`--limit`, `--filter`, `--columns`, `--describe`). Run\n# `trilogy source describe ./fibonacci.py` to print the datasource block below\n# instead of writing it against a guessed schema.\n#\n# IMPORTANT: stdout must contain ONLY the Arrow IPC stream. Send diagnostics\n# to stderr. A PEP 723 block lets `uv run` install script dependencies.\n#\n# File 2: query.preql\n# ---------------------------------------------------------------------------\nkey index int;\nproperty index.value int;\n\n# The left side is the physical Arrow column; the right side is the Trilogy\n# concept declared above. The file suffix identifies this as a Python source.\ndatasource fibonacci_numbers (\n index: index,\n value: value,\n)\ngrain (index)\nfile `./fibonacci.py`;\n\n# A datasource declaration does NOT create a namespace. Use local concepts as\n# `index` and `value`, NOT `fibonacci_numbers.index` / `.value`.\nselect\n index,\n value,\norder by index;\n"
1689
- },
1690
- {
1691
- name: "query-structure",
1692
- title: "Full query structure - every clause and where it goes (+ rowsets)",
1693
- summary: "the exact clause order of a query, and how to define a rowset",
1694
- body: "# Every Trilogy query has the SAME fixed skeleton. Top-level statements come\n# first (each ends with `;`), then ONE query whose clauses appear in this exact\n# order - each clause is OPTIONAL except `select`:\n#\n# <top-level statements>; # import / key / property / auto / metric /\n# # rowset / merge / def / datasource / parameter\n# where <row condition> # 1. filter INPUT rows (BEFORE aggregation)\n# select <col>, <agg> as name, # 2. the projection - grouping is AUTOMATIC by the\n# # non-aggregated columns (never write GROUP BY)\n# <subset|union join a = b (=c)?>* # 3. blend models (zero or more joins; one per concept RIGHT AFTER the\n# # select list - the SQL-like spot)\n# having <result condition> # 4. filter on an AGGREGATED / joined RESULT\n# order by <col> asc|desc # 5. sort\n# limit <n> # 6. cap rows\nimport enrollments as enroll;\nimport students as students;\n\n# --- TOP-LEVEL DEFINITIONS (reusable; all live ABOVE the query) -------------\n# An `auto`/`metric`/`property`/`key` defines a field. It expands inline wherever\n# referenced (a macro), re-evaluated in the referencing query's scope.\nauto completed_credits <- sum(enroll.credits ? enroll.completed = true);\n\n# A ROWSET is a NAMED select: `rowset <name> <- <a full select>;`. It runs as its\n# own scoped query (its own where/select/having) and exposes its outputs as\n# `<name>.<col>`. ALIAS every column you reuse downstream with `as`.\nrowset dept_totals <- select\n enroll.department as department,\n sum(enroll.credits) as total_credits,\n;\n\n# --- THE QUERY - clauses in the fixed order above ---------------------------\nwhere enroll.year >= 2015 # 1. WHERE: per-row filter, before aggregation\nselect # 2. SELECT: grouped automatically by students.major\n students.major,\n count(enroll.id) as enrollments,\n --completed_credits, # a leading `--` HIDES a column from the output\nsubset join enroll.student_id = students.id # 3. JOIN: blend students onto enrollments (after the select list)\nhaving completed_credits > 0 # 4. HAVING: condition on an aggregated RESULT\norder by enrollments desc nulls first # 5. ORDER BY\nlimit 100; # 6. LIMIT\n\n# Reference a rowset's outputs as `<rowset>.<output>` in a later query.\nselect\n dept_totals.department,\n dept_totals.total_credits,\norder by dept_totals.total_credits desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Joins sit RIGHT AFTER the select list (the SQL-like, preferred spot). They\n# may also appear before `select` - both parse - but keep them after the\n# select list for consistency.\n# - `where` filters INPUT rows (before aggregation). To filter on something only\n# known AFTER the join / aggregation (e.g. comparing two joined rowsets'\n# counts), select that value (hide it with `--`) and test it in `having` -\n# `having` is the post-aggregation/output filter. There is only ONE `where`.\n# - Output rows dedupe to the select grain. To keep legitimate duplicates (one\n# row per matching fact), select the fact's grain keys - hide with `--` if\n# they shouldn't appear in the output.\n# - No FROM, GROUP BY, DISTINCT, SELECT *, subqueries, or SQL-style set/JOIN\n# operators. Grouping is automatic; blend with the scoped `join` above.\n"
1695
- },
1696
- {
1697
- name: "rank-over-rollup",
1698
- title: "Rank within each rollup level (one window over the rollup)",
1699
- summary: "rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level",
1700
- body: "# When ranking the rows of a ROLLUP within each hierarchy level, use ONE\n# multi-key window over the rollup output. `grouping()` gives the level; the\n# window partitions by (level, parent) so leaves rank within their parent and\n# subtotals rank among themselves - all in one pass. Do NOT write separate rank\n# concepts per level (they mis-rank across the wrong row set). The aggregates and\n# grouping() level concepts are plain - the select's `by rollup (…)` clause stamps\n# the grouping onto all of them.\nimport enrollments as enroll;\n\nauto total <- count(enroll.id);\nauto g_course <- grouping(enroll.course);\nauto g_year <- grouping(enroll.year);\nauto level <- g_course + g_year; # 0 = leaf, 1 = course subtotal, 2 = grand total\nauto parent <- case when g_year = 0 then enroll.course else null end;\nauto rnk <- rank(enroll.course, enroll.year)\n over (partition by level, parent order by total desc);\n\nselect\n enroll.course,\n enroll.year,\n total,\n level,\n --parent,\n rnk,\nby rollup (enroll.course, enroll.year)\norder by level desc nulls first, parent asc nulls first, rnk asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTE - a composite aggregate over a rollup (a difference/ratio of sums) needs\n# NO special handling: `sum(profit) - sum(loss) as net` rolls up both operands\n# to the same levels, because `by rollup (…)` is a property of the whole select,\n# not of one aggregate.\n"
1701
- },
1702
- {
1703
- name: "rollup",
1704
- title: "Subtotals + grand total in one pass with `by rollup (…)`",
1705
- summary: "subtotals and a grand total in one pass, and labeling the levels",
1706
- body: "# A `by rollup (d1, d2)` clause AFTER the select list (before having/order by)\n# computes the whole select at MULTIPLE grain levels in one pass - the grouping\n# sets (d1, d2), (d1), () - i.e. per-(d1,d2) leaf rows, a subtotal per d1, and a\n# grand total. (`by cube (d1, d2)` = every combination; `by rollup ()` over the\n# select's own grain.) It applies to EVERY aggregate in the select, so all\n# measures share one consistent grouping.\nimport enrollments as enroll;\n\n# `grouping(d)` = 1 when d is ROLLED UP on a row (a subtotal/grand-total row), 0 on\n# a leaf - use it to LABEL those rows: the rolled-up dimension is otherwise NULL\n# and would collide with any genuine NULL in the data.\nselect\n case when grouping(enroll.department) = 1 then 'ALL DEPARTMENTS'\n else enroll.department end as department,\n case when grouping(enroll.course) = 1 then 'all courses'\n else enroll.course end as course,\n sum(enroll.credits) as total_credits\nby rollup (enroll.department, enroll.course)\nhaving total_credits > 0\norder by department asc nulls first, course asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - CLAUSE ORDER matches SQL: the `by rollup (…)` grouping clause comes BEFORE\n# `having` (select list -> by rollup -> having -> order by -> limit). The\n# HAVING filters EVERY grouping level - subtotal and grand-total rows too.\n# - SORT: use EXACTLY the sort the question asks for. Rollup rows are ordinary rows\n# and sort with everything else (their rolled-up dimensions are NULL, hence\n# `nulls first`/`nulls last` decides where they land). Sorting by LEVEL is OPTIONAL\n# and only when you actually want leaves/subtotals/totals grouped together: ORDER BY\n# cannot compute a fresh aggregate, so PROJECT the level (`grouping(a) + grouping(b)`)\n# as a HIDDEN (`--`) column and sort by its alias:\n# --grouping(enroll.department) + grouping(enroll.course) as _level\n# order by _level asc, department asc nulls first\n# Do NOT add `_level` to a sort the question already specified - a leading sort key\n# changes WHICH rows survive a `limit`, so it can silently drop the totals.\n# - COMPOSITE measures just work: `sum(a) - sum(b) as net` rolls up BOTH operands\n# to the same levels because the `by rollup (…)` clause covers the whole select.\n# To zero-fill a measure, wrap it: `coalesce(sum(a), 0) as a`.\n# - FILTER-BEFORE-ROLLUP vs `having`: `by rollup (…) having x > t` filters EVERY level AFTER\n# the rollup, so the subtotal/total rows still AGGREGATE the below-threshold leaves (they\n# just aren't displayed). To keep only leaf groups meeting a threshold and roll up JUST\n# those survivors, filter the leaves in a rowset FIRST (`rowset kept <- select … having x >\n# t`), then `by rollup ()` over `kept`. `having` on the rollup itself is the wrong tool when\n# subtotals must exclude the filtered-out leaves.\n"
1707
- },
1708
- {
1709
- name: "row-level-where-vs-having",
1710
- title: "Row-level filter (WHERE) vs aggregate filter (HAVING) - don't wrap a per-row threshold in max()/min()",
1711
- summary: "choosing between WHERE and HAVING for a threshold condition",
1712
- body: "# A non-aggregate, per-ROW condition belongs in WHERE - it filters the rows that\n# feed every aggregate, BEFORE grouping. HAVING is ONLY for filtering on an\n# aggregate RESULT. These are different operations; do not substitute one for the\n# other.\nimport iris as iris;\n\n# RIGHT - keep only large-sepal FLOWERS, then average their petals.\n# `sepal_length > 6.0` is a per-row test on a column -> WHERE.\nwhere iris.sepal_length > 6.0\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\norder by iris.species asc;\n\n# WRONG - `having max(iris.sepal_length) by iris.species > 6.0` does NOT keep\n# large-sepal rows. It keeps whole SPECIES whose single largest flower exceeds\n# 6.0, then averages ALL of that species' flowers (small ones included). Wrapping\n# a per-row threshold in max()/min() in HAVING silently changes the meaning, and\n# AND-ing it with another filter (e.g. a rank cutoff) often yields 0 rows because\n# the two now select disjoint populations.\n#\n# Pick by what the condition is ABOUT:\n# per-row value of a column -> WHERE `where x.amount > 10000`\n# an aggregate's result -> HAVING `having total_amount > 10000`\n# (select it, hide with `--`)\n# aggregate just SOME rows -> inline `?` `sum(x.amount ? x.amount > 10000)`\n#\n# An aggregate condition CAN go in WHERE as a pre-filter (`where count(x) > 0`);\n# it groups by the SELECT grain unless pinned with `by <grain>` (`by *` = a\n# whole-table total). SCOPE: a WHERE aggregate computes over ALL rows of the\n# model - the other WHERE predicates never narrow its input (HAVING, selected,\n# and inline-`?` aggregates DO see the filtered rows). `by` pins the groups,\n# never the input rows; for an aggregate over the filtered rows use `having`\n# or a `then where` stage.\n"
1713
- },
1714
- {
1715
- name: "scoped-join",
1716
- title: "Blend two models in one query with a scoped subset/union join",
1717
- summary: "blending a second fact model or rowset into one query",
1718
- body: "# A query-scoped `join` blends a second model into ONE select without editing the\n# model files (the query-local equivalent of `merge`). A join DECLARES how the\n# two keys' value domains relate - it never drops rows (rendering is always\n# row-preserving; restrict rows with explicit `is not null` filters):\n# subset join a = b -> declares a's values are contained in b's (a is the\n# narrow side, b superset, all values in a in b)\n# union join a = b -> neither domain contains the other; the key coalesces\n# and unmatched rows from EITHER side are kept\n# Place the clause(s) RIGHT AFTER the `select` list (the SQL-like spot).\nimport enrollments as enroll;\nimport students as students;\n\n# (1) SINGLE-KEY blend: bring students onto enrollments by the shared student\n# key. Enrollment student ids are a SUBSET of all student ids, so declare that;\n# majors with no enrollments are preserved with a NULL count.\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id\norder by enrollments desc nulls first\nlimit 100;\n\n# The select plus its join clause(s) is ONE statement, so it must end with `;`\n# even when no `order by`/`limit` follows the join - a quick probe that stops\n# right after the join clause still needs the terminator:\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id;\n\n# ---------------------------------------------------------------------------\n# (2) MULTI-KEY blend - JOIN ON THE FULL GRAIN. When facts share a COMPOSITE\n# grain, write ONE join clause per key. `trilogy explore` shows each fact's grain\n# as `@<k1, k2>` (e.g. a sales/returns fact keyed `@<department, course>`);\n# matching only ONE key fans out and DOUBLE-COUNTS. Two rowsets, joined on both\n# of their shared keys:\nrowset completed_by <- where enroll.completed = true\n select enroll.department as dept, enroll.course as course, count(enroll.id) as completed_cnt;\nrowset incomplete_by <- where enroll.completed = false\n select enroll.department as dept, enroll.course as course, count(enroll.id) as incomplete_cnt;\nrowset null_completed <- where enroll.completed is null\n select enroll.department as dept, enroll.course as course, count(enroll.id) as null_completed_cnt;\n\nselect\n completed_by.dept,\n completed_by.course,\n completed_by.completed_cnt,\n incomplete_by.incomplete_cnt,\n null_completed.null_completed_cnt,\nunion join completed_by.dept = incomplete_by.dept = null_completed.dept\nunion join completed_by.course = incomplete_by.course = null_completed.course\norder by completed_by.dept asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (3) SELF-PAIR across two periods/sets (period-over-period) - a useful shape\n# for \"this year vs last year\": build one rowset per period (each aggregated to\n# the SAME keys), filter to restrict to output to one side or the other\n# in having clause.\n\nrowset y2020 <- where enroll.year = 2020 select enroll.department as dept, count(enroll.id) as cnt;\nrowset y2021 <- where enroll.year = 2021 select enroll.department as dept, count(enroll.id) as cnt;\n\nselect\n y2020.dept,\n y2020.cnt as cnt_2020,\n y2021.cnt as cnt_2021,\n y2021.cnt - y2020.cnt as yoy_diff,\nunion join y2020.dept = y2021.dept\nhaving y2021.cnt is not null\norder by yoy_diff desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (4) EXPRESSION join keys - a key may be ANY expression (arithmetic, an\n# aggregate, a window), not just a bare field. Here each year is matched to the\n# PRIOR year by joining on an OFFSET key (`prev_year.yr + 1`), so the prior\n# year's count lands on the current row without a self-merge. Only `=` equality\n# is supported (a key below comparison level - wrap a comparison in parens).\nrowset cur_year <- select enroll.year as yr, count(enroll.id) as cnt;\nrowset prev_year <- select enroll.year as yr, count(enroll.id) as cnt;\n\nselect\n cur_year.yr,\n cur_year.cnt as this_year,\n prev_year.cnt as prior_year,\nunion join cur_year.yr = prev_year.yr + 1\norder by cur_year.yr asc\nlimit 100;\n\n# NOTES:\n# - JOIN ON THE FULL GRAIN: one `join` clause per key in the two facts' shared\n# `@<...>` grain. Under-joining (one key of a multi-key grain) can cause\n# duplication. Chain `= c` to also pull a base key into the join\n# group so its properties stay reachable (`union join a.k = b.k = base.k`).\n# - Stacked clauses of the SAME type can share one prefix via `and`:\n# `union join a.k1 = b.k1 and a.k2 = b.k2` == two stacked `union join` clauses.\n# `and` joins distinct KEY-EQUALITY groups (not filters); `= c` chains keys into\n# ONE group - both compose: `union join a.k = b.k = base.k and a.k2 = b.k2`.\n# A COMMA never separates join groups (it is a select-list separator only):\n# `union join a.k1 = b.k1, a.k2 = b.k2` is an error - use `and`.\n# - Terminate the statement with `;` after the LAST clause, whatever that clause\n# is (join, `having`, `order by`, `limit`).\n# - A join key may be ANY expression (a computed/offset key, an aggregate, a\n# window), not only a bare field - see (4). Only `=` equality is supported.\n# - Joins NEVER drop rows: for an intersection (rows present in BOTH sides),\n# add explicit conditions - `where <side_a attr> is not null and <side_b\n# attr> is not null`. A one-sided `is not null` keeps the other side's\n# exclusive rows. Either spelling reads as \"did this side match?\": the\n# side's own JOIN KEY (`where a.k is not null`, tested per side even though\n# the projected key is the coalesced group axis) or any of its measures /\n# attributes (`where a.cnt is not null`). Same in `having`.\n# - `left`/`full`/`inner`/`right`/`cross join` are NOT valid query-scoped joins\n# (they error) - only `subset`/`union`. Map an old `left join a = b` to\n# `subset join b = a` (swap operands) and `full join a = b` to\n# `union join a = b`. A union key-group must be ALL union (can't mix with\n# subset on the SAME key; `union join a = b = c` chains one union group);\n"
1719
- },
1720
- {
1721
- name: "set-intersect-difference",
1722
- title: "Set intersection / difference on a multi-column key (presence per group)",
1723
- summary: "count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom",
1724
- body: "# To intersect or difference rows on a MULTI-COLUMN key (the SQL INTERSECT /\n# EXCEPT of several columns), group by the full key tuple and build a presence\n# flag per set with `sum(case when <set> then 1 else 0 end)`. Grouping keeps\n# NULL key parts as their own group (correct INTERSECT/EXCEPT semantics).\n# (Avoid concatenating nullable fields into a single key: `||` over a NULL\n# yields NULL, and `concat()`/`concat_ws()` skip NULLs, which conflates\n# distinct tuples - tuple grouping handles NULL parts correctly.)\nimport enrollments as enroll;\n\n# Presence of each (student_id, course) tuple among completed vs open enrollments.\nauto in_completed <- sum(case when enroll.completed = true then 1 else 0 end)\n by enroll.student_id, enroll.course;\nauto in_open <- sum(case when enroll.completed = false then 1 else 0 end)\n by enroll.student_id, enroll.course;\n\nselect\n # intersection: tuples appearing in BOTH sets\n sum(case when in_completed > 0 and in_open > 0 then 1 else 0 end) as in_both,\n # difference: tuples completed but never open (completed EXCEPT open)\n sum(case when in_completed > 0 and in_open = 0 then 1 else 0 end) as completed_only,\n;\n"
1725
- },
1726
- {
1727
- name: "staged-membership",
1728
- title: "Stage a coarse-grain membership set, then filter a fine-grain query",
1729
- summary: "filtering a fine-grain query by a set computed at a coarser grain",
1730
- body: "# When a query filters on a condition computed at a COARSER grain (e.g. \"items\n# sold in all 3 channels\", \"courses offered in more than one year\"), compute that\n# membership set in its own `rowset` and filter the fine-grain query with `in`.\n# Staging keeps the coarse aggregate from entangling with the fine-grain\n# aggregates (which otherwise re-aggregate / over-count).\nimport enrollments as enroll;\n\n# Coarse stage: courses offered in more than one year.\nrowset multi_year <- select\n enroll.course as course,\n --count_distinct(enroll.year) as year_count,\nhaving year_count > 1\n;\n\n# Fine stage: enrollments per course, only for those courses.\nwhere enroll.course in multi_year.course\nselect\n enroll.course,\n count(enroll.id) as enrollments,\norder by enrollments desc\nlimit 100;\n"
1731
- },
1732
- {
1733
- name: "union-stack-channels",
1734
- title: "Stack rows from several sources/channels with the union(...) TVF",
1735
- summary: "stacking several sources/channels together as rows",
1736
- body: "# `union(...)` is a relational table-valued function: it STACKS the rows of TWO OR\n# MORE self-contained `select` arms, positionally (like SQL `UNION ALL`), into one\n# named result. Use it to combine CHANNELS / SOURCES / labeled populations - ONE\n# ARM PER SOURCE, to any amount. It\n# is a row STACK, NOT a key-join: arms are matched by COLUMN POSITION, so every arm\n# must project the same number of columns, in the same order and types, as the\n# trailing `-> (...)` output list.\nimport enrollments as enroll;\n\n# IMPORTANT: each arm is an INLINE, self-contained select written directly inside\n# the `union(...)` - you CANNOT pass the name of a previously-defined rowset as an\n# arm; put the full select logic (its own `where`, its own aggregation) in the arm.\n# The arms' i-th column maps to the i-th name in the `-> (...)` output list; the\n# surface names inside each arm need not match - only POSITION does. Each arm may\n# group by its own dims (here `course`), so an arm can emit MANY rows.\nwith by_channel as union(\n (where enroll.department = 'Biology'\n select\n 'Biology' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n ),\n (where enroll.department = 'Chemistry'\n select\n 'Chemistry' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n ),\n (where enroll.department = 'Physics'\n select\n 'Physics' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n )\n) -> (channel, course, enrollments);\n\n# Reference the stacked outputs as `<rowset>.<output>`.\nselect\n by_channel.channel,\n by_channel.course,\n by_channel.enrollments,\norder by by_channel.channel, by_channel.enrollments desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Pass TWO OR MORE arms - add a fourth/fifth the same way; one arm per source.\n# - The trailing `-> (channel, course, enrollments)` NAMES the positional outputs;\n# column order / count / type must line up across every arm.\n# - Every union output is treated as a KEY (grain component). To RE-AGGREGATE the\n# stack (e.g. a grand total across channels), wrap it in an outer aggregate:\n# select sum(by_channel.enrollments) by rollup by_channel.channel as total;\n# - An arm may carry its OWN query-scoped join (`select ... subset join b = a`\n# after its select list) - localize each source's join to the arm that needs it.\n# - This is NOT the forbidden SQL `UNION` keyword between two selects; it is the\n# `union(...)` function form - the cleanest way to stack rows from several\n# sources.\n# - AGGREGATING across a union/set-op: an arm's `select` dedups to its own output\n# grain BEFORE stacking (all selects are select distinct), \n# so stacking raw per-row values (a measure, a `1` flag)\n# and summing/counting OUTSIDE the union silently undercounts whenever two source\n# rows in one arm project the same tuple (e.g. two same-priced sales in a week).\n# Pre-aggregate INSIDE each arm (`sum(sales) as v`, `sum(1) as cnt` - as this\n# example's `count(enroll.id)` does), or pull the arm's grain key through as an\n# extra output column so every fact row stays distinct (downstream selects can\n# just ignore it). And count a GUARANTEED-non-null key, never a nullable display\n# column: `except`/`intersect` keep all-null rows (NULL-safe), which\n# `count(nullable_col)` then silently skips.\n"
1737
- },
1738
- {
1739
- name: "window-period-over-period",
1740
- title: "Period-over-period ratio with a window function",
1741
- summary: "`lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`",
1742
- body: "# `<col> ? <cond>` builds a filtered set of values you can match with `in`.\n# lead()/lag() over (order by ...) compares each row to an offset row - here\n# each year's enrollment count divided by the PRIOR year's.\nimport enrollments as enroll;\n\nauto recent_years <- enroll.year ? enroll.year >= 2015;\n\ndef vs_prior_year(amt) -> round(amt / (lag(amt, 1) over (order by enroll.year asc)), 2);\n\nwhere enroll.year in recent_years\nselect\n enroll.year,\n @vs_prior_year(count(enroll.id)) as enrollment_growth,\nhaving\n enrollment_growth is not null\norder by enroll.year asc nulls first;\n"
1743
- }
1744
- ];
1745
- //#endregion
1746
- //#region dashboards/prompts.ts
1747
- function ot(e) {
1748
- let t;
1749
- try {
1750
- t = JSON.parse(e);
1751
- } catch (e) {
1752
- throw Error(`Invalid JSON format: ${e.message}`);
1753
- }
1754
- if (!t.name || typeof t.name != "string") throw Error("Dashboard must have a name property of type string");
1755
- if (!t.description || typeof t.description != "string") throw Error("Dashboard must have a description property of type string");
1756
- if (!Array.isArray(t.layout)) throw Error("Dashboard must have a layout property of type array");
1757
- if (!t.gridItems || typeof t.gridItems != "object") throw Error("Dashboard must have a gridItems property of type object");
1758
- let n = /* @__PURE__ */ new Set();
1759
- for (let e = 0; e < t.layout.length; e++) {
1760
- let r = t.layout[e];
1761
- if (!r.id || typeof r.id != "string") throw Error(`Layout item at index ${e} must have an id property of type string`);
1762
- if (typeof r.x != "number") throw Error(`Layout item with id '${r.id}' must have an x property of type number`);
1763
- if (typeof r.y != "number") throw Error(`Layout item with id '${r.id}' must have a y property of type number`);
1764
- if (typeof r.w != "number") throw Error(`Layout item with id '${r.id}' must have a w property of type number`);
1765
- if (typeof r.h != "number") throw Error(`Layout item with id '${r.id}' must have an h property of type number`);
1766
- n.add(r.id);
1767
- }
1768
- for (let [e, r] of Object.entries(t.gridItems)) {
1769
- if (!n.has(e)) throw Error(`Grid item with id '${e}' does not have a corresponding layout item`);
1770
- if (!r.type || typeof r.type != "string") throw Error(`Grid item with id '${e}' must have a type property of type string`);
1771
- if (!r.content || typeof r.content != "string") throw Error(`Grid item with id '${e}' must have a content property of type string`);
1772
- if (!r.name || typeof r.name != "string") throw Error(`Grid item with id '${e}' must have a name property of type string`);
1773
- if ("width" in r && typeof r.width != "number") throw Error(`Grid item with id '${e}' has an invalid width property (must be a number)`);
1774
- }
1775
- for (let e of n) if (!(e in t.gridItems)) throw Error(`Layout item with id '${e}' does not have a corresponding grid item`);
1776
- return t;
1777
- }
1778
- var st = "You are a world-class dashboard designer and analyst who produces striking insights. Given this typescript template format for a dashboard config, and a list of available fields,\ngenerate a JSON spec that can be imported into these typescript objects that will appropriately visualize data for the provided prompt.\n\nYou build information dense by clear dashboards, in the style of the New York Times or Edward Tufte. Focus on laying out content for an interactive, click based exploration; do not\nmention anything related to global dashboard filters, however.\n\nAppropriately mix in description and markdown to explain the data and the visualizations and how the dashboard should be used. \n\nThe horizontal grid components are sized on a 0-20 scale, which will respond to width. Rows are 30 pixels high. Markdown components should be at least 3 rows high, but make them higher\nif you judge appropriate. \n\nMake the dashboard comprehensive to explore the user question. If the user asks for something detailed, it should typically be 5-8 rows (5-10 cells); otherwise 3-6 is appropriate (4-6 cells). \n\nFor the chart and table types, in the content, write a a prompt for the data you want to visualize. A data engineer\nwill transform this into a real SQL query based on knowledge of the underlying database.\n\nThe prompt should be descriptive and precise to ensure they understand your intent, but should avoid being too 'SQL' - speak business.\n\nThey only have access to the same general concepts as you will be given below, so plan appropriately\nand do not ask them for something they will be unable to answer with that data.\n\nFor markdown, you can directly fill in the appropriate markdown syntax. Markdown cells are static and will not run/fetch data. The other\ntwo cells will be populated by queries. \n\nTypescript format \"\nexport const CELL_TYPES = {\n CHART: 'chart',\n MARKDOWN: 'markdown',\n TABLE: 'table',\n SECTION_HEADER: 'section-header',\n } as const\n \nexport type CellType = (typeof CELL_TYPES)[keyof typeof CELL_TYPES]\n\nexport interface PromptLayoutItem {\n x: number\n y: number\n w: number\n h: number\n id: string\n}\n\n// This contains details about the layout\n// the \nexport interface PromptGridItemData {\n type: CellType\n content: string\n name: string\n width?: number\n height?: number\n}\n\nexport interface PromptDashboard {\n name: string\n layout: PromptLayoutItem[]\n // the key here will associate a grid item with a layout item (in the id field)\n gridItems: Record<string, PromptGridItemData>\n description: string\n}\n\n\"\n\n";
1779
- function ct(e, t) {
1780
- return `
1781
- ${st}
1782
- The prompt is: "${e}"
1783
- The available concepts are:
1784
- ${t}
1785
-
1786
- Return your answer as a JSON object surrounded by triple backticks. Include your reasoning before the object.
1787
- `;
1788
- }
1789
- //#endregion
1790
- //#region llm/data/conceptFilters.ts
1791
- var lt = [
1792
- ".year",
1793
- ".quarter",
1794
- ".month",
1795
- ".week",
1796
- ".day",
1797
- ".day_of_week",
1798
- ".year_start",
1799
- ".month_start",
1800
- ".hour",
1801
- ".minute",
1802
- ".second"
1803
- ];
1804
- function ut(e) {
1805
- return lt.some((t) => e.endsWith(t));
1806
- }
1807
- function dt(e) {
1808
- return e.filter((e) => !ut(e.name || e.label || ""));
1809
- }
1810
- //#endregion
1811
- //#region llm/data/prompts.ts
1812
- function ft(e) {
1813
- return dt(e);
1814
- }
1815
- var pt = `You are a world-class assistant for generating queries in Trilogy, a SQL inspired language with similar syntax, Use the the syntax description below and field information to answer the user request. Return your answer with a short reasoning and a valid Trilogy query in triple double quotes.
1816
-
1817
- ${A}
1818
-
1819
- For any response to the user, use this format -> put your actual response within triple double quotes with thinking and justification before it, in this format (replace placeholders with relevant content): Reasoning: {{reasoning}} """{{ response }}"""
1820
- `;
1821
- function j(e) {
1822
- return ft(e).map((e) => `[name: ${e.name} | type:${e.type}${e.calculation ? " | alias_for:" + e.calculation : ""}${e.description ? " | description:" + e.description : ""}]`).join(", ");
1823
- }
1824
- function mt(e, t) {
1825
- return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${j(t)}, create the best valid Trilogy query to answer the following user input: "${e}" Return the query within triple double quotes with your thinking and justification before it, so of this form as a jinja template: Reasoning: {{reasoning_placeholder}} """{{ trilogy }}""". Example: Because the user asked for sales by year, and revenue is the best sales related field available, we can aggregate revenue by year: """SELECT order.year, sum(revenue) as year_revenue order by order.year asc;"""`;
1826
- }
1827
- function ht(e, t) {
1828
- return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${j(t)}, create the closest matching syntactically valid WHERE CLAUSE to filter a dashboard from this request """${e}""". Return only the portion of a SQL query representing the where clause, excluding any ordering. Do not include comments. After you generate the code, review it carefully to make sure it is only the partial where clause of a select, starting with WHERE (imagine if you put a "select 1 ..." in front of it.), with no semicolon at the end. Return your generated code within triple double quotes with thinking and justification before it in the form (as a jinja example) Reasoning: {{reasoning}} """{{ trilogy }}""". Example: Reasoning: Because the user asked for rainbow colors, we can filter to where the color is a primary color """where color in ('red', 'blue', 'violet')"""`;
1829
- }
1830
- function gt(e, t) {
1831
- return ct(e, j(t));
1832
- }
1833
- //#endregion
1834
- //#region llm/sharedToolSchemas.ts
1835
- var M = [
1836
- "line",
1837
- "bar",
1838
- "barh",
1839
- "point",
1840
- "geo-map",
1841
- "tree",
1842
- "area",
1843
- "headline",
1844
- "donut",
1845
- "heatmap",
1846
- "boxplot",
1847
- "treemap",
1848
- "beeswarm"
1849
- ], _t = {
1850
- chartType: "bar",
1851
- xField: "category",
1852
- yField: "revenue",
1853
- yField2: "cost",
1854
- colorField: "region",
1855
- sizeField: "quantity",
1856
- groupField: "year",
1857
- trellisField: "department",
1858
- trellisRowField: "quarter",
1859
- geoField: "state_code",
1860
- annotationField: "notes",
1861
- hideLegend: !1,
1862
- showTitle: !0,
1863
- scaleX: "linear",
1864
- scaleY: "linear",
1865
- linkY2: !1
1866
- }, vt = {
1867
- type: "object",
1868
- description: "One layer of a layered chart, drawn over the same result set.",
1869
- properties: {
1870
- chartType: {
1871
- type: "string",
1872
- enum: M,
1873
- description: "Type of chart to render"
1874
- },
1875
- xField: {
1876
- type: "string",
1877
- description: "Field name for x-axis. Longitude for geo-map charts if geofield not provided."
1878
- },
1879
- yField: {
1880
- type: "string",
1881
- description: "Field name for y-axis. Latitude for geo-map charts if geofield not provided."
1882
- },
1883
- colorField: {
1884
- type: "string",
1885
- description: "Field for color encoding (optional)"
1886
- },
1887
- sizeField: {
1888
- type: "string",
1889
- description: "Field for size encoding (optional)"
1890
- },
1891
- annotationField: {
1892
- type: "string",
1893
- description: "Field for data point annotations/labels (optional)"
1894
- },
1895
- layerLabel: {
1896
- type: "string",
1897
- description: "Name for this layer in the chart's legend (optional; defaults to the y field name)"
1898
- },
1899
- yAxisOrient: {
1900
- type: "string",
1901
- enum: ["left", "right"],
1902
- description: "Which side to draw this layer value axis on (optional)"
1903
- }
1904
- }
1905
- }, yt = {
1906
- chartType: "bar",
1907
- showTitle: !0,
1908
- layers: [{
1909
- chartType: "bar",
1910
- xField: "category",
1911
- yField: "revenue"
1912
- }, {
1913
- chartType: "line",
1914
- xField: "category",
1915
- yField: "avg_revenue"
1916
- }],
1917
- placements: [{
1918
- kind: "hline",
1919
- value: 1e3,
1920
- label: "target"
1921
- }]
1922
- }, N = {
1923
- type: "object",
1924
- description: "Chart configuration object",
1925
- properties: {
1926
- chartType: {
1927
- type: "string",
1928
- enum: M,
1929
- description: "Type of chart to render"
1930
- },
1931
- xField: {
1932
- type: "string",
1933
- description: "Field name for x-axis. Longitude for geo-map charts if geofield not provided."
1934
- },
1935
- yField: {
1936
- type: "string",
1937
- description: "Field name for y-axis. Latitude for geo-map charts if geofield not provided."
1938
- },
1939
- yField2: {
1940
- type: "string",
1941
- description: "Secondary y-axis field (optional)"
1942
- },
1943
- colorField: {
1944
- type: "string",
1945
- description: "Field for color encoding (optional)"
1946
- },
1947
- sizeField: {
1948
- type: "string",
1949
- description: "Field for size encoding (optional)"
1950
- },
1951
- groupField: {
1952
- type: "string",
1953
- description: "Field for grouping data (optional)"
1954
- },
1955
- trellisField: {
1956
- type: "string",
1957
- description: "Field for small multiples/faceting columns (optional)"
1958
- },
1959
- trellisRowField: {
1960
- type: "string",
1961
- description: "Field for small multiples/faceting rows (optional)"
1962
- },
1963
- geoField: {
1964
- type: "string",
1965
- description: "Field for geographic data (optional)"
1966
- },
1967
- annotationField: {
1968
- type: "string",
1969
- description: "Field for data point annotations/labels (optional)"
1970
- },
1971
- hideLegend: {
1972
- type: "boolean",
1973
- description: "Whether to hide the legend (optional)"
1974
- },
1975
- showTitle: {
1976
- type: "boolean",
1977
- description: "Whether to show the chart title (optional)"
1978
- },
1979
- scaleX: {
1980
- type: "string",
1981
- enum: [
1982
- "linear",
1983
- "log",
1984
- "sqrt"
1985
- ],
1986
- description: "Scale type for x-axis (optional)"
1987
- },
1988
- scaleY: {
1989
- type: "string",
1990
- enum: [
1991
- "linear",
1992
- "log",
1993
- "sqrt"
1994
- ],
1995
- description: "Scale type for y-axis (optional)"
1996
- },
1997
- linkY2: {
1998
- type: "boolean",
1999
- description: "Whether to link the secondary y-axis scale to the primary y-axis (optional)"
2000
- },
2001
- layers: {
2002
- type: "array",
2003
- items: vt,
2004
- description: "Draw several marks over one result set, e.g. bars for a total with a line for an average. When set, the chart type and field bindings above are ignored and each layer supplies its own; the settings above (title, legend, scales) still apply to the chart as a whole. Only line, bar, barh, area, point, boxplot, donut and heatmap can be layers. Cannot be combined with trellisField/trellisRowField. Leave unset for an ordinary single-mark chart."
2005
- },
2006
- placements: {
2007
- type: "array",
2008
- description: "Constant reference lines drawn over the chart (optional)",
2009
- items: {
2010
- type: "object",
2011
- properties: {
2012
- kind: {
2013
- type: "string",
2014
- enum: ["hline", "vline"]
2015
- },
2016
- value: {
2017
- type: "number",
2018
- description: "Where on the axis to draw the line"
2019
- },
2020
- label: {
2021
- type: "string",
2022
- description: "Optional text label pinned to the line"
2023
- }
2024
- },
2025
- required: ["kind", "value"]
2026
- }
2027
- },
2028
- linkLayerY: {
2029
- type: "boolean",
2030
- description: "Force layers to share one y-axis (true) or get independent ones (false). Omit to let the chart decide from the layers value formats (optional)"
2031
- }
2032
- }
2033
- }, bt = M.map((e) => `'${e}'`).join(", "), P = `Chart type is auto-detected based on data shape unless you specify a chartConfig. Only provide chartConfig if the user specifically requests a chart type or configuration; otherwise let auto-detection handle it.
2034
-
2035
- [Important!] Hiding fields used only for filtering/query structure from output - such as ranks - with the "--" trilogy select syntax may be useful to get the right auto-formatting.
2036
-
2037
- [Important] To get deterministic coloring, if there is a hex field available that maps to a dimension it can be pulled in to automatically be used as the relevant color when dimension is set as colorField.
2038
- You DO NOT need to specify colorField as the hexField if it is included; it will automatically be used to populate the color range for the dimension field.
2039
- You can also add a default available Trilogy import "import std.color;" at the top of a query and cast a string case statement of hex values to ::string::hex to create mappings yourself.
2040
- Do this if the user asks for specific coloring.
2041
-
2042
- Example chartConfig: ${JSON.stringify(_t)}
2043
-
2044
- To draw more than one mark over the same results — bars for a total with a line for an average, say — set 'layers' instead of the top-level chart type and fields. Each layer names its own chartType and fields; title, legend and scale settings stay on the parent. Add 'placements' for constant reference lines.
2045
- Example layered chartConfig: ${JSON.stringify(yt)}
2046
-
2047
- Available chartTypes: ${bt}`, F = {
2048
- name: "connect_data_connection",
2049
- description: "Connect or reconnect a data connection that is not currently active. Use this when a query fails because the connection is not active, or when you need to establish a connection before running queries.",
2050
- input_schema: {
2051
- type: "object",
2052
- properties: { connection: {
2053
- type: "string",
2054
- description: "The name of the data connection to connect"
2055
- } },
2056
- required: ["connection"]
2057
- }
2058
- };
2059
- //#endregion
2060
- //#region llm/chatAgentPrompt.ts
2061
- function xt(e) {
2062
- return e({
2063
- trilogySyntaxReference: A,
2064
- rulesInput: tt,
2065
- aggFunctions: nt,
2066
- functions: rt,
2067
- datatypes: it
2068
- });
2069
- }
2070
- var I = {
2071
- name: "return_to_user",
2072
- description: "Signal that you are done and return control to the user. You MUST call this tool when you have completed all requested tasks and are ready for the user's next input. Provide a brief summary of what was accomplished. Never end a turn with plain text — always call this tool when finished.",
2073
- input_schema: {
2074
- type: "object",
2075
- properties: { message: {
2076
- type: "string",
2077
- description: "A brief summary of what was accomplished or a response to the user."
2078
- } },
2079
- required: ["message"]
2080
- }
2081
- }, St = [
2082
- {
2083
- name: "run_trilogy_query",
2084
- description: "Execute a Trilogy query and return tabular results. Use this for data exploration, aggregations, and when the user wants to see raw data in a table format.",
2085
- input_schema: {
2086
- type: "object",
2087
- properties: {
2088
- query: {
2089
- type: "string",
2090
- description: "The Trilogy query to execute"
2091
- },
2092
- connection: {
2093
- type: "string",
2094
- description: "Data connection name to run the query against"
2095
- }
2096
- },
2097
- required: ["query", "connection"]
2098
- }
2099
- },
2100
- {
2101
- name: "chart_trilogy_query",
2102
- description: `Execute a Trilogy query and display results as a chart. ${P}`,
2103
- input_schema: {
2104
- type: "object",
2105
- properties: {
2106
- query: {
2107
- type: "string",
2108
- description: "The Trilogy query to execute"
2109
- },
2110
- connection: {
2111
- type: "string",
2112
- description: "Data connection name to run the query against"
2113
- },
2114
- chartConfig: {
2115
- ...N,
2116
- description: "Optional chart configuration. Only provide if user specifically requests a chart type or configuration."
2117
- }
2118
- },
2119
- required: ["query", "connection"]
2120
- }
2121
- },
2122
- {
2123
- name: "select_active_import",
2124
- description: "Select a single data source import to use for queries. This replaces any previously selected import. Only one import can be active at a time. After selecting an import, the tool will return the full list of available fields/concepts from that data source including their descriptions. Use list_available_imports first to see what data sources are available.",
2125
- input_schema: {
2126
- type: "object",
2127
- properties: { import_name: {
2128
- type: "string",
2129
- description: "The name of the import to select (e.g., \"sales.orders\" or \"finance.revenue\"). Use list_available_imports to see available options. Pass an empty string or null to clear the current selection."
2130
- } },
2131
- required: ["import_name"]
2132
- }
2133
- },
2134
- {
2135
- name: "list_available_imports",
2136
- description: "List all available data source imports for the current connection. Use this to discover what data models are available before adding an import.",
2137
- input_schema: {
2138
- type: "object",
2139
- properties: {},
2140
- required: []
2141
- }
2142
- },
2143
- F,
2144
- {
2145
- name: "create_markdown",
2146
- description: "Create a rich markdown artifact to display formatted text, summaries, reports, or data-driven content. Supports standard markdown syntax (headers, lists, bold, italic, links, tables, code blocks). When a query is provided, data from the query results can be referenced in the markdown template. Use this for narrative reports, executive summaries, annotated insights, or any rich text content.",
2147
- input_schema: {
2148
- type: "object",
2149
- properties: {
2150
- markdown: {
2151
- type: "string",
2152
- description: "Markdown content to display. Supports standard markdown syntax.\n\nIf a query is also provided, inject query results using these template expressions:\n\nBASIC SUBSTITUTION:\n- {field_name} — value from the first result row (e.g. {total_flights})\n- {data[N].field_name} — value from row N (e.g. {data[0].airline})\n- {data.length} — total number of result rows\n- {field_name || \"fallback\"} — value with a fallback if null/missing\n- {{#each data limit=N}} ...{field}... {{/each}} — loop over rows\n\nFORMAT SPECIFIERS (numeric fields only):\n- {field_name:,} — thousands separator (e.g. {total_flights:,} → \"5,797\")\n- {field_name:.2f} — fixed decimal places (e.g. {rate:.2f} → \"1.43\")\n- {field_name:,.2f} — both (e.g. {revenue:,.2f} → \"1,234.56\")\n- {field_name:.1%} — multiply by 100 and show as percentage (e.g. {rate:.1%} where rate=0.014 → \"1.4%\")\n\nARITHMETIC WITH FORMAT:\n- {(expr):format} — evaluate arithmetic then format; only field names and + - * / are allowed\n- Example: {(cancelled/total*100):.1f} computes the ratio and shows 1 decimal place\n\nLIMITATIONS — what the template engine cannot do:\n- No comparisons or conditional logic: {{#if (avg < 500)}} is NOT supported\n- No string functions, date arithmetic, or complex expressions\n- No nested loops or grouping logic\nIf you need conditional output (e.g. \"Regional\" vs \"Major/National\" based on a threshold), compute it in SQL/Trilogy using a CASE expression and return it as a field, then reference the field in the template."
2153
- },
2154
- title: {
2155
- type: "string",
2156
- description: "Optional title for the artifact"
2157
- },
2158
- query: {
2159
- type: "string",
2160
- description: "Optional Trilogy query to execute. Results will be available for template substitution in the markdown."
2161
- },
2162
- connection: {
2163
- type: "string",
2164
- description: "Data connection name for executing the query (required if query is provided)"
2165
- }
2166
- },
2167
- required: ["markdown"]
2168
- }
2169
- },
2170
- {
2171
- name: "list_artifacts",
2172
- description: "List all artifacts in the current chat session with their IDs, types, and summaries. Use this to see what artifacts exist before updating or removing them.",
2173
- input_schema: {
2174
- type: "object",
2175
- properties: {},
2176
- required: []
2177
- }
2178
- },
2179
- {
2180
- name: "get_artifact",
2181
- description: "Get the full contents and metadata of an artifact by ID. Returns the artifact type, configuration, and data. Large result sets are truncated; use get_artifact_rows to fetch specific row ranges.",
2182
- input_schema: {
2183
- type: "object",
2184
- properties: { artifact_id: {
2185
- type: "string",
2186
- description: "The ID of the artifact to retrieve"
2187
- } },
2188
- required: ["artifact_id"]
2189
- }
2190
- },
2191
- {
2192
- name: "get_artifact_rows",
2193
- description: "Fetch a specific range of rows from a query result or chart artifact. Use this to inspect data in large result sets that were truncated in the initial response. Rows are 0-indexed.",
2194
- input_schema: {
2195
- type: "object",
2196
- properties: {
2197
- artifact_id: {
2198
- type: "string",
2199
- description: "The ID of the results or chart artifact to fetch rows from"
2200
- },
2201
- start_row: {
2202
- type: "number",
2203
- description: "First row to return (0-indexed, inclusive)"
2204
- },
2205
- end_row: {
2206
- type: "number",
2207
- description: "Last row to return (0-indexed, inclusive)"
2208
- }
2209
- },
2210
- required: [
2211
- "artifact_id",
2212
- "start_row",
2213
- "end_row"
2214
- ]
2215
- }
2216
- },
2217
- {
2218
- name: "update_artifact",
2219
- description: "Update an existing artifact by ID. Can update markdown content, title, or chart configuration. Use list_artifacts first to find artifact IDs.",
2220
- input_schema: {
2221
- type: "object",
2222
- properties: {
2223
- artifact_id: {
2224
- type: "string",
2225
- description: "The ID of the artifact to update (from list_artifacts or tool results)"
2226
- },
2227
- markdown: {
2228
- type: "string",
2229
- description: "New markdown content (for markdown artifacts)"
2230
- },
2231
- title: {
2232
- type: "string",
2233
- description: "New title for the artifact"
2234
- },
2235
- chartConfig: {
2236
- ...N,
2237
- description: "New chart configuration (for chart/results artifacts)"
2238
- }
2239
- },
2240
- required: ["artifact_id"]
2241
- }
2242
- },
2243
- {
2244
- name: "hide_artifact",
2245
- description: "Hide one or more artifacts from the main view. Hidden artifacts are soft-deleted: they move to a collapsed \"Hidden\" section the user can expand, and you can still reference them by ID. Use this to remove stale, intermediate, or superseded artifacts during curation. Use list_artifacts first to find artifact IDs.",
2246
- input_schema: {
2247
- type: "object",
2248
- properties: { artifact_ids: {
2249
- type: "array",
2250
- items: { type: "string" },
2251
- description: "One or more artifact IDs to hide."
2252
- } },
2253
- required: ["artifact_ids"]
2254
- }
2255
- },
2256
- {
2257
- name: "reorder_artifacts",
2258
- description: "Reorder artifacts in the chat session by providing the desired order of artifact IDs. The artifacts panel displays artifacts in order, so arrange them for maximum clarity and impact (e.g., summary/overview first, supporting details after). Any artifact IDs not included will be appended at the end in their original order.",
2259
- input_schema: {
2260
- type: "object",
2261
- properties: { artifact_ids: {
2262
- type: "array",
2263
- items: { type: "string" },
2264
- description: "Ordered list of artifact IDs representing the desired display order. Use list_artifacts to find IDs."
2265
- } },
2266
- required: ["artifact_ids"]
2267
- }
2268
- },
2269
- I
2270
- ];
2271
- function Ct(e, t) {
2272
- if (!t || t.length === 0) return e;
2273
- let n = new Set(t);
2274
- return e.filter((e) => !n.has(e.name));
2275
- }
2276
- function wt(e, t) {
2277
- if (!t || t.length === 0) return e;
2278
- let n = new Set(e.map((e) => e.name)), r = /* @__PURE__ */ new Set(), i = t.map((e) => {
2279
- let t = e.definition.name;
2280
- if (n.has(t)) throw Error(`Host tool '${t}' shadows a built-in chat tool`);
2281
- if (r.has(t)) throw Error(`Host tool '${t}' is declared twice`);
2282
- return r.add(t), e.definition;
2283
- }), a = e.findIndex((e) => e.name === I.name);
2284
- return a === -1 ? [...e, ...i] : [
2285
- ...e.slice(0, a),
2286
- ...i,
2287
- ...e.slice(a)
2288
- ];
2289
- }
2290
- var Tt = [
2291
- { text: "- Every query and chart tool call returns an artifact ID. Use these IDs to reference, update, or remove artifacts later." },
2292
- {
2293
- tool: "create_markdown",
2294
- text: "- Use create_markdown to create rich formatted content: reports, summaries, annotated insights, data-driven narratives.\n - Markdown supports template expressions when a query is provided: {field_name}, {data[0].field}, {{#each data limit=5}} {field} {{/each}}"
2295
- },
2296
- {
2297
- tool: "list_artifacts",
2298
- text: "- Use list_artifacts to see all artifacts with their IDs, types, and metadata."
2299
- },
2300
- {
2301
- tool: "get_artifact",
2302
- text: "- Use get_artifact to inspect the full contents and configuration of a specific artifact."
2303
- },
2304
- {
2305
- tool: "update_artifact",
2306
- text: "- Use update_artifact to modify existing artifacts (change markdown content, title, or chart configuration)."
2307
- },
2308
- {
2309
- tool: "hide_artifact",
2310
- text: "- Use hide_artifact to remove stale or superseded artifacts from the main view. Hidden artifacts are preserved and accessible to both you and the user — they are not deleted."
2311
- },
2312
- {
2313
- tool: "create_markdown",
2314
- text: "- When the user asks for a summary, report, or narrative, prefer create_markdown over just text responses - it renders in the artifacts panel."
2315
- }
2316
- ], Et = [{
2317
- tool: "search_docs",
2318
- text: "- When you are unsure how to express something in Trilogy — window functions, date handling, filtering on aggregates, an unfamiliar keyword — call search_docs (kind: language or function) and then read_doc, rather than guessing at syntax. The reference above is a summary; the docs carry the full idioms."
2319
- }, {
2320
- tool: "read_doc",
2321
- text: "- read_doc returns the full article for an id from search_docs; quote the relevant part back to the user when they ask how the language works."
2322
- }], Dt = [
2323
- {
2324
- tool: "list_artifacts",
2325
- text: "Call list_artifacts to see everything currently in the panel."
2326
- },
2327
- {
2328
- tool: "hide_artifact",
2329
- text: "Hide stale or superseded artifacts using hide_artifact — failed queries, test runs, intermediate steps, or results from earlier questions that are no longer relevant to the current ask. Hidden artifacts are preserved (the user can restore them) and you can still reference them by ID."
2330
- },
2331
- {
2332
- tool: "update_artifact",
2333
- text: "Update titles: give each remaining artifact a clear, descriptive title that explains what it shows (via update_artifact)."
2334
- },
2335
- {
2336
- tool: "reorder_artifacts",
2337
- text: "Reorder artifacts for maximum impact — put the most important artifact first (e.g., a summary markdown or key chart), followed by supporting detail. The artifacts panel is the primary view the user sees."
2338
- },
2339
- { text: "The artifact panel should tell a coherent story that directly answers the user's latest request — not accumulate a growing pile from every prior turn." }
2340
- ];
2341
- function L(e, t) {
2342
- return e.filter((e) => !e.tool || !t.includes(e.tool));
2343
- }
2344
- function Ot(e) {
2345
- let { dataConnectionName: t, availableConnections: n, availableConcepts: r, activeImports: i = [], availableImportsForConnection: a = [], isDataConnectionActive: o = !0, disabledTools: s = [] } = e, c = L(Tt, s).map((e) => e.text).join("\n"), l = L(Dt, s).map((e, t) => `${t + 1}. ${e.text}`).join("\n"), u = L(Et, s), d = u.length > 0 ? `\n\nDOCUMENTATION:\n${u.map((e) => e.text).join("\n")}` : "", f = r && r.length > 0 ? `\n\nAVAILABLE FIELDS FOR QUERIES:\n${j(r)}` : "", p = !s.includes("connect_data_connection"), m = t && !o ? p ? " (NOT CONNECTED - use connect_data_connection tool to connect before running queries)" : " (NOT CONNECTED - queries will fail; tell the user the data connection is not available and call return_to_user)" : "", h = p ? "\n8. If the data connection is not active, use connect_data_connection to establish the connection before running queries" : "", g = t ? `ACTIVE DATA CONNECTION: ${t}${m}` : "No data connection currently selected. Ask the user which connection to use.", _ = i.length > 0 ? `\nACTIVE DATA SOURCE: ${i[0].name}` : "\nNo data source currently selected. Use select_active_import to select a data source, or use list_available_imports to see what is available.", v = a.length > 0 ? `\n\nAVAILABLE DATA SOURCES:\n${a.map((e) => `- ${e.name}${i.some((t) => t.id === e.id) ? " (active)" : ""}`).join("\n")}` : "";
2346
- return `You are a data analysis assistant with access to Trilogy query capabilities.
2347
-
2348
- You have access to tools that can select datasources, execute queries and generate visualizations. When a user asks about data, analyze their request, make sure you are looking at the right source, write a valid Trilogy query, and use the appropriate tool.
2349
-
2350
- ${g}
2351
- AVAILABLE DATA CONNECTIONS: ${n.length > 0 ? n.join(", ") : "None configured - user needs to set up a data connection first"}
2352
- ${_}${v}
2353
-
2354
- TRILOGY LANGUAGE REFERENCE:
2355
- ${A}
2356
- ${f}
2357
-
2358
- IMPORTANT GUIDELINES:
2359
- 1. Always use a reasonable LIMIT (e.g., 100-1000) unless the request is specifically for a time series, line chart, or the user asks for all data
2360
- 2. For charts, let auto-detection choose the chart type unless the user specifies one (e.g., "show me a bar chart", "as a line graph")
2361
- 3. If a query fails, explain the error clearly and try a corrected version
2362
- 4. When showing data, prefer tables for detailed exploration and charts for trends/comparisons
2363
- 5. Use the full field path (e.g., 'order.product.id') - never use FROM clauses
2364
- 6. Remember: No GROUP BY clause - grouping is implicit by non-aggregated fields in SELECT
2365
- 7. If the user question needs fields that are not in the same source, use select_active_import to switch to a different data source (only one can be active at a time). Always consider this when they change topics.${h}${d}
2366
-
2367
- ARTIFACT MANAGEMENT:
2368
- ${c}
2369
-
2370
- ARTIFACT CURATION (required before every return_to_user call):
2371
- Before calling return_to_user, you MUST curate the artifact panel so it reflects a clean, coherent answer to the user's current request:
2372
- ${l}
2373
-
2374
- COMPLETING YOUR RESPONSE:
2375
- - When you have finished addressing the user's request AND curated the artifact panel, call return_to_user with a brief summary.
2376
- - Never end a turn with plain text only — you must always call a tool. return_to_user is always your final tool call.
2377
- - The return_to_user message should be concise — the artifacts panel carries the detail.
2378
- `;
2379
- }
2380
- //#endregion
2381
- //#region llm/subchatSummarize.ts
2382
- var kt = "You are summarizing a subchat that has just finished. Read the transcript below (messages and tool calls) and produce a 2–4 sentence summary of what was accomplished, what (if anything) failed or remains, and any concrete artifacts (file names, queries, errors). Be specific. The overseer will use this report to update the user — there is no other source of truth, so do not omit important results.", At = "You are summarizing a subchat's progress for an overseer agent. Read the transcript below (messages and tool calls) and produce a concise 2–4 sentence summary of what has happened so far, what has been accomplished, and what (if anything) remains. Be specific about file names, queries, or errors. Do not editorialize.";
2383
- async function jt(e, t, n = "completion") {
2384
- let r = Mt(t.messages), i = n === "completion" ? kt : At, a = e.getFastModel(), o = e.model, s = !!a && a !== o;
2385
- try {
2386
- return s && (e.model = a), (await e.generateCompletion({
2387
- prompt: "",
2388
- systemPrompt: i
2389
- }, [{
2390
- role: "user",
2391
- content: `Subchat kind: ${t.kind}\nName: ${t.name}\n\n--- TRANSCRIPT ---\n${r}`
2392
- }])).text.trim();
2393
- } finally {
2394
- s && (e.model = o);
2395
- }
2396
- }
2397
- function Mt(e) {
2398
- return e.map((e, t) => {
2399
- let n = [`[${t}] ${e.role}: ${Nt(e.content || "", 800)}`];
2400
- if (e.toolCalls && e.toolCalls.length > 0) for (let t of e.toolCalls) n.push(` → ${t.name}(${Nt(JSON.stringify(t.input), 200)})`);
2401
- if (e.toolResults && e.toolResults.length > 0) for (let t of e.toolResults) n.push(` ← ${t.toolName}: ${Nt(t.result, 300)}`);
2402
- return n.join("\n");
2403
- }).join("\n\n");
2404
- }
2405
- function Nt(e, t) {
2406
- return e.length <= t ? e : e.slice(0, t - 1) + "…";
2407
- }
2408
- var Pt = 4, Ft = 8, It = 1600;
2409
- function R(e) {
2410
- return e ? Math.ceil(e.length / 4) : 0;
2411
- }
2412
- function Lt(e) {
2413
- let t = Pt + R(e.content);
2414
- for (let n of e.toolCalls ?? []) {
2415
- t += Ft + R(n.name);
2416
- try {
2417
- t += R(JSON.stringify(n.input));
2418
- } catch {}
2419
- }
2420
- for (let n of e.toolResults ?? []) t += Ft + R(n.toolName), t += R(n.result), n.imageData && (t += It);
2421
- return t;
2422
- }
2423
- function Rt(e) {
2424
- return e.reduce((e, t) => e + Lt(t), 0);
2425
- }
2426
- //#endregion
2427
- //#region llm/toolLoopCore.ts
2428
- var zt = "<user_input>", Bt = "</user_input>", z = "<system_input>", B = "</system_input>";
2429
- function Vt(e) {
2430
- return e.replace(/<\/?(?:user_input|system_input)>/gi, "");
2431
- }
2432
- function Ht(e) {
2433
- let t = e?.data;
2434
- return !Array.isArray(t) || t.length <= 100 ? {
2435
- head: t ?? [],
2436
- tail: [],
2437
- totalRows: t?.length ?? 0,
2438
- cutCount: 0
2439
- } : {
2440
- head: t.slice(0, 50),
2441
- tail: t.slice(t.length - 50),
2442
- totalRows: t.length,
2443
- cutCount: t.length - 100
2444
- };
2445
- }
2446
- function Ut(e) {
2447
- if (e.success) if (e.artifact) {
2448
- let t = e.artifact.config, n = e.artifact.data, r = e.artifact.id, i = "";
2449
- if (n) {
2450
- let e = typeof n.toJSON == "function" ? n.toJSON() : n, a = t?.resultSize ?? (Array.isArray(e?.data) ? e.data.length : 0), { head: o, tail: s, cutCount: c } = Ht(e);
2451
- if (c > 0) {
2452
- let t = JSON.stringify({
2453
- ...e,
2454
- data: o
2455
- }, null, 2), n = JSON.stringify({
2456
- headers: e.headers,
2457
- data: s
2458
- }, null, 2);
2459
- i = `\n\nQuery results (${a} rows total — showing first ${o.length} and last ${s.length}, use get_artifact_rows to fetch any range):\n${t}\n...(${c} of ${a} rows cut off — use get_artifact_rows with artifact ID: ${r})...\n${n}`;
2460
- } else i = `\n\nQuery results (${a} rows):\n${JSON.stringify(e, null, 2)}`;
2461
- }
2462
- let a = t && typeof t.executionTime == "number" ? ` in ${t.executionTime}ms` : "", o = t ? `Results: ${t.resultSize || 0} rows, ${t.columnCount || 0} columns${a}.` : "";
2463
- return `Success. ${r ? `Artifact ID: ${r}. ` : ""}${e.message || o}${i}`;
2464
- } else if (e.message) return e.message;
2465
- else return "Success.";
2466
- else return `Error: ${e.error}`;
2467
- }
2468
- function Wt(e) {
2469
- if (e instanceof Error) return e.message;
2470
- if (typeof e == "string") return e;
2471
- try {
2472
- return JSON.stringify(e);
2473
- } catch {
2474
- return String(e);
2475
- }
2476
- }
2477
- async function Gt(e, t, n, r, i, a, o) {
2478
- let s = o.maxIterations ?? 20, c = r.getMessages().slice(0, -1).map((e) => e.role === "user" && !e.hidden && e.content ? {
2479
- ...e,
2480
- content: `${zt}${Vt(e.content)}${Bt}`
2481
- } : e), l = `${zt}${Vt(e)}${Bt}`, u = l, d = !1, f = 0;
2482
- for (let e = 0; e < s; e++) {
2483
- if (a.checkAborted()) return e === 0 && r.addMessage({
2484
- role: "assistant",
2485
- content: "(Response in progress when stopped)"
2486
- }), r.addMessage({
2487
- role: "user",
2488
- content: `${z}[User requested pause - conversation can be continued]${B}`,
2489
- hidden: !0
2490
- }), {
2491
- terminated: !1,
2492
- stopped: !0,
2493
- finalMessage: "Stopped by user"
2494
- };
2495
- for (; a.isPaused?.() && !a.checkAborted();) await new Promise((e) => setTimeout(e, 200));
2496
- if (a.checkAborted()) return e === 0 && r.addMessage({
2497
- role: "assistant",
2498
- content: "(Response in progress when stopped)"
2499
- }), r.addMessage({
2500
- role: "user",
2501
- content: `${z}[User requested pause - conversation can be continued]${B}`,
2502
- hidden: !0
2503
- }), {
2504
- terminated: !1,
2505
- stopped: !0,
2506
- finalMessage: "Stopped by user"
2507
- };
2508
- let p = i.getToolExecutor(), m = {
2509
- prompt: u,
2510
- systemPrompt: o.buildSystemPrompt(),
2511
- tools: o.tools
2512
- }, h = await n.generateCompletion(t, m, c), g = h.text, _ = h.toolCalls ?? [];
2513
- if (a.checkAborted()) return r.addMessage({
2514
- role: "assistant",
2515
- content: g
2516
- }), r.addMessage({
2517
- role: "user",
2518
- content: `${z}[User requested pause - conversation can be continued]${B}`,
2519
- hidden: !0
2520
- }), {
2521
- terminated: !1,
2522
- stopped: !0,
2523
- finalMessage: "Stopped by user"
2524
- };
2525
- if (_.length === 0) {
2526
- if (r.addMessage({
2527
- role: "assistant",
2528
- content: g
2529
- }), o.terminateOnNoToolCall) return {
2530
- terminated: !0,
2531
- finalMessage: g
2532
- };
2533
- if (f++, f >= 3) return console.log("[toolLoopCore] Model ignored repeated tool reminders — ending turn"), {
2534
- terminated: !0,
2535
- finalMessage: g
2536
- };
2537
- let e = o.noToolCallReminder ?? "You must call a tool to proceed. If you are finished, call the appropriate completion tool to return control to the user. Do not respond with text only.";
2538
- console.log("[toolLoopCore] No tool call — re-prompting agent to use a tool");
2539
- let t = `${z}${e}${B}`;
2540
- r.addMessage({
2541
- role: "user",
2542
- content: t,
2543
- hidden: !0
2544
- }), c = [
2545
- ...c,
2546
- ...d ? [] : [{
2547
- role: "user",
2548
- content: l
2549
- }],
2550
- {
2551
- role: "assistant",
2552
- content: g
2553
- },
2554
- {
2555
- role: "user",
2556
- content: t,
2557
- hidden: !0
2558
- }
2559
- ], d = !0, u = "";
2560
- continue;
2561
- }
2562
- f = 0;
2563
- let v = [], y = [];
2564
- for (let e of _) {
2565
- if (a.checkAborted()) return r.addMessage({
2566
- role: "assistant",
2567
- content: g,
2568
- toolCalls: _.slice(0, y.length),
2569
- executedToolCalls: y.length > 0 ? y : void 0
2570
- }), v.length > 0 && r.addMessage({
2571
- role: "user",
2572
- content: "",
2573
- toolResults: v,
2574
- hidden: !0
2575
- }), r.addMessage({
2576
- role: "user",
2577
- content: `${z}[User requested pause - conversation can be continued]${B}`,
2578
- hidden: !0
2579
- }), {
2580
- terminated: !1,
2581
- stopped: !0,
2582
- finalMessage: "Stopped by user"
2583
- };
2584
- a.setActiveToolName(e.name);
2585
- let t;
2586
- try {
2587
- t = await p.executeToolCall(e.name, e.input);
2588
- } catch (n) {
2589
- t = {
2590
- success: !1,
2591
- error: `Tool "${e.name}" failed: ${Wt(n)}`
2592
- };
2593
- }
2594
- let n = {
2595
- id: e.id,
2596
- name: e.name,
2597
- input: e.input,
2598
- result: {
2599
- success: t.success,
2600
- message: t.message,
2601
- error: t.error,
2602
- subchatId: t.subchatId
2603
- }
2604
- };
2605
- if (y.push(n), o.onToolResult?.(e.name, t), t.artifact && r.addArtifact(t.artifact), t.terminatesLoop) {
2606
- v.push({
2607
- toolCallId: e.id,
2608
- toolName: e.name,
2609
- result: ""
2610
- });
2611
- let n = g?.trim() ? g : t.message ?? "";
2612
- return r.addMessage({
2613
- role: "assistant",
2614
- content: n,
2615
- toolCalls: _.slice(0, y.length),
2616
- executedToolCalls: y
2617
- }), r.addMessage({
2618
- role: "user",
2619
- content: "",
2620
- toolResults: v,
2621
- hidden: !0
2622
- }), {
2623
- terminated: !0,
2624
- finalMessage: t.message
2625
- };
2626
- }
2627
- if (t.awaitsUserInput) {
2628
- let n = t.message || "Awaiting user input.";
2629
- return v.push({
2630
- toolCallId: e.id,
2631
- toolName: e.name,
2632
- result: n
2633
- }), r.addMessage({
2634
- role: "assistant",
2635
- content: t.message || "",
2636
- toolCalls: _.slice(0, y.length),
2637
- executedToolCalls: y
2638
- }), r.addMessage({
2639
- role: "user",
2640
- content: "",
2641
- toolResults: v,
2642
- hidden: !0
2643
- }), o.onAwaitsUserInput?.(t, y), {
2644
- terminated: !1,
2645
- finalMessage: t.message
2646
- };
2647
- }
2648
- let i = Ut(t);
2649
- v.push({
2650
- toolCallId: e.id,
2651
- toolName: e.name,
2652
- result: i,
2653
- imageData: t.imageData
2654
- });
2655
- }
2656
- if (a.setActiveToolName(""), r.addMessage({
2657
- role: "assistant",
2658
- content: g,
2659
- toolCalls: _,
2660
- executedToolCalls: y
2661
- }), r.addMessage({
2662
- role: "user",
2663
- content: "",
2664
- toolResults: v,
2665
- hidden: !0
2666
- }), c = [
2667
- ...c,
2668
- ...d ? [] : [{
2669
- role: "user",
2670
- content: l
2671
- }],
2672
- {
2673
- role: "assistant",
2674
- content: g,
2675
- toolCalls: _
2676
- },
2677
- {
2678
- role: "user",
2679
- content: "",
2680
- toolResults: v,
2681
- hidden: !0
2682
- }
2683
- ], d = !0, u = "", e === s - 1) return r.addMessage({
2684
- role: "assistant",
2685
- content: "(Max tool iterations reached)"
2686
- }), {
2687
- terminated: !1,
2688
- finalMessage: g + "\n\n(Max tool iterations reached)"
2689
- };
2690
- }
2691
- return { terminated: !1 };
2692
- }
2693
- //#endregion
2694
- //#region llm/compaction.ts
2695
- var Kt = 2e4, qt = 6, Jt = 20, Yt = 6, Xt = "You are a context summarization assistant. You are given the earlier portion of an assistant conversation that is about to fall out of the context window. Produce a structured summary that the assistant will rely on INSTEAD of those messages.\n\nUse exactly these sections:\n## Goal — what the user is ultimately trying to accomplish.\n## Progress — what was built, changed, or discovered, with exact names and ids (editors, dashboards, jobs, connections, queries, fields).\n## Key decisions — choices made and why, including approaches tried and rejected.\n## Current state — where the work stands right now.\n## Open items — unresolved questions, known failures, and obvious next steps.\n\nBe concrete: prefer exact identifiers, query text and error messages over paraphrase. Aim for 300-600 words. Omit nothing the assistant would need to continue seamlessly. Do not editorialize and do not add recommendations of your own.";
2696
- function Zt(e) {
2697
- return e.role === "user" && !(e.toolResults && e.toolResults.length > 0);
2698
- }
2699
- function Qt(e, t) {
2700
- let n = t.keepRecentTokens ?? 2e4, r = t.minKeepRecentCount ?? qt, i = t.maxKeepRecentCount ?? Jt, a = t.minArchiveCount ?? Yt, o = 0, s = 0;
2701
- for (let t = e.length - 1; t >= 0 && !(o >= i || o >= r && s >= n); t--) s += Lt(e[t]), o++;
2702
- let c = e.length - o;
2703
- for (; c > 0 && !Zt(e[c]);) c--;
2704
- return c < a ? -1 : c;
2705
- }
2706
- function $t(e, t, n) {
2707
- return !t || t <= 0 ? !1 : (n ?? Rt(e.getLLMMessages())) > t;
2708
- }
2709
- async function en(e, t, n) {
2710
- let r = Mt(t), i = e.getFastModel(), a = e.model, o = !!i && i !== a;
2711
- try {
2712
- return o && (e.model = i), (await e.generateCompletion({
2713
- prompt: "",
2714
- systemPrompt: n ? `${Xt}\n\nAdditional emphasis requested: ${n}` : Xt
2715
- }, [{
2716
- role: "user",
2717
- content: `--- TRANSCRIPT ---\n${r}`
2718
- }])).text.trim();
2719
- } finally {
2720
- o && (e.model = a);
2721
- }
2722
- }
2723
- function tn(e) {
2724
- return {
2725
- role: "user",
2726
- hidden: !0,
2727
- content: e
2728
- };
2729
- }
2730
- async function nn(e, t, n = {}) {
2731
- let r = t.getLLMMessages(), i = Qt(r, n);
2732
- if (i < 0) return null;
2733
- let a = r.slice(0, i), o = await en(e, a, n.focus), s = t.messages.indexOf(r[i]);
2734
- if (s < 0) return null;
2735
- a.forEach((e) => {
2736
- e.archived = !0;
2737
- });
2738
- let c = n.createSummaryMessage ?? tn, l = `${z}[Conversation compacted. Summary of the earlier conversation:]\n\n${o}${n.appendix ?? ""}${B}`;
2739
- t.messages.splice(s, 0, c(l));
2740
- let u = {
2741
- archivedCount: a.length,
2742
- summary: o
2743
- };
2744
- return n.onCompacted?.(u), u;
2745
- }
2746
- function rn(e) {
2747
- let t = e.artifacts.filter((e) => !e.hidden);
2748
- return t.length === 0 ? "" : `\n\nArtifacts still available via get_artifact/get_artifact_rows:\n${t.map((e) => {
2749
- let t = e.config?.title || e.config?.query?.slice(0, 60) || "", n = e.config?.resultSize == null ? "" : `, ${e.config.resultSize} rows`;
2750
- return `- [${e.id}] ${e.type}${t ? ` "${t}"` : ""}${n}`;
2751
- }).join("\n")}`;
2752
- }
2753
- async function an(e, t, n = {}) {
2754
- return nn(e, t, {
2755
- ...n,
2756
- appendix: n.appendix ?? rn(t),
2757
- createSummaryMessage: n.createSummaryMessage ?? ((e) => ({
2758
- role: "user",
2759
- hidden: !0,
2760
- compaction: !0,
2761
- content: e
2762
- })),
2763
- onCompacted: (e) => {
2764
- t.updatedAt = /* @__PURE__ */ new Date(), t.changed = !0, n.onCompacted?.(e);
2765
- }
2766
- });
2767
- }
2768
- async function on(e, t, n = {}) {
2769
- if (!$t(t, e.getCompactionThresholdTokens(), t.lastContextTokens)) return null;
2770
- try {
2771
- let r = await an(e, t, n);
2772
- return r && (t.lastContextTokens = void 0), r;
2773
- } catch (e) {
2774
- return console.error("Automatic conversation compaction failed:", e), null;
2775
- }
2776
- }
2777
- //#endregion
2778
- //#region llm/editorRefinementTools.ts
2779
- function sn(e) {
2780
- return e.filter((e) => e.trilogyType === "concept").map((e) => ({
2781
- name: e.label,
2782
- type: e.datatype || e.type,
2783
- description: e.description || void 0,
2784
- calculation: e.calculation || void 0,
2785
- keys: e.keys || void 0
2786
- }));
2787
- }
2788
- function cn(e) {
2789
- return j(sn(e));
2790
- }
2791
- var ln = [
2792
- {
2793
- name: "validate_query",
2794
- description: "Validate a Trilogy query without executing it. Returns validation errors or confirms the query is valid, along with the list of available symbols/fields that can be used in the query. Use this to check your work before or after writing to the editor. IMPORTANT: Always validate after changing import statements, as this will refresh the available symbols from the imported sources.",
2795
- input_schema: {
2796
- type: "object",
2797
- properties: { query: {
2798
- type: "string",
2799
- description: "The Trilogy query to validate"
2800
- } },
2801
- required: ["query"]
2802
- }
2803
- },
2804
- {
2805
- name: "run_query",
2806
- description: "Execute a Trilogy query and return results to you. Use this to test queries and see what data they return before writing to the editor.",
2807
- input_schema: {
2808
- type: "object",
2809
- properties: { query: {
2810
- type: "string",
2811
- description: "The Trilogy query to execute"
2812
- } },
2813
- required: ["query"]
2814
- }
2815
- },
2816
- {
2817
- name: "format_query",
2818
- description: "Format/prettify a Trilogy query. Returns the formatted query string. Use this to clean up query formatting.",
2819
- input_schema: {
2820
- type: "object",
2821
- properties: { query: {
2822
- type: "string",
2823
- description: "The Trilogy query to format"
2824
- } },
2825
- required: ["query"]
2826
- }
2827
- },
2828
- {
2829
- name: "edit_chart_config",
2830
- description: `Update the chart configuration for visualizing query results. Use this when the user wants to change how results are displayed.
2831
-
2832
- ${P}`,
2833
- input_schema: {
2834
- type: "object",
2835
- properties: { chartConfig: N },
2836
- required: ["chartConfig"]
2837
- }
2838
- },
2839
- {
2840
- name: "edit_editor",
2841
- description: "Write content to the editor and automatically validate it. Returns validation results (errors or success with available fields). Use this to update the query - no need to call validate_query separately after editing.",
2842
- input_schema: {
2843
- type: "object",
2844
- properties: {
2845
- content: {
2846
- type: "string",
2847
- description: "The new content to write to the editor"
2848
- },
2849
- replaceSelection: {
2850
- type: "boolean",
2851
- description: "When true and the user has an active selection, replace only the selected text with the content instead of overwriting the whole document. Only use when the user's request targets their selection."
2852
- }
2853
- },
2854
- required: ["content"]
2855
- }
2856
- },
2857
- {
2858
- name: "run_active_editor_query",
2859
- description: "Run the current editor query and display results in the main results pane as well as return them to you. Use this when the user asks to \"see results\", \"run it\", \"show me\", or after making changes to the chart configuration. This runs the query that is currently in the editor (not a test query).",
2860
- input_schema: {
2861
- type: "object",
2862
- properties: {},
2863
- required: []
2864
- }
2865
- },
2866
- {
2867
- name: "get_artifact_rows",
2868
- description: "Fetch a specific range of rows from a query result artifact created by run_query or run_active_editor_query in this session. Use this to inspect data in large result sets that were truncated in the initial response. Rows are 0-indexed.",
2869
- input_schema: {
2870
- type: "object",
2871
- properties: {
2872
- artifact_id: {
2873
- type: "string",
2874
- description: "The artifact ID to fetch rows from (shown in the truncation notice)"
2875
- },
2876
- start_row: {
2877
- type: "number",
2878
- description: "First row to return (0-indexed, inclusive)"
2879
- },
2880
- end_row: {
2881
- type: "number",
2882
- description: "Last row to return (0-indexed, inclusive)"
2883
- }
2884
- },
2885
- required: [
2886
- "artifact_id",
2887
- "start_row",
2888
- "end_row"
2889
- ]
2890
- }
2891
- },
2892
- {
2893
- name: "request_close",
2894
- description: "Request to close the refinement session. Call this when you believe you are done making changes. The user will have a chance to reply with follow-up requests before confirming. Include a summary of what was done.",
2895
- input_schema: {
2896
- type: "object",
2897
- properties: { message: {
2898
- type: "string",
2899
- description: "Summary message describing what changes were made"
2900
- } },
2901
- required: ["message"]
2902
- }
2903
- },
2904
- {
2905
- name: "close_session",
2906
- description: "Immediately close the refinement session. Only call this after the user has confirmed they are satisfied with the changes, or if they explicitly ask to close/finish/done.",
2907
- input_schema: {
2908
- type: "object",
2909
- properties: {},
2910
- required: []
2911
- }
2912
- },
2913
- F
2914
- ], un = [
2915
- {
2916
- name: "browse_database_tree",
2917
- description: "Browse connection metadata using the same loading pattern as the sidebar tree. Use this to list databases, schemas, tables, and columns before writing SQL. You can optionally scope to a specific database/schema/table and refresh metadata from the connection.",
2918
- input_schema: {
2919
- type: "object",
2920
- properties: {
2921
- database: {
2922
- type: "string",
2923
- description: "Optional database/catalog name to inspect"
2924
- },
2925
- schema: {
2926
- type: "string",
2927
- description: "Optional schema name (requires database)"
2928
- },
2929
- table: {
2930
- type: "string",
2931
- description: "Optional table/view name (requires database and schema)"
2932
- },
2933
- refresh: {
2934
- type: "boolean",
2935
- description: "Whether to refresh metadata from the backend before returning results"
2936
- }
2937
- },
2938
- required: []
2939
- }
2940
- },
2941
- {
2942
- name: "run_query",
2943
- description: "Execute a SQL query and return results to you. Use this to test SQL before writing final editor content.",
2944
- input_schema: {
2945
- type: "object",
2946
- properties: { query: {
2947
- type: "string",
2948
- description: "The SQL query to execute"
2949
- } },
2950
- required: ["query"]
2951
- }
2952
- },
2953
- {
2954
- name: "edit_editor",
2955
- description: "Write content to the SQL editor. Use this to update the query in the editor when you are ready.",
2956
- input_schema: {
2957
- type: "object",
2958
- properties: {
2959
- content: {
2960
- type: "string",
2961
- description: "The new SQL content to write to the editor"
2962
- },
2963
- replaceSelection: {
2964
- type: "boolean",
2965
- description: "When true and the user has an active selection, replace only the selected text with the content instead of overwriting the whole document. Only use when the user's request targets their selection."
2966
- }
2967
- },
2968
- required: ["content"]
2969
- }
2970
- },
2971
- {
2972
- name: "run_active_editor_query",
2973
- description: "Run the current editor query and display results in the main results pane as well as return them to you. Use this when the user asks to run or show results from the current editor content.",
2974
- input_schema: {
2975
- type: "object",
2976
- properties: {},
2977
- required: []
2978
- }
2979
- },
2980
- {
2981
- name: "get_artifact_rows",
2982
- description: "Fetch a specific range of rows from a query result artifact created by run_query or run_active_editor_query in this session. Use this when results were truncated.",
2983
- input_schema: {
2984
- type: "object",
2985
- properties: {
2986
- artifact_id: {
2987
- type: "string",
2988
- description: "The artifact ID to fetch rows from"
2989
- },
2990
- start_row: {
2991
- type: "number",
2992
- description: "First row to return (0-indexed, inclusive)"
2993
- },
2994
- end_row: {
2995
- type: "number",
2996
- description: "Last row to return (0-indexed, inclusive)"
2997
- }
2998
- },
2999
- required: [
3000
- "artifact_id",
3001
- "start_row",
3002
- "end_row"
3003
- ]
3004
- }
3005
- },
3006
- {
3007
- name: "request_close",
3008
- description: "Request to close the refinement session. Call this when you believe you are done making changes. The user will have a chance to reply with follow-up requests before confirming.",
3009
- input_schema: {
3010
- type: "object",
3011
- properties: { message: {
3012
- type: "string",
3013
- description: "Summary message describing what changes were made"
3014
- } },
3015
- required: ["message"]
3016
- }
3017
- },
3018
- {
3019
- name: "close_session",
3020
- description: "Immediately close the refinement session. Only call this after the user has confirmed they are satisfied with the changes, or if they explicitly ask to close/finish/done.",
3021
- input_schema: {
3022
- type: "object",
3023
- properties: {},
3024
- required: []
3025
- }
3026
- },
3027
- F
3028
- ], dn = ln;
3029
- function fn(e) {
3030
- return e === "sql" ? un : ln;
3031
- }
3032
- function pn(e) {
3033
- let { editorType: t, connectionName: n, editorContents: r, selectedText: i, chartConfig: a, completionSymbols: o = [] } = e, s = i ? `
3034
- SELECTED TEXT (user wants to modify this specifically):
3035
- \`\`\`
3036
- ${i}
3037
- \`\`\`
3038
- ` : "", c = a ? `
3039
- CURRENT CHART CONFIG:
3040
- ${JSON.stringify(a, null, 2)}
3041
- ` : "", l = sn(o), u = l.length > 0 ? `
3042
- AVAILABLE FIELDS FOR QUERIES (${l.length} concepts):
3043
- ${cn(o)}
3044
- ` : "";
3045
- return t === "sql" ? `You are a SQL refinement assistant helping the user edit and test SQL queries.
3046
-
3047
- CURRENT EDITOR CONTENTS:
3048
- \`\`\`sql
3049
- ${r}
3050
- \`\`\`
3051
- ${s}
3052
- DATA CONNECTION: ${n}
3053
-
3054
- AVAILABLE TOOLS:
3055
- - browse_database_tree: Explore databases/schemas/tables/columns using the same metadata loading methods as the sidebar
3056
- - run_query: Execute SQL privately to test your changes
3057
- - run_active_editor_query: Run the current editor query and show results in the main pane
3058
- - get_artifact_rows: Fetch a row range from a truncated result artifact
3059
- - edit_editor: Write changes to the editor
3060
- - request_close: Request to close the session (user can reply with follow-ups)
3061
- - close_session: Immediately close (only after user confirms or says "done")
3062
-
3063
- WORKFLOW:
3064
- 1. Understand what the user wants to change
3065
- 2. Use browse_database_tree when you need metadata context (tables/columns)
3066
- 3. Use run_query to test candidate SQL
3067
- 4. Use edit_editor to apply final SQL changes
3068
- 5. Use run_active_editor_query when the user asks to run/show current editor results
3069
- 6. Always call request_close with a summary when done
3070
-
3071
- IMPORTANT:
3072
- - You MUST always call a tool; never respond with text only
3073
- - Use browse_database_tree instead of guessing table/column names
3074
- - Always use edit_editor for final query updates
3075
- - request_close gives the user a chance to ask for more changes before closing
3076
- - Only use close_session after the user explicitly confirms (e.g., says "done", "looks good", "close it")` : `You are a query refinement assistant helping the user edit their Trilogy query.
3077
-
3078
- CURRENT EDITOR CONTENTS:
3079
- \`\`\`trilogy
3080
- ${r}
3081
- \`\`\`
3082
- ${s}${c}
3083
- DATA CONNECTION: ${n}
3084
-
3085
- AVAILABLE TOOLS:
3086
- - validate_query: Check if a query is valid without running it (also returns available fields)
3087
- - run_query: Execute a query to see results (returns results privately to you)
3088
- - run_active_editor_query: Run the current editor query and show results in the main results pane
3089
- - get_artifact_rows: Fetch a row range from a truncated result artifact (use when results were cut off)
3090
- - format_query: Format/prettify a query
3091
- - edit_chart_config: Update chart visualization settings
3092
- - edit_editor: Write changes to the editor
3093
- - request_close: Request to close the session (user can reply with follow-ups)
3094
- - close_session: Immediately close (only after user confirms or says "done")
3095
- ${u}
3096
- TRILOGY LANGUAGE REFERENCE:
3097
- ${A}
3098
-
3099
- WORKFLOW:
3100
- 1. Understand what the user wants to change
3101
- 2. Use edit_editor to write your changes (this automatically validates and returns errors/available fields)
3102
- 3. Optionally use run_query to test and see results privately
3103
- 4. Use format_query to clean up formatting if needed
3104
- 5. Always call request_close with a summary when done — you MUST call a tool, never end with text only
3105
-
3106
- IMPORTANT:
3107
- - Always use edit_editor to write your final answer - don't just show the query in text
3108
- - edit_editor automatically validates, so you don't need to call validate_query separately after editing
3109
- - You can iterate: write → fix based on validation errors → write again
3110
- - You MUST always call a tool — never respond with text only. When done, call request_close.
3111
- - request_close gives the user a chance to ask for more changes before closing
3112
- - Only use close_session after the user explicitly confirms (e.g., says "done", "looks good", "close it")
3113
- - If the user selected specific text, use replaceSelection: true to only modify that part
3114
- - After updating chart configuration with edit_chart_config, use run_active_editor_query to show the user the updated visualization
3115
- - When the user asks to "run it", "see the results", "show me", or similar, use run_active_editor_query to execute and display in the main results pane`;
3116
- }
3117
- //#endregion
3118
- //#region chats/chat.ts
3119
- function V() {
3120
- return `art-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 6)}`;
3121
- }
3122
- function mn() {
3123
- return `Chat ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
3124
- }
3125
- var hn = class e {
3126
- id;
3127
- name;
3128
- dataConnectionName;
3129
- dataConnectionId;
3130
- llmConnectionName;
3131
- messages;
3132
- artifacts;
3133
- activeArtifactIndex;
3134
- imports;
3135
- createdAt;
3136
- updatedAt;
3137
- storage;
3138
- source;
3139
- sourceRefId;
3140
- kind;
3141
- parentChatId;
3142
- parentProjectId;
3143
- pendingInjections;
3144
- pendingContextNote;
3145
- lastContextTokens;
3146
- changed;
3147
- deleted;
3148
- constructor(e = {}) {
3149
- this.id = e.id || `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, this.name = e.name || mn(), this.dataConnectionName = e.dataConnectionName || "", this.dataConnectionId = e.dataConnectionId || "", this.llmConnectionName = e.llmConnectionName || "", this.messages = e.messages || [], this.artifacts = e.artifacts || [], this.activeArtifactIndex = e.activeArtifactIndex ?? -1, this.imports = e.imports || [], this.createdAt = e.createdAt || /* @__PURE__ */ new Date(), this.updatedAt = e.updatedAt || /* @__PURE__ */ new Date(), this.storage = e.storage || "local", this.source = e.source || "user", this.sourceRefId = e.sourceRefId ?? null, this.kind = e.kind || "user", this.parentChatId = e.parentChatId ?? null, this.parentProjectId = e.parentProjectId ?? null, this.pendingInjections = e.pendingInjections || [], this.pendingContextNote = e.pendingContextNote ?? null, this.lastContextTokens = e.lastContextTokens ?? void 0, this.changed = e.changed ?? !0, this.deleted = e.deleted ?? !1;
3150
- }
3151
- setName(e) {
3152
- this.name !== e && (this.name = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
3153
- }
3154
- setLLMConnection(e) {
3155
- this.llmConnectionName !== e && (this.llmConnectionName = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
3156
- }
3157
- setDataConnection(e, t = "") {
3158
- (this.dataConnectionName !== e || t && this.dataConnectionId !== t) && (this.dataConnectionName = e, this.dataConnectionId = t, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
3159
- }
3160
- addMessage(e) {
3161
- this.messages.push(e), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
3162
- }
3163
- addArtifact(e) {
3164
- return e.id ||= V(), this.artifacts.push(e), this.activeArtifactIndex = this.artifacts.length - 1, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, this.activeArtifactIndex;
3165
- }
3166
- getArtifactById(e) {
3167
- return this.artifacts.find((t) => t.id === e) || null;
3168
- }
3169
- updateArtifact(e, t) {
3170
- let n = this.artifacts.find((t) => t.id === e);
3171
- return n ? (t.data !== void 0 && (n.data = t.data), t.config !== void 0 && (n.config = {
3172
- ...n.config,
3173
- ...t.config
3174
- }), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0) : !1;
3175
- }
3176
- removeArtifact(e) {
3177
- let t = this.artifacts.findIndex((t) => t.id === e);
3178
- return t === -1 ? !1 : (this.artifacts.splice(t, 1), this.activeArtifactIndex >= this.artifacts.length && (this.activeArtifactIndex = this.artifacts.length - 1), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0);
3179
- }
3180
- hideArtifact(e) {
3181
- let t = this.artifacts.find((t) => t.id === e);
3182
- if (!t) return !1;
3183
- t.hidden = !0;
3184
- let n = this.artifacts.indexOf(t);
3185
- return this.activeArtifactIndex === n && (this.activeArtifactIndex = -1), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0;
3186
- }
3187
- unhideArtifact(e) {
3188
- let t = this.artifacts.find((t) => t.id === e);
3189
- return t ? (t.hidden = !1, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0) : !1;
3190
- }
3191
- setActiveArtifact(e) {
3192
- e >= -1 && e < this.artifacts.length && (this.activeArtifactIndex = e);
3193
- }
3194
- getActiveArtifact() {
3195
- return this.activeArtifactIndex >= 0 && this.activeArtifactIndex < this.artifacts.length ? this.artifacts[this.activeArtifactIndex] : null;
3196
- }
3197
- clearMessages() {
3198
- this.messages = [], this.artifacts = [], this.activeArtifactIndex = -1, this.lastContextTokens = void 0, this.name = mn(), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
3199
- }
3200
- addImport(e) {
3201
- return this.imports.some((t) => t.id === e.id) ? !1 : (this.imports.push(e), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0);
3202
- }
3203
- removeImport(e) {
3204
- let t = this.imports.findIndex((t) => t.id === e);
3205
- return t === -1 ? !1 : (this.imports.splice(t, 1), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0);
3206
- }
3207
- setImports(e) {
3208
- this.imports = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
3209
- }
3210
- getLastMessage() {
3211
- return this.messages.length > 0 ? this.messages[this.messages.length - 1] : null;
3212
- }
3213
- getVisibleMessages() {
3214
- return this.messages.filter((e) => !e.hidden);
3215
- }
3216
- getLLMMessages() {
3217
- return this.messages.filter((e) => !e.archived && !(e.artifact && !e.content && !e.toolCalls?.length));
3218
- }
3219
- serialize() {
3220
- return {
3221
- id: this.id,
3222
- name: this.name,
3223
- dataConnectionName: this.dataConnectionName,
3224
- dataConnectionId: this.dataConnectionId,
3225
- llmConnectionName: this.llmConnectionName,
3226
- messages: this.messages,
3227
- artifacts: this.artifacts,
3228
- activeArtifactIndex: this.activeArtifactIndex,
3229
- imports: this.imports,
3230
- createdAt: this.createdAt.toISOString(),
3231
- updatedAt: this.updatedAt.toISOString(),
3232
- storage: this.storage,
3233
- source: this.source,
3234
- sourceRefId: this.sourceRefId ?? null,
3235
- kind: this.kind,
3236
- parentChatId: this.parentChatId ?? null,
3237
- parentProjectId: this.parentProjectId ?? null,
3238
- pendingInjections: this.pendingInjections,
3239
- pendingContextNote: this.pendingContextNote ?? null,
3240
- lastContextTokens: this.lastContextTokens
3241
- };
3242
- }
3243
- static fromSerialized(t) {
3244
- return new e({
3245
- ...t,
3246
- createdAt: new Date(t.createdAt),
3247
- updatedAt: new Date(t.updatedAt),
3248
- changed: !1
3249
- });
3250
- }
3251
- }, gn = [
3252
- "line",
3253
- "area",
3254
- "bar",
3255
- "barh",
3256
- "point",
3257
- "heatmap"
3258
- ], _n = [
3259
- "geo-map",
3260
- "tree",
3261
- "donut",
3262
- "headline"
3263
- ], vn = [
3264
- "bar",
3265
- "barh",
3266
- "line",
3267
- "area",
3268
- "point",
3269
- "boxplot",
3270
- "donut",
3271
- "heatmap"
3272
- ], yn = [
3273
- {
3274
- id: "group-by",
3275
- label: "Group By",
3276
- field: "groupField",
3277
- columnFilter: "categorical",
3278
- allowEmpty: !1,
3279
- visibleFor: ["boxplot"],
3280
- filterGroup: "axes"
3281
- },
3282
- {
3283
- id: "category-axis",
3284
- label: "Category Axis",
3285
- field: "yField",
3286
- columnFilter: "categorical",
3287
- allowEmpty: !1,
3288
- visibleFor: ["barh", "donut"],
3289
- filterGroup: "axes"
3290
- },
3291
- {
3292
- id: "value-axis",
3293
- label: "Value Axis",
3294
- field: "xField",
3295
- columnFilter: "numeric",
3296
- allowEmpty: !1,
3297
- visibleFor: ["barh", "donut"],
3298
- filterGroup: "axes"
3299
- },
3300
- {
3301
- id: "x-axis",
3302
- label: "X Axis",
3303
- field: "xField",
3304
- columnFilter: "all",
3305
- allowEmpty: !1,
3306
- visibleFor: [
3307
- "bar",
3308
- "line",
3309
- "point",
3310
- "area",
3311
- "heatmap",
3312
- "beeswarm"
3313
- ],
3314
- filterGroup: "axes"
3315
- },
3316
- {
3317
- id: "y-axis-numeric",
3318
- label: "Y Axis",
3319
- field: "yField",
3320
- columnFilter: "numeric",
3321
- allowEmpty: !1,
3322
- visibleFor: [
3323
- "bar",
3324
- "line",
3325
- "area"
3326
- ],
3327
- filterGroup: "axes"
3328
- },
3329
- {
3330
- id: "y-axis-numeric-two",
3331
- label: "Secondary Y Axis",
3332
- field: "yField2",
3333
- columnFilter: "numeric",
3334
- allowEmpty: !0,
3335
- visibleFor: [
3336
- "bar",
3337
- "line",
3338
- "area"
3339
- ],
3340
- filterGroup: "axes"
3341
- },
3342
- {
3343
- id: "y-axis-all",
3344
- label: "Y Axis",
3345
- field: "yField",
3346
- columnFilter: "all",
3347
- allowEmpty: !1,
3348
- visibleFor: ["heatmap", "point"],
3349
- filterGroup: "axes"
3350
- },
3351
- {
3352
- id: "color-field",
3353
- label: "Color Scale",
3354
- field: "colorField",
3355
- columnFilter: "all",
3356
- allowEmpty: !0,
3357
- visibleFor: ["heatmap", "geo-map"],
3358
- filterGroup: "appearance"
3359
- },
3360
- {
3361
- id: "color-by",
3362
- label: "Color By",
3363
- field: "colorField",
3364
- columnFilter: "all",
3365
- allowEmpty: !0,
3366
- visibleFor: [
3367
- "bar",
3368
- "barh",
3369
- "point",
3370
- "donut"
3371
- ],
3372
- filterGroup: "appearance"
3373
- },
3374
- {
3375
- id: "color-by",
3376
- label: "Color By",
3377
- field: "colorField",
3378
- columnFilter: "categorical",
3379
- allowEmpty: !0,
3380
- visibleFor: [
3381
- "line",
3382
- "area",
3383
- "beeswarm"
3384
- ],
3385
- filterGroup: "appearance"
3386
- },
3387
- {
3388
- id: "size",
3389
- label: "Size",
3390
- field: "sizeField",
3391
- columnFilter: "numeric",
3392
- allowEmpty: !0,
3393
- visibleFor: [
3394
- "point",
3395
- "geo-map",
3396
- "tree",
3397
- "beeswarm"
3398
- ],
3399
- filterGroup: "appearance"
3400
- },
3401
- {
3402
- id: "trellis-field",
3403
- label: "Split Columns By",
3404
- field: "trellisField",
3405
- columnFilter: "categorical",
3406
- allowEmpty: !0,
3407
- visibleFor: gn,
3408
- filterGroup: "appearance"
3409
- },
3410
- {
3411
- id: "trellis-row-field",
3412
- label: "Split Rows By",
3413
- field: "trellisRowField",
3414
- columnFilter: "categorical",
3415
- allowEmpty: !0,
3416
- visibleFor: gn,
3417
- filterGroup: "appearance"
3418
- },
3419
- {
3420
- id: "geo-field",
3421
- label: "Geo Field",
3422
- field: "geoField",
3423
- visibleFor: ["geo-map"],
3424
- allowEmpty: !0,
3425
- columnFilter: "categorical",
3426
- filterGroup: "axes"
3427
- },
3428
- {
3429
- id: "latitude",
3430
- label: "Latitude",
3431
- field: "yField",
3432
- visibleFor: ["geo-map"],
3433
- allowEmpty: !0,
3434
- columnFilter: "latitude",
3435
- filterGroup: "axes"
3436
- },
3437
- {
3438
- id: "longitude",
3439
- label: "Longtitude",
3440
- field: "xField",
3441
- visibleFor: ["geo-map"],
3442
- allowEmpty: !0,
3443
- columnFilter: "longitude",
3444
- filterGroup: "axes"
3445
- },
3446
- {
3447
- id: "tree",
3448
- label: "Split Field",
3449
- field: "xField",
3450
- visibleFor: ["tree"],
3451
- allowEmpty: !1,
3452
- columnFilter: "categorical",
3453
- filterGroup: "axes"
3454
- },
3455
- {
3456
- id: "annotation",
3457
- label: "Annotation",
3458
- field: "annotationField",
3459
- columnFilter: "all",
3460
- allowEmpty: !0,
3461
- visibleFor: [
3462
- "geo-map",
3463
- "point",
3464
- "heatmap",
3465
- "beeswarm",
3466
- "line"
3467
- ],
3468
- filterGroup: "appearance"
3469
- },
3470
- {
3471
- id: "scale-x",
3472
- label: "X Axis Scale",
3473
- field: "scaleX",
3474
- columnFilter: "numeric",
3475
- allowEmpty: !0,
3476
- visibleFor: ["point", "barh"],
3477
- options: ["linear", "log"],
3478
- filterGroup: "appearance"
3479
- },
3480
- {
3481
- id: "scale-y",
3482
- label: "Y Axis Scale",
3483
- field: "scaleY",
3484
- columnFilter: "numeric",
3485
- allowEmpty: !0,
3486
- visibleFor: [
3487
- "point",
3488
- "area",
3489
- "line"
3490
- ],
3491
- options: ["linear", "log"],
3492
- filterGroup: "appearance"
3493
- },
3494
- {
3495
- id: "hide-legend",
3496
- label: "Hide Legend",
3497
- field: "hideLegend",
3498
- columnFilter: "all",
3499
- allowEmpty: !0,
3500
- visibleFor: [
3501
- "geo-map",
3502
- "bar",
3503
- "barh",
3504
- "line",
3505
- "point",
3506
- "area",
3507
- "heatmap"
3508
- ],
3509
- filterGroup: "appearance"
3510
- },
3511
- {
3512
- id: "hide-label",
3513
- label: "Hide Label",
3514
- field: "hideLegend",
3515
- columnFilter: "all",
3516
- allowEmpty: !0,
3517
- visibleFor: ["headline"],
3518
- filterGroup: "appearance"
3519
- },
3520
- {
3521
- id: "linky2",
3522
- label: "Link Secondary Y Axis",
3523
- field: "linkY2",
3524
- columnFilter: "all",
3525
- allowEmpty: !0,
3526
- visibleFor: ["bar", "line"],
3527
- filterGroup: "appearance"
3528
- },
3529
- {
3530
- id: "show-title",
3531
- label: "Show Title",
3532
- field: "showTitle",
3533
- columnFilter: "all",
3534
- allowEmpty: !0,
3535
- visibleFor: [
3536
- "geo-map",
3537
- "bar",
3538
- "barh",
3539
- "line",
3540
- "point",
3541
- "area",
3542
- "headline",
3543
- "beeswarm"
3544
- ],
3545
- filterGroup: "appearance"
3546
- }
3547
- ], bn = [
3548
- {
3549
- value: "bar",
3550
- label: "Bar Chart",
3551
- icon: "mdi mdi-chart-bar"
3552
- },
3553
- {
3554
- value: "barh",
3555
- label: "Horizontal Bar",
3556
- icon: "mdi mdi-chart-timeline"
3557
- },
3558
- {
3559
- value: "line",
3560
- label: "Line Chart",
3561
- icon: "mdi mdi-chart-line"
3562
- },
3563
- {
3564
- value: "point",
3565
- label: "Scatter Plot",
3566
- icon: "mdi mdi-chart-scatter-plot"
3567
- },
3568
- {
3569
- value: "area",
3570
- label: "Area Chart",
3571
- icon: "mdi mdi-chart-areaspline"
3572
- },
3573
- {
3574
- value: "heatmap",
3575
- label: "Heatmap",
3576
- icon: "mdi mdi-sun-thermometer-outline"
3577
- },
3578
- {
3579
- value: "boxplot",
3580
- label: "Box Plot",
3581
- icon: "mdi mdi-chart-box"
3582
- },
3583
- {
3584
- value: "geo-map",
3585
- label: "Map",
3586
- icon: "mdi mdi-map"
3587
- },
3588
- {
3589
- value: "headline",
3590
- label: "Headline",
3591
- icon: "mdi mdi-numeric-1-box-outline"
3592
- },
3593
- {
3594
- value: "tree",
3595
- label: "Tree",
3596
- icon: "mdi mdi-tree"
3597
- },
3598
- {
3599
- value: "donut",
3600
- label: "Donut",
3601
- icon: "mdi mdi-chart-donut"
3602
- },
3603
- {
3604
- value: "beeswarm",
3605
- label: "Beeswarm",
3606
- icon: "mdi mdi-chart-bubble"
3607
- }
3608
- ], xn = "#1f77b4", Sn = "#9467bd", Cn = "#FF7F7F", H = (e, t) => e[t], wn = [
3609
- "year",
3610
- "week",
3611
- "day",
3612
- "hour",
3613
- "minute",
3614
- "second",
3615
- "quarter"
3616
- ], Tn = [
3617
- "decade",
3618
- "year",
3619
- "month",
3620
- "week",
3621
- "day",
3622
- "hour",
3623
- "minute",
3624
- "second",
3625
- "day_of_week"
3626
- ], U = (e) => e.traits ? e.traits.some((e) => Tn.some((t) => e === t || e.endsWith("_" + t))) : !1, En = (e, t) => {
3627
- let n = t.get(e);
3628
- return n ? U(n) : !1;
3629
- }, Dn = [
3630
- "rank",
3631
- "index",
3632
- "grade",
3633
- "flag",
3634
- "letter_grade"
3635
- ], On = [
3636
- ...wn,
3637
- ...Dn,
3638
- "identifier",
3639
- "decade"
3640
- ];
3641
- function kn(e) {
3642
- return new Date(e);
3643
- }
3644
- var W = (e, t, n) => {
3645
- if (!e || !t.get(e)) return !1;
3646
- let r = t.get(e);
3647
- return r ? An(r, n) : !1;
3648
- }, An = (e, t) => !!(e.traits && e.traits.some((e) => e.endsWith(t))), G = (e) => e.purpose === "key" || U(e) ? !1 : [
3649
- n.NUMBER,
3650
- n.INTEGER,
3651
- n.FLOAT
3652
- ].includes(e.type) && !e.traits?.some((e) => e.endsWith("latitude") || e.endsWith("longitude")), jn = (e) => !!(e.traits && Dn.some((t) => e.traits?.includes(t))), Mn = (e) => !!([n.STRING].includes(e.type) && e.traits?.some((e) => e.endsWith("url_image"))), K = (e) => !!([
3653
- n.DATE,
3654
- n.DATETIME,
3655
- n.TIME,
3656
- n.TIMESTAMP
3657
- ].includes(e.type) || e.traits && wn.some((t) => e.traits?.includes(t))), Nn = (e) => !!([n.STRING, n.BOOLEAN].includes(e.type) || e.purpose === "key" || e.traits && On.some((t) => e.traits?.includes(t))), Pn = (e) => e.traits?.includes("hex") ?? !1, q = (e, t) => e.traits ? e.traits.some((e) => e.endsWith(t)) : !1, J = (e, t) => {
3658
- let n = [];
3659
- return t.forEach((t, r) => {
3660
- (e === "all" || e === "numeric" && G(t) && !K(t) || e === "categorical" && Nn(t) || e === "temporal" && K(t) || e === "latitude" && q(t, "latitude") || e === "longitude" && q(t, "longitude") || e === "geographic" && t.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country"))) && n.push(t);
3661
- }), n;
3662
- }, Fn = (e, t) => {
3663
- let n = t.filter((t) => t.name !== e.xField && t.name !== e.yField);
3664
- return n.length > 0 ? n[0] : t[0];
3665
- }, In = (e, t, n) => {
3666
- let r = {
3667
- showTitle: !1,
3668
- hideLegend: !1
3669
- }, i = J("numeric", t), a = J("categorical", t).filter((e) => !Pn(e)), o = J("temporal", t), s = J("latitude", t), c = J("longitude", t), l = J("geographic", t);
3670
- if (t.size === 0) return {};
3671
- if (!n && i.length === 0 && l.length === 0 && c.length === 0 && s.length === 0) return r.chartType = "headline", r;
3672
- let u = a.filter((e) => U(e));
3673
- if (n) r.chartType = n;
3674
- else if (c.length > 0 && s.length > 0) r.chartType = "geo-map";
3675
- else if (l.length > 0) r.chartType = "geo-map";
3676
- else if (o.length > 0 && i.length > 0) r.chartType = "line";
3677
- else if (u.length > 0 && i.length > 0) r.chartType = "bar";
3678
- else if (a.length > 0 && i.length > 0) {
3679
- let t = a[0].name, n = /* @__PURE__ */ new Set();
3680
- for (let r = 0; r < Math.min(e.length, 50); r++) e[r] && e[r][t] !== void 0 && n.add(e[r][t]);
3681
- n.size > 7 ? r.chartType = "barh" : r.chartType = "bar";
3682
- } else i.length >= 2 ? e.length === 1 ? r.chartType = "headline" : r.chartType = "point" : a.length >= 2 && i.length > 0 ? r.chartType = "heatmap" : i.length > 0 && a.length == 0 && (r.chartType = "headline");
3683
- if (r.chartType === "barh") {
3684
- r.yField = H(a, 0)?.name, i.length > 0 && (r.xField = Fn(r, i).name);
3685
- let e = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3686
- e.length > 0 && (r.colorField = e[0].name), i.length > 1 && !r.colorField && (r.colorField = i[1].name);
3687
- let t = a.filter((e) => e.name !== r.yField && e.name !== r.xField && e.name !== r.colorField);
3688
- t.length > 0 && (r.trellisRowField = t[0].name), t.length > 1 && (r.trellisField = t[1].name);
3689
- } else if (r.chartType === "donut") {
3690
- r.yField = H(a, 0)?.name, r.xField = H(i, 0)?.name;
3691
- let e = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3692
- e.length > 0 && (r.colorField = e[0].name), i.length > 1 && !r.colorField && (r.colorField = i[1].name);
3693
- } else if (r.chartType === "point") {
3694
- r.xField = H(i, 0)?.name, r.yField = H(i, 1)?.name;
3695
- let e = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3696
- e.length > 0 && (r.colorField = e[0].name);
3697
- let t = i.filter((e) => e.name !== r.yField && e.name !== r.xField);
3698
- t.length > 0 && (r.sizeField = t[0].name), !r.colorField && t.length > 1 && (r.colorField = t[1].name);
3699
- let n = a.filter((e) => e.name !== r.yField && e.name !== r.xField && e.name !== r.colorField);
3700
- n.length > 0 && (r.trellisRowField = n[0].name), n.length > 1 && (r.trellisField = n[1].name);
3701
- } else if (r.chartType === "heatmap") {
3702
- r.xField = H(a, 0)?.name, r.yField = H(a, 1)?.name, r.colorField = H(i, 0)?.name;
3703
- let e = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3704
- e.length > 0 && (r.trellisRowField = e[0].name), e.length > 1 && (r.trellisField = e[1].name);
3705
- } else if (r.chartType === "bar") {
3706
- r.xField = H(u, 0)?.name ?? H(a, 0)?.name;
3707
- let e = i.filter((e) => !K(e) && e.name !== r.xField);
3708
- r.yField = e.length > 0 ? e[0].name : H(i, 0)?.name;
3709
- let t = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3710
- t.length > 0 && (r.colorField = t[0].name), i.length > 1 && !r.colorField && (r.colorField = i[1].name);
3711
- let n = a.filter((e) => e.name !== r.yField && e.name !== r.xField && e.name !== r.colorField);
3712
- n.length > 0 && (r.trellisRowField = n[0].name), n.length > 1 && (r.trellisField = n[1].name);
3713
- } else if (r.chartType === "headline") r.xField = H(i, 0)?.name;
3714
- else if (["line", "area"].includes(r.chartType || "")) {
3715
- let e = H(o, 0) ?? H(u, 0) ?? null;
3716
- r.xField = e?.name;
3717
- let t = i.filter((t) => t.name !== e?.name);
3718
- r.yField = t.length > 0 ? t[0].name : H(i, 0)?.name, r.yField2 = t.length > 1 ? t[1].name : void 0;
3719
- let n = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
3720
- n.length > 0 && (r.colorField = n[0].name);
3721
- let s = a.filter((e) => e.name !== r.yField && e.name !== r.xField && e.name !== r.colorField);
3722
- s.length > 0 && (r.trellisRowField = s[0].name), s.length > 1 && (r.trellisField = s[1].name);
3723
- } else if (r.chartType === "geo-map") {
3724
- let e = !1;
3725
- s.length > 0 && c.length > 0 && (e = !0, r.yField = s[0].name, r.xField = c[0].name, i.length > 0 && (r.sizeField = i[0].name), i.length > 1 && (r.colorField = i[1].name)), l.length > 0 && (r.geoField = l[0].name, i.length > 0 && !e && (r.colorField = i[0].name));
3726
- let t = a.filter((e) => e.name !== r.colorField && e.name !== r.xField && e.name !== r.yField && e.name !== r.geoField);
3727
- t.length > 0 && !r.annotationField && (r.annotationField = t[0].name);
3728
- let n = a.filter((e) => e.name !== r.geoField && e.name !== r.xField && e.name !== r.yField && e.name !== r.colorField && e.name !== r.annotationField);
3729
- n.length > 0 && (r.trellisRowField = n[0].name), n.length > 1 && (r.trellisField = n[1].name);
3730
- }
3731
- return r;
3732
- }, Ln = (e, t) => {
3733
- let n = J("numeric", t), r = J("categorical", t), i = J("temporal", t), a = J("latitude", t), o = J("longitude", t), s = J("geographic", t), c = [];
3734
- if (r.length > 0 && c.push("beeswarm"), n.length === 0) return (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.push("headline"), c;
3735
- let l = r.filter((e) => U(e));
3736
- if ((i.length > 0 || l.length > 0) && n.length > 0 && (c.push("line"), c.push("area")), r.length > 0 && n.length > 0) {
3737
- let t = r[0].name, n = /* @__PURE__ */ new Set();
3738
- for (let r = 0; r < Math.min(e.length, 50); r++) e[r] && e[r][t] !== void 0 && n.add(e[r][t]);
3739
- c.push("bar"), c.push("barh"), c.push("donut");
3740
- }
3741
- return n.length >= 2 && c.push("point"), r.length >= 2 && n.length > 0 && c.push("heatmap"), n.length > 0 && (c.push("boxplot"), c.push("headline")), (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.filter((e) => bn.map((e) => e.value).includes(e));
3742
- }, Rn = (e) => q(e, "latitude") ? "latitude" : q(e, "longitude") ? "longitude" : e.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country")) ? "geographic" : K(e) ? "temporal" : U(e) ? "categorical" : G(e) ? "numeric" : Nn(e) ? "categorical" : "other", zn = (e) => {
3743
- let t = [];
3744
- return e.forEach((e) => {
3745
- t.push(`${e.name} (${Rn(e)})`);
3746
- }), t.length > 0 ? t.join(", ") : "no fields";
3747
- }, Bn = (e, n, r) => {
3748
- let i = Ln(e, n), a = [];
3749
- if (r.layers && r.layers.length > 0) {
3750
- let e = [], a = Array.from(n.keys());
3751
- return r.layers.forEach((r, i) => {
3752
- let o = `Layer ${i + 1}`;
3753
- vn.includes(r.chartType) || e.push(`${o}: chart type "${r.chartType}" cannot be used as a layer. Layerable types: ${vn.join(", ")}.`);
3754
- for (let i of t) {
3755
- let t = r[i];
3756
- typeof t == "string" && t !== "" && !n.has(t) && e.push(`${o}: field "${t}" (${i}) does not exist in the query results. Available columns: ${a.join(", ")}.`);
3757
- }
3758
- !r.xField && !r.yField && e.push(`${o}: needs at least one of xField or yField.`);
3759
- }), (r.trellisField || r.trellisRowField) && e.push("Trellis roles cannot be combined with multiple layers or reference lines."), {
3760
- valid: e.length === 0,
3761
- fieldErrors: e,
3762
- eligibleChartTypes: i,
3763
- suggestedConfig: r
3764
- };
3765
- }
3766
- if (!i.includes(r.chartType)) {
3767
- let t = zn(n);
3768
- return {
3769
- valid: !1,
3770
- chartTypeError: `Chart type "${r.chartType}" is not compatible with the available fields (${t}). Eligible chart types: ${i.join(", ")}.`,
3771
- fieldErrors: [],
3772
- eligibleChartTypes: i,
3773
- suggestedConfig: In(e, n)
3774
- };
3775
- }
3776
- let o = In(e, n, [
3777
- "line",
3778
- "bar",
3779
- "barh",
3780
- "point",
3781
- "geo-map",
3782
- "tree",
3783
- "area",
3784
- "heatmap",
3785
- "headline",
3786
- "donut"
3787
- ].includes(r.chartType) ? r.chartType : void 0), s = Array.from(n.keys());
3788
- for (let e of [
3789
- "xField",
3790
- "yField",
3791
- "yField2",
3792
- "colorField",
3793
- "sizeField",
3794
- "groupField",
3795
- "trellisField",
3796
- "trellisRowField",
3797
- "geoField",
3798
- "annotationField"
3799
- ]) {
3800
- let t = r[e];
3801
- typeof t == "string" && t !== "" && !n.has(t) && a.push(`Field "${t}" (${e}) does not exist in the query results. Available columns: ${s.join(", ")}.`);
3802
- }
3803
- let c = yn.filter((e) => !e.allowEmpty && e.visibleFor.includes(r.chartType));
3804
- for (let e of c) {
3805
- let t = r[e.field];
3806
- (typeof t != "string" || t === "") && a.push(`Chart type "${r.chartType}" requires ${e.label} (${String(e.field)}) to be specified, but it is missing.`);
3807
- }
3808
- if (r.chartType === "geo-map") {
3809
- let e = J("latitude", n), t = J("longitude", n), i = J("geographic", n), o = e.length > 0 && t.length > 0, c = i.length > 0;
3810
- !o && !c && a.push(`geo-map requires geographic columns (latitude/longitude pairs or state/country codes) but none were found. Available columns: ${s.join(", ")}. Consider using a different chart type or adjusting the query to include geographic fields.`), !r.geoField && !c && (r.xField && t.length > 0 && !t.some((e) => e.name === r.xField) && a.push(`For geo-map, xField should be a longitude column. "${r.xField}" is not a longitude field. Available longitude columns: ${t.map((e) => e.name).join(", ") || "none"}.`), r.yField && e.length > 0 && !e.some((e) => e.name === r.yField) && a.push(`For geo-map, yField should be a latitude column. "${r.yField}" is not a latitude field. Available latitude columns: ${e.map((e) => e.name).join(", ") || "none"}.`));
3811
- }
3812
- if ((r.chartType === "line" || r.chartType === "area") && r.xField && n.has(r.xField)) {
3813
- let e = n.get(r.xField);
3814
- !K(e) && !Nn(e) && !G(e) && a.push(`For ${r.chartType} charts, xField should typically be a temporal or categorical column.`);
3815
- }
3816
- let l = { ...o };
3817
- for (let [e, t] of Object.entries(r)) t != null && (typeof t == "string" && t === "" || (l[e] = t));
3818
- return {
3819
- valid: a.length === 0,
3820
- fieldErrors: a,
3821
- eligibleChartTypes: i,
3822
- suggestedConfig: l
3823
- };
3824
- }, Vn = (e) => `Chart configuration is invalid for the data:\n${[e.chartTypeError, ...e.fieldErrors].filter(Boolean).map((e) => ` - ${e}`).join("\n")}\nEligible chart types for this data: ${e.eligibleChartTypes.join(", ") || "none"}.\nSuggested auto-detected config: ${JSON.stringify(e.suggestedConfig)}\nTip: Omit chartConfig to let auto-detection choose the best chart type and field mapping.`, Y = (e, t) => {
3825
- if (!e || !t.get(e)) return {};
3826
- let r = t.get(e);
3827
- if (!r) return {};
3828
- if (W(e, t, "usd")) return { format: "$,.2f" };
3829
- if (W(e, t, "percent")) return { format: ".1%" };
3830
- if (W(e, t, "year")) return {
3831
- labelAngle: -45,
3832
- format: "%Y",
3833
- timeUnit: "year"
3834
- };
3835
- if (W(e, t, "month") || W(e, t, "day_of_week")) return { format: "d" };
3836
- switch (r.type) {
3837
- case n.DATE: return {
3838
- timeUnit: "yearmonthdate",
3839
- labelAngle: -45
3840
- };
3841
- case n.TIME: return {
3842
- timeUnit: "hoursminutesseconds",
3843
- labelAngle: -45
3844
- };
3845
- case n.DATETIME: return { timeUnit: "yearmonthdate-hours" };
3846
- case n.INTEGER: return {};
3847
- default: return {};
3848
- }
3849
- }, Hn = (e, t, n = null) => {
3850
- if (!e || !t.get(e)) return {};
3851
- let r = t.get(e);
3852
- return r ? W(e, t, "day_of_week_name") ? { sort: [
3853
- "Sunday",
3854
- "Monday",
3855
- "Tuesday",
3856
- "Wednesday",
3857
- "Thursday",
3858
- "Friday",
3859
- "Saturday"
3860
- ] } : jn(r) || U(r) || K(r) ? { sort: {
3861
- field: e,
3862
- order: "ascending"
3863
- } } : G(r) ? n ? { sort: {
3864
- field: n,
3865
- op: "sum",
3866
- order: "descending"
3867
- } } : { sort: {
3868
- field: e,
3869
- order: "descending"
3870
- } } : n ? { sort: {
3871
- field: n,
3872
- op: "sum",
3873
- order: "descending"
3874
- } } : { sort: {
3875
- field: e,
3876
- order: "ascending"
3877
- } } : {};
3878
- }, X = (e, t) => {
3879
- if (!e || !t.get(e)) return "nominal";
3880
- let r = t.get(e);
3881
- return r ? K(r) ? [
3882
- n.DATETIME,
3883
- n.TIMESTAMP,
3884
- n.INTEGER
3885
- ].includes(r.type) || [n.DATE].includes(r.type) ? "temporal" : "ordinal" : jn(r) || U(r) ? "ordinal" : G(r) ? "quantitative" : "nominal" : "nominal";
3886
- }, Un = (e, t, n = {}, i = !0, a = {}) => e ? {
3887
- field: e,
3888
- type: X(e, t),
3889
- title: r(t.get(e)?.description || e),
3890
- ...Y(e, t),
3891
- ...n,
3892
- ...i ? Hn(e, t) : {},
3893
- ...a.scale !== void 0 || a.zero !== void 0 ? { scale: {
3894
- ...a.scale !== void 0 && { type: a.scale },
3895
- ...a.zero !== void 0 && { zero: a.zero }
3896
- } } : {}
3897
- } : {}, Wn = (e = 0) => e > 0 ? `_l${e}` : "", Z = (e, t = "") => `${e}${t}`, Gn = (e = "") => ({
3898
- fillOpacity: {
3899
- condition: {
3900
- param: Z("select", e),
3901
- value: 1
3902
- },
3903
- value: .3
3904
- },
3905
- strokeWidth: {
3906
- condition: [{
3907
- param: Z("select", e),
3908
- empty: !1,
3909
- value: 2
3910
- }, {
3911
- param: Z("highlight", e),
3912
- empty: !1,
3913
- value: 1
3914
- }],
3915
- value: 0
3916
- }
3917
- }), Kn = (e, t, n, r = "color") => {
3918
- let i = !1;
3919
- return n === "quantitative" && r === "color" && (i = !0), e && e.length > 10 ? {
3920
- orient: t ? "bottom" : "right",
3921
- titleOrient: t ? "center" : i ? "right" : "top",
3922
- direction: t ? "horizontal" : "vertical",
3923
- titleFontSize: 10
3924
- } : {
3925
- orient: t ? "bottom" : "right",
3926
- titleOrient: t ? "bottom" : "top",
3927
- direction: t ? "horizontal" : "vertical",
3928
- titleFontSize: t ? 10 : 12
3929
- };
3930
- }, qn = 15, Jn = (e, t, n, i = !1, a = "light", o = !1, s = [], c = "") => {
3931
- let l = { tickCount: qn }, u = [], d = [], f = s || [], p = Array.from(n.entries()).filter(([e, t]) => t.traits?.includes("hex")).map(([e, t]) => e);
3932
- if (t) {
3933
- let e = X(t, n);
3934
- l = {
3935
- ...l,
3936
- ...Kn(t, i, e)
3937
- }, (e === "nominal" || e === "ordinal") && (d = Array.from(new Set(f.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), u = d.slice(0, qn);
3938
- }
3939
- let m = (e) => (!o && Object.keys(l).length > 0 ? e.legend = l : o && (e.legend = null), e);
3940
- if (t && n.get(t)) {
3941
- let e = X(t, n);
3942
- l = {
3943
- ...l,
3944
- ...Y(t, n),
3945
- ...(e === "nominal" || e === "ordinal") && u.length > 0 ? { values: u } : {}
3946
- };
3947
- let i = e === "quantitative" ? { scheme: a === "light" ? "viridis" : "plasma" } : { scheme: a === "light" ? "category20c" : "plasma" };
3948
- if (p.length > 0) {
3949
- let e = p[0], n = d.map((n) => {
3950
- let r = f.find((r) => String(r[t]) === n && r[e] != null);
3951
- return r ? String(r[e]) : "#999999";
3952
- });
3953
- i = {
3954
- domain: d,
3955
- range: n
3956
- };
3957
- }
3958
- return m({
3959
- field: t,
3960
- type: e,
3961
- title: r(n.get(t)?.description || t),
3962
- scale: i,
3963
- ...c === null ? {} : { condition: [{
3964
- param: Z("highlight", c),
3965
- empty: !1,
3966
- value: Cn
3967
- }, {
3968
- param: Z("select", c),
3969
- empty: !1,
3970
- value: Cn
3971
- }] },
3972
- ...Y(t, n)
3973
- });
3974
- }
3975
- return m({});
3976
- }, Yn = (e, t, n = !1, i = !1) => {
3977
- let a = { tickCount: 5 };
3978
- if (e) {
3979
- let r = X(e, t);
3980
- a = {
3981
- ...a,
3982
- ...Kn(e, n, r, "size")
3983
- };
3984
- }
3985
- return e && t.get(e) ? ((e) => (!i && Object.keys(a).length > 0 ? e.legend = a : i && (e.legend = null), e))({
3986
- scale: {
3987
- rangeMin: 30,
3988
- nice: !0,
3989
- type: "linear"
3990
- },
3991
- field: e,
3992
- title: r(t.get(e)?.description || e)
3993
- }) : {};
3994
- };
3995
- //#endregion
3996
- //#region llm/importConcepts.ts
3997
- async function Xn(e, t, n) {
3998
- let { connectionStore: r, editorStore: i, queryExecutionService: a } = e;
3999
- try {
4000
- if (!i?.editors[t.id]?.contents) return "No fields found in this data source.";
4001
- let e = r.connections[n] || r.connectionByName(n), o = e?.id || n, s = i && e ? Object.values(i.editors).filter((t) => !t.deleted && t.connectionId === e.id).map((e) => ({
4002
- alias: e.name,
4003
- contents: e.contents
4004
- })) : [], c = r.getConnectionSources(o), l = /* @__PURE__ */ new Map();
4005
- c.forEach((e) => l.set(e.alias, e.contents)), s.forEach((e) => l.set(e.alias, e.contents));
4006
- let u = Array.from(l.entries()).map(([e, t]) => ({
4007
- alias: e,
4008
- contents: t
4009
- })), d = {
4010
- text: "SELECT 1",
4011
- editorType: "trilogy",
4012
- imports: [{
4013
- name: t.name,
4014
- alias: t.alias || ""
4015
- }],
4016
- extraContent: u
4017
- }, f = await a.validateQuery(o, d, !1);
4018
- if (f?.data?.completion_items) {
4019
- let e = f.data.completion_items.filter((e) => e.trilogyType === "concept");
4020
- if (e.length === 0) return "No fields found in this data source.";
4021
- let t = e.map((e) => {
4022
- let t = `- ${e.label} (${e.datatype || e.type})`;
4023
- return e.description && (t += `: ${e.description}`), e.calculation && (t += ` [calculated: ${e.calculation}]`), t;
4024
- }).join("\n");
4025
- return `AVAILABLE FIELDS (${e.length} total):\n${t}`;
4026
- }
4027
- return "Unable to fetch field information for this data source.";
4028
- } catch (e) {
4029
- return console.error("Failed to fetch concepts for import:", e), "Error fetching field information for this data source.";
4030
- }
4031
- }
4032
- //#endregion
4033
- //#region llm/sharedToolHelpers.ts
4034
- function Q(e) {
4035
- return Object.values(e.connections).map((e) => e.name).join(", ");
4036
- }
4037
- function $(e, t) {
4038
- return e.connections[t] || e.connectionByName(t) || null;
4039
- }
4040
- async function Zn(e, t, n) {
4041
- if (!t || typeof t != "string") return {
4042
- success: !1,
4043
- error: `Connection name is required. Available connections: ${Q(e) || "None"}`
4044
- };
4045
- let r = $(e, t);
4046
- if (!r) return {
4047
- success: !1,
4048
- error: `Connection "${t}" not found. Available connections: ${Q(e) || "None"}`
4049
- };
4050
- if (r.connected) return n?.onConnected?.(r.name, r.id), {
4051
- success: !0,
4052
- message: `Connection "${r.name}" is already active.`
4053
- };
4054
- try {
4055
- return await e.connectConnection(r.id), n?.onConnected?.(r.name, r.id), {
4056
- success: !0,
4057
- message: `Successfully connected to "${r.name}".`,
4058
- triggersSymbolRefresh: !0
4059
- };
4060
- } catch (e) {
4061
- return {
4062
- success: !1,
4063
- error: `Failed to connect to "${r.name}": ${e instanceof Error ? e.message : "Unknown error"}`
4064
- };
4065
- }
4066
- }
4067
- function Qn(e, t, n, r) {
4068
- let i = /* @__PURE__ */ new Map(), a = $(e, n), o = a?.id || n;
4069
- return e.getConnectionSources(o).forEach((e) => i.set(e.alias, e.contents)), t && a && Object.values(t.editors).filter((e) => !e.deleted && e.connectionId === a.id).forEach((e) => {
4070
- i.set(e.name, e.contents);
4071
- }), r && r.forEach((e) => {
4072
- let n = e.alias || e.name, r = t?.editors[e.id]?.contents || "";
4073
- r && i.set(n, r);
4074
- }), Array.from(i.entries()).map(([e, t]) => ({
4075
- alias: e,
4076
- contents: t
4077
- }));
4078
- }
4079
- function $n(e, t, n, r) {
4080
- let i = typeof t?.toJSON == "function" ? t.toJSON() : t, a = i?.data;
4081
- if (!Array.isArray(a)) return {
4082
- success: !1,
4083
- error: `Artifact "${e}" has no row data.`
4084
- };
4085
- let o = Number(n), s = Number(r);
4086
- if (!Number.isFinite(o) || !Number.isFinite(s)) return {
4087
- success: !1,
4088
- error: "start_row and end_row must be numbers (0-indexed, inclusive)."
4089
- };
4090
- if (a.length === 0) return {
4091
- success: !1,
4092
- error: `Artifact "${e}" has 0 rows.`
4093
- };
4094
- let c = a.length, l = Math.max(0, Math.min(o, c - 1)), u = Math.max(l, Math.min(s, c - 1)), d = a.slice(l, u + 1);
4095
- return {
4096
- success: !0,
4097
- message: `Rows ${l}-${u} of ${c} total from artifact "${e}":\n` + JSON.stringify({
4098
- headers: i.headers,
4099
- data: d
4100
- }, null, 2)
4101
- };
4102
- }
4103
- function er(e, t, n) {
4104
- if (!e) return [];
4105
- let r = n ? $(n, t) : null;
4106
- return Object.values(e.editors).filter((e) => e.deleted ? !1 : r ? e.connectionId === r.id : e.connection === t).sort((e, t) => e.name.localeCompare(t.name)).map((e) => ({
4107
- id: e.id,
4108
- name: e.name.replace(/\//g, "."),
4109
- alias: ""
4110
- }));
4111
- }
4112
- async function tr(e, t, n, r, i) {
4113
- let a = t.getConnectionName(), o = i && a ? $(i, a) : null;
4114
- if (!e || e.trim() === "") return t.setActiveImports([]), {
4115
- success: !0,
4116
- message: "Cleared active data source selection.",
4117
- triggersSymbolRefresh: !0
4118
- };
4119
- if (!a) return {
4120
- success: !1,
4121
- error: "No data connection selected. Please select a data connection first."
4122
- };
4123
- let s = er(r, o?.id || a, i), c = s.find((t) => t.name === e || t.name.endsWith(`.${e}`));
4124
- if (!c) return {
4125
- success: !1,
4126
- error: `Import "${e}" not found. Available imports: ${s.map((e) => e.name).join(", ") || "none"}`
4127
- };
4128
- let l = t.getActiveImports(), u = l.length === 1 && l[0].id === c.id;
4129
- u || t.setActiveImports([c]);
4130
- let d = await Xn(n, c, o?.id || a);
4131
- return {
4132
- success: !0,
4133
- message: `${u ? `"${c.name}" is already the active data source.` : `Successfully selected "${c.name}" as the active data source.`}\n\n${d}`,
4134
- triggersSymbolRefresh: !u
4135
- };
4136
- }
4137
- function nr(e, t, n) {
4138
- let r = e.getConnectionName(), i = n && r ? $(n, r) : null, a = i?.name || r;
4139
- if (!r) return {
4140
- success: !1,
4141
- error: "No data connection selected. Please select a data connection first."
4142
- };
4143
- let o = er(t, i?.id || r, n), s = e.getActiveImports(), c = s.length > 0 ? s[0] : null;
4144
- return o.length === 0 ? {
4145
- success: !0,
4146
- message: `No data sources available on connection "${a}".`
4147
- } : {
4148
- success: !0,
4149
- message: `${c ? `Current active data source: ${c.name}\n\n` : "No data source currently selected.\n\n"}Available data sources for connection "${a}":\n${o.map((e) => `- ${e.name}${c?.id === e.id ? " (active)" : ""}`).join("\n")}\n\nUse select_active_import to select a data source.`
4150
- };
4151
- }
4152
- async function rr(e, t, n, r, i, a) {
4153
- if (!i || typeof i != "string" || i.trim() === "") return {
4154
- success: !1,
4155
- error: "Query is required and must be a non-empty string",
4156
- query: i
4157
- };
4158
- if (!r || typeof r != "string") return {
4159
- success: !1,
4160
- error: `Connection name is required. Available connections: ${Q(t) || "None"}`,
4161
- query: i
4162
- };
4163
- let o = $(t, r);
4164
- if (!o) return {
4165
- success: !1,
4166
- error: `Connection "${r}" not found. Available connections: ${Q(t) || "None"}`,
4167
- query: i
4168
- };
4169
- if (!o.connected) return {
4170
- success: !1,
4171
- error: `Connection "${o.name}" is not connected. Use connect_data_connection first.`,
4172
- query: i
4173
- };
4174
- let s = Qn(t, n, o.id, a), c = (a || []).map((e) => ({
4175
- name: e.name,
4176
- alias: e.alias || ""
4177
- })), l = {
4178
- text: i.trim(),
4179
- editorType: "trilogy",
4180
- imports: c,
4181
- extraContent: s
4182
- };
4183
- try {
4184
- let t = await (await e.executeQuery(o.id, l)).resultPromise;
4185
- return t.success ? t.results ? {
4186
- success: !0,
4187
- queryResult: t,
4188
- query: i
4189
- } : {
4190
- success: !1,
4191
- error: "Query returned no results",
4192
- query: i,
4193
- generatedSql: t.generatedSql
4194
- } : {
4195
- success: !1,
4196
- error: t.error || "Query execution failed",
4197
- executionTime: t.executionTime,
4198
- query: i,
4199
- generatedSql: t.generatedSql
4200
- };
4201
- } catch (e) {
4202
- return {
4203
- success: !1,
4204
- error: e instanceof Error ? e.message : "Unknown error during query execution",
4205
- query: i
4206
- };
4207
- }
4208
- }
4209
- //#endregion
4210
- //#region llm/editorRefinementToolExecutor.ts
4211
- function ir() {
4212
- return {
4213
- artifactRegistry: /* @__PURE__ */ new Map(),
4214
- lastQueryResults: { value: null }
4215
- };
4216
- }
4217
- var ar = class {
4218
- queryExecutionService;
4219
- connectionStore;
4220
- editorStore;
4221
- editorContext;
4222
- artifactRegistry;
4223
- lastQueryResults;
4224
- constructor(e, t, n, r, i) {
4225
- this.queryExecutionService = e, this.connectionStore = t, this.editorContext = n, this.editorStore = r || null;
4226
- let a = i ?? ir();
4227
- this.artifactRegistry = a.artifactRegistry, this.lastQueryResults = a.lastQueryResults;
4228
- }
4229
- async executeToolCall(e, t) {
4230
- switch (e) {
4231
- case "browse_database_tree": return this.browseDatabaseTree(t);
4232
- case "validate_query": return this.validateQuery(t.query);
4233
- case "run_query": return this.runQuery(t.query);
4234
- case "format_query": return this.formatQuery(t.query);
4235
- case "edit_chart_config": return this.editChartConfig(t.chartConfig);
4236
- case "edit_editor": return this.editEditor(t.content, t.replaceSelection === !0);
4237
- case "run_active_editor_query": return this.runActiveEditorQuery();
4238
- case "request_close": return this.requestClose(t.message);
4239
- case "close_session": return this.closeSession();
4240
- case "finish": return this.closeSession();
4241
- case "connect_data_connection": return this.connectDataConnection(t.connection);
4242
- case "get_artifact_rows": return this.getArtifactRows(t.artifact_id, t.start_row, t.end_row);
4243
- default: return {
4244
- success: !1,
4245
- error: `Unknown tool: ${e}.`
4246
- };
4247
- }
4248
- }
4249
- async browseDatabaseTree(e) {
4250
- let t = this.editorContext.connectionName, n = this.connectionStore.connectionByName(t);
4251
- if (!n) return {
4252
- success: !1,
4253
- error: `Connection "${t}" not found`
4254
- };
4255
- if (!n.connected) return {
4256
- success: !1,
4257
- error: `Connection "${t}" is not currently connected`
4258
- };
4259
- let r = typeof e.database == "string" ? e.database : void 0, i = typeof e.schema == "string" ? e.schema : void 0, a = typeof e.table == "string" ? e.table : void 0, o = e.refresh !== !1;
4260
- if (a && (!r || !i)) return {
4261
- success: !1,
4262
- error: "table lookup requires both database and schema"
4263
- };
4264
- if (i && !r) return {
4265
- success: !1,
4266
- error: "schema lookup requires database"
4267
- };
4268
- try {
4269
- if ((o || !n.databases || n.databases.length === 0) && await n.getDatabases(), r && o && (await n.refreshDatabase(r), !n.hasSchema)) {
4270
- let e = n.databases?.find((e) => e.name === r);
4271
- e && e.schemas.length > 0 && await n.refreshSchema(r, e.schemas[0].name);
4272
- }
4273
- if (r && i && o && await n.refreshSchema(r, i), r && i && a && o) {
4274
- let e = await n.getColumns(r, i, a), t = n.databases?.find((e) => e.name === r)?.schemas.find((e) => e.name === i)?.tables.find((e) => e.name === a);
4275
- t && (t.columns = e);
4276
- }
4277
- let e = this.serializeDatabaseTree(r, i, a);
4278
- return {
4279
- success: !0,
4280
- message: JSON.stringify(e, null, 2)
4281
- };
4282
- } catch (e) {
4283
- return {
4284
- success: !1,
4285
- error: e instanceof Error ? e.message : "Failed to browse database tree"
4286
- };
4287
- }
4288
- }
4289
- async validateQuery(e) {
4290
- if (!e || typeof e != "string" || e.trim() === "") return {
4291
- success: !1,
4292
- error: "Query is required and must be a non-empty string"
4293
- };
4294
- let t = this.editorContext.connectionName, n = this.connectionStore.connectionByName(t);
4295
- if (!n) return {
4296
- success: !1,
4297
- error: `Connection "${t}" not found`
4298
- };
4299
- let r = this.buildQueryInput(e);
4300
- if (r.editorType === "sql") return {
4301
- success: !0,
4302
- message: "SQL parser validation is not available. Use run_query to validate by execution.",
4303
- query: e
4304
- };
4305
- try {
4306
- let t = await this.queryExecutionService.validateQuery(n.id, r);
4307
- if (t && t.data) {
4308
- let n = t.data.items.filter((e) => e.severity >= 8), r = (t.data.completion_items || []).filter((e) => e.trilogyType === "concept");
4309
- if (n.length > 0) {
4310
- let t = this.buildSymbolsSummary(r);
4311
- return {
4312
- success: !1,
4313
- error: `Validation errors:\n${n.map((e) => `- ${e.message}`).join("\n")}${t}`,
4314
- query: e,
4315
- availableSymbols: r
4316
- };
4317
- }
4318
- return {
4319
- success: !0,
4320
- message: `Query is valid${this.buildSymbolsSummary(r)}`,
4321
- query: e,
4322
- availableSymbols: r
4323
- };
4324
- }
4325
- return {
4326
- success: !1,
4327
- error: "Query validation failed - no response from validator",
4328
- query: e
4329
- };
4330
- } catch (t) {
4331
- return {
4332
- success: !1,
4333
- error: t instanceof Error ? t.message : "Validation error",
4334
- query: e
4335
- };
4336
- }
4337
- }
4338
- buildSymbolsSummary(e) {
4339
- return e.length === 0 ? "" : `\n\nAvailable fields (${e.length} concepts):\n${cn(e)}`;
4340
- }
4341
- async runQuery(e) {
4342
- if (!e || typeof e != "string" || e.trim() === "") return {
4343
- success: !1,
4344
- error: "Query is required and must be a non-empty string"
4345
- };
4346
- let t = this.editorContext.connectionName, n = this.connectionStore.connectionByName(t);
4347
- if (!n) return {
4348
- success: !1,
4349
- error: `Connection "${t}" not found`,
4350
- query: e
4351
- };
4352
- if (!n.connected) return {
4353
- success: !1,
4354
- error: `Connection "${t}" is not currently connected`,
4355
- query: e
4356
- };
4357
- let r = this.buildQueryInput(e);
4358
- try {
4359
- let i = await (await this.queryExecutionService.executeQuery(n.id, r, void 0, void 0, void 0, void 0, !1)).resultPromise;
4360
- if (i.selectCount !== void 0 && i.selectCount > 1) return {
4361
- success: !1,
4362
- error: `Query contains ${i.selectCount} SELECT statements. Each run_query call must contain exactly one SELECT statement. Please split your selects across multiple run_query calls.`,
4363
- query: e
4364
- };
4365
- if (!i.success) return {
4366
- success: !1,
4367
- error: i.error || "Query execution failed",
4368
- executionTime: i.executionTime,
4369
- query: e,
4370
- generatedSql: i.generatedSql
4371
- };
4372
- if (!i.results) return {
4373
- success: !1,
4374
- error: "Query returned no results",
4375
- query: e,
4376
- generatedSql: i.generatedSql
4377
- };
4378
- let a = {
4379
- id: V(),
4380
- type: "results",
4381
- data: i.results,
4382
- config: {
4383
- query: e,
4384
- connectionName: t,
4385
- generatedSql: i.generatedSql,
4386
- executionTime: i.executionTime,
4387
- resultSize: i.resultSize,
4388
- columnCount: i.columnCount
4389
- }
4390
- };
4391
- this.artifactRegistry.set(a.id, a);
4392
- let o = i.results;
4393
- return o && o.headers && (this.lastQueryResults.value = o), {
4394
- success: !0,
4395
- artifact: a,
4396
- executionTime: i.executionTime,
4397
- query: e,
4398
- generatedSql: i.generatedSql,
4399
- message: `Query executed successfully. ${i.resultSize} rows, ${i.columnCount} columns.`
4400
- };
4401
- } catch (t) {
4402
- return {
4403
- success: !1,
4404
- error: t instanceof Error ? t.message : "Unknown error during query execution",
4405
- query: e
4406
- };
4407
- }
4408
- }
4409
- async formatQuery(e) {
4410
- if (!e || typeof e != "string" || e.trim() === "") return {
4411
- success: !1,
4412
- error: "Query is required and must be a non-empty string"
4413
- };
4414
- let t = this.editorContext.connectionName, n = this.connectionStore.connectionByName(t), r = this.buildEditorExtraContent();
4415
- try {
4416
- let i = await this.queryExecutionService.formatQuery(e.trim(), n?.id || t, this.editorContext.editorType, [], r);
4417
- return i ? {
4418
- success: !0,
4419
- formattedQuery: i,
4420
- message: `Query formatted successfully:\n\`\`\`${this.editorContext.editorType}\n${i}\n\`\`\``
4421
- } : {
4422
- success: !1,
4423
- error: "Could not format query - no result returned",
4424
- query: e
4425
- };
4426
- } catch (t) {
4427
- return {
4428
- success: !1,
4429
- error: t instanceof Error ? t.message : "Format error",
4430
- query: e
4431
- };
4432
- }
4433
- }
4434
- editChartConfig(e) {
4435
- if (this.editorContext.editorType === "sql") return {
4436
- success: !1,
4437
- error: "Chart configuration editing is not supported for SQL editors"
4438
- };
4439
- if (!e || typeof e != "object") return {
4440
- success: !1,
4441
- error: "chartConfig is required and must be an object"
4442
- };
4443
- let t = this.editorContext.getCurrentResults?.() ?? this.lastQueryResults.value ?? null;
4444
- if (t && t.headers) {
4445
- let n = Bn(t.data, t.headers, e);
4446
- if (!n.valid) {
4447
- let e = n.suggestedConfig;
4448
- try {
4449
- this.editorContext.onChartConfigChange(e);
4450
- } catch {}
4451
- return {
4452
- success: !1,
4453
- error: "Auto-corrected to default configuration.\n" + Vn(n)
4454
- };
4455
- }
4456
- }
4457
- try {
4458
- return this.editorContext.onChartConfigChange(e), {
4459
- success: !0,
4460
- message: `Chart configuration updated: ${JSON.stringify(e)}`
4461
- };
4462
- } catch (e) {
4463
- return {
4464
- success: !1,
4465
- error: e instanceof Error ? e.message : "Failed to update chart config"
4466
- };
4467
- }
4468
- }
4469
- async editEditor(e, t) {
4470
- if (!e || typeof e != "string") return {
4471
- success: !1,
4472
- error: "Content is required and must be a string"
4473
- };
4474
- try {
4475
- if (this.editorContext.onEditorContentChange(e, t), this.editorContext.editorType === "sql") return {
4476
- success: !0,
4477
- message: t ? "Updated selected text in editor." : "Updated editor contents."
4478
- };
4479
- let n = await this.validateQuery(e), r = t ? "Updated selected text in editor" : "Updated editor contents";
4480
- return n.success ? {
4481
- success: !0,
4482
- message: `${r}. ${n.message}`,
4483
- availableSymbols: n.availableSymbols
4484
- } : {
4485
- success: !0,
4486
- message: `${r}.\n\nValidation errors found:\n${n.error}`,
4487
- availableSymbols: n.availableSymbols
4488
- };
4489
- } catch (e) {
4490
- return {
4491
- success: !1,
4492
- error: e instanceof Error ? e.message : "Failed to update editor"
4493
- };
4494
- }
4495
- }
4496
- async runActiveEditorQuery() {
4497
- if (!this.editorContext.onRunActiveEditorQuery) return {
4498
- success: !1,
4499
- error: "Run active editor query is not available in this context"
4500
- };
4501
- let e = this.editorContext.editorContents;
4502
- if (!e || e.trim() === "") return {
4503
- success: !1,
4504
- error: "Editor is empty - no query to run"
4505
- };
4506
- try {
4507
- let t = await this.editorContext.onRunActiveEditorQuery();
4508
- if (!t.success) return {
4509
- success: !1,
4510
- error: t.error || "Query execution failed",
4511
- executionTime: t.executionTime,
4512
- query: e,
4513
- generatedSql: t.generatedSql
4514
- };
4515
- if (!t.results) return {
4516
- success: !1,
4517
- error: "Query returned no results",
4518
- query: e,
4519
- generatedSql: t.generatedSql
4520
- };
4521
- let n = {
4522
- id: V(),
4523
- type: "results",
4524
- data: t.results,
4525
- config: {
4526
- query: e,
4527
- connectionName: this.editorContext.connectionName,
4528
- generatedSql: t.generatedSql,
4529
- executionTime: t.executionTime,
4530
- resultSize: t.resultSize,
4531
- columnCount: t.columnCount
4532
- }
4533
- };
4534
- return this.artifactRegistry.set(n.id, n), {
4535
- success: !0,
4536
- artifact: n,
4537
- executionTime: t.executionTime,
4538
- query: e,
4539
- generatedSql: t.generatedSql,
4540
- message: `Query executed successfully. ${t.resultSize} rows, ${t.columnCount} columns.`
4541
- };
4542
- } catch (t) {
4543
- return {
4544
- success: !1,
4545
- error: t instanceof Error ? t.message : "Failed to execute query",
4546
- query: e
4547
- };
4548
- }
4549
- }
4550
- getArtifactRows(e, t, n) {
4551
- let r = this.artifactRegistry.get(e);
4552
- return r ? $n(e, r.data, t, n) : {
4553
- success: !1,
4554
- error: `Artifact "${e}" not found. Only artifacts created by run_query or run_active_editor_query in this session are available.`
4555
- };
4556
- }
4557
- requestClose(e) {
4558
- return {
4559
- success: !0,
4560
- message: `${e}\n\nReady to close. Reply with any follow-up requests, or say "done" to close the session.`,
4561
- awaitsUserInput: !0
4562
- };
4563
- }
4564
- closeSession() {
4565
- try {
4566
- return this.editorContext.onFinish(), {
4567
- success: !0,
4568
- message: "Session closed. Changes saved.",
4569
- terminatesLoop: !0
4570
- };
4571
- } catch (e) {
4572
- return {
4573
- success: !1,
4574
- error: e instanceof Error ? e.message : "Failed to close session"
4575
- };
4576
- }
4577
- }
4578
- async connectDataConnection(e) {
4579
- return Zn(this.connectionStore, e);
4580
- }
4581
- serializeDatabaseTree(e, t, n) {
4582
- let r = this.connectionStore.connectionByName(this.editorContext.connectionName), i = r?.databases || [], a = (e ? i.filter((t) => t.name === e) : i).map((e) => {
4583
- let r = t ? e.schemas.filter((e) => e.name === t) : e.schemas;
4584
- return {
4585
- database: e.name,
4586
- schemas: r.map((e) => {
4587
- let t = n ? e.tables.filter((e) => e.name === n) : e.tables;
4588
- return {
4589
- schema: e.name,
4590
- tables: t.map((e) => ({
4591
- name: e.name,
4592
- assetType: e.assetType,
4593
- description: e.description,
4594
- columns: e.columns.map((e) => ({
4595
- name: e.name,
4596
- type: e.type,
4597
- trilogyType: e.trilogyType,
4598
- nullable: e.nullable
4599
- }))
4600
- }))
4601
- };
4602
- })
4603
- };
4604
- });
4605
- return {
4606
- connection: this.editorContext.connectionName,
4607
- hasSchema: r?.hasSchema,
4608
- tree: a
4609
- };
4610
- }
4611
- buildQueryInput(e) {
4612
- return {
4613
- text: e.trim(),
4614
- editorType: this.editorContext.editorType,
4615
- imports: [],
4616
- extraContent: this.buildEditorExtraContent()
4617
- };
4618
- }
4619
- buildEditorExtraContent() {
4620
- return Qn(this.connectionStore, this.editorStore, this.editorContext.connectionName);
4621
- }
4622
- };
4623
- //#endregion
4624
- export { Vt as $, ie as $t, bn as A, qe as At, dn as B, Re as Bt, X as C, Xe as Ct, Wn as D, $e as Dt, Mn as E, Qe as Et, Sn as F, D as Ft, an as G, ae as Gt, sn as H, O as Ht, xn as I, Be as It, $t as J, ne as Jt, nn as K, te as Kt, hn as L, Ve as Lt, vn as M, Ue as Mt, _n as N, We as Nt, Z as O, k as Ot, gn as P, Le as Pt, Gt as Q, le as Qt, mn as R, ke as Rt, Hn as S, Ze as St, Pn as T, et as Tt, fn as U, He as Ut, pn as V, ze as Vt, Kt as W, De as Wt, z as X, re as Xt, B as Y, se as Yt, Ut as Z, b as Zt, Ln as _, mt as _t, rr as a, m as an, St as at, W as b, at as bt, nr as c, d as cn, xt as ct, kn as d, P as dt, Ce as en, Ht as et, Jn as f, N as ft, In as g, ht as gt, Yn as h, gt as ht, Zn as i, y as in, jt as it, yn as j, Ge as jt, Bn as k, Je as kt, tr as l, u as ln, Ct as lt, Gn as m, j as mt, ir as n, ye as nn, Lt as nt, $n as o, f as on, I as ot, Un as p, F as pt, on as q, oe as qt, Qn as r, Se as rn, R as rt, er as s, c as sn, Ot as st, ar as t, xe as tn, Rt as tt, Cn as u, wt as ut, J as v, ut as vt, En as w, Ye as wt, Y as x, A as xt, Vn as y, ot as yt, V as z, Fe as zt };