@oxy-hq/sdk 2.1.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/email.cjs +14 -0
- package/dist/email.cjs.map +1 -0
- package/dist/email.d.cts +8 -0
- package/dist/email.d.cts.map +1 -0
- package/dist/email.d.mts +8 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +13 -0
- package/dist/email.mjs.map +1 -0
- package/dist/index.cjs +21 -2091
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -580
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +112 -580
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2046
- package/dist/index.mjs.map +1 -1
- package/dist/react-B7Blpmt7.cjs +2385 -0
- package/dist/react-B7Blpmt7.cjs.map +1 -0
- package/dist/react-BLsczFL4.d.cts +646 -0
- package/dist/react-BLsczFL4.d.cts.map +1 -0
- package/dist/react-BLsczFL4.d.mts +646 -0
- package/dist/react-BLsczFL4.d.mts.map +1 -0
- package/dist/react-DArzs6_l.mjs +2238 -0
- package/dist/react-DArzs6_l.mjs.map +1 -0
- package/dist/shell.cjs +2091 -0
- package/dist/shell.cjs.map +1 -0
- package/dist/shell.css +1187 -0
- package/dist/shell.d.cts +337 -0
- package/dist/shell.d.cts.map +1 -0
- package/dist/shell.d.mts +337 -0
- package/dist/shell.d.mts.map +1 -0
- package/dist/shell.mjs +2071 -0
- package/dist/shell.mjs.map +1 -0
- package/package.json +55 -10
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @oxy/sdk - TypeScript SDK for Oxy data platform
|
|
2
|
-
import
|
|
3
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { _ as interpretCustomerAppError, a as useFunction, c as useQuery, d as useTrackEvent, f as _resetCustomerAppManifestCacheForTest, g as apiErrorFromResponse, h as OxyApiError, i as useAgentRun, l as useResolvedManifest, m as readInjectedAppConfig, n as OxyAppProvider, o as useOxyApp, p as loadCustomerAppManifest, r as OxyChat, s as useProcedureRun, t as OxyAnswer, u as useSemanticQuery, v as getOxyAppLogger, y as setOxyAppLogger } from "./react-DArzs6_l.mjs";
|
|
4
3
|
|
|
5
4
|
//#region src/anomalies.ts
|
|
6
5
|
/**
|
|
@@ -82,46 +81,6 @@ var AnomaliesClient = class {
|
|
|
82
81
|
}
|
|
83
82
|
};
|
|
84
83
|
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/customer-app/logger.ts
|
|
87
|
-
let activeLogger = createConsoleLogger();
|
|
88
|
-
/** Replace the global logger. Pass `null` to silence everything. */
|
|
89
|
-
function setOxyAppLogger(logger) {
|
|
90
|
-
activeLogger = logger ?? silentLogger();
|
|
91
|
-
}
|
|
92
|
-
/** Used by the SDK internals; not part of the public surface. */
|
|
93
|
-
function getOxyAppLogger() {
|
|
94
|
-
return activeLogger;
|
|
95
|
-
}
|
|
96
|
-
function createConsoleLogger() {
|
|
97
|
-
return { log(level, msg, ctx) {
|
|
98
|
-
if (typeof console === "undefined") return;
|
|
99
|
-
const prefix = "[oxy-app]";
|
|
100
|
-
const args = ctx ? [
|
|
101
|
-
prefix,
|
|
102
|
-
msg,
|
|
103
|
-
ctx
|
|
104
|
-
] : [prefix, msg];
|
|
105
|
-
switch (level) {
|
|
106
|
-
case "debug":
|
|
107
|
-
console.debug(...args);
|
|
108
|
-
break;
|
|
109
|
-
case "info":
|
|
110
|
-
console.info(...args);
|
|
111
|
-
break;
|
|
112
|
-
case "warn":
|
|
113
|
-
console.warn(...args);
|
|
114
|
-
break;
|
|
115
|
-
case "error":
|
|
116
|
-
console.error(...args);
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
} };
|
|
120
|
-
}
|
|
121
|
-
function silentLogger() {
|
|
122
|
-
return { log() {} };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
84
|
//#endregion
|
|
126
85
|
//#region src/customer-app/debug.ts
|
|
127
86
|
/**
|
|
@@ -145,2009 +104,6 @@ async function getCustomerAppDebug(resolved) {
|
|
|
145
104
|
return snapshot;
|
|
146
105
|
}
|
|
147
106
|
|
|
148
|
-
//#endregion
|
|
149
|
-
//#region src/customer-app/errors.ts
|
|
150
|
-
const ARCH_DOC = "internal-docs/customer-apps.md";
|
|
151
|
-
/** Interpret a thrown error as a structured report for UI display. */
|
|
152
|
-
function interpretCustomerAppError(err) {
|
|
153
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
-
if (/Failed to load oxy-app\.json.*HTTP 404/.test(message)) return {
|
|
155
|
-
title: "Manifest not found",
|
|
156
|
-
message,
|
|
157
|
-
hint: "The bundle is being served, but oxy-app.json was not. Check that public/oxy-app.json is committed in the customer-app repo and that the build copied it into the static output. If you're using Next.js, anything under public/ is auto-copied to out/.",
|
|
158
|
-
docs: ARCH_DOC
|
|
159
|
-
};
|
|
160
|
-
if (/Failed to load oxy-app\.json/.test(message)) return {
|
|
161
|
-
title: "Manifest could not be loaded",
|
|
162
|
-
message,
|
|
163
|
-
hint: "Network error fetching the manifest. Confirm the bundle is being served from a path that matches OXY_APP_BASE_PATH at build time — a mismatch causes assets and the manifest to 404.",
|
|
164
|
-
docs: ARCH_DOC
|
|
165
|
-
};
|
|
166
|
-
if (/schemaVersion/i.test(message)) return {
|
|
167
|
-
title: "Manifest schema mismatch",
|
|
168
|
-
message,
|
|
169
|
-
hint: "This bundle was built against a different version of the data-product contract than the SDK it ships. Rebuild the bundle with a compatible @oxy-hq/sdk version.",
|
|
170
|
-
docs: ARCH_DOC
|
|
171
|
-
};
|
|
172
|
-
if (/^401:/m.test(message)) return {
|
|
173
|
-
title: "Session expired",
|
|
174
|
-
message,
|
|
175
|
-
hint: "Reload the page to re-authenticate via oxy's session cookie.",
|
|
176
|
-
docs: ARCH_DOC
|
|
177
|
-
};
|
|
178
|
-
if (/^403:.*origin not allowed/im.test(message)) return {
|
|
179
|
-
title: "Request origin not allowed",
|
|
180
|
-
message,
|
|
181
|
-
hint: "The bundle's host isn't in oxy's OXY_ALLOWED_ORIGINS. Production: add the bundle's serving origin to the env var. Local dev: oxy auto-allows http://localhost:5173 and :5174.",
|
|
182
|
-
docs: ARCH_DOC
|
|
183
|
-
};
|
|
184
|
-
if (/^403:.*not a member/im.test(message)) return {
|
|
185
|
-
title: "Access denied",
|
|
186
|
-
message,
|
|
187
|
-
hint: "Your account isn't a member of the org that owns this project. Ask an org owner to add you.",
|
|
188
|
-
docs: ARCH_DOC
|
|
189
|
-
};
|
|
190
|
-
if (/^403:.*SELECT.*WITH/im.test(message)) return {
|
|
191
|
-
title: "Query rejected — read-only endpoint",
|
|
192
|
-
message,
|
|
193
|
-
hint: "This proxy only runs SELECT or WITH queries. Mutations (INSERT/UPDATE/DELETE/DROP) are not allowed from customer-app bundles.",
|
|
194
|
-
docs: ARCH_DOC
|
|
195
|
-
};
|
|
196
|
-
if (/^403:/m.test(message)) return {
|
|
197
|
-
title: "Access denied",
|
|
198
|
-
message,
|
|
199
|
-
hint: "The request was rejected by the server. Check the oxy server logs for details.",
|
|
200
|
-
docs: ARCH_DOC
|
|
201
|
-
};
|
|
202
|
-
if (/^404:/m.test(message) && /project/i.test(message)) return {
|
|
203
|
-
title: "Project not found",
|
|
204
|
-
message,
|
|
205
|
-
hint: "The projectId in oxy-app.json doesn't match any registered project. Confirm the manifest's projectId is a real UUID for this deployment.",
|
|
206
|
-
docs: ARCH_DOC
|
|
207
|
-
};
|
|
208
|
-
if (/^400:.*sql.*must be non-empty/im.test(message)) return {
|
|
209
|
-
title: "Empty SQL",
|
|
210
|
-
message,
|
|
211
|
-
hint: "useQuery was called with an empty or whitespace-only `sql`. Pass a real query, or set `enabled: false` to skip the call.",
|
|
212
|
-
docs: ARCH_DOC
|
|
213
|
-
};
|
|
214
|
-
if (/^400:/m.test(message) && /query failed/i.test(message)) return {
|
|
215
|
-
title: "Query failed",
|
|
216
|
-
message,
|
|
217
|
-
hint: "The SQL ran but the warehouse rejected it. Full error in the oxy server logs (look for the projects::query span).",
|
|
218
|
-
docs: ARCH_DOC
|
|
219
|
-
};
|
|
220
|
-
if (/^502:/m.test(message)) return {
|
|
221
|
-
title: "Warehouse unreachable",
|
|
222
|
-
message,
|
|
223
|
-
hint: "Oxy couldn't reach the configured database. Check connector config + warehouse health.",
|
|
224
|
-
docs: ARCH_DOC
|
|
225
|
-
};
|
|
226
|
-
if (/Unexpected token '<'|<!doctype/i.test(message)) return {
|
|
227
|
-
title: "Fetched HTML where JSON was expected",
|
|
228
|
-
message,
|
|
229
|
-
hint: "Most likely the built bundle is stale — built against an old SDK whose endpoints no longer exist on the server. Rebuild the bundle (vite build) with @oxy-hq/sdk@^2.0.0 and reload. If the bundle is current, check that OXY_APP_BASE_PATH matches the path the customer-app row is served at.",
|
|
230
|
-
docs: ARCH_DOC
|
|
231
|
-
};
|
|
232
|
-
return {
|
|
233
|
-
title: "Unexpected error loading the dashboard",
|
|
234
|
-
message,
|
|
235
|
-
hint: "Check the browser console for the full stack trace, and the oxy server logs for the corresponding request.",
|
|
236
|
-
docs: ARCH_DOC
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
//#endregion
|
|
241
|
-
//#region src/customer-app/inject.ts
|
|
242
|
-
/**
|
|
243
|
-
* Read the runtime app-config oxy injected at serve time. Returns
|
|
244
|
-
* `undefined` outside the browser or when the global isn't set
|
|
245
|
-
* (`pnpm dev` against a non-oxy server, etc. — manifest hints are
|
|
246
|
-
* the fallback).
|
|
247
|
-
*/
|
|
248
|
-
function readInjectedAppConfig() {
|
|
249
|
-
if (typeof window === "undefined") return void 0;
|
|
250
|
-
return window.__OXY_APP__;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
//#endregion
|
|
254
|
-
//#region src/customer-app/manifest.ts
|
|
255
|
-
let cached = null;
|
|
256
|
-
/**
|
|
257
|
-
* Load + validate the manifest. Cached after the first call so callers
|
|
258
|
-
* can invoke this from every component without coordinating.
|
|
259
|
-
*/
|
|
260
|
-
function loadCustomerAppManifest(options = {}) {
|
|
261
|
-
if (!cached) cached = fetchAndValidate(options);
|
|
262
|
-
return cached;
|
|
263
|
-
}
|
|
264
|
-
/** For tests: reset the cache between runs. */
|
|
265
|
-
function _resetCustomerAppManifestCacheForTest() {
|
|
266
|
-
cached = null;
|
|
267
|
-
}
|
|
268
|
-
async function fetchAndValidate(options) {
|
|
269
|
-
const log = getOxyAppLogger();
|
|
270
|
-
const injected = readInjectedAppConfig();
|
|
271
|
-
const manifestUrl = options.manifestUrl ?? defaultManifestUrl(injected);
|
|
272
|
-
log.log("info", "loading manifest", {
|
|
273
|
-
manifestUrl,
|
|
274
|
-
injectionPresent: !!injected,
|
|
275
|
-
orgSlug: injected?.orgSlug,
|
|
276
|
-
appSlug: injected?.slug,
|
|
277
|
-
appId: injected?.appId
|
|
278
|
-
});
|
|
279
|
-
const startedAt = Date.now();
|
|
280
|
-
const res = await fetch(manifestUrl, { credentials: "same-origin" });
|
|
281
|
-
if (!res.ok) {
|
|
282
|
-
log.log("error", "manifest fetch failed", {
|
|
283
|
-
manifestUrl,
|
|
284
|
-
status: res.status,
|
|
285
|
-
statusText: res.statusText
|
|
286
|
-
});
|
|
287
|
-
throw new Error(`Failed to load oxy-app.json from ${manifestUrl} (HTTP ${res.status}). The customer-app repo must commit this file alongside the bundle.`);
|
|
288
|
-
}
|
|
289
|
-
const manifest = validateManifest(await res.json(), manifestUrl);
|
|
290
|
-
const resolved = {
|
|
291
|
-
manifest,
|
|
292
|
-
productNames: [],
|
|
293
|
-
orgSlug: injected?.orgSlug ?? "",
|
|
294
|
-
appSlug: injected?.slug ?? "",
|
|
295
|
-
apiBaseUrl: injected?.apiBaseUrl || "",
|
|
296
|
-
appId: injected?.appId,
|
|
297
|
-
projectId: injected?.projectId ?? manifest.projectId
|
|
298
|
-
};
|
|
299
|
-
log.log("info", "manifest ready", {
|
|
300
|
-
durationMs: Date.now() - startedAt,
|
|
301
|
-
schemaVersion: manifest.schemaVersion,
|
|
302
|
-
slug: manifest.slug
|
|
303
|
-
});
|
|
304
|
-
return resolved;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Default manifest URL.
|
|
308
|
-
*
|
|
309
|
-
* Resolution order (bundler-agnostic):
|
|
310
|
-
* 1. `window.__OXY_APP__.orgSlug`/`slug` injection → the canonical
|
|
311
|
-
* `/customer-apps/<org>/<app>/oxy-app.json`. Works for every
|
|
312
|
-
* bundle oxy serves regardless of how it was built.
|
|
313
|
-
* 2. `NEXT_PUBLIC_APP_BASE_PATH` env var — kept for backward compat
|
|
314
|
-
* with Next.js bundles that bake basePath at build time.
|
|
315
|
-
* 3. Empty basePath → `/oxy-app.json` (only matches when running in
|
|
316
|
-
* a `vite dev` / `next dev` root mount; will 404 under oxy).
|
|
317
|
-
*/
|
|
318
|
-
function defaultManifestUrl(injected) {
|
|
319
|
-
if (injected?.orgSlug && injected?.slug) return `/customer-apps/${encodeURIComponent(injected.orgSlug)}/${encodeURIComponent(injected.slug)}/oxy-app.json`;
|
|
320
|
-
return "/oxy-app.json";
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Validate a v2 manifest. Required: schemaVersion === 2, slug (non-empty).
|
|
324
|
-
* Optional: name (display), orgSlug (dev-time hint for the admin dialog),
|
|
325
|
-
* projectId (dev-time hint when there's no server-side injection).
|
|
326
|
-
*
|
|
327
|
-
* At serve time, oxy's identity injection (window.__OXY_APP__) overrides
|
|
328
|
-
* the manifest's orgSlug/projectId — the manifest fields are advisory.
|
|
329
|
-
*/
|
|
330
|
-
function validateManifest(raw, url) {
|
|
331
|
-
if (!isRecord(raw)) throw new Error(`Manifest at ${url} is not a JSON object`);
|
|
332
|
-
if (raw.schemaVersion !== 2) throw new Error(`oxy-app.json: schemaVersion must be 2 (got ${JSON.stringify(raw.schemaVersion)}). v1 manifests are no longer supported — upgrade to the identity-only shape.`);
|
|
333
|
-
if (raw.products !== void 0 || raw.writers !== void 0) throw new Error(`oxy-app.json is schemaVersion 2 (identity-only); \`products\` and \`writers\` are no longer supported`);
|
|
334
|
-
if (typeof raw.slug !== "string" || !raw.slug.trim()) throw new Error("oxy-app.json: `slug` is required and must be a non-empty string");
|
|
335
|
-
return {
|
|
336
|
-
schemaVersion: 2,
|
|
337
|
-
name: typeof raw.name === "string" ? raw.name : void 0,
|
|
338
|
-
slug: raw.slug,
|
|
339
|
-
orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
|
|
340
|
-
projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
|
|
341
|
-
functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
345
|
-
/**
|
|
346
|
-
* Validate the optional `functions` map. Each key is a function name;
|
|
347
|
-
* each value declares how the function is invoked. Mirrors the
|
|
348
|
-
* server-side validation in `customer_apps_publish.rs` so a bad
|
|
349
|
-
* manifest fails at build, not at publish.
|
|
350
|
-
*/
|
|
351
|
-
function validateFunctions(raw) {
|
|
352
|
-
if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
|
|
353
|
-
const out = {};
|
|
354
|
-
for (const [fnName, value] of Object.entries(raw)) {
|
|
355
|
-
if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
|
|
356
|
-
if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
|
|
357
|
-
const fn = {};
|
|
358
|
-
if (value.entry !== void 0) {
|
|
359
|
-
if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
|
|
360
|
-
fn.entry = value.entry;
|
|
361
|
-
}
|
|
362
|
-
if (value.schedule !== void 0) {
|
|
363
|
-
if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
|
|
364
|
-
fn.schedule = value.schedule;
|
|
365
|
-
}
|
|
366
|
-
if (value.timezone !== void 0) {
|
|
367
|
-
if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
|
|
368
|
-
fn.timezone = value.timezone;
|
|
369
|
-
}
|
|
370
|
-
if (value.route !== void 0) {
|
|
371
|
-
if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
|
|
372
|
-
fn.route = value.route;
|
|
373
|
-
}
|
|
374
|
-
if (value.airwayStep !== void 0) {
|
|
375
|
-
const step = value.airwayStep;
|
|
376
|
-
if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
|
|
377
|
-
fn.airwayStep = {
|
|
378
|
-
pipeline: step.pipeline,
|
|
379
|
-
resource: step.resource
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
if (value.timeoutSeconds !== void 0) {
|
|
383
|
-
const t = value.timeoutSeconds;
|
|
384
|
-
if (typeof t !== "number" || !Number.isInteger(t) || t < 1 || t > 300) throw new Error(`oxy-app.json: function "${fnName}" \`timeoutSeconds\` must be an integer in [1, 300]`);
|
|
385
|
-
fn.timeoutSeconds = t;
|
|
386
|
-
}
|
|
387
|
-
if (value.cache !== void 0) {
|
|
388
|
-
const c = value.cache;
|
|
389
|
-
if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
|
|
390
|
-
if (c.ttlSeconds !== void 0) {
|
|
391
|
-
const ttl = c.ttlSeconds;
|
|
392
|
-
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
|
|
393
|
-
fn.cache = { ttlSeconds: ttl };
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
const hasSchedule = fn.schedule !== void 0;
|
|
397
|
-
const hasAirway = fn.airwayStep !== void 0;
|
|
398
|
-
if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
|
|
399
|
-
out[fnName] = fn;
|
|
400
|
-
}
|
|
401
|
-
return out;
|
|
402
|
-
}
|
|
403
|
-
function isRecord(v) {
|
|
404
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
//#endregion
|
|
408
|
-
//#region src/customer-app/function-invoke.ts
|
|
409
|
-
const inflight = /* @__PURE__ */ new Map();
|
|
410
|
-
/**
|
|
411
|
-
* Dedup key for an invocation: function name + its (stable-serialized) body,
|
|
412
|
-
* joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
|
|
413
|
-
* the separator can never collide with a name.
|
|
414
|
-
*/
|
|
415
|
-
function functionInvokeKey(name, body) {
|
|
416
|
-
return `${name}\n${JSON.stringify(body ?? {})}`;
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Run `run()` unless an identical invocation is already in flight, in which
|
|
420
|
-
* case share its promise. The entry is removed once the promise settles — so
|
|
421
|
-
* this dedups concurrency only, it does NOT memoize the result.
|
|
422
|
-
*/
|
|
423
|
-
function sharedFunctionInvoke(key, run) {
|
|
424
|
-
const existing = inflight.get(key);
|
|
425
|
-
if (existing) return existing;
|
|
426
|
-
const p = run().finally(() => {
|
|
427
|
-
inflight.delete(key);
|
|
428
|
-
});
|
|
429
|
-
inflight.set(key, p);
|
|
430
|
-
return p;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
//#endregion
|
|
434
|
-
//#region src/customer-app/function-sse.ts
|
|
435
|
-
/**
|
|
436
|
-
* Read a `text/event-stream` function response to completion. Resolves with the
|
|
437
|
-
* decoded result + captured logs, or rejects (with `.logs` attached) on an
|
|
438
|
-
* `event: error` frame / a stream that ends without a terminal event.
|
|
439
|
-
*/
|
|
440
|
-
async function readFunctionSseStream(resp) {
|
|
441
|
-
const reader = resp.body?.getReader();
|
|
442
|
-
if (!reader) throw new Error("function response has no body stream");
|
|
443
|
-
const decoder = new TextDecoder();
|
|
444
|
-
let buffer = "";
|
|
445
|
-
let dataPayload = "";
|
|
446
|
-
const logs = [];
|
|
447
|
-
const handleFrame = (frame) => {
|
|
448
|
-
let event = "message";
|
|
449
|
-
let data = "";
|
|
450
|
-
for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
451
|
-
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
452
|
-
if (event === "log") try {
|
|
453
|
-
const l = JSON.parse(data);
|
|
454
|
-
logs.push({
|
|
455
|
-
level: String(l.level ?? "info"),
|
|
456
|
-
message: String(l.message ?? "")
|
|
457
|
-
});
|
|
458
|
-
} catch {}
|
|
459
|
-
else if (event === "data") dataPayload = data;
|
|
460
|
-
else if (event === "done") return {
|
|
461
|
-
done: true,
|
|
462
|
-
value: dataPayload ? JSON.parse(dataPayload) : null
|
|
463
|
-
};
|
|
464
|
-
else if (event === "error") {
|
|
465
|
-
const payload = data ? JSON.parse(data) : {};
|
|
466
|
-
const err = new Error(payload.message || payload.error || "function invocation failed");
|
|
467
|
-
err.name = payload.error || "FunctionError";
|
|
468
|
-
err.logs = logs;
|
|
469
|
-
throw err;
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
for (;;) {
|
|
473
|
-
const { done, value } = await reader.read();
|
|
474
|
-
if (done) break;
|
|
475
|
-
buffer += decoder.decode(value, { stream: true });
|
|
476
|
-
let sep;
|
|
477
|
-
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
478
|
-
const frame = buffer.slice(0, sep);
|
|
479
|
-
buffer = buffer.slice(sep + 2);
|
|
480
|
-
const result = handleFrame(frame);
|
|
481
|
-
if (result) return {
|
|
482
|
-
value: result.value,
|
|
483
|
-
logs
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
throw new Error("function stream ended without a terminal event");
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
//#endregion
|
|
491
|
-
//#region src/customer-app/interpolate.ts
|
|
492
|
-
/**
|
|
493
|
-
* Interpolate `{{ params.X }}` and `{{ params.X | sqlquote }}` placeholders
|
|
494
|
-
* in a SQL template.
|
|
495
|
-
*
|
|
496
|
-
* - `{{ params.X | sqlquote }}` — quote strings ('foo'), pass numbers and
|
|
497
|
-
* booleans raw, nullish becomes NULL. Mirrors the server's Jinja sqlquote
|
|
498
|
-
* filter.
|
|
499
|
-
* - `{{ params.X }}` — raw pass-through. Used for already-trusted values
|
|
500
|
-
* (numbers, identifiers the caller has validated). Caller is responsible
|
|
501
|
-
* for safety.
|
|
502
|
-
*
|
|
503
|
-
* Not a security boundary. The server still gates SQL execution by
|
|
504
|
-
* project membership. Bundles that accept untrusted user input should
|
|
505
|
-
* use `| sqlquote` or validate/coerce before passing.
|
|
506
|
-
*/
|
|
507
|
-
function interpolateSqlParams(sql, params) {
|
|
508
|
-
return sql.replace(/\{\{\s*params\.([a-zA-Z0-9_]+)(\s*\|\s*sqlquote)?\s*\}\}/g, (_match, key, sqlquote) => {
|
|
509
|
-
const v = params[key];
|
|
510
|
-
if (v === null || v === void 0) return "NULL";
|
|
511
|
-
if (sqlquote) {
|
|
512
|
-
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
513
|
-
return `'${String(v).replace(/'/g, "''")}'`;
|
|
514
|
-
}
|
|
515
|
-
return String(v);
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
//#endregion
|
|
520
|
-
//#region src/customer-app/markdown.ts
|
|
521
|
-
/**
|
|
522
|
-
* Allowlist for `[text](url)` href values in agent-emitted markdown.
|
|
523
|
-
* Markdown comes from an LLM, which sits across an external trust
|
|
524
|
-
* boundary — without this filter, a `javascript:` URL produced by
|
|
525
|
-
* the model would render as a clickable XSS in the bundle's origin.
|
|
526
|
-
*
|
|
527
|
-
* Accepts:
|
|
528
|
-
* - http(s):// absolute URLs
|
|
529
|
-
* - mailto: addresses
|
|
530
|
-
* - root-relative paths (`/foo`)
|
|
531
|
-
* - same-page fragments (`#section`)
|
|
532
|
-
*
|
|
533
|
-
* Rejects everything else, including `javascript:`, `data:`,
|
|
534
|
-
* protocol-relative `//evil.com`, and any other scheme. Comparison
|
|
535
|
-
* is case-insensitive after stripping leading whitespace + ASCII
|
|
536
|
-
* control bytes (browsers strip these before scheme resolution, so
|
|
537
|
-
* `java\tscript:` would otherwise slip past a naive prefix check).
|
|
538
|
-
*/
|
|
539
|
-
function isSafeLinkHref(raw) {
|
|
540
|
-
let cleaned = "";
|
|
541
|
-
for (let i = 0; i < raw.length; i++) {
|
|
542
|
-
const cc = raw.charCodeAt(i);
|
|
543
|
-
if (cc > 32 && cc !== 127) cleaned += raw[i];
|
|
544
|
-
}
|
|
545
|
-
if (cleaned === "") return false;
|
|
546
|
-
if (cleaned.startsWith("#") || cleaned.startsWith("/")) {
|
|
547
|
-
if (cleaned.startsWith("//")) return false;
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
550
|
-
const lower = cleaned.toLowerCase();
|
|
551
|
-
return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
//#endregion
|
|
555
|
-
//#region src/customer-app/react.tsx
|
|
556
|
-
function defaultFetcher(input, init) {
|
|
557
|
-
return fetch(input, {
|
|
558
|
-
credentials: "include",
|
|
559
|
-
...init
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
const OxyAppContext = React.createContext(void 0);
|
|
563
|
-
/**
|
|
564
|
-
* Top-level provider. Loads the manifest once on mount; children only
|
|
565
|
-
* render after the manifest is ready (or the error fallback fires).
|
|
566
|
-
*/
|
|
567
|
-
function OxyAppProvider(props) {
|
|
568
|
-
const { manifestOptions, fallback, errorFallback, fetcher: fetcherProp, children } = props;
|
|
569
|
-
const fetcher = fetcherProp ?? defaultFetcher;
|
|
570
|
-
const [state, setState] = React.useState({
|
|
571
|
-
status: "loading",
|
|
572
|
-
fetcher
|
|
573
|
-
});
|
|
574
|
-
React.useEffect(() => {
|
|
575
|
-
let cancelled = false;
|
|
576
|
-
loadCustomerAppManifest(manifestOptions).then((resolved) => {
|
|
577
|
-
if (!cancelled) setState({
|
|
578
|
-
status: "ready",
|
|
579
|
-
resolved,
|
|
580
|
-
fetcher
|
|
581
|
-
});
|
|
582
|
-
}).catch((e) => {
|
|
583
|
-
if (!cancelled) setState({
|
|
584
|
-
status: "error",
|
|
585
|
-
error: interpretCustomerAppError(e),
|
|
586
|
-
fetcher
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
return () => {
|
|
590
|
-
cancelled = true;
|
|
591
|
-
};
|
|
592
|
-
}, [manifestOptions, fetcher]);
|
|
593
|
-
if (state.status === "error" && state.error) {
|
|
594
|
-
const err = state.error;
|
|
595
|
-
return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
596
|
-
value: state,
|
|
597
|
-
children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
if (state.status === "loading") return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
601
|
-
value: state,
|
|
602
|
-
children: fallback ?? null
|
|
603
|
-
});
|
|
604
|
-
return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
|
|
605
|
-
value: state,
|
|
606
|
-
children
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
function defaultErrorFallback(err) {
|
|
610
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
611
|
-
style: {
|
|
612
|
-
margin: "2rem auto",
|
|
613
|
-
maxWidth: "640px",
|
|
614
|
-
padding: "1rem",
|
|
615
|
-
border: "1px solid #fca5a5",
|
|
616
|
-
background: "#fee2e2",
|
|
617
|
-
color: "#991b1b",
|
|
618
|
-
borderRadius: "8px",
|
|
619
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
620
|
-
fontSize: "14px"
|
|
621
|
-
},
|
|
622
|
-
children: [
|
|
623
|
-
/* @__PURE__ */ jsx("div", {
|
|
624
|
-
style: { fontWeight: 600 },
|
|
625
|
-
children: err.title
|
|
626
|
-
}),
|
|
627
|
-
/* @__PURE__ */ jsx("pre", {
|
|
628
|
-
style: {
|
|
629
|
-
fontSize: "12px",
|
|
630
|
-
marginTop: "4px"
|
|
631
|
-
},
|
|
632
|
-
children: err.message
|
|
633
|
-
}),
|
|
634
|
-
/* @__PURE__ */ jsxs("div", {
|
|
635
|
-
style: { marginTop: "12px" },
|
|
636
|
-
children: [
|
|
637
|
-
/* @__PURE__ */ jsx("strong", { children: "What to try:" }),
|
|
638
|
-
" ",
|
|
639
|
-
err.hint
|
|
640
|
-
]
|
|
641
|
-
})
|
|
642
|
-
]
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Emit a one-time `console.warn` the first time a beta hook is
|
|
647
|
-
* used in a given page load. Bundles upgrading from a future GA
|
|
648
|
-
* release won't see the warning; the message lets us flag rough
|
|
649
|
-
* edges without breaking the build.
|
|
650
|
-
*/
|
|
651
|
-
const _warnedBeta = /* @__PURE__ */ new Set();
|
|
652
|
-
function warnBetaOnce(name) {
|
|
653
|
-
if (_warnedBeta.has(name)) return;
|
|
654
|
-
_warnedBeta.add(name);
|
|
655
|
-
if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[@oxy-hq/sdk] \`${name}\` is in beta — interface and behavior may change. See https://github.com/oxy-hq/customer-apps for caveats and the migration guide.`);
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Error thrown by all customer-app hooks when an API call returns a
|
|
659
|
-
* non-2xx response. Carries the structured `code` + `hint` the server
|
|
660
|
-
* emits so bundle UIs can render an actionable message instead of
|
|
661
|
-
* "404: { ...json... }".
|
|
662
|
-
*
|
|
663
|
-
* The server contract is documented in
|
|
664
|
-
* `crates/app/src/server/api/projects/agent_ask.rs` and
|
|
665
|
-
* `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
|
|
666
|
-
* Hooks that previously wrapped the raw text in `new Error()` now
|
|
667
|
-
* throw this type instead.
|
|
668
|
-
*/
|
|
669
|
-
var OxyApiError = class extends Error {
|
|
670
|
-
constructor(opts) {
|
|
671
|
-
const base = opts.message || `HTTP ${opts.status}`;
|
|
672
|
-
const code = opts.code ? ` [${opts.code}]` : "";
|
|
673
|
-
const hint = opts.hint ? `\n\n${opts.hint}` : "";
|
|
674
|
-
super(`${base}${code}${hint}`);
|
|
675
|
-
this.name = "OxyApiError";
|
|
676
|
-
this.status = opts.status;
|
|
677
|
-
this.code = opts.code ?? null;
|
|
678
|
-
this.hint = opts.hint ?? null;
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
/**
|
|
682
|
-
* Read a non-2xx response from oxy and return an `OxyApiError`.
|
|
683
|
-
* Parses the JSON envelope when present; falls back to raw text
|
|
684
|
-
* (truncated to 240 chars so a runaway HTML error page doesn't
|
|
685
|
-
* dominate the bundle UI).
|
|
686
|
-
*/
|
|
687
|
-
async function apiErrorFromResponse(resp) {
|
|
688
|
-
let body;
|
|
689
|
-
let raw = "";
|
|
690
|
-
try {
|
|
691
|
-
raw = await resp.text();
|
|
692
|
-
body = raw ? JSON.parse(raw) : void 0;
|
|
693
|
-
} catch {}
|
|
694
|
-
if (body && typeof body === "object") {
|
|
695
|
-
const b = body;
|
|
696
|
-
return new OxyApiError({
|
|
697
|
-
status: resp.status,
|
|
698
|
-
message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
|
|
699
|
-
code: typeof b.code === "string" ? b.code : null,
|
|
700
|
-
hint: typeof b.hint === "string" ? b.hint : null
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
|
|
704
|
-
return new OxyApiError({
|
|
705
|
-
status: resp.status,
|
|
706
|
-
message: snippet || `HTTP ${resp.status}`
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Read the resolved manifest from context. Throws if called outside
|
|
711
|
-
* `<OxyAppProvider>` — that's a programmer error worth surfacing
|
|
712
|
-
* loudly, not silently swallowing.
|
|
713
|
-
*/
|
|
714
|
-
function useResolvedManifest() {
|
|
715
|
-
const ctx = React.useContext(OxyAppContext);
|
|
716
|
-
if (!ctx) throw new Error("useResolvedManifest must be called inside <OxyAppProvider>");
|
|
717
|
-
if (ctx.status !== "ready" || !ctx.resolved) throw new Error("useResolvedManifest called before manifest finished loading. Use the provider's `fallback` prop to render while loading.");
|
|
718
|
-
return ctx.resolved;
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Low-level hook that returns the raw context value (including the
|
|
722
|
-
* fetcher). Prefer `useResolvedManifest` for manifest access; use
|
|
723
|
-
* this only when you need the fetcher or projectId without requiring
|
|
724
|
-
* the manifest to be ready (e.g. inside `useQuery`).
|
|
725
|
-
*/
|
|
726
|
-
function useOxyApp() {
|
|
727
|
-
const ctx = React.useContext(OxyAppContext);
|
|
728
|
-
if (!ctx) throw new Error("useOxyApp must be called inside <OxyAppProvider>");
|
|
729
|
-
return {
|
|
730
|
-
projectId: ctx.resolved?.projectId,
|
|
731
|
-
fetcher: ctx.fetcher
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Execute an ad-hoc SQL query against the project linked to this
|
|
736
|
-
* customer app. The query is specified inline by the caller; no
|
|
737
|
-
* manifest declaration is involved.
|
|
738
|
-
*
|
|
739
|
-
* Re-runs whenever `input` or enabled `params` change. Use the
|
|
740
|
-
* `enabled` option to defer the first fetch until required data is
|
|
741
|
-
* available (e.g. a user-supplied filter value).
|
|
742
|
-
*/
|
|
743
|
-
function useQuery(input, opts = {}) {
|
|
744
|
-
const { projectId, fetcher } = useOxyApp();
|
|
745
|
-
const enabled = opts.enabled !== false;
|
|
746
|
-
const paramsKey = JSON.stringify(opts.params);
|
|
747
|
-
const sqlWithParams = React.useMemo(() => interpolateSqlParams(input.sql, opts.params ?? {}), [input.sql, paramsKey]);
|
|
748
|
-
const [state, setState] = React.useState({
|
|
749
|
-
rows: [],
|
|
750
|
-
columns: [],
|
|
751
|
-
loading: enabled && !!projectId,
|
|
752
|
-
error: null
|
|
753
|
-
});
|
|
754
|
-
const [nonce, setNonce] = React.useState(0);
|
|
755
|
-
React.useEffect(() => {
|
|
756
|
-
if (!enabled || !projectId) {
|
|
757
|
-
setState((s) => s.loading ? {
|
|
758
|
-
...s,
|
|
759
|
-
loading: false
|
|
760
|
-
} : s);
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
const ctrl = new AbortController();
|
|
764
|
-
let cancelled = false;
|
|
765
|
-
setState((s) => ({
|
|
766
|
-
...s,
|
|
767
|
-
loading: true,
|
|
768
|
-
error: null
|
|
769
|
-
}));
|
|
770
|
-
const body = JSON.stringify({
|
|
771
|
-
sql: sqlWithParams,
|
|
772
|
-
...input.database ? { database: input.database } : {}
|
|
773
|
-
});
|
|
774
|
-
fetcher(`/api/projects/${projectId}/query`, {
|
|
775
|
-
method: "POST",
|
|
776
|
-
headers: { "content-type": "application/json" },
|
|
777
|
-
body,
|
|
778
|
-
signal: ctrl.signal
|
|
779
|
-
}).then(async (resp) => {
|
|
780
|
-
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
781
|
-
return resp.json();
|
|
782
|
-
}).then(({ columns, rows }) => {
|
|
783
|
-
if (cancelled) return;
|
|
784
|
-
setState({
|
|
785
|
-
rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
|
|
786
|
-
columns,
|
|
787
|
-
loading: false,
|
|
788
|
-
error: null
|
|
789
|
-
});
|
|
790
|
-
}).catch((err) => {
|
|
791
|
-
if (cancelled) return;
|
|
792
|
-
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
793
|
-
setState((s) => ({
|
|
794
|
-
...s,
|
|
795
|
-
loading: false,
|
|
796
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
797
|
-
}));
|
|
798
|
-
});
|
|
799
|
-
return () => {
|
|
800
|
-
cancelled = true;
|
|
801
|
-
ctrl.abort();
|
|
802
|
-
};
|
|
803
|
-
}, [
|
|
804
|
-
enabled,
|
|
805
|
-
projectId,
|
|
806
|
-
sqlWithParams,
|
|
807
|
-
input.database,
|
|
808
|
-
nonce,
|
|
809
|
-
fetcher
|
|
810
|
-
]);
|
|
811
|
-
return {
|
|
812
|
-
rows: state.rows,
|
|
813
|
-
columns: state.columns,
|
|
814
|
-
loading: state.loading,
|
|
815
|
-
error: state.error,
|
|
816
|
-
refetch: () => setNonce((n) => n + 1)
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Imperative hook for invoking an Oxy Function by name.
|
|
821
|
-
*
|
|
822
|
-
* ```tsx
|
|
823
|
-
* const refresh = useFunction("refresh-sales");
|
|
824
|
-
* <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
|
|
825
|
-
* Refresh
|
|
826
|
-
* </button>
|
|
827
|
-
* ```
|
|
828
|
-
*/
|
|
829
|
-
function useFunction(name) {
|
|
830
|
-
const ctx = React.useContext(OxyAppContext);
|
|
831
|
-
if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
|
|
832
|
-
const fetcher = ctx.fetcher;
|
|
833
|
-
const resolved = ctx.resolved;
|
|
834
|
-
const [state, setState] = React.useState({
|
|
835
|
-
data: null,
|
|
836
|
-
isLoading: false,
|
|
837
|
-
error: null,
|
|
838
|
-
logs: []
|
|
839
|
-
});
|
|
840
|
-
return {
|
|
841
|
-
invoke: React.useCallback(async (body, opts) => {
|
|
842
|
-
if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
|
|
843
|
-
const { orgSlug, appSlug, apiBaseUrl } = resolved;
|
|
844
|
-
const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
|
|
845
|
-
setState((s) => ({
|
|
846
|
-
...s,
|
|
847
|
-
isLoading: true,
|
|
848
|
-
error: null
|
|
849
|
-
}));
|
|
850
|
-
try {
|
|
851
|
-
const headers = {
|
|
852
|
-
"content-type": "application/json",
|
|
853
|
-
accept: "text/event-stream"
|
|
854
|
-
};
|
|
855
|
-
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
856
|
-
const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
|
|
857
|
-
const resp = await fetcher(url, {
|
|
858
|
-
method: "POST",
|
|
859
|
-
headers,
|
|
860
|
-
body: JSON.stringify(body ?? {})
|
|
861
|
-
});
|
|
862
|
-
if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
|
|
863
|
-
return readFunctionSseStream(resp);
|
|
864
|
-
});
|
|
865
|
-
setState({
|
|
866
|
-
data: result.value,
|
|
867
|
-
isLoading: false,
|
|
868
|
-
error: null,
|
|
869
|
-
logs: result.logs
|
|
870
|
-
});
|
|
871
|
-
return result.value;
|
|
872
|
-
} catch (err) {
|
|
873
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
874
|
-
const logs = e.logs ?? [];
|
|
875
|
-
setState((s) => ({
|
|
876
|
-
...s,
|
|
877
|
-
isLoading: false,
|
|
878
|
-
error: e,
|
|
879
|
-
logs
|
|
880
|
-
}));
|
|
881
|
-
throw e;
|
|
882
|
-
}
|
|
883
|
-
}, [
|
|
884
|
-
resolved,
|
|
885
|
-
fetcher,
|
|
886
|
-
name
|
|
887
|
-
]),
|
|
888
|
-
data: state.data,
|
|
889
|
-
isLoading: state.isLoading,
|
|
890
|
-
error: state.error,
|
|
891
|
-
logs: state.logs
|
|
892
|
-
};
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* Run a semantic-layer query against the project's `.view.yml` /
|
|
896
|
-
* `.topic.yml` definitions. The server compiles to SQL and executes
|
|
897
|
-
* through the same connector path as `useQuery`, so result shape
|
|
898
|
-
* matches.
|
|
899
|
-
*
|
|
900
|
-
* Re-runs whenever the input shape changes (deep-compared via JSON).
|
|
901
|
-
* Use `opts.enabled = false` to defer the first fetch until required
|
|
902
|
-
* inputs (e.g. a user-picked filter value) are available.
|
|
903
|
-
*/
|
|
904
|
-
function useSemanticQuery(input, opts = {}) {
|
|
905
|
-
const { projectId, fetcher } = useOxyApp();
|
|
906
|
-
const enabled = opts.enabled !== false;
|
|
907
|
-
const debug = opts.debug === true;
|
|
908
|
-
const inputKey = React.useMemo(() => JSON.stringify(input), [input]);
|
|
909
|
-
const [state, setState] = React.useState({
|
|
910
|
-
rows: [],
|
|
911
|
-
columns: [],
|
|
912
|
-
truncated: false,
|
|
913
|
-
sql: null,
|
|
914
|
-
loading: enabled && !!projectId,
|
|
915
|
-
error: null
|
|
916
|
-
});
|
|
917
|
-
const [nonce, setNonce] = React.useState(0);
|
|
918
|
-
React.useEffect(() => {
|
|
919
|
-
if (!enabled || !projectId) {
|
|
920
|
-
setState((s) => s.loading ? {
|
|
921
|
-
...s,
|
|
922
|
-
loading: false
|
|
923
|
-
} : s);
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
const ctrl = new AbortController();
|
|
927
|
-
let cancelled = false;
|
|
928
|
-
setState((s) => ({
|
|
929
|
-
...s,
|
|
930
|
-
loading: true,
|
|
931
|
-
error: null
|
|
932
|
-
}));
|
|
933
|
-
const body = JSON.stringify({
|
|
934
|
-
v: 1,
|
|
935
|
-
topic: input.topic,
|
|
936
|
-
dimensions: input.dimensions ?? [],
|
|
937
|
-
measures: input.measures ?? [],
|
|
938
|
-
time_dimensions: input.time_dimensions ?? [],
|
|
939
|
-
filters: input.filters ?? [],
|
|
940
|
-
...input.limit != null ? { limit: input.limit } : {}
|
|
941
|
-
});
|
|
942
|
-
fetcher(`/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`, {
|
|
943
|
-
method: "POST",
|
|
944
|
-
headers: { "content-type": "application/json" },
|
|
945
|
-
body,
|
|
946
|
-
signal: ctrl.signal
|
|
947
|
-
}).then(async (resp) => {
|
|
948
|
-
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
949
|
-
return resp.json();
|
|
950
|
-
}).then(({ columns, rows, truncated, sql }) => {
|
|
951
|
-
if (cancelled) return;
|
|
952
|
-
setState({
|
|
953
|
-
rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
|
|
954
|
-
columns,
|
|
955
|
-
truncated,
|
|
956
|
-
sql: sql ?? null,
|
|
957
|
-
loading: false,
|
|
958
|
-
error: null
|
|
959
|
-
});
|
|
960
|
-
}).catch((err) => {
|
|
961
|
-
if (cancelled) return;
|
|
962
|
-
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
963
|
-
setState((s) => ({
|
|
964
|
-
...s,
|
|
965
|
-
loading: false,
|
|
966
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
967
|
-
}));
|
|
968
|
-
});
|
|
969
|
-
return () => {
|
|
970
|
-
cancelled = true;
|
|
971
|
-
ctrl.abort();
|
|
972
|
-
};
|
|
973
|
-
}, [
|
|
974
|
-
enabled,
|
|
975
|
-
projectId,
|
|
976
|
-
inputKey,
|
|
977
|
-
debug,
|
|
978
|
-
nonce,
|
|
979
|
-
fetcher
|
|
980
|
-
]);
|
|
981
|
-
return {
|
|
982
|
-
rows: state.rows,
|
|
983
|
-
columns: state.columns,
|
|
984
|
-
truncated: state.truncated,
|
|
985
|
-
sql: state.sql,
|
|
986
|
-
loading: state.loading,
|
|
987
|
-
error: state.error,
|
|
988
|
-
refetch: () => setNonce((n) => n + 1)
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
const PROCEDURE_POLL_MS = 2e3;
|
|
992
|
-
const PROCEDURE_BACKOFF_MS = 5e3;
|
|
993
|
-
const PROCEDURE_MAX_WAIT_MS = 3600 * 1e3;
|
|
994
|
-
/**
|
|
995
|
-
* @beta Long-running procedure runner. The wire shape works end-to-end
|
|
996
|
-
* (start → poll → cancel; runs survive server restarts via the
|
|
997
|
-
* `customer_app_procedure_runs` table) but a few rough edges remain
|
|
998
|
-
* before this is GA-ready:
|
|
999
|
-
*
|
|
1000
|
-
* - Hint surfaces for `procedure_not_found` are correct but the
|
|
1001
|
-
* procedure-discovery rules (which directories the server scans,
|
|
1002
|
-
* case-sensitivity, branch awareness) aren't documented yet.
|
|
1003
|
-
* - Cancellation across multi-instance deployments leans on a
|
|
1004
|
-
* periodic sweep — fine for now, but expect occasional latency
|
|
1005
|
-
* between `cancel()` and the run actually stopping.
|
|
1006
|
-
* - Progress reporting requires the procedure to emit named
|
|
1007
|
-
* steps; bundles get `progress: null` until that lands.
|
|
1008
|
-
*
|
|
1009
|
-
* The API surface is stable; expect breaking changes only if the
|
|
1010
|
-
* server-side `customer_app_procedure_runs` schema changes.
|
|
1011
|
-
*/
|
|
1012
|
-
function useProcedureRun(input, opts = {}) {
|
|
1013
|
-
warnBetaOnce("useProcedureRun");
|
|
1014
|
-
const { projectId, fetcher } = useOxyApp();
|
|
1015
|
-
const pollMs = opts.pollIntervalMs ?? PROCEDURE_POLL_MS;
|
|
1016
|
-
const backoffMs = opts.pollIntervalBackoffMs ?? PROCEDURE_BACKOFF_MS;
|
|
1017
|
-
const maxWaitMs = opts.maxWaitMs ?? PROCEDURE_MAX_WAIT_MS;
|
|
1018
|
-
const [state, setState] = React.useState({
|
|
1019
|
-
state: "idle",
|
|
1020
|
-
progress: null,
|
|
1021
|
-
result: null,
|
|
1022
|
-
error: null
|
|
1023
|
-
});
|
|
1024
|
-
const inflight = React.useRef({});
|
|
1025
|
-
const cancel = React.useCallback(() => {
|
|
1026
|
-
const runId = inflight.current.runId;
|
|
1027
|
-
if (!projectId || !runId) return;
|
|
1028
|
-
inflight.current.abort?.abort();
|
|
1029
|
-
inflight.current.runId = void 0;
|
|
1030
|
-
fetcher(`/api/projects/${projectId}/procedures/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" }).catch(() => {});
|
|
1031
|
-
setState({
|
|
1032
|
-
state: "failed",
|
|
1033
|
-
progress: null,
|
|
1034
|
-
result: null,
|
|
1035
|
-
error: /* @__PURE__ */ new Error("procedure cancelled by user")
|
|
1036
|
-
});
|
|
1037
|
-
}, [projectId, fetcher]);
|
|
1038
|
-
const run = React.useCallback((params) => {
|
|
1039
|
-
if (!projectId) {
|
|
1040
|
-
setState((s) => ({
|
|
1041
|
-
...s,
|
|
1042
|
-
state: "failed",
|
|
1043
|
-
error: /* @__PURE__ */ new Error("project not configured")
|
|
1044
|
-
}));
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
inflight.current.abort?.abort();
|
|
1048
|
-
const ctrl = new AbortController();
|
|
1049
|
-
inflight.current = { abort: ctrl };
|
|
1050
|
-
setState({
|
|
1051
|
-
state: "running",
|
|
1052
|
-
progress: null,
|
|
1053
|
-
result: null,
|
|
1054
|
-
error: null
|
|
1055
|
-
});
|
|
1056
|
-
(async () => {
|
|
1057
|
-
try {
|
|
1058
|
-
const body = JSON.stringify({
|
|
1059
|
-
v: 1,
|
|
1060
|
-
...params ? { params } : {}
|
|
1061
|
-
});
|
|
1062
|
-
const startResp = await fetcher(`/api/projects/${projectId}/procedures/${encodeURIComponent(input.procedureId)}/runs`, {
|
|
1063
|
-
method: "POST",
|
|
1064
|
-
headers: { "content-type": "application/json" },
|
|
1065
|
-
body,
|
|
1066
|
-
signal: ctrl.signal
|
|
1067
|
-
});
|
|
1068
|
-
if (!startResp.ok) throw await apiErrorFromResponse(startResp);
|
|
1069
|
-
const { run_id } = await startResp.json();
|
|
1070
|
-
inflight.current.runId = run_id;
|
|
1071
|
-
const startedAt = Date.now();
|
|
1072
|
-
let pollCount = 0;
|
|
1073
|
-
while (true) {
|
|
1074
|
-
if (ctrl.signal.aborted) return;
|
|
1075
|
-
if (Date.now() - startedAt > maxWaitMs) throw new Error("procedure run timed out client-side");
|
|
1076
|
-
await sleep(pollCount < 6 ? pollMs : backoffMs, ctrl.signal);
|
|
1077
|
-
if (ctrl.signal.aborted) return;
|
|
1078
|
-
pollCount += 1;
|
|
1079
|
-
const pollResp = await fetcher(`/api/projects/${projectId}/procedures/runs/${encodeURIComponent(run_id)}`, {
|
|
1080
|
-
method: "GET",
|
|
1081
|
-
signal: ctrl.signal
|
|
1082
|
-
});
|
|
1083
|
-
if (!pollResp.ok) throw await apiErrorFromResponse(pollResp);
|
|
1084
|
-
const poll = await pollResp.json();
|
|
1085
|
-
if (poll.status === "running") {
|
|
1086
|
-
if (poll.progress) setState((s) => ({
|
|
1087
|
-
...s,
|
|
1088
|
-
progress: poll.progress ?? null
|
|
1089
|
-
}));
|
|
1090
|
-
continue;
|
|
1091
|
-
}
|
|
1092
|
-
if (poll.status === "done") {
|
|
1093
|
-
inflight.current.runId = void 0;
|
|
1094
|
-
setState({
|
|
1095
|
-
state: "done",
|
|
1096
|
-
progress: null,
|
|
1097
|
-
result: poll.result,
|
|
1098
|
-
error: null
|
|
1099
|
-
});
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
if (poll.status === "cancelled") {
|
|
1103
|
-
inflight.current.runId = void 0;
|
|
1104
|
-
setState({
|
|
1105
|
-
state: "failed",
|
|
1106
|
-
progress: null,
|
|
1107
|
-
result: null,
|
|
1108
|
-
error: /* @__PURE__ */ new Error("procedure cancelled")
|
|
1109
|
-
});
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
inflight.current.runId = void 0;
|
|
1113
|
-
setState({
|
|
1114
|
-
state: "failed",
|
|
1115
|
-
progress: null,
|
|
1116
|
-
result: null,
|
|
1117
|
-
error: new Error(poll.error.message)
|
|
1118
|
-
});
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
} catch (e) {
|
|
1122
|
-
if (e instanceof DOMException && e.name === "AbortError") return;
|
|
1123
|
-
inflight.current.runId = void 0;
|
|
1124
|
-
setState({
|
|
1125
|
-
state: "failed",
|
|
1126
|
-
progress: null,
|
|
1127
|
-
result: null,
|
|
1128
|
-
error: e instanceof Error ? e : new Error(String(e))
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
})();
|
|
1132
|
-
}, [
|
|
1133
|
-
projectId,
|
|
1134
|
-
fetcher,
|
|
1135
|
-
input.procedureId,
|
|
1136
|
-
pollMs,
|
|
1137
|
-
backoffMs,
|
|
1138
|
-
maxWaitMs
|
|
1139
|
-
]);
|
|
1140
|
-
React.useEffect(() => {
|
|
1141
|
-
return () => {
|
|
1142
|
-
inflight.current.abort?.abort();
|
|
1143
|
-
};
|
|
1144
|
-
}, []);
|
|
1145
|
-
return {
|
|
1146
|
-
state: state.state,
|
|
1147
|
-
run,
|
|
1148
|
-
cancel,
|
|
1149
|
-
progress: state.progress,
|
|
1150
|
-
result: state.result,
|
|
1151
|
-
error: state.error
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
function useAgentRun(input) {
|
|
1155
|
-
const { projectId, fetcher } = useOxyApp();
|
|
1156
|
-
const [state, setState] = React.useState({
|
|
1157
|
-
state: "idle",
|
|
1158
|
-
events: [],
|
|
1159
|
-
artifacts: [],
|
|
1160
|
-
answer: null,
|
|
1161
|
-
clarification: null,
|
|
1162
|
-
threadId: null,
|
|
1163
|
-
threadUrl: null,
|
|
1164
|
-
error: null
|
|
1165
|
-
});
|
|
1166
|
-
const inflight = React.useRef({});
|
|
1167
|
-
const cancel = React.useCallback(() => {
|
|
1168
|
-
const runId = inflight.current.runId;
|
|
1169
|
-
if (!projectId || !runId) return;
|
|
1170
|
-
inflight.current.abort?.abort();
|
|
1171
|
-
inflight.current.runId = void 0;
|
|
1172
|
-
fetcher(`/api/projects/${projectId}/agents/asks/${encodeURIComponent(runId)}/cancel`, { method: "POST" }).catch(() => {});
|
|
1173
|
-
setState((s) => ({
|
|
1174
|
-
...s,
|
|
1175
|
-
state: "failed",
|
|
1176
|
-
error: /* @__PURE__ */ new Error("agent run cancelled by user")
|
|
1177
|
-
}));
|
|
1178
|
-
}, [projectId, fetcher]);
|
|
1179
|
-
const ask = React.useCallback((question, opts = {}) => {
|
|
1180
|
-
if (!projectId) {
|
|
1181
|
-
setState((s) => ({
|
|
1182
|
-
...s,
|
|
1183
|
-
state: "failed",
|
|
1184
|
-
error: /* @__PURE__ */ new Error("project not configured")
|
|
1185
|
-
}));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
inflight.current.abort?.abort();
|
|
1189
|
-
const ctrl = new AbortController();
|
|
1190
|
-
inflight.current = { abort: ctrl };
|
|
1191
|
-
setState({
|
|
1192
|
-
state: "running",
|
|
1193
|
-
events: [],
|
|
1194
|
-
artifacts: [],
|
|
1195
|
-
answer: null,
|
|
1196
|
-
clarification: null,
|
|
1197
|
-
threadId: opts.threadId ?? null,
|
|
1198
|
-
threadUrl: opts.threadId ? `/threads/${opts.threadId}` : null,
|
|
1199
|
-
error: null
|
|
1200
|
-
});
|
|
1201
|
-
(async () => {
|
|
1202
|
-
try {
|
|
1203
|
-
const body = JSON.stringify({
|
|
1204
|
-
v: 1,
|
|
1205
|
-
question,
|
|
1206
|
-
...opts.threadId ? { thread_id: opts.threadId } : {}
|
|
1207
|
-
});
|
|
1208
|
-
const startResp = await fetcher(`/api/projects/${projectId}/agents/${encodeURIComponent(input.agentId)}/asks`, {
|
|
1209
|
-
method: "POST",
|
|
1210
|
-
headers: { "content-type": "application/json" },
|
|
1211
|
-
body,
|
|
1212
|
-
signal: ctrl.signal
|
|
1213
|
-
});
|
|
1214
|
-
if (!startResp.ok) throw await apiErrorFromResponse(startResp);
|
|
1215
|
-
const { run_id, thread_id, thread_url } = await startResp.json();
|
|
1216
|
-
inflight.current.runId = run_id;
|
|
1217
|
-
setState((s) => ({
|
|
1218
|
-
...s,
|
|
1219
|
-
threadId: thread_id,
|
|
1220
|
-
threadUrl: thread_url ?? `/threads/${thread_id}`
|
|
1221
|
-
}));
|
|
1222
|
-
let lastEventId = "";
|
|
1223
|
-
let attempts = 0;
|
|
1224
|
-
let terminated = false;
|
|
1225
|
-
while (true) {
|
|
1226
|
-
if (ctrl.signal.aborted) return;
|
|
1227
|
-
if (terminated) return;
|
|
1228
|
-
attempts += 1;
|
|
1229
|
-
try {
|
|
1230
|
-
await consumeSseStream({
|
|
1231
|
-
url: `/api/projects/${projectId}/agents/runs/${encodeURIComponent(run_id)}/events`,
|
|
1232
|
-
fetcher,
|
|
1233
|
-
signal: ctrl.signal,
|
|
1234
|
-
lastEventId,
|
|
1235
|
-
onEvent: (ev) => {
|
|
1236
|
-
if (ev.id) lastEventId = ev.id;
|
|
1237
|
-
const data = parseSseData(ev.data);
|
|
1238
|
-
const eventType = ev.event || "message";
|
|
1239
|
-
const artifact = extractSqlArtifact(eventType, ev.id, data);
|
|
1240
|
-
const token = eventType === "text_delta" && typeof data === "object" && data !== null && "token" in data ? String(data.token) : null;
|
|
1241
|
-
setState((s) => ({
|
|
1242
|
-
...s,
|
|
1243
|
-
events: [...s.events, {
|
|
1244
|
-
type: eventType,
|
|
1245
|
-
data
|
|
1246
|
-
}],
|
|
1247
|
-
artifacts: artifact ? [...s.artifacts, artifact] : s.artifacts,
|
|
1248
|
-
answer: token !== null ? (s.answer ?? "") + token : s.answer
|
|
1249
|
-
}));
|
|
1250
|
-
if (ev.event === "done") {
|
|
1251
|
-
terminated = true;
|
|
1252
|
-
inflight.current.runId = void 0;
|
|
1253
|
-
setState((s) => ({
|
|
1254
|
-
...s,
|
|
1255
|
-
state: "done"
|
|
1256
|
-
}));
|
|
1257
|
-
} else if (ev.event === "failed" || ev.event === "error" || ev.event === "cancelled") {
|
|
1258
|
-
terminated = true;
|
|
1259
|
-
inflight.current.runId = void 0;
|
|
1260
|
-
const message = typeof data === "object" && data !== null && "message" in data ? String(data.message) : `agent run ${ev.event}`;
|
|
1261
|
-
setState((s) => ({
|
|
1262
|
-
...s,
|
|
1263
|
-
state: "failed",
|
|
1264
|
-
error: new Error(message)
|
|
1265
|
-
}));
|
|
1266
|
-
} else if (ev.event === "ask_user") {
|
|
1267
|
-
terminated = true;
|
|
1268
|
-
const clarification = typeof data === "object" && data !== null && "question" in data ? String(data.question) : "Agent needs clarification.";
|
|
1269
|
-
setState((s) => ({
|
|
1270
|
-
...s,
|
|
1271
|
-
state: "needs_clarification",
|
|
1272
|
-
clarification
|
|
1273
|
-
}));
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1277
|
-
} catch (err) {
|
|
1278
|
-
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
1279
|
-
if (attempts >= 5) {
|
|
1280
|
-
inflight.current.runId = void 0;
|
|
1281
|
-
setState((s) => ({
|
|
1282
|
-
...s,
|
|
1283
|
-
state: "failed",
|
|
1284
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
1285
|
-
}));
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
if (terminated) return;
|
|
1290
|
-
await sleep(1e3, ctrl.signal);
|
|
1291
|
-
}
|
|
1292
|
-
} catch (e) {
|
|
1293
|
-
if (e instanceof DOMException && e.name === "AbortError") return;
|
|
1294
|
-
inflight.current.runId = void 0;
|
|
1295
|
-
setState((s) => ({
|
|
1296
|
-
...s,
|
|
1297
|
-
state: "failed",
|
|
1298
|
-
error: e instanceof Error ? e : new Error(String(e))
|
|
1299
|
-
}));
|
|
1300
|
-
}
|
|
1301
|
-
})();
|
|
1302
|
-
}, [
|
|
1303
|
-
projectId,
|
|
1304
|
-
fetcher,
|
|
1305
|
-
input.agentId
|
|
1306
|
-
]);
|
|
1307
|
-
React.useEffect(() => {
|
|
1308
|
-
return () => {
|
|
1309
|
-
inflight.current.abort?.abort();
|
|
1310
|
-
};
|
|
1311
|
-
}, []);
|
|
1312
|
-
return {
|
|
1313
|
-
state: state.state,
|
|
1314
|
-
ask,
|
|
1315
|
-
cancel,
|
|
1316
|
-
events: state.events,
|
|
1317
|
-
artifacts: state.artifacts,
|
|
1318
|
-
answer: state.answer,
|
|
1319
|
-
clarification: state.clarification,
|
|
1320
|
-
threadId: state.threadId,
|
|
1321
|
-
threadUrl: state.threadUrl,
|
|
1322
|
-
error: state.error
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
/** Parsed JSON payload of an SSE `data:` line, falling back to raw
|
|
1326
|
-
* string on parse failure. */
|
|
1327
|
-
function parseSseData(raw) {
|
|
1328
|
-
try {
|
|
1329
|
-
return JSON.parse(raw);
|
|
1330
|
-
} catch {
|
|
1331
|
-
return raw;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
/** UI event types in the analytics taxonomy that carry SQL the bundle
|
|
1335
|
-
* may want to render alongside the answer. Each carries the same
|
|
1336
|
-
* shape (`query` / `columns` / `rows` / `success`) so we can parse
|
|
1337
|
-
* uniformly. New types added upstream don't surface as artifacts
|
|
1338
|
-
* until added here — that's intentional, the renderer needs to
|
|
1339
|
-
* know how to display each. */
|
|
1340
|
-
const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1341
|
-
"query_executed",
|
|
1342
|
-
"query_generated",
|
|
1343
|
-
"verified_sql",
|
|
1344
|
-
"semantic_query",
|
|
1345
|
-
"omni_query"
|
|
1346
|
-
]);
|
|
1347
|
-
/** Extract a SQL artifact from a single SSE event when the type +
|
|
1348
|
-
* payload shape match. Returns `null` for events that aren't SQL
|
|
1349
|
-
* carriers or whose payload doesn't include the expected fields. */
|
|
1350
|
-
function extractSqlArtifact(eventType, eventId, data) {
|
|
1351
|
-
if (!SQL_EVENT_TYPES.has(eventType)) return null;
|
|
1352
|
-
if (typeof data !== "object" || data === null) return null;
|
|
1353
|
-
const obj = data;
|
|
1354
|
-
const sql = typeof obj.query === "string" ? obj.query : typeof obj.sql === "string" ? obj.sql : null;
|
|
1355
|
-
if (!sql) return null;
|
|
1356
|
-
const columns = Array.isArray(obj.columns) ? obj.columns.map(String) : void 0;
|
|
1357
|
-
const rows = Array.isArray(obj.rows) ? obj.rows : void 0;
|
|
1358
|
-
const rowCount = typeof obj.row_count === "number" ? obj.row_count : typeof obj.rowCount === "number" ? obj.rowCount : rows?.length;
|
|
1359
|
-
const artifact = {
|
|
1360
|
-
type: "sql",
|
|
1361
|
-
id: eventId || `${eventType}-${sql.slice(0, 32)}`,
|
|
1362
|
-
source: eventType,
|
|
1363
|
-
sql
|
|
1364
|
-
};
|
|
1365
|
-
if (columns && rows) artifact.results = {
|
|
1366
|
-
columns,
|
|
1367
|
-
rows,
|
|
1368
|
-
rowCount: rowCount ?? rows.length
|
|
1369
|
-
};
|
|
1370
|
-
const errMsg = typeof obj.error === "string" ? obj.error : obj.success === false && typeof obj.message === "string" ? obj.message : void 0;
|
|
1371
|
-
if (errMsg) artifact.error = errMsg;
|
|
1372
|
-
return artifact;
|
|
1373
|
-
}
|
|
1374
|
-
/**
|
|
1375
|
-
* Hand-rolled SSE consumer using `fetch` + `ReadableStream`. We use
|
|
1376
|
-
* this in place of `EventSource` because:
|
|
1377
|
-
* 1. EventSource doesn't expose the connection's `Last-Event-ID`
|
|
1378
|
-
* header in a way you can control. The browser tracks it
|
|
1379
|
-
* internally but you can't pass a starting value, so a hook
|
|
1380
|
-
* that wants to resume after a tab switch / network blip has
|
|
1381
|
-
* no way to ask the server to replay from a known point.
|
|
1382
|
-
* 2. EventSource can't pass `Authorization` / other custom
|
|
1383
|
-
* headers — only `withCredentials` for cookies. Fine today,
|
|
1384
|
-
* but couples us to cookie auth forever.
|
|
1385
|
-
*
|
|
1386
|
-
* The parser handles the message-block model from the SSE spec
|
|
1387
|
-
* verbatim: lines split by `\n` (or `\r\n` / `\r`), event blocks
|
|
1388
|
-
* separated by blank lines, `id:` / `event:` / `data:` fields
|
|
1389
|
-
* accumulated per block. Multiple `data:` lines concatenate with
|
|
1390
|
-
* `\n` (per spec) — we honor that even though the server emits
|
|
1391
|
-
* single-line data today.
|
|
1392
|
-
*
|
|
1393
|
-
* Throws on network error or non-2xx. Returns when the stream ends
|
|
1394
|
-
* normally (server closed connection cleanly).
|
|
1395
|
-
*/
|
|
1396
|
-
async function consumeSseStream(opts) {
|
|
1397
|
-
const headers = {
|
|
1398
|
-
accept: "text/event-stream",
|
|
1399
|
-
"cache-control": "no-cache"
|
|
1400
|
-
};
|
|
1401
|
-
if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
|
|
1402
|
-
const resp = await opts.fetcher(opts.url, {
|
|
1403
|
-
method: "GET",
|
|
1404
|
-
headers,
|
|
1405
|
-
signal: opts.signal
|
|
1406
|
-
});
|
|
1407
|
-
if (!resp.ok) throw await apiErrorFromResponse(resp);
|
|
1408
|
-
if (!resp.body) throw new Error("SSE response has no body");
|
|
1409
|
-
const reader = resp.body.getReader();
|
|
1410
|
-
const decoder = new TextDecoder();
|
|
1411
|
-
let buffer = "";
|
|
1412
|
-
let currentId = "";
|
|
1413
|
-
let currentEvent = "message";
|
|
1414
|
-
let currentData = [];
|
|
1415
|
-
const dispatch = () => {
|
|
1416
|
-
if (currentData.length === 0 && currentEvent === "message" && !currentId) return;
|
|
1417
|
-
opts.onEvent({
|
|
1418
|
-
id: currentId,
|
|
1419
|
-
event: currentEvent,
|
|
1420
|
-
data: currentData.join("\n")
|
|
1421
|
-
});
|
|
1422
|
-
currentEvent = "message";
|
|
1423
|
-
currentData = [];
|
|
1424
|
-
};
|
|
1425
|
-
while (true) {
|
|
1426
|
-
const { value, done } = await reader.read();
|
|
1427
|
-
if (done) break;
|
|
1428
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1429
|
-
let nlIndex;
|
|
1430
|
-
while ((nlIndex = buffer.search(/\r\n|\r|\n/)) !== -1) {
|
|
1431
|
-
if (nlIndex === buffer.length - 1 && buffer[nlIndex] === "\r") break;
|
|
1432
|
-
const line = buffer.slice(0, nlIndex);
|
|
1433
|
-
const sep = buffer.slice(nlIndex, nlIndex + 2);
|
|
1434
|
-
buffer = buffer.slice(nlIndex + (sep === "\r\n" ? 2 : 1));
|
|
1435
|
-
if (line === "") {
|
|
1436
|
-
dispatch();
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
if (line.startsWith(":")) continue;
|
|
1440
|
-
const colonAt = line.indexOf(":");
|
|
1441
|
-
const field = colonAt === -1 ? line : line.slice(0, colonAt);
|
|
1442
|
-
let value = colonAt === -1 ? "" : line.slice(colonAt + 1);
|
|
1443
|
-
if (value.startsWith(" ")) value = value.slice(1);
|
|
1444
|
-
switch (field) {
|
|
1445
|
-
case "id":
|
|
1446
|
-
currentId = value;
|
|
1447
|
-
break;
|
|
1448
|
-
case "event":
|
|
1449
|
-
currentEvent = value;
|
|
1450
|
-
break;
|
|
1451
|
-
case "data":
|
|
1452
|
-
currentData.push(value);
|
|
1453
|
-
break;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (currentData.length > 0) dispatch();
|
|
1458
|
-
}
|
|
1459
|
-
function sleep(ms, signal) {
|
|
1460
|
-
return new Promise((resolve, reject) => {
|
|
1461
|
-
const t = setTimeout(resolve, ms);
|
|
1462
|
-
signal?.addEventListener("abort", () => {
|
|
1463
|
-
clearTimeout(t);
|
|
1464
|
-
reject(new DOMException("aborted", "AbortError"));
|
|
1465
|
-
}, { once: true });
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
/**
|
|
1469
|
-
* Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
|
|
1470
|
-
* `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
|
|
1471
|
-
* (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
|
|
1472
|
-
* grouped by name, with drill-down into recent occurrences.
|
|
1473
|
-
*
|
|
1474
|
-
* The handler returned by [`useTrackEvent`] is **fire-and-forget**:
|
|
1475
|
-
* it enqueues the event into an in-memory batch flushed every second
|
|
1476
|
-
* (and on `pagehide` so a navigation away doesn't drop the tail).
|
|
1477
|
-
* No await semantics — call it inline from a click handler without
|
|
1478
|
-
* awaiting it. Server-side validation errors are logged to the
|
|
1479
|
-
* console; the call site doesn't need to handle them.
|
|
1480
|
-
*
|
|
1481
|
-
* Example:
|
|
1482
|
-
* ```tsx
|
|
1483
|
-
* const track = useTrackEvent();
|
|
1484
|
-
* <button
|
|
1485
|
-
* onClick={() => {
|
|
1486
|
-
* track("export-clicked", { format: "csv", rowCount });
|
|
1487
|
-
* doExport();
|
|
1488
|
-
* }}
|
|
1489
|
-
* >Export</button>
|
|
1490
|
-
* ```
|
|
1491
|
-
*
|
|
1492
|
-
* Rate-limited at 60/min per (user, app) on the server. A burst that
|
|
1493
|
-
* trips the limit drops the excess events with a console warning;
|
|
1494
|
-
* within-limit events are unaffected.
|
|
1495
|
-
*/
|
|
1496
|
-
function useTrackEvent() {
|
|
1497
|
-
const { projectId, fetcher } = useOxyApp();
|
|
1498
|
-
const queueRef = React.useRef([]);
|
|
1499
|
-
const flushTimerRef = React.useRef(null);
|
|
1500
|
-
const flush = React.useCallback(() => {
|
|
1501
|
-
flushTimerRef.current = null;
|
|
1502
|
-
if (!projectId) return;
|
|
1503
|
-
const batch = queueRef.current;
|
|
1504
|
-
if (batch.length === 0) return;
|
|
1505
|
-
queueRef.current = [];
|
|
1506
|
-
for (const evt of batch) {
|
|
1507
|
-
const url = `/api/customer-apps/${projectId}/events`;
|
|
1508
|
-
const body = JSON.stringify(evt);
|
|
1509
|
-
try {
|
|
1510
|
-
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
|
1511
|
-
else fetcher(url, {
|
|
1512
|
-
method: "POST",
|
|
1513
|
-
headers: { "content-type": "application/json" },
|
|
1514
|
-
body
|
|
1515
|
-
}).catch((e) => {
|
|
1516
|
-
console.warn("[oxy] useTrackEvent flush failed:", e);
|
|
1517
|
-
});
|
|
1518
|
-
} catch (e) {
|
|
1519
|
-
console.warn("[oxy] useTrackEvent enqueue failed:", e);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
}, [projectId, fetcher]);
|
|
1523
|
-
React.useEffect(() => {
|
|
1524
|
-
if (typeof window === "undefined") return;
|
|
1525
|
-
const onHide = () => flush();
|
|
1526
|
-
window.addEventListener("pagehide", onHide);
|
|
1527
|
-
return () => {
|
|
1528
|
-
window.removeEventListener("pagehide", onHide);
|
|
1529
|
-
flush();
|
|
1530
|
-
if (flushTimerRef.current !== null) {
|
|
1531
|
-
clearTimeout(flushTimerRef.current);
|
|
1532
|
-
flushTimerRef.current = null;
|
|
1533
|
-
}
|
|
1534
|
-
};
|
|
1535
|
-
}, [flush]);
|
|
1536
|
-
return React.useCallback((name, payload) => {
|
|
1537
|
-
queueRef.current.push({
|
|
1538
|
-
event_name: name,
|
|
1539
|
-
payload: payload ?? {}
|
|
1540
|
-
});
|
|
1541
|
-
if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
|
|
1542
|
-
}, [flush]);
|
|
1543
|
-
}
|
|
1544
|
-
/**
|
|
1545
|
-
* Renders an agent run's answer + artifacts + thread link as a
|
|
1546
|
-
* single block. The default styling is intentionally neutral
|
|
1547
|
-
* (system fonts, gray surfaces) so it blends into any bundle.
|
|
1548
|
-
*
|
|
1549
|
-
* Designed to be paired with `useAgentRun`:
|
|
1550
|
-
*
|
|
1551
|
-
* ```tsx
|
|
1552
|
-
* const run = useAgentRun({ agentId: "analyst" });
|
|
1553
|
-
* return (
|
|
1554
|
-
* <>
|
|
1555
|
-
* <button onClick={() => run.ask("how many users last week?")}>Ask</button>
|
|
1556
|
-
* <OxyAnswer {...run} />
|
|
1557
|
-
* </>
|
|
1558
|
-
* );
|
|
1559
|
-
* ```
|
|
1560
|
-
*/
|
|
1561
|
-
function OxyAnswer(props) {
|
|
1562
|
-
const { answer, artifacts = [], state, clarification, error, threadUrl, threadLinkLabel = "Continue this thread in Oxy", maxArtifactRows = 10, className } = props;
|
|
1563
|
-
const isRunning = state === "running";
|
|
1564
|
-
const isFailed = state === "failed";
|
|
1565
|
-
const needsClarification = state === "needs_clarification";
|
|
1566
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1567
|
-
className,
|
|
1568
|
-
style: styles.answerWrap,
|
|
1569
|
-
children: [
|
|
1570
|
-
isRunning && answer === null ? /* @__PURE__ */ jsxs("div", {
|
|
1571
|
-
style: styles.statusRow,
|
|
1572
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
1573
|
-
style: styles.spinner,
|
|
1574
|
-
"aria-hidden": "true"
|
|
1575
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
1576
|
-
style: styles.statusText,
|
|
1577
|
-
children: "Thinking…"
|
|
1578
|
-
})]
|
|
1579
|
-
}) : null,
|
|
1580
|
-
artifacts.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
1581
|
-
style: styles.artifactList,
|
|
1582
|
-
children: artifacts.map((a) => /* @__PURE__ */ jsx(SqlArtifactBlock, {
|
|
1583
|
-
artifact: a,
|
|
1584
|
-
maxRows: maxArtifactRows
|
|
1585
|
-
}, a.id))
|
|
1586
|
-
}) : null,
|
|
1587
|
-
answer ? /* @__PURE__ */ jsx("div", {
|
|
1588
|
-
style: styles.markdown,
|
|
1589
|
-
children: /* @__PURE__ */ jsx(MarkdownText, { text: answer })
|
|
1590
|
-
}) : null,
|
|
1591
|
-
needsClarification && clarification ? /* @__PURE__ */ jsxs("div", {
|
|
1592
|
-
style: styles.clarification,
|
|
1593
|
-
children: [/* @__PURE__ */ jsx("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ jsx("div", {
|
|
1594
|
-
style: { marginTop: 4 },
|
|
1595
|
-
children: clarification
|
|
1596
|
-
})]
|
|
1597
|
-
}) : null,
|
|
1598
|
-
isFailed && error ? /* @__PURE__ */ jsx(ErrorBlock, { error }) : null,
|
|
1599
|
-
threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ jsxs("div", {
|
|
1600
|
-
style: styles.threadLinkRow,
|
|
1601
|
-
children: [/* @__PURE__ */ jsxs("a", {
|
|
1602
|
-
href: threadUrl,
|
|
1603
|
-
target: "_blank",
|
|
1604
|
-
rel: "noreferrer noopener",
|
|
1605
|
-
style: styles.threadLink,
|
|
1606
|
-
children: [threadLinkLabel, " →"]
|
|
1607
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
1608
|
-
style: styles.betaBadge,
|
|
1609
|
-
title: "Thread linking is in beta — see docs",
|
|
1610
|
-
children: "beta"
|
|
1611
|
-
})]
|
|
1612
|
-
}) : null
|
|
1613
|
-
]
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
/**
|
|
1617
|
-
* Complete drop-in chat surface. One agent, one input, one answer
|
|
1618
|
-
* view. The chat is single-turn by default — each new question
|
|
1619
|
-
* cancels the previous run and clears the answer. Bundles that
|
|
1620
|
-
* want a multi-turn conversation history compose their own UI
|
|
1621
|
-
* using `useAgentRun` directly.
|
|
1622
|
-
*
|
|
1623
|
-
* Single-turn keeps the surface dead simple: bundles use this for
|
|
1624
|
-
* the "ask anything about your data" widget that sits next to
|
|
1625
|
-
* structured panels. Multi-turn is rare in those contexts and
|
|
1626
|
-
* better expressed by the bundle.
|
|
1627
|
-
*/
|
|
1628
|
-
function OxyChat(props) {
|
|
1629
|
-
const { agentId, placeholder = "Ask a question about your data…", submitLabel = "Ask", emptyState, maxArtifactRows, className } = props;
|
|
1630
|
-
const run = useAgentRun({ agentId });
|
|
1631
|
-
const [question, setQuestion] = React.useState("");
|
|
1632
|
-
const submit = React.useCallback((e) => {
|
|
1633
|
-
e?.preventDefault();
|
|
1634
|
-
const q = question.trim();
|
|
1635
|
-
if (!q || run.state === "running") return;
|
|
1636
|
-
run.ask(q);
|
|
1637
|
-
}, [question, run]);
|
|
1638
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1639
|
-
className,
|
|
1640
|
-
style: styles.chatWrap,
|
|
1641
|
-
children: [/* @__PURE__ */ jsxs("form", {
|
|
1642
|
-
onSubmit: submit,
|
|
1643
|
-
style: styles.chatForm,
|
|
1644
|
-
children: [
|
|
1645
|
-
/* @__PURE__ */ jsx("input", {
|
|
1646
|
-
type: "text",
|
|
1647
|
-
value: question,
|
|
1648
|
-
onChange: (e) => setQuestion(e.target.value),
|
|
1649
|
-
placeholder,
|
|
1650
|
-
disabled: run.state === "running",
|
|
1651
|
-
style: styles.chatInput,
|
|
1652
|
-
"aria-label": "Question"
|
|
1653
|
-
}),
|
|
1654
|
-
/* @__PURE__ */ jsx("button", {
|
|
1655
|
-
type: "submit",
|
|
1656
|
-
disabled: run.state === "running" || question.trim() === "",
|
|
1657
|
-
style: styles.chatSubmit,
|
|
1658
|
-
children: run.state === "running" ? "…" : submitLabel
|
|
1659
|
-
}),
|
|
1660
|
-
run.state === "running" ? /* @__PURE__ */ jsx("button", {
|
|
1661
|
-
type: "button",
|
|
1662
|
-
onClick: run.cancel,
|
|
1663
|
-
style: styles.chatCancel,
|
|
1664
|
-
children: "Stop"
|
|
1665
|
-
}) : null
|
|
1666
|
-
]
|
|
1667
|
-
}), run.state === "idle" ? emptyState ?? /* @__PURE__ */ jsx("div", {
|
|
1668
|
-
style: styles.emptyState,
|
|
1669
|
-
children: "Ask a question to get started."
|
|
1670
|
-
}) : /* @__PURE__ */ jsx(OxyAnswer, {
|
|
1671
|
-
answer: run.answer,
|
|
1672
|
-
artifacts: run.artifacts,
|
|
1673
|
-
state: run.state,
|
|
1674
|
-
clarification: run.clarification,
|
|
1675
|
-
error: run.error,
|
|
1676
|
-
threadUrl: run.threadUrl,
|
|
1677
|
-
maxArtifactRows
|
|
1678
|
-
})]
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1681
|
-
function SqlArtifactBlock(props) {
|
|
1682
|
-
const { artifact, maxRows } = props;
|
|
1683
|
-
const [open, setOpen] = React.useState(false);
|
|
1684
|
-
const results = artifact.results;
|
|
1685
|
-
const truncated = results ? results.rows.length > maxRows : false;
|
|
1686
|
-
const visibleRows = results ? results.rows.slice(0, maxRows) : [];
|
|
1687
|
-
const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
|
|
1688
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1689
|
-
style: styles.artifact,
|
|
1690
|
-
children: [/* @__PURE__ */ jsxs("button", {
|
|
1691
|
-
type: "button",
|
|
1692
|
-
onClick: () => setOpen((o) => !o),
|
|
1693
|
-
style: styles.artifactHeader,
|
|
1694
|
-
children: [
|
|
1695
|
-
/* @__PURE__ */ jsx("span", {
|
|
1696
|
-
style: styles.artifactBadge,
|
|
1697
|
-
children: sourceLabel
|
|
1698
|
-
}),
|
|
1699
|
-
/* @__PURE__ */ jsx("span", {
|
|
1700
|
-
style: styles.artifactSummary,
|
|
1701
|
-
children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
|
|
1702
|
-
}),
|
|
1703
|
-
/* @__PURE__ */ jsx("span", {
|
|
1704
|
-
style: styles.artifactToggle,
|
|
1705
|
-
children: open ? "Hide" : "Show"
|
|
1706
|
-
})
|
|
1707
|
-
]
|
|
1708
|
-
}), open ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("pre", {
|
|
1709
|
-
style: styles.sqlBlock,
|
|
1710
|
-
children: artifact.sql
|
|
1711
|
-
}), artifact.error ? /* @__PURE__ */ jsx("div", {
|
|
1712
|
-
style: styles.error,
|
|
1713
|
-
children: artifact.error
|
|
1714
|
-
}) : results ? /* @__PURE__ */ jsxs("div", {
|
|
1715
|
-
style: styles.resultsWrap,
|
|
1716
|
-
children: [/* @__PURE__ */ jsxs("table", {
|
|
1717
|
-
style: styles.resultsTable,
|
|
1718
|
-
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: results.columns.map((c) => /* @__PURE__ */ jsx("th", {
|
|
1719
|
-
style: styles.resultsTh,
|
|
1720
|
-
children: c
|
|
1721
|
-
}, c)) }) }), /* @__PURE__ */ jsx("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx("td", {
|
|
1722
|
-
style: styles.resultsTd,
|
|
1723
|
-
children: formatCell(cell)
|
|
1724
|
-
}, j)) }, i)) })]
|
|
1725
|
-
}), truncated ? /* @__PURE__ */ jsxs("div", {
|
|
1726
|
-
style: styles.truncatedNote,
|
|
1727
|
-
children: [
|
|
1728
|
-
"+",
|
|
1729
|
-
results.rows.length - maxRows,
|
|
1730
|
-
" more rows. Open the thread in Oxy to see all."
|
|
1731
|
-
]
|
|
1732
|
-
}) : null]
|
|
1733
|
-
}) : null] }) : null]
|
|
1734
|
-
});
|
|
1735
|
-
}
|
|
1736
|
-
function formatCell(value) {
|
|
1737
|
-
if (value === null || value === void 0) return "—";
|
|
1738
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
1739
|
-
return String(value);
|
|
1740
|
-
}
|
|
1741
|
-
/**
|
|
1742
|
-
* Renders a thrown Error with the server's `hint` line broken out
|
|
1743
|
-
* if the error is an `OxyApiError`. Falls back to `error.message`
|
|
1744
|
-
* for plain Errors. Whitespace-preserving so multi-line hints from
|
|
1745
|
-
* the server land readably.
|
|
1746
|
-
*/
|
|
1747
|
-
function ErrorBlock(props) {
|
|
1748
|
-
const { error } = props;
|
|
1749
|
-
if (error instanceof OxyApiError) return /* @__PURE__ */ jsxs("div", {
|
|
1750
|
-
style: styles.error,
|
|
1751
|
-
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
1752
|
-
/* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
|
|
1753
|
-
" ",
|
|
1754
|
-
error.message.split("\n\n")[0]
|
|
1755
|
-
] }), error.hint ? /* @__PURE__ */ jsxs("div", {
|
|
1756
|
-
style: {
|
|
1757
|
-
marginTop: 6,
|
|
1758
|
-
fontWeight: 400,
|
|
1759
|
-
whiteSpace: "pre-wrap"
|
|
1760
|
-
},
|
|
1761
|
-
children: [
|
|
1762
|
-
/* @__PURE__ */ jsx("strong", { children: "Hint:" }),
|
|
1763
|
-
" ",
|
|
1764
|
-
error.hint
|
|
1765
|
-
]
|
|
1766
|
-
}) : null]
|
|
1767
|
-
});
|
|
1768
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1769
|
-
style: styles.error,
|
|
1770
|
-
children: [
|
|
1771
|
-
/* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
|
|
1772
|
-
" ",
|
|
1773
|
-
error.message
|
|
1774
|
-
]
|
|
1775
|
-
});
|
|
1776
|
-
}
|
|
1777
|
-
function MarkdownText(props) {
|
|
1778
|
-
return /* @__PURE__ */ jsx(Fragment, { children: React.useMemo(() => parseMarkdown(props.text), [props.text]) });
|
|
1779
|
-
}
|
|
1780
|
-
function parseMarkdown(text) {
|
|
1781
|
-
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
1782
|
-
const blocks = [];
|
|
1783
|
-
let i = 0;
|
|
1784
|
-
while (i < lines.length) {
|
|
1785
|
-
const line = lines[i];
|
|
1786
|
-
if (line === void 0) {
|
|
1787
|
-
i++;
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
const fence = line.match(/^```(\w*)\s*$/);
|
|
1791
|
-
if (fence) {
|
|
1792
|
-
const lang = fence[1] ?? "";
|
|
1793
|
-
const buf = [];
|
|
1794
|
-
i++;
|
|
1795
|
-
while (i < lines.length && !/^```\s*$/.test(lines[i] ?? "")) {
|
|
1796
|
-
buf.push(lines[i] ?? "");
|
|
1797
|
-
i++;
|
|
1798
|
-
}
|
|
1799
|
-
i++;
|
|
1800
|
-
blocks.push({
|
|
1801
|
-
kind: "code",
|
|
1802
|
-
lang,
|
|
1803
|
-
code: buf.join("\n")
|
|
1804
|
-
});
|
|
1805
|
-
continue;
|
|
1806
|
-
}
|
|
1807
|
-
const h = line.match(/^(#{1,3})\s+(.+)$/);
|
|
1808
|
-
if (h) {
|
|
1809
|
-
blocks.push({
|
|
1810
|
-
kind: "h",
|
|
1811
|
-
level: h[1]?.length,
|
|
1812
|
-
text: h[2]
|
|
1813
|
-
});
|
|
1814
|
-
i++;
|
|
1815
|
-
continue;
|
|
1816
|
-
}
|
|
1817
|
-
if (/^\s*[-*]\s+/.test(line)) {
|
|
1818
|
-
const items = [];
|
|
1819
|
-
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i] ?? "")) {
|
|
1820
|
-
items.push((lines[i] ?? "").replace(/^\s*[-*]\s+/, ""));
|
|
1821
|
-
i++;
|
|
1822
|
-
}
|
|
1823
|
-
blocks.push({
|
|
1824
|
-
kind: "list",
|
|
1825
|
-
items
|
|
1826
|
-
});
|
|
1827
|
-
continue;
|
|
1828
|
-
}
|
|
1829
|
-
if (line.trim() === "") {
|
|
1830
|
-
i++;
|
|
1831
|
-
continue;
|
|
1832
|
-
}
|
|
1833
|
-
const buf = [line];
|
|
1834
|
-
i++;
|
|
1835
|
-
while (i < lines.length) {
|
|
1836
|
-
const next = lines[i] ?? "";
|
|
1837
|
-
if (next.trim() === "" || /^#{1,3}\s+/.test(next) || /^```/.test(next) || /^\s*[-*]\s+/.test(next)) break;
|
|
1838
|
-
buf.push(next);
|
|
1839
|
-
i++;
|
|
1840
|
-
}
|
|
1841
|
-
blocks.push({
|
|
1842
|
-
kind: "p",
|
|
1843
|
-
text: buf.join(" ")
|
|
1844
|
-
});
|
|
1845
|
-
}
|
|
1846
|
-
return blocks.map((b, idx) => {
|
|
1847
|
-
switch (b.kind) {
|
|
1848
|
-
case "h": return /* @__PURE__ */ jsx(`h${b.level}`, {
|
|
1849
|
-
style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
|
|
1850
|
-
children: renderInline(b.text)
|
|
1851
|
-
}, idx);
|
|
1852
|
-
case "code": return /* @__PURE__ */ jsx("pre", {
|
|
1853
|
-
style: styles.codeBlock,
|
|
1854
|
-
"data-lang": b.lang || void 0,
|
|
1855
|
-
children: /* @__PURE__ */ jsx("code", { children: b.code })
|
|
1856
|
-
}, idx);
|
|
1857
|
-
case "list": return /* @__PURE__ */ jsx("ul", {
|
|
1858
|
-
style: styles.list,
|
|
1859
|
-
children: b.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: renderInline(item) }, i))
|
|
1860
|
-
}, idx);
|
|
1861
|
-
case "p": return /* @__PURE__ */ jsx("p", {
|
|
1862
|
-
style: styles.paragraph,
|
|
1863
|
-
children: renderInline(b.text)
|
|
1864
|
-
}, idx);
|
|
1865
|
-
}
|
|
1866
|
-
});
|
|
1867
|
-
}
|
|
1868
|
-
/**
|
|
1869
|
-
* Inline tokenizer for **bold**, *italic*, `code`, and [text](url).
|
|
1870
|
-
* Lazy: scan once, split into segments. The patterns are matched in
|
|
1871
|
-
* priority order (code first so backticks don't get eaten by bold).
|
|
1872
|
-
*/
|
|
1873
|
-
function renderInline(text) {
|
|
1874
|
-
const segments = [];
|
|
1875
|
-
let remaining = text;
|
|
1876
|
-
let key = 0;
|
|
1877
|
-
const patterns = [
|
|
1878
|
-
{
|
|
1879
|
-
re: /`([^`]+)`/,
|
|
1880
|
-
render: (m) => /* @__PURE__ */ jsx("code", {
|
|
1881
|
-
style: styles.inlineCode,
|
|
1882
|
-
children: m[1]
|
|
1883
|
-
})
|
|
1884
|
-
},
|
|
1885
|
-
{
|
|
1886
|
-
re: /\[([^\]]+)\]\(([^)]+)\)/,
|
|
1887
|
-
render: (m) => {
|
|
1888
|
-
if (isSafeLinkHref(m[2])) return /* @__PURE__ */ jsx("a", {
|
|
1889
|
-
href: m[2],
|
|
1890
|
-
target: "_blank",
|
|
1891
|
-
rel: "noreferrer noopener",
|
|
1892
|
-
style: styles.link,
|
|
1893
|
-
children: m[1]
|
|
1894
|
-
});
|
|
1895
|
-
return /* @__PURE__ */ jsx(Fragment, { children: m[1] });
|
|
1896
|
-
}
|
|
1897
|
-
},
|
|
1898
|
-
{
|
|
1899
|
-
re: /\*\*([^*]+)\*\*/,
|
|
1900
|
-
render: (m) => /* @__PURE__ */ jsx("strong", { children: m[1] })
|
|
1901
|
-
},
|
|
1902
|
-
{
|
|
1903
|
-
re: /\*([^*]+)\*/,
|
|
1904
|
-
render: (m) => /* @__PURE__ */ jsx("em", { children: m[1] })
|
|
1905
|
-
}
|
|
1906
|
-
];
|
|
1907
|
-
while (remaining.length > 0) {
|
|
1908
|
-
let earliest = null;
|
|
1909
|
-
for (const { re, render } of patterns) {
|
|
1910
|
-
const m = re.exec(remaining);
|
|
1911
|
-
if (m && (earliest === null || m.index < earliest.idx)) earliest = {
|
|
1912
|
-
idx: m.index,
|
|
1913
|
-
len: m[0].length,
|
|
1914
|
-
node: render(m)
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
if (earliest === null) {
|
|
1918
|
-
segments.push(remaining);
|
|
1919
|
-
break;
|
|
1920
|
-
}
|
|
1921
|
-
if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
|
|
1922
|
-
segments.push(/* @__PURE__ */ jsx(React.Fragment, { children: earliest.node }, key++));
|
|
1923
|
-
remaining = remaining.slice(earliest.idx + earliest.len);
|
|
1924
|
-
}
|
|
1925
|
-
return segments;
|
|
1926
|
-
}
|
|
1927
|
-
const SANS = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif";
|
|
1928
|
-
const MONO = "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace";
|
|
1929
|
-
const styles = {
|
|
1930
|
-
answerWrap: {
|
|
1931
|
-
fontFamily: SANS,
|
|
1932
|
-
fontSize: 14,
|
|
1933
|
-
lineHeight: 1.5,
|
|
1934
|
-
color: "#1f2937"
|
|
1935
|
-
},
|
|
1936
|
-
statusRow: {
|
|
1937
|
-
display: "flex",
|
|
1938
|
-
alignItems: "center",
|
|
1939
|
-
gap: 8,
|
|
1940
|
-
padding: "8px 0"
|
|
1941
|
-
},
|
|
1942
|
-
spinner: {
|
|
1943
|
-
display: "inline-block",
|
|
1944
|
-
width: 12,
|
|
1945
|
-
height: 12,
|
|
1946
|
-
borderRadius: "50%",
|
|
1947
|
-
border: "2px solid #d1d5db",
|
|
1948
|
-
borderTopColor: "#6b7280",
|
|
1949
|
-
animation: "oxy-spin 0.8s linear infinite"
|
|
1950
|
-
},
|
|
1951
|
-
statusText: { color: "#6b7280" },
|
|
1952
|
-
markdown: { marginTop: 8 },
|
|
1953
|
-
h1: {
|
|
1954
|
-
fontSize: 20,
|
|
1955
|
-
fontWeight: 600,
|
|
1956
|
-
margin: "16px 0 8px"
|
|
1957
|
-
},
|
|
1958
|
-
h2: {
|
|
1959
|
-
fontSize: 17,
|
|
1960
|
-
fontWeight: 600,
|
|
1961
|
-
margin: "14px 0 6px"
|
|
1962
|
-
},
|
|
1963
|
-
h3: {
|
|
1964
|
-
fontSize: 15,
|
|
1965
|
-
fontWeight: 600,
|
|
1966
|
-
margin: "12px 0 4px"
|
|
1967
|
-
},
|
|
1968
|
-
paragraph: { margin: "0 0 8px" },
|
|
1969
|
-
list: {
|
|
1970
|
-
margin: "0 0 8px",
|
|
1971
|
-
paddingLeft: 20
|
|
1972
|
-
},
|
|
1973
|
-
codeBlock: {
|
|
1974
|
-
fontFamily: MONO,
|
|
1975
|
-
fontSize: 12,
|
|
1976
|
-
background: "#f3f4f6",
|
|
1977
|
-
border: "1px solid #e5e7eb",
|
|
1978
|
-
borderRadius: 6,
|
|
1979
|
-
padding: "8px 10px",
|
|
1980
|
-
overflowX: "auto",
|
|
1981
|
-
margin: "8px 0"
|
|
1982
|
-
},
|
|
1983
|
-
inlineCode: {
|
|
1984
|
-
fontFamily: MONO,
|
|
1985
|
-
fontSize: "0.92em",
|
|
1986
|
-
background: "#f3f4f6",
|
|
1987
|
-
padding: "1px 4px",
|
|
1988
|
-
borderRadius: 3
|
|
1989
|
-
},
|
|
1990
|
-
link: {
|
|
1991
|
-
color: "#2563eb",
|
|
1992
|
-
textDecoration: "underline"
|
|
1993
|
-
},
|
|
1994
|
-
clarification: {
|
|
1995
|
-
marginTop: 12,
|
|
1996
|
-
padding: "10px 12px",
|
|
1997
|
-
background: "#fef3c7",
|
|
1998
|
-
border: "1px solid #fcd34d",
|
|
1999
|
-
borderRadius: 6,
|
|
2000
|
-
color: "#78350f"
|
|
2001
|
-
},
|
|
2002
|
-
error: {
|
|
2003
|
-
marginTop: 12,
|
|
2004
|
-
padding: "10px 12px",
|
|
2005
|
-
background: "#fee2e2",
|
|
2006
|
-
border: "1px solid #fca5a5",
|
|
2007
|
-
borderRadius: 6,
|
|
2008
|
-
color: "#991b1b"
|
|
2009
|
-
},
|
|
2010
|
-
threadLinkRow: {
|
|
2011
|
-
marginTop: 12,
|
|
2012
|
-
textAlign: "right",
|
|
2013
|
-
display: "flex",
|
|
2014
|
-
justifyContent: "flex-end",
|
|
2015
|
-
alignItems: "center",
|
|
2016
|
-
gap: 6
|
|
2017
|
-
},
|
|
2018
|
-
threadLink: {
|
|
2019
|
-
fontSize: 12,
|
|
2020
|
-
color: "#6b7280",
|
|
2021
|
-
textDecoration: "none"
|
|
2022
|
-
},
|
|
2023
|
-
betaBadge: {
|
|
2024
|
-
fontSize: 9,
|
|
2025
|
-
fontWeight: 600,
|
|
2026
|
-
letterSpacing: .5,
|
|
2027
|
-
textTransform: "uppercase",
|
|
2028
|
-
padding: "1px 5px",
|
|
2029
|
-
borderRadius: 3,
|
|
2030
|
-
background: "#fef3c7",
|
|
2031
|
-
color: "#92400e",
|
|
2032
|
-
border: "1px solid #fcd34d"
|
|
2033
|
-
},
|
|
2034
|
-
artifactList: {
|
|
2035
|
-
display: "flex",
|
|
2036
|
-
flexDirection: "column",
|
|
2037
|
-
gap: 8,
|
|
2038
|
-
marginBottom: 8
|
|
2039
|
-
},
|
|
2040
|
-
artifact: {
|
|
2041
|
-
border: "1px solid #e5e7eb",
|
|
2042
|
-
borderRadius: 6,
|
|
2043
|
-
background: "#fafafa",
|
|
2044
|
-
overflow: "hidden"
|
|
2045
|
-
},
|
|
2046
|
-
artifactHeader: {
|
|
2047
|
-
display: "flex",
|
|
2048
|
-
alignItems: "center",
|
|
2049
|
-
gap: 10,
|
|
2050
|
-
width: "100%",
|
|
2051
|
-
padding: "6px 10px",
|
|
2052
|
-
background: "transparent",
|
|
2053
|
-
border: "none",
|
|
2054
|
-
borderBottom: "1px solid transparent",
|
|
2055
|
-
cursor: "pointer",
|
|
2056
|
-
fontFamily: SANS,
|
|
2057
|
-
fontSize: 12,
|
|
2058
|
-
color: "#374151"
|
|
2059
|
-
},
|
|
2060
|
-
artifactBadge: {
|
|
2061
|
-
fontWeight: 600,
|
|
2062
|
-
fontSize: 11,
|
|
2063
|
-
textTransform: "uppercase",
|
|
2064
|
-
letterSpacing: .4,
|
|
2065
|
-
color: "#4b5563"
|
|
2066
|
-
},
|
|
2067
|
-
artifactSummary: {
|
|
2068
|
-
color: "#6b7280",
|
|
2069
|
-
flex: 1
|
|
2070
|
-
},
|
|
2071
|
-
artifactToggle: { color: "#2563eb" },
|
|
2072
|
-
sqlBlock: {
|
|
2073
|
-
fontFamily: MONO,
|
|
2074
|
-
fontSize: 12,
|
|
2075
|
-
margin: 0,
|
|
2076
|
-
padding: "8px 10px",
|
|
2077
|
-
background: "#0f172a",
|
|
2078
|
-
color: "#e2e8f0",
|
|
2079
|
-
overflowX: "auto"
|
|
2080
|
-
},
|
|
2081
|
-
resultsWrap: {
|
|
2082
|
-
padding: 8,
|
|
2083
|
-
overflowX: "auto"
|
|
2084
|
-
},
|
|
2085
|
-
resultsTable: {
|
|
2086
|
-
width: "100%",
|
|
2087
|
-
borderCollapse: "collapse",
|
|
2088
|
-
fontSize: 12
|
|
2089
|
-
},
|
|
2090
|
-
resultsTh: {
|
|
2091
|
-
textAlign: "left",
|
|
2092
|
-
padding: "4px 8px",
|
|
2093
|
-
borderBottom: "1px solid #e5e7eb",
|
|
2094
|
-
fontWeight: 600,
|
|
2095
|
-
color: "#374151"
|
|
2096
|
-
},
|
|
2097
|
-
resultsTd: {
|
|
2098
|
-
padding: "4px 8px",
|
|
2099
|
-
borderBottom: "1px solid #f3f4f6",
|
|
2100
|
-
color: "#1f2937"
|
|
2101
|
-
},
|
|
2102
|
-
truncatedNote: {
|
|
2103
|
-
fontSize: 11,
|
|
2104
|
-
color: "#6b7280",
|
|
2105
|
-
padding: "6px 8px"
|
|
2106
|
-
},
|
|
2107
|
-
chatWrap: {
|
|
2108
|
-
fontFamily: SANS,
|
|
2109
|
-
fontSize: 14,
|
|
2110
|
-
color: "#1f2937"
|
|
2111
|
-
},
|
|
2112
|
-
chatForm: {
|
|
2113
|
-
display: "flex",
|
|
2114
|
-
gap: 8,
|
|
2115
|
-
marginBottom: 12
|
|
2116
|
-
},
|
|
2117
|
-
chatInput: {
|
|
2118
|
-
flex: 1,
|
|
2119
|
-
padding: "8px 12px",
|
|
2120
|
-
border: "1px solid #d1d5db",
|
|
2121
|
-
borderRadius: 6,
|
|
2122
|
-
fontSize: 14,
|
|
2123
|
-
fontFamily: SANS
|
|
2124
|
-
},
|
|
2125
|
-
chatSubmit: {
|
|
2126
|
-
padding: "8px 16px",
|
|
2127
|
-
border: "none",
|
|
2128
|
-
borderRadius: 6,
|
|
2129
|
-
background: "#2563eb",
|
|
2130
|
-
color: "#ffffff",
|
|
2131
|
-
fontSize: 14,
|
|
2132
|
-
fontWeight: 500,
|
|
2133
|
-
cursor: "pointer"
|
|
2134
|
-
},
|
|
2135
|
-
chatCancel: {
|
|
2136
|
-
padding: "8px 12px",
|
|
2137
|
-
border: "1px solid #d1d5db",
|
|
2138
|
-
borderRadius: 6,
|
|
2139
|
-
background: "#ffffff",
|
|
2140
|
-
color: "#374151",
|
|
2141
|
-
fontSize: 14,
|
|
2142
|
-
cursor: "pointer"
|
|
2143
|
-
},
|
|
2144
|
-
emptyState: {
|
|
2145
|
-
padding: "12px 0",
|
|
2146
|
-
color: "#9ca3af",
|
|
2147
|
-
fontStyle: "italic"
|
|
2148
|
-
}
|
|
2149
|
-
};
|
|
2150
|
-
|
|
2151
107
|
//#endregion
|
|
2152
108
|
//#region src/metricTree.ts
|
|
2153
109
|
/**
|
|
@@ -2277,5 +233,5 @@ var MetricTreeClient = class {
|
|
|
2277
233
|
};
|
|
2278
234
|
|
|
2279
235
|
//#endregion
|
|
2280
|
-
export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
|
|
236
|
+
export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, apiErrorFromResponse, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useOxyApp, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
|
|
2281
237
|
//# sourceMappingURL=index.mjs.map
|