@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/shell.cjs
ADDED
|
@@ -0,0 +1,2091 @@
|
|
|
1
|
+
// @oxy/sdk - TypeScript SDK for Oxy data platform
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3
|
+
const require_react = require('./react-B7Blpmt7.cjs');
|
|
4
|
+
let react = require("react");
|
|
5
|
+
react = require_react.__toESM(react, 1);
|
|
6
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
|
+
let _radix_ui_react_tooltip = require("@radix-ui/react-tooltip");
|
|
8
|
+
_radix_ui_react_tooltip = require_react.__toESM(_radix_ui_react_tooltip, 1);
|
|
9
|
+
|
|
10
|
+
//#region src/shell/cx.ts
|
|
11
|
+
/** Join truthy class names. Local minimal `clsx` — the shell uses fixed
|
|
12
|
+
* namespaced classes, so no Tailwind-style merge logic is needed. */
|
|
13
|
+
function cx(...parts) {
|
|
14
|
+
return parts.filter(Boolean).join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/shell/AnswerChart.tsx
|
|
19
|
+
const PALETTE = [
|
|
20
|
+
"#3b82f6",
|
|
21
|
+
"#22c55e",
|
|
22
|
+
"#eab308",
|
|
23
|
+
"#a855f7",
|
|
24
|
+
"#06b6d4",
|
|
25
|
+
"#f97316"
|
|
26
|
+
];
|
|
27
|
+
const compact = new Intl.NumberFormat("en", {
|
|
28
|
+
notation: "compact",
|
|
29
|
+
maximumFractionDigits: 1
|
|
30
|
+
});
|
|
31
|
+
const toNum = (v) => {
|
|
32
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
33
|
+
return Number.isFinite(n) ? n : 0;
|
|
34
|
+
};
|
|
35
|
+
const toStr = (v) => v === null || v === void 0 ? "" : String(v);
|
|
36
|
+
/** "sales_daily__total_net_sales" → "Total Net Sales" — a friendly series
|
|
37
|
+
* label instead of the raw semantic column id. */
|
|
38
|
+
function humanizeCol(name) {
|
|
39
|
+
return ((name ?? "").split("__").pop() ?? "").replace(/_/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
40
|
+
}
|
|
41
|
+
/** Pivot rows into per-series value arrays aligned on x categories. */
|
|
42
|
+
function pivotChart(block) {
|
|
43
|
+
const { config, columns, rows } = block;
|
|
44
|
+
const xi = config.x ? columns.indexOf(config.x) : 0;
|
|
45
|
+
const yi = config.y ? columns.indexOf(config.y) : columns.length > 1 ? 1 : 0;
|
|
46
|
+
const si = config.series ? columns.indexOf(config.series) : -1;
|
|
47
|
+
const categories = [];
|
|
48
|
+
const catIndex = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const cat = toStr(row[xi]);
|
|
51
|
+
if (!catIndex.has(cat)) {
|
|
52
|
+
catIndex.set(cat, categories.length);
|
|
53
|
+
categories.push(cat);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (si === -1) {
|
|
57
|
+
const values = new Array(categories.length).fill(0);
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const idx = catIndex.get(toStr(row[xi]));
|
|
60
|
+
if (idx !== void 0) values[idx] = toNum(row[yi]);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
categories,
|
|
64
|
+
series: [{
|
|
65
|
+
name: config.y ?? "value",
|
|
66
|
+
values
|
|
67
|
+
}]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const seriesIndex = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const name = toStr(row[si]);
|
|
73
|
+
let values = seriesIndex.get(name);
|
|
74
|
+
if (!values) {
|
|
75
|
+
values = new Array(categories.length).fill(0);
|
|
76
|
+
seriesIndex.set(name, values);
|
|
77
|
+
}
|
|
78
|
+
const idx = catIndex.get(toStr(row[xi]));
|
|
79
|
+
if (idx !== void 0) values[idx] = toNum(row[yi]);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
categories,
|
|
83
|
+
series: [...seriesIndex.entries()].map(([name, values]) => ({
|
|
84
|
+
name,
|
|
85
|
+
values
|
|
86
|
+
}))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** A "nice" axis ceiling so tick labels land on round numbers. */
|
|
90
|
+
function niceCeil(max) {
|
|
91
|
+
if (max <= 0) return 1;
|
|
92
|
+
const pow = 10 ** Math.floor(Math.log10(max));
|
|
93
|
+
const unit = max / pow;
|
|
94
|
+
return (unit <= 1 ? 1 : unit <= 2 ? 2 : unit <= 5 ? 5 : 10) * pow;
|
|
95
|
+
}
|
|
96
|
+
/** Axis domain `[lo, hi]` that always includes zero, with nice round
|
|
97
|
+
* bounds. Signed measures (profit, net change) must render below a zero
|
|
98
|
+
* baseline instead of being clamped to an invisible 0..max axis. */
|
|
99
|
+
function axisDomain(values) {
|
|
100
|
+
const rawMax = Math.max(...values, 0);
|
|
101
|
+
const rawMin = Math.min(...values, 0);
|
|
102
|
+
const hi = rawMax > 0 ? niceCeil(rawMax) : 0;
|
|
103
|
+
const lo = rawMin < 0 ? -niceCeil(-rawMin) : 0;
|
|
104
|
+
return hi === lo ? {
|
|
105
|
+
lo: 0,
|
|
106
|
+
hi: 1
|
|
107
|
+
} : {
|
|
108
|
+
lo,
|
|
109
|
+
hi
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const W = 360;
|
|
113
|
+
const H = 190;
|
|
114
|
+
const M = {
|
|
115
|
+
top: 8,
|
|
116
|
+
right: 8,
|
|
117
|
+
bottom: 34,
|
|
118
|
+
left: 42
|
|
119
|
+
};
|
|
120
|
+
const IW = W - M.left - M.right;
|
|
121
|
+
const IH = H - M.top - M.bottom;
|
|
122
|
+
const TICKS = 4;
|
|
123
|
+
function truncate(label, max) {
|
|
124
|
+
return label.length > max ? `${label.slice(0, max - 1)}…` : label;
|
|
125
|
+
}
|
|
126
|
+
/** Map a value to its y pixel within the plot area for domain `[lo, hi]`. */
|
|
127
|
+
const yFor = (v, lo, hi) => M.top + IH * (1 - (v - lo) / (hi - lo || 1));
|
|
128
|
+
function Axes({ lo, hi }) {
|
|
129
|
+
const span = hi - lo || 1;
|
|
130
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
|
|
131
|
+
className: "oxy-chart__grid",
|
|
132
|
+
children: Array.from({ length: 5 }, (_, i) => {
|
|
133
|
+
const y = M.top + IH * i / TICKS;
|
|
134
|
+
const value = hi - span * i / TICKS;
|
|
135
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
136
|
+
x1: M.left,
|
|
137
|
+
x2: M.left + IW,
|
|
138
|
+
y1: y,
|
|
139
|
+
y2: y
|
|
140
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
141
|
+
x: M.left - 5,
|
|
142
|
+
y: y + 3,
|
|
143
|
+
textAnchor: "end",
|
|
144
|
+
className: "oxy-chart__tick",
|
|
145
|
+
children: compact.format(value)
|
|
146
|
+
})] }, i);
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function XLabels({ categories }) {
|
|
151
|
+
const slot = IW / categories.length;
|
|
152
|
+
const maxChars = Math.max(4, Math.floor(slot / 5.5));
|
|
153
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", { children: categories.map((cat, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("text", {
|
|
154
|
+
x: M.left + slot * (i + .5),
|
|
155
|
+
y: H - M.bottom + 12,
|
|
156
|
+
textAnchor: "middle",
|
|
157
|
+
className: "oxy-chart__tick",
|
|
158
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: cat }), truncate(cat, maxChars)]
|
|
159
|
+
}, cat)) });
|
|
160
|
+
}
|
|
161
|
+
function BarChart({ data }) {
|
|
162
|
+
const { lo, hi } = axisDomain(data.series.flatMap((s) => s.values));
|
|
163
|
+
const zeroY = yFor(0, lo, hi);
|
|
164
|
+
const slot = IW / data.categories.length;
|
|
165
|
+
const group = slot * .7;
|
|
166
|
+
const barW = group / data.series.length;
|
|
167
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
168
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
169
|
+
className: "oxy-chart__svg",
|
|
170
|
+
role: "img",
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Axes, {
|
|
173
|
+
lo,
|
|
174
|
+
hi
|
|
175
|
+
}),
|
|
176
|
+
data.series.map((s, si) => s.values.map((v, ci) => {
|
|
177
|
+
const vy = yFor(v, lo, hi);
|
|
178
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
179
|
+
x: M.left + slot * ci + (slot - group) / 2 + barW * si,
|
|
180
|
+
y: Math.min(zeroY, vy),
|
|
181
|
+
width: Math.max(1, barW - 1),
|
|
182
|
+
height: Math.max(0, Math.abs(vy - zeroY)),
|
|
183
|
+
rx: 1.5,
|
|
184
|
+
fill: PALETTE[si % PALETTE.length],
|
|
185
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: `${data.categories[ci]}${data.series.length > 1 ? ` · ${s.name}` : ""}: ${compact.format(v)}` })
|
|
186
|
+
}, `${s.name}-${data.categories[ci]}`);
|
|
187
|
+
})),
|
|
188
|
+
lo < 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
189
|
+
className: "oxy-chart__grid-zero",
|
|
190
|
+
x1: M.left,
|
|
191
|
+
x2: M.left + IW,
|
|
192
|
+
y1: zeroY,
|
|
193
|
+
y2: zeroY
|
|
194
|
+
}),
|
|
195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(XLabels, { categories: data.categories })
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function LineChart({ data }) {
|
|
200
|
+
const { lo, hi } = axisDomain(data.series.flatMap((s) => s.values));
|
|
201
|
+
const zeroY = yFor(0, lo, hi);
|
|
202
|
+
const slot = IW / Math.max(1, data.categories.length - 1 || 1);
|
|
203
|
+
const px = (ci) => data.categories.length === 1 ? M.left + IW / 2 : M.left + slot * ci;
|
|
204
|
+
const py = (v) => yFor(v, lo, hi);
|
|
205
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
206
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
207
|
+
className: "oxy-chart__svg",
|
|
208
|
+
role: "img",
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Axes, {
|
|
211
|
+
lo,
|
|
212
|
+
hi
|
|
213
|
+
}),
|
|
214
|
+
lo < 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
215
|
+
className: "oxy-chart__grid-zero",
|
|
216
|
+
x1: M.left,
|
|
217
|
+
x2: M.left + IW,
|
|
218
|
+
y1: zeroY,
|
|
219
|
+
y2: zeroY
|
|
220
|
+
}),
|
|
221
|
+
data.series.map((s, si) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", {
|
|
222
|
+
points: s.values.map((v, ci) => `${px(ci)},${py(v)}`).join(" "),
|
|
223
|
+
fill: "none",
|
|
224
|
+
stroke: PALETTE[si % PALETTE.length],
|
|
225
|
+
strokeWidth: 1.8
|
|
226
|
+
}), s.values.map((v, ci) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
227
|
+
cx: px(ci),
|
|
228
|
+
cy: py(v),
|
|
229
|
+
r: 2.2,
|
|
230
|
+
fill: PALETTE[si % PALETTE.length],
|
|
231
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: `${data.categories[ci]}${data.series.length > 1 ? ` · ${s.name}` : ""}: ${compact.format(v)}` })
|
|
232
|
+
}, data.categories[ci]))] }, s.name)),
|
|
233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(XLabels, { categories: data.categories })
|
|
234
|
+
]
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function PieChart({ block }) {
|
|
238
|
+
const { config, columns, rows } = block;
|
|
239
|
+
const ni = config.name ? columns.indexOf(config.name) : config.x ? columns.indexOf(config.x) : 0;
|
|
240
|
+
const vi = config.value ? columns.indexOf(config.value) : config.y ? columns.indexOf(config.y) : 1;
|
|
241
|
+
const slices = rows.map((r) => ({
|
|
242
|
+
name: toStr(r[ni]),
|
|
243
|
+
value: Math.max(0, toNum(r[vi]))
|
|
244
|
+
}));
|
|
245
|
+
const total = slices.reduce((sum, s) => sum + s.value, 0) || 1;
|
|
246
|
+
const cxp = W / 2;
|
|
247
|
+
const cyp = H / 2;
|
|
248
|
+
const r = Math.min(W, H) / 2 - 14;
|
|
249
|
+
let angle = -Math.PI / 2;
|
|
250
|
+
const paths = slices.map((s, i) => {
|
|
251
|
+
const sweep = s.value / total * Math.PI * 2;
|
|
252
|
+
const title = `${s.name}: ${compact.format(s.value)} (${Math.round(s.value / total * 100)}%)`;
|
|
253
|
+
const fill = PALETTE[i % PALETTE.length];
|
|
254
|
+
if (sweep >= Math.PI * 2 - 1e-6) {
|
|
255
|
+
angle += sweep;
|
|
256
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
257
|
+
cx: cxp,
|
|
258
|
+
cy: cyp,
|
|
259
|
+
r,
|
|
260
|
+
fill,
|
|
261
|
+
stroke: "var(--oxy-shell-background, #fff)",
|
|
262
|
+
strokeWidth: 1,
|
|
263
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: title })
|
|
264
|
+
}, s.name);
|
|
265
|
+
}
|
|
266
|
+
const x1 = cxp + r * Math.cos(angle);
|
|
267
|
+
const y1 = cyp + r * Math.sin(angle);
|
|
268
|
+
angle += sweep;
|
|
269
|
+
const x2 = cxp + r * Math.cos(angle);
|
|
270
|
+
const y2 = cyp + r * Math.sin(angle);
|
|
271
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
272
|
+
d: `M ${cxp} ${cyp} L ${x1} ${y1} A ${r} ${r} 0 ${sweep > Math.PI ? 1 : 0} 1 ${x2} ${y2} Z`,
|
|
273
|
+
fill,
|
|
274
|
+
stroke: "var(--oxy-shell-background, #fff)",
|
|
275
|
+
strokeWidth: 1,
|
|
276
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("title", { children: title })
|
|
277
|
+
}, s.name);
|
|
278
|
+
});
|
|
279
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
280
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
281
|
+
className: "oxy-chart__svg",
|
|
282
|
+
role: "img",
|
|
283
|
+
children: paths
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const TABLE_MAX_ROWS = 10;
|
|
287
|
+
function TableChart({ block }) {
|
|
288
|
+
const visible = block.rows.slice(0, TABLE_MAX_ROWS);
|
|
289
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
290
|
+
className: "oxy-chart__table-wrap",
|
|
291
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
292
|
+
className: "oxy-chart__table",
|
|
293
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: block.columns.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: c }, c)) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: visible.map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: block.columns.map((c, j) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: toStr(row[j]) }, c)) }, row.map(toStr).join("|") || i)) })]
|
|
294
|
+
}), block.rows.length > TABLE_MAX_ROWS && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
295
|
+
className: "oxy-chart__note",
|
|
296
|
+
children: [
|
|
297
|
+
"+",
|
|
298
|
+
block.rows.length - TABLE_MAX_ROWS,
|
|
299
|
+
" more rows"
|
|
300
|
+
]
|
|
301
|
+
})]
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/** Dependency-free SVG rendering — the fallback when the host app has no
|
|
305
|
+
* `echarts`. */
|
|
306
|
+
function SvgChart({ block }) {
|
|
307
|
+
const data = block.config.chart_type === "bar_chart" || block.config.chart_type === "line_chart" ? pivotChart(block) : null;
|
|
308
|
+
let body;
|
|
309
|
+
switch (block.config.chart_type) {
|
|
310
|
+
case "bar_chart":
|
|
311
|
+
body = data && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BarChart, { data });
|
|
312
|
+
break;
|
|
313
|
+
case "line_chart":
|
|
314
|
+
body = data && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineChart, { data });
|
|
315
|
+
break;
|
|
316
|
+
case "pie_chart":
|
|
317
|
+
body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PieChart, { block });
|
|
318
|
+
break;
|
|
319
|
+
default: body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TableChart, { block });
|
|
320
|
+
}
|
|
321
|
+
const multiSeries = data && data.series.length > 1;
|
|
322
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [body, multiSeries && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
323
|
+
className: "oxy-chart__legend",
|
|
324
|
+
children: data.series.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
325
|
+
className: "oxy-chart__legend-item",
|
|
326
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
327
|
+
className: "oxy-chart__legend-dot",
|
|
328
|
+
style: { background: PALETTE[i % PALETTE.length] }
|
|
329
|
+
}), s.name]
|
|
330
|
+
}, s.name))
|
|
331
|
+
})] });
|
|
332
|
+
}
|
|
333
|
+
/** Read the shell's design tokens off an element so the ECharts chart
|
|
334
|
+
* matches the surrounding light/dark theme. */
|
|
335
|
+
function tokenColors(el) {
|
|
336
|
+
const cs = getComputedStyle(el);
|
|
337
|
+
const pick = (name, fallback) => cs.getPropertyValue(name).trim() || fallback;
|
|
338
|
+
return {
|
|
339
|
+
fg: pick("--oxy-shell-foreground", "#0a0a0a"),
|
|
340
|
+
muted: pick("--oxy-shell-muted-fg", "#71717a"),
|
|
341
|
+
border: pick("--oxy-shell-border", "#d4d4d8"),
|
|
342
|
+
bg: pick("--oxy-shell-background", "#ffffff")
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** Build the ECharts option for a `chart_rendered` block — the analytics
|
|
346
|
+
* pipeline's `{chart_type, x, y, series, …}` config, the same shape the
|
|
347
|
+
* main web-app charts. Interactive: axis/item tooltips, legend, hover. */
|
|
348
|
+
function buildEchartsOption(block, el) {
|
|
349
|
+
const { fg, muted, border } = tokenColors(el);
|
|
350
|
+
const axisLabel = {
|
|
351
|
+
color: muted,
|
|
352
|
+
fontSize: 10
|
|
353
|
+
};
|
|
354
|
+
const ct = block.config.chart_type;
|
|
355
|
+
if (ct === "pie_chart") {
|
|
356
|
+
const { config, columns, rows } = block;
|
|
357
|
+
const ni = config.name ? columns.indexOf(config.name) : config.x ? columns.indexOf(config.x) : 0;
|
|
358
|
+
const vi = config.value ? columns.indexOf(config.value) : config.y ? columns.indexOf(config.y) : 1;
|
|
359
|
+
const data = rows.map((r) => ({
|
|
360
|
+
name: toStr(r[ni]),
|
|
361
|
+
value: Math.max(0, toNum(r[vi]))
|
|
362
|
+
}));
|
|
363
|
+
return {
|
|
364
|
+
color: PALETTE,
|
|
365
|
+
tooltip: {
|
|
366
|
+
trigger: "item",
|
|
367
|
+
formatter: "{b}: {c} ({d}%)"
|
|
368
|
+
},
|
|
369
|
+
legend: {
|
|
370
|
+
bottom: 0,
|
|
371
|
+
textStyle: {
|
|
372
|
+
color: muted,
|
|
373
|
+
fontSize: 10
|
|
374
|
+
},
|
|
375
|
+
type: "scroll"
|
|
376
|
+
},
|
|
377
|
+
series: [{
|
|
378
|
+
type: "pie",
|
|
379
|
+
radius: ["38%", "66%"],
|
|
380
|
+
center: ["50%", "44%"],
|
|
381
|
+
data,
|
|
382
|
+
label: {
|
|
383
|
+
color: fg,
|
|
384
|
+
fontSize: 10
|
|
385
|
+
},
|
|
386
|
+
labelLine: { lineStyle: { color: border } }
|
|
387
|
+
}]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const pivot = pivotChart(block);
|
|
391
|
+
const multi = pivot.series.length > 1;
|
|
392
|
+
const singleName = block.config.y_axis_label || humanizeCol(block.config.y) || "Value";
|
|
393
|
+
const longLabels = pivot.categories.length > 4 || pivot.categories.some((c) => c.length > 10);
|
|
394
|
+
return {
|
|
395
|
+
color: PALETTE,
|
|
396
|
+
tooltip: {
|
|
397
|
+
trigger: "axis",
|
|
398
|
+
valueFormatter: (v) => typeof v === "number" ? compact.format(v) : String(v)
|
|
399
|
+
},
|
|
400
|
+
grid: {
|
|
401
|
+
left: 6,
|
|
402
|
+
right: 12,
|
|
403
|
+
top: multi ? 30 : 12,
|
|
404
|
+
bottom: 4,
|
|
405
|
+
containLabel: true
|
|
406
|
+
},
|
|
407
|
+
legend: multi ? {
|
|
408
|
+
top: 0,
|
|
409
|
+
textStyle: {
|
|
410
|
+
color: muted,
|
|
411
|
+
fontSize: 10
|
|
412
|
+
},
|
|
413
|
+
type: "scroll"
|
|
414
|
+
} : void 0,
|
|
415
|
+
xAxis: {
|
|
416
|
+
type: "category",
|
|
417
|
+
data: pivot.categories,
|
|
418
|
+
name: block.config.x_axis_label,
|
|
419
|
+
nameLocation: "middle",
|
|
420
|
+
nameGap: longLabels ? 42 : 24,
|
|
421
|
+
nameTextStyle: {
|
|
422
|
+
color: muted,
|
|
423
|
+
fontSize: 10
|
|
424
|
+
},
|
|
425
|
+
axisLabel: {
|
|
426
|
+
...axisLabel,
|
|
427
|
+
interval: 0,
|
|
428
|
+
rotate: longLabels ? 30 : 0,
|
|
429
|
+
formatter: (v) => v.length > 18 ? `${v.slice(0, 17)}…` : v
|
|
430
|
+
},
|
|
431
|
+
axisTick: { show: false },
|
|
432
|
+
axisLine: { lineStyle: { color: border } }
|
|
433
|
+
},
|
|
434
|
+
yAxis: {
|
|
435
|
+
type: "value",
|
|
436
|
+
name: block.config.y_axis_label,
|
|
437
|
+
nameTextStyle: {
|
|
438
|
+
color: muted,
|
|
439
|
+
fontSize: 10
|
|
440
|
+
},
|
|
441
|
+
axisLabel: {
|
|
442
|
+
...axisLabel,
|
|
443
|
+
formatter: (v) => compact.format(v)
|
|
444
|
+
},
|
|
445
|
+
splitLine: { lineStyle: {
|
|
446
|
+
color: border,
|
|
447
|
+
opacity: .5
|
|
448
|
+
} }
|
|
449
|
+
},
|
|
450
|
+
series: pivot.series.map((s, i) => ({
|
|
451
|
+
name: multi ? s.name : singleName,
|
|
452
|
+
type: ct === "bar_chart" ? "bar" : "line",
|
|
453
|
+
data: s.values,
|
|
454
|
+
itemStyle: { color: PALETTE[i % PALETTE.length] },
|
|
455
|
+
...ct === "line_chart" ? {
|
|
456
|
+
lineStyle: { width: 2 },
|
|
457
|
+
symbolSize: 5,
|
|
458
|
+
smooth: false
|
|
459
|
+
} : {},
|
|
460
|
+
barMaxWidth: 32
|
|
461
|
+
}))
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/** Renders one `chart_rendered` block. Uses ECharts (interactive, matching
|
|
465
|
+
* the main web-app) when the host app has it installed; otherwise falls
|
|
466
|
+
* back to a dependency-free SVG render. Table type always uses SVG. */
|
|
467
|
+
function AnswerChart({ block, className }) {
|
|
468
|
+
const ref = react.useRef(null);
|
|
469
|
+
const [engine, setEngine] = react.useState(block.config.chart_type === "table" ? "svg" : "pending");
|
|
470
|
+
react.useEffect(() => {
|
|
471
|
+
if (engine === "svg") return;
|
|
472
|
+
let disposed = false;
|
|
473
|
+
let chart = null;
|
|
474
|
+
let ro = null;
|
|
475
|
+
import(
|
|
476
|
+
/* @vite-ignore */
|
|
477
|
+
"echarts"
|
|
478
|
+
).then((echarts) => {
|
|
479
|
+
const el = ref.current;
|
|
480
|
+
if (disposed || !el) return;
|
|
481
|
+
chart = echarts.init(el, void 0, { renderer: "canvas" });
|
|
482
|
+
chart.setOption(buildEchartsOption(block, el));
|
|
483
|
+
ro = new ResizeObserver(() => chart?.resize());
|
|
484
|
+
ro.observe(el);
|
|
485
|
+
}).catch(() => {
|
|
486
|
+
if (!disposed) setEngine("svg");
|
|
487
|
+
});
|
|
488
|
+
return () => {
|
|
489
|
+
disposed = true;
|
|
490
|
+
ro?.disconnect();
|
|
491
|
+
chart?.dispose();
|
|
492
|
+
};
|
|
493
|
+
}, [block, engine]);
|
|
494
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
495
|
+
className: cx("oxy-chart", className),
|
|
496
|
+
"data-testid": "askdock-chart",
|
|
497
|
+
children: [block.config.title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
|
|
498
|
+
className: "oxy-chart__title",
|
|
499
|
+
children: block.config.title
|
|
500
|
+
}), engine === "svg" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SvgChart, { block }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
501
|
+
ref,
|
|
502
|
+
className: "oxy-chart__echarts",
|
|
503
|
+
style: {
|
|
504
|
+
width: "100%",
|
|
505
|
+
height: 240
|
|
506
|
+
}
|
|
507
|
+
})]
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/shell/marks.tsx
|
|
513
|
+
/** The Oxygen "O" glyph path, in a 0 0 44 44 viewBox, used by `OxyMark`. */
|
|
514
|
+
const OXY_MARK_PATH = "M38.4 22C39.2837 22 40.0074 22.7178 39.9289 23.5979C39.6619 26.5942 38.6471 29.485 36.9665 32.0003C34.9886 34.9603 32.1774 37.2674 28.8883 38.6298C25.5992 39.9922 21.98 40.3487 18.4884 39.6541C14.9967 38.9596 11.7894 37.2453 9.27209 34.7279C6.75474 32.2106 5.04041 29.0033 4.34587 25.5116C3.65134 22.02 4.0078 18.4008 5.37018 15.1117C6.73255 11.8226 9.03966 9.0114 11.9997 7.03353C14.515 5.35287 17.4058 4.33807 20.4021 4.07104C21.2822 3.9926 22 4.71633 22 5.59998L22 11.2C22 12.0837 22.7163 12.8 23.6 12.8H29.2C30.0837 12.8 30.8 12.0837 30.8 11.2V5.6C30.8 4.71634 31.5163 4 32.4 4H38.4C39.2836 4 40 4.71634 40 5.6V11.6C40 12.4837 39.2836 13.2 38.4 13.2H32.8C31.9163 13.2 31.2 13.9163 31.2 14.8V20.4C31.2 21.2837 31.9163 22 32.8 22L38.4 22ZM23.6 22C22.7163 22 22 21.2837 22 20.4L22 14.8371C22 13.9534 21.2778 13.2223 20.4089 13.3827C19.2427 13.5981 18.1268 14.0489 17.1316 14.7139C15.6905 15.6768 14.5674 17.0454 13.9041 18.6466C13.2409 20.2478 13.0674 22.0097 13.4055 23.7095C13.7436 25.4094 14.5782 26.9708 15.8037 28.1963C17.0292 29.4218 18.5906 30.2564 20.2904 30.5945C21.9903 30.9326 23.7522 30.7591 25.3534 30.0959C26.9546 29.4326 28.3232 28.3095 29.2861 26.8684C29.9511 25.8732 30.4019 24.7573 30.6172 23.5911C30.7777 22.7222 30.0466 22 29.1629 22L23.6 22Z";
|
|
515
|
+
/** The Oxygen "O" mark as an inline SVG that inherits `currentColor`, so it
|
|
516
|
+
* reads correctly on any surface in light or dark mode. */
|
|
517
|
+
function OxyMark({ className }) {
|
|
518
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
519
|
+
viewBox: "0 0 44 44",
|
|
520
|
+
fill: "currentColor",
|
|
521
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
522
|
+
className,
|
|
523
|
+
"aria-hidden": "true",
|
|
524
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
525
|
+
fillRule: "evenodd",
|
|
526
|
+
clipRule: "evenodd",
|
|
527
|
+
d: OXY_MARK_PATH
|
|
528
|
+
})
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const FACTORY_PATHS = [
|
|
532
|
+
"M64.7079 60.8981C66.2159 61.5769 66.42 63.424 64.8831 64.0349C53.9237 68.3911 22.4112 68.2339 12.6945 62.9613C11.4576 62.2901 11.6244 60.6958 12.7979 59.9191C17.3748 56.8895 25.3973 51.7874 31.0332 48.4864C31.9361 47.9575 33.0153 47.8364 34.0043 48.1784C41.0152 50.6027 55.7832 56.8809 64.7079 60.8981Z",
|
|
533
|
+
"M40.8612 14.4068C40.6005 13.1343 41.6723 12.0367 42.847 12.591C53.7523 17.7375 68.2377 45.3963 68.9964 57.4238C69.0787 58.7282 67.7002 59.4009 66.5675 58.7485C62.2989 56.2901 53.2249 51.0848 48.6689 48.6258C47.5323 48.0123 46.7649 46.8912 46.5938 45.611C45.4688 37.1953 42.1634 20.7629 40.8612 14.4068Z",
|
|
534
|
+
"M39.2805 32.7689C39.2947 33.8426 38.8352 34.8657 38.0314 35.5777C28.4104 44.1006 18.5237 51.9152 13.0125 56.3133C11.7919 57.2874 10.0954 56.4483 10.3571 54.9087C12.5527 41.9933 25.758 17.8554 35.9571 12.7314C36.9805 12.2172 38.0789 12.9771 38.1857 14.1175C38.5311 17.8049 39.186 25.6228 39.2805 32.7689Z"
|
|
535
|
+
];
|
|
536
|
+
/** The Oxygen Factory mark (three-shard glyph) as an inline SVG in
|
|
537
|
+
* `currentColor`, so it follows the shell foreground in either theme.
|
|
538
|
+
* (The web-app's asset files are fixed-color; this inline variant frees
|
|
539
|
+
* external bundles from carrying those assets.) Decorative — the rail
|
|
540
|
+
* button supplies the "Oxygen Factory" aria-label. */
|
|
541
|
+
function OxygenFactoryMark({ className }) {
|
|
542
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
543
|
+
viewBox: "0 0 80 80",
|
|
544
|
+
fill: "none",
|
|
545
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
546
|
+
className,
|
|
547
|
+
"aria-hidden": "true",
|
|
548
|
+
children: FACTORY_PATHS.map((d) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
549
|
+
d,
|
|
550
|
+
fill: "currentColor"
|
|
551
|
+
}, d.slice(0, 16)))
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/shell/trace.ts
|
|
557
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
558
|
+
const num = (v) => typeof v === "number" ? v : void 0;
|
|
559
|
+
/** "search_catalog" -> "Search Catalog" — matches the web-app pill labels. */
|
|
560
|
+
function prettyToolName(name) {
|
|
561
|
+
return name.split(/[_-]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
562
|
+
}
|
|
563
|
+
/** Compact one-line preview of a tool input (string arg or first string
|
|
564
|
+
* field of an object), truncated for the trace row. */
|
|
565
|
+
function inputPreview(input, max = 60) {
|
|
566
|
+
if (typeof input === "string" && /^[[{]/.test(input.trim())) try {
|
|
567
|
+
return inputPreview(JSON.parse(input), max);
|
|
568
|
+
} catch {}
|
|
569
|
+
let text;
|
|
570
|
+
if (typeof input === "string") text = input;
|
|
571
|
+
else if (Array.isArray(input)) {
|
|
572
|
+
const first = input.find((v) => typeof v === "string" && v.trim());
|
|
573
|
+
text = typeof first === "string" ? first : JSON.stringify(input);
|
|
574
|
+
} else if (input && typeof input === "object") {
|
|
575
|
+
for (const v of Object.values(input)) if (typeof v === "string" && v.trim()) {
|
|
576
|
+
text = v;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
if (!text) {
|
|
580
|
+
const json = JSON.stringify(input);
|
|
581
|
+
text = json === "{}" ? void 0 : json;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (!text) return void 0;
|
|
585
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
586
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
587
|
+
}
|
|
588
|
+
/** Full pretty-printed payload for the expandable row detail: unwraps
|
|
589
|
+
* pre-serialized JSON strings, indents objects, caps runaway sizes. */
|
|
590
|
+
function prettyPayload(v, max = 2e3) {
|
|
591
|
+
if (v === null || v === void 0) return void 0;
|
|
592
|
+
let val = v;
|
|
593
|
+
if (typeof val === "string") {
|
|
594
|
+
const t = val.trim();
|
|
595
|
+
if (!t) return void 0;
|
|
596
|
+
if (/^[[{]/.test(t)) try {
|
|
597
|
+
val = JSON.parse(t);
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
const text = typeof val === "string" ? val : JSON.stringify(val, null, 2);
|
|
601
|
+
if (!text || text === "{}" || text === "[]") return void 0;
|
|
602
|
+
return text.length > max ? `${text.slice(0, max)}\n…` : text;
|
|
603
|
+
}
|
|
604
|
+
function formatMs(ms) {
|
|
605
|
+
return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
|
|
606
|
+
}
|
|
607
|
+
/** Header meta, same as the web-app trace: LLM call count + total LLM time
|
|
608
|
+
* folded from `llm_usage` events. */
|
|
609
|
+
function aggregateLlmStats(events) {
|
|
610
|
+
let calls = 0;
|
|
611
|
+
let totalMs = 0;
|
|
612
|
+
for (const ev of events) {
|
|
613
|
+
if (ev.type !== "llm_usage") continue;
|
|
614
|
+
calls++;
|
|
615
|
+
const d = ev.data ?? {};
|
|
616
|
+
totalMs += num(d.duration_ms) ?? 0;
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
calls,
|
|
620
|
+
totalMs
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** Reconstruct the streamed answer markdown from persisted events —
|
|
624
|
+
* concatenate the `text_delta` tokens, the same accumulation the live
|
|
625
|
+
* `useAgentRun` does. Used to rebuild a turn's answer on history restore. */
|
|
626
|
+
function extractAnswer(events) {
|
|
627
|
+
let out = "";
|
|
628
|
+
for (const ev of events) {
|
|
629
|
+
if (ev.type !== "text_delta") continue;
|
|
630
|
+
const d = ev.data ?? {};
|
|
631
|
+
if (typeof d.token === "string") out += d.token;
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
/** Pull the clarifying-question prompt from a persisted `awaiting_input`
|
|
636
|
+
* event (`{ questions: [{ prompt, suggestions }] }`) so a turn that ended
|
|
637
|
+
* in a suspension restores with its question instead of a blank answer.
|
|
638
|
+
* Mirrors the live `useAgentRun` clarification parse. */
|
|
639
|
+
function extractClarification(events) {
|
|
640
|
+
for (const ev of events) {
|
|
641
|
+
if (ev.type !== "awaiting_input") continue;
|
|
642
|
+
const d = ev.data ?? {};
|
|
643
|
+
const first = (Array.isArray(d.questions) ? d.questions : [])[0];
|
|
644
|
+
if (first && typeof first.prompt === "string") return first.prompt;
|
|
645
|
+
if (typeof d.question === "string") return d.question;
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
function sqlDetail(d) {
|
|
650
|
+
const parts = [];
|
|
651
|
+
const rows = num(d.row_count) ?? (Array.isArray(d.rows) ? d.rows.length : void 0);
|
|
652
|
+
if (rows !== void 0) parts.push(`${rows} row${rows === 1 ? "" : "s"}`);
|
|
653
|
+
const ms = num(d.duration_ms);
|
|
654
|
+
if (ms !== void 0) parts.push(formatMs(ms));
|
|
655
|
+
return parts.length ? parts.join(" · ") : void 0;
|
|
656
|
+
}
|
|
657
|
+
/** Mark any still-running items in a step as settled — called when the step
|
|
658
|
+
* closes so a tool/SQL/semantic row whose matching result never arrived
|
|
659
|
+
* (cancel, error mid-flight, terminal event) doesn't spin forever on a
|
|
660
|
+
* finished turn. */
|
|
661
|
+
function settleStepItems(step, asError = false) {
|
|
662
|
+
for (const item of step.items) if (item.running) {
|
|
663
|
+
item.running = false;
|
|
664
|
+
if (asError) item.error = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Fold raw SSE events into display steps. Pure — safe to re-run on every
|
|
668
|
+
* render over the accumulated event array. */
|
|
669
|
+
function buildTraceSteps(events) {
|
|
670
|
+
const steps = [];
|
|
671
|
+
let open = null;
|
|
672
|
+
let n = 0;
|
|
673
|
+
const id = (prefix) => `${prefix}-${n++}`;
|
|
674
|
+
const ensureStep = () => {
|
|
675
|
+
if (open) return open;
|
|
676
|
+
open = {
|
|
677
|
+
id: id("step"),
|
|
678
|
+
label: "Working",
|
|
679
|
+
status: "running",
|
|
680
|
+
items: []
|
|
681
|
+
};
|
|
682
|
+
steps.push(open);
|
|
683
|
+
return open;
|
|
684
|
+
};
|
|
685
|
+
const lastRunning = (kind) => {
|
|
686
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
687
|
+
const items = steps[i].items;
|
|
688
|
+
for (let j = items.length - 1; j >= 0; j--) {
|
|
689
|
+
const item = items[j];
|
|
690
|
+
if (item.kind === kind && item.running) return item;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
for (const ev of events) {
|
|
695
|
+
const d = ev.data ?? {};
|
|
696
|
+
switch (ev.type) {
|
|
697
|
+
case "step_start":
|
|
698
|
+
if (open) {
|
|
699
|
+
settleStepItems(open);
|
|
700
|
+
open.status = "done";
|
|
701
|
+
}
|
|
702
|
+
open = {
|
|
703
|
+
id: id("step"),
|
|
704
|
+
label: str(d.label) ?? "Step",
|
|
705
|
+
summary: str(d.summary),
|
|
706
|
+
status: "running",
|
|
707
|
+
items: []
|
|
708
|
+
};
|
|
709
|
+
steps.push(open);
|
|
710
|
+
break;
|
|
711
|
+
case "step_end": {
|
|
712
|
+
if (!open) break;
|
|
713
|
+
const outcome = str(d.outcome);
|
|
714
|
+
open.status = outcome === "failed" ? "failed" : outcome === "suspended" ? "suspended" : "done";
|
|
715
|
+
settleStepItems(open, open.status === "failed");
|
|
716
|
+
open = null;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
case "step_summary_update": {
|
|
720
|
+
const summary = str(d.summary);
|
|
721
|
+
if (open && summary) open.summary = summary;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "tool_call": {
|
|
725
|
+
const name = str(d.name) ?? "tool";
|
|
726
|
+
ensureStep().items.push({
|
|
727
|
+
id: id("tool"),
|
|
728
|
+
kind: "tool",
|
|
729
|
+
label: prettyToolName(name),
|
|
730
|
+
preview: inputPreview(d.input),
|
|
731
|
+
input: prettyPayload(d.input),
|
|
732
|
+
running: true
|
|
733
|
+
});
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case "tool_result": {
|
|
737
|
+
const item = lastRunning("tool");
|
|
738
|
+
if (item) {
|
|
739
|
+
item.running = false;
|
|
740
|
+
item.error = d.is_error === true;
|
|
741
|
+
item.output = prettyPayload(d.output);
|
|
742
|
+
const ms = num(d.duration_ms);
|
|
743
|
+
if (ms !== void 0) item.detail = formatMs(ms);
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "thinking_start":
|
|
748
|
+
ensureStep().items.push({
|
|
749
|
+
id: id("think"),
|
|
750
|
+
kind: "thinking",
|
|
751
|
+
label: "Thinking",
|
|
752
|
+
text: "",
|
|
753
|
+
running: true
|
|
754
|
+
});
|
|
755
|
+
break;
|
|
756
|
+
case "thinking_token": {
|
|
757
|
+
const item = lastRunning("thinking");
|
|
758
|
+
if (item) item.text = (item.text ?? "") + (str(d.token) ?? "");
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
case "thinking_end": {
|
|
762
|
+
const item = lastRunning("thinking");
|
|
763
|
+
if (item) item.running = false;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case "semantic_shortcut_attempted":
|
|
767
|
+
ensureStep().items.push({
|
|
768
|
+
id: id("sem"),
|
|
769
|
+
kind: "tool",
|
|
770
|
+
label: "Compile semantic query",
|
|
771
|
+
input: prettyPayload(d),
|
|
772
|
+
running: true
|
|
773
|
+
});
|
|
774
|
+
break;
|
|
775
|
+
case "semantic_shortcut_resolved": {
|
|
776
|
+
const item = lastRunning("tool");
|
|
777
|
+
if (item) {
|
|
778
|
+
item.running = false;
|
|
779
|
+
item.output = prettyPayload(d.sql ?? d);
|
|
780
|
+
}
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case "query_executed": {
|
|
784
|
+
const failed = d.success === false;
|
|
785
|
+
ensureStep().items.push({
|
|
786
|
+
id: id("sql"),
|
|
787
|
+
kind: "sql",
|
|
788
|
+
label: failed ? "Query failed" : "Query",
|
|
789
|
+
detail: failed ? str(d.error) ?? void 0 : sqlDetail(d),
|
|
790
|
+
input: prettyPayload(d.query ?? d.sql),
|
|
791
|
+
output: failed ? prettyPayload(d.error) : void 0,
|
|
792
|
+
error: failed
|
|
793
|
+
});
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
case "verified_sql":
|
|
797
|
+
ensureStep().items.push({
|
|
798
|
+
id: id("sql"),
|
|
799
|
+
kind: "sql",
|
|
800
|
+
label: "Verified query",
|
|
801
|
+
detail: sqlDetail(d),
|
|
802
|
+
input: prettyPayload(d.query ?? d.sql)
|
|
803
|
+
});
|
|
804
|
+
break;
|
|
805
|
+
case "schema_resolved": {
|
|
806
|
+
const tables = Array.isArray(d.tables) ? d.tables.length : void 0;
|
|
807
|
+
ensureStep().items.push({
|
|
808
|
+
id: id("schema"),
|
|
809
|
+
kind: "note",
|
|
810
|
+
label: "Schema resolved",
|
|
811
|
+
detail: tables ? `${tables} table${tables === 1 ? "" : "s"}` : void 0
|
|
812
|
+
});
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
case "recovery_resumed":
|
|
816
|
+
if (open) {
|
|
817
|
+
settleStepItems(open);
|
|
818
|
+
open.status = "done";
|
|
819
|
+
open = null;
|
|
820
|
+
}
|
|
821
|
+
steps.push({
|
|
822
|
+
id: id("step"),
|
|
823
|
+
label: "Resuming",
|
|
824
|
+
summary: str(d.message) ?? "Resuming from server restart",
|
|
825
|
+
status: "done",
|
|
826
|
+
items: []
|
|
827
|
+
});
|
|
828
|
+
break;
|
|
829
|
+
case "awaiting_input":
|
|
830
|
+
if (open) {
|
|
831
|
+
settleStepItems(open);
|
|
832
|
+
open.status = "suspended";
|
|
833
|
+
open = null;
|
|
834
|
+
}
|
|
835
|
+
break;
|
|
836
|
+
case "error":
|
|
837
|
+
case "failed":
|
|
838
|
+
if (open) {
|
|
839
|
+
settleStepItems(open, true);
|
|
840
|
+
open.status = "failed";
|
|
841
|
+
open = null;
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
case "done":
|
|
845
|
+
case "cancelled":
|
|
846
|
+
if (open) {
|
|
847
|
+
settleStepItems(open);
|
|
848
|
+
open.status = "done";
|
|
849
|
+
open = null;
|
|
850
|
+
}
|
|
851
|
+
break;
|
|
852
|
+
default: break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return steps;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/shell/ReasoningTrace.tsx
|
|
860
|
+
function StatusIcon({ status }) {
|
|
861
|
+
if (status === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
862
|
+
role: "status",
|
|
863
|
+
className: "oxy-trace__spin",
|
|
864
|
+
"aria-label": "running"
|
|
865
|
+
});
|
|
866
|
+
if (status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
867
|
+
className: "oxy-trace__icon oxy-trace__icon--fail",
|
|
868
|
+
children: "✕"
|
|
869
|
+
});
|
|
870
|
+
if (status === "suspended") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
871
|
+
className: "oxy-trace__icon oxy-trace__icon--wait",
|
|
872
|
+
children: "…"
|
|
873
|
+
});
|
|
874
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
875
|
+
className: "oxy-trace__icon oxy-trace__icon--ok",
|
|
876
|
+
children: "✓"
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function ChevronIcon({ open }) {
|
|
880
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
881
|
+
width: "12",
|
|
882
|
+
height: "12",
|
|
883
|
+
viewBox: "0 0 24 24",
|
|
884
|
+
fill: "none",
|
|
885
|
+
stroke: "currentColor",
|
|
886
|
+
strokeWidth: "2",
|
|
887
|
+
strokeLinecap: "round",
|
|
888
|
+
strokeLinejoin: "round",
|
|
889
|
+
"aria-hidden": "true",
|
|
890
|
+
style: {
|
|
891
|
+
transform: open ? "rotate(90deg)" : void 0,
|
|
892
|
+
transition: "transform 0.15s"
|
|
893
|
+
},
|
|
894
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 18 6-6-6-6" })
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function TraceItemRows({ step, itemToggles, onToggleItem }) {
|
|
898
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
899
|
+
className: "oxy-trace__items",
|
|
900
|
+
children: step.items.map((item) => {
|
|
901
|
+
if (item.kind === "thinking") return (item.text ?? "").trim() ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
902
|
+
className: "oxy-trace__think",
|
|
903
|
+
children: item.text
|
|
904
|
+
}, item.id) : null;
|
|
905
|
+
const hasDetail = Boolean(item.input || item.output);
|
|
906
|
+
const itemOpen = hasDetail && (itemToggles[item.id] ?? item.running === true);
|
|
907
|
+
const row = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
908
|
+
item.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
909
|
+
role: "status",
|
|
910
|
+
className: "oxy-trace__spin oxy-trace__spin--sm",
|
|
911
|
+
"aria-label": "running"
|
|
912
|
+
}),
|
|
913
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
914
|
+
className: "oxy-trace__row-label",
|
|
915
|
+
children: item.label
|
|
916
|
+
}),
|
|
917
|
+
item.preview && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
918
|
+
className: "oxy-trace__row-preview",
|
|
919
|
+
children: item.preview
|
|
920
|
+
}),
|
|
921
|
+
item.detail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
922
|
+
className: "oxy-trace__row-detail",
|
|
923
|
+
children: item.detail
|
|
924
|
+
})
|
|
925
|
+
] });
|
|
926
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
927
|
+
className: "oxy-trace__item",
|
|
928
|
+
children: [hasDetail ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
929
|
+
type: "button",
|
|
930
|
+
className: cx("oxy-trace__row", "oxy-trace__row--btn", item.error && "oxy-trace__row--error"),
|
|
931
|
+
onClick: () => onToggleItem(item.id, !itemOpen),
|
|
932
|
+
"aria-expanded": itemOpen,
|
|
933
|
+
"data-testid": `reasoning-pill-${item.label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
|
|
934
|
+
children: [row, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
935
|
+
className: "oxy-trace__step-chevron",
|
|
936
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { open: itemOpen })
|
|
937
|
+
})]
|
|
938
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
939
|
+
className: cx("oxy-trace__row", item.error && "oxy-trace__row--error"),
|
|
940
|
+
"data-testid": `reasoning-pill-${item.label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
|
|
941
|
+
children: row
|
|
942
|
+
}), itemOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
943
|
+
className: "oxy-trace__payloads",
|
|
944
|
+
children: [item.input && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
945
|
+
className: "oxy-trace__payload",
|
|
946
|
+
children: item.input
|
|
947
|
+
}), item.output && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
948
|
+
className: "oxy-trace__payload oxy-trace__payload--out",
|
|
949
|
+
children: item.output
|
|
950
|
+
})]
|
|
951
|
+
})]
|
|
952
|
+
}, item.id);
|
|
953
|
+
})
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/** Renders nothing when there are no steps (runs whose stream carries no
|
|
957
|
+
* step events degrade to the plain answer, same as before). */
|
|
958
|
+
function ReasoningTrace({ steps, streaming = false, llm, className }) {
|
|
959
|
+
const [userToggled, setUserToggled] = (0, react.useState)(null);
|
|
960
|
+
const [stepToggles, setStepToggles] = (0, react.useState)({});
|
|
961
|
+
const [itemToggles, setItemToggles] = (0, react.useState)({});
|
|
962
|
+
const runKey = steps[0]?.id ?? "";
|
|
963
|
+
const [prevRunKey, setPrevRunKey] = (0, react.useState)(runKey);
|
|
964
|
+
if (prevRunKey !== runKey) {
|
|
965
|
+
setPrevRunKey(runKey);
|
|
966
|
+
setUserToggled(null);
|
|
967
|
+
setStepToggles({});
|
|
968
|
+
setItemToggles({});
|
|
969
|
+
}
|
|
970
|
+
const open = userToggled ?? streaming;
|
|
971
|
+
const meta = (0, react.useMemo)(() => {
|
|
972
|
+
const parts = [];
|
|
973
|
+
if (llm && llm.calls > 0) {
|
|
974
|
+
parts.push(`${llm.calls} LLM ${llm.calls === 1 ? "call" : "calls"}`);
|
|
975
|
+
if (llm.totalMs > 0) parts.push(formatMs(llm.totalMs));
|
|
976
|
+
}
|
|
977
|
+
parts.push(`${steps.length} step${steps.length === 1 ? "" : "s"}`);
|
|
978
|
+
if (steps.some((s) => s.status === "failed")) parts.push("failed");
|
|
979
|
+
return parts.join(" · ");
|
|
980
|
+
}, [llm, steps]);
|
|
981
|
+
if (steps.length === 0) return null;
|
|
982
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
983
|
+
className: cx("oxy-trace", className),
|
|
984
|
+
"data-run": runKey,
|
|
985
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
986
|
+
type: "button",
|
|
987
|
+
className: "oxy-trace__head",
|
|
988
|
+
onClick: () => setUserToggled(!open),
|
|
989
|
+
"aria-expanded": open,
|
|
990
|
+
children: [
|
|
991
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { open }),
|
|
992
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
993
|
+
className: "oxy-trace__title",
|
|
994
|
+
children: "Reasoning trace"
|
|
995
|
+
}),
|
|
996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
997
|
+
className: "oxy-trace__meta",
|
|
998
|
+
children: meta
|
|
999
|
+
}),
|
|
1000
|
+
streaming && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1001
|
+
role: "status",
|
|
1002
|
+
className: "oxy-trace__spin",
|
|
1003
|
+
"aria-label": "running"
|
|
1004
|
+
})
|
|
1005
|
+
]
|
|
1006
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1007
|
+
className: "oxy-trace__body",
|
|
1008
|
+
children: steps.map((step) => {
|
|
1009
|
+
const stepOpen = stepToggles[step.id] ?? step.status === "running";
|
|
1010
|
+
if (step.items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1011
|
+
className: "oxy-trace__step",
|
|
1012
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1013
|
+
className: "oxy-trace__step-row",
|
|
1014
|
+
children: [
|
|
1015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusIcon, { status: step.status }),
|
|
1016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1017
|
+
className: "oxy-trace__label",
|
|
1018
|
+
children: step.label
|
|
1019
|
+
}),
|
|
1020
|
+
step.summary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1021
|
+
className: "oxy-trace__summary",
|
|
1022
|
+
children: step.summary
|
|
1023
|
+
})
|
|
1024
|
+
]
|
|
1025
|
+
})
|
|
1026
|
+
}, step.id);
|
|
1027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1028
|
+
className: "oxy-trace__step",
|
|
1029
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1030
|
+
type: "button",
|
|
1031
|
+
className: "oxy-trace__step-row oxy-trace__step-row--btn",
|
|
1032
|
+
onClick: () => setStepToggles((t) => ({
|
|
1033
|
+
...t,
|
|
1034
|
+
[step.id]: !stepOpen
|
|
1035
|
+
})),
|
|
1036
|
+
"aria-expanded": stepOpen,
|
|
1037
|
+
children: [
|
|
1038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusIcon, { status: step.status }),
|
|
1039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1040
|
+
className: "oxy-trace__label",
|
|
1041
|
+
children: step.label
|
|
1042
|
+
}),
|
|
1043
|
+
step.summary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1044
|
+
className: "oxy-trace__summary",
|
|
1045
|
+
children: step.summary
|
|
1046
|
+
}),
|
|
1047
|
+
!stepOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1048
|
+
className: "oxy-trace__count",
|
|
1049
|
+
children: [
|
|
1050
|
+
step.items.length,
|
|
1051
|
+
" action",
|
|
1052
|
+
step.items.length === 1 ? "" : "s"
|
|
1053
|
+
]
|
|
1054
|
+
}),
|
|
1055
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1056
|
+
className: "oxy-trace__step-chevron",
|
|
1057
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { open: stepOpen })
|
|
1058
|
+
})
|
|
1059
|
+
]
|
|
1060
|
+
}), stepOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceItemRows, {
|
|
1061
|
+
step,
|
|
1062
|
+
itemToggles,
|
|
1063
|
+
onToggleItem: (id, next) => setItemToggles((t) => ({
|
|
1064
|
+
...t,
|
|
1065
|
+
[id]: next
|
|
1066
|
+
}))
|
|
1067
|
+
})]
|
|
1068
|
+
}, step.id);
|
|
1069
|
+
})
|
|
1070
|
+
})]
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/shell/threadHistory.ts
|
|
1076
|
+
/**
|
|
1077
|
+
* List the viewer's persistent chat threads for the current project.
|
|
1078
|
+
* Non-fatal on failure (older server / no session): returns an empty
|
|
1079
|
+
* list so the dock falls back to session-only history.
|
|
1080
|
+
*/
|
|
1081
|
+
function useThreadHistory() {
|
|
1082
|
+
const { projectId, fetcher } = require_react.useOxyApp();
|
|
1083
|
+
const [state, setState] = react.useState({
|
|
1084
|
+
threads: [],
|
|
1085
|
+
loading: false,
|
|
1086
|
+
error: null
|
|
1087
|
+
});
|
|
1088
|
+
const [nonce, setNonce] = react.useState(0);
|
|
1089
|
+
const refetch = react.useCallback(() => setNonce((n) => n + 1), []);
|
|
1090
|
+
react.useEffect(() => {
|
|
1091
|
+
if (!projectId) {
|
|
1092
|
+
setState({
|
|
1093
|
+
threads: [],
|
|
1094
|
+
loading: false,
|
|
1095
|
+
error: null
|
|
1096
|
+
});
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
let cancelled = false;
|
|
1100
|
+
setState((s) => ({
|
|
1101
|
+
...s,
|
|
1102
|
+
loading: true
|
|
1103
|
+
}));
|
|
1104
|
+
fetcher(`/api/projects/${projectId}/threads`, { method: "GET" }).then(async (resp) => {
|
|
1105
|
+
if (!resp.ok) throw new Error(`threads list failed: HTTP ${resp.status}`);
|
|
1106
|
+
const threads = await resp.json();
|
|
1107
|
+
if (!cancelled) setState({
|
|
1108
|
+
threads,
|
|
1109
|
+
loading: false,
|
|
1110
|
+
error: null
|
|
1111
|
+
});
|
|
1112
|
+
}).catch((e) => {
|
|
1113
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
1114
|
+
require_react.getOxyAppLogger().log("warn", "thread history unavailable", { error: error.message });
|
|
1115
|
+
if (!cancelled) setState({
|
|
1116
|
+
threads: [],
|
|
1117
|
+
loading: false,
|
|
1118
|
+
error
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
return () => {
|
|
1122
|
+
cancelled = true;
|
|
1123
|
+
};
|
|
1124
|
+
}, [
|
|
1125
|
+
projectId,
|
|
1126
|
+
fetcher,
|
|
1127
|
+
nonce
|
|
1128
|
+
]);
|
|
1129
|
+
return {
|
|
1130
|
+
...state,
|
|
1131
|
+
refetch
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Fetch a thread's transcript for restore. Throws on failure so the
|
|
1136
|
+
* caller can surface a toast / fall back.
|
|
1137
|
+
*/
|
|
1138
|
+
async function fetchThreadTranscript(fetcher, projectId, threadId) {
|
|
1139
|
+
const resp = await fetcher(`/api/projects/${projectId}/threads/${threadId}`, { method: "GET" });
|
|
1140
|
+
if (!resp.ok) throw new Error(`thread transcript failed: HTTP ${resp.status}`);
|
|
1141
|
+
return await resp.json();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
//#endregion
|
|
1145
|
+
//#region src/shell/AskDock.tsx
|
|
1146
|
+
function CloseIcon() {
|
|
1147
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1148
|
+
width: "14",
|
|
1149
|
+
height: "14",
|
|
1150
|
+
viewBox: "0 0 24 24",
|
|
1151
|
+
fill: "none",
|
|
1152
|
+
stroke: "currentColor",
|
|
1153
|
+
strokeWidth: "2",
|
|
1154
|
+
strokeLinecap: "round",
|
|
1155
|
+
strokeLinejoin: "round",
|
|
1156
|
+
"aria-hidden": "true",
|
|
1157
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })]
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function PlusIcon() {
|
|
1161
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1162
|
+
width: "15",
|
|
1163
|
+
height: "15",
|
|
1164
|
+
viewBox: "0 0 24 24",
|
|
1165
|
+
fill: "none",
|
|
1166
|
+
stroke: "currentColor",
|
|
1167
|
+
strokeWidth: "2",
|
|
1168
|
+
strokeLinecap: "round",
|
|
1169
|
+
strokeLinejoin: "round",
|
|
1170
|
+
"aria-hidden": "true",
|
|
1171
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12h14" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 5v14" })]
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
function HistoryIcon() {
|
|
1175
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1176
|
+
width: "14",
|
|
1177
|
+
height: "14",
|
|
1178
|
+
viewBox: "0 0 24 24",
|
|
1179
|
+
fill: "none",
|
|
1180
|
+
stroke: "currentColor",
|
|
1181
|
+
strokeWidth: "2",
|
|
1182
|
+
strokeLinecap: "round",
|
|
1183
|
+
strokeLinejoin: "round",
|
|
1184
|
+
"aria-hidden": "true",
|
|
1185
|
+
children: [
|
|
1186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
1187
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 3v5h5" }),
|
|
1188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 7v5l4 2" })
|
|
1189
|
+
]
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
function ExternalIcon() {
|
|
1193
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1194
|
+
width: "14",
|
|
1195
|
+
height: "14",
|
|
1196
|
+
viewBox: "0 0 24 24",
|
|
1197
|
+
fill: "none",
|
|
1198
|
+
stroke: "currentColor",
|
|
1199
|
+
strokeWidth: "2",
|
|
1200
|
+
strokeLinecap: "round",
|
|
1201
|
+
strokeLinejoin: "round",
|
|
1202
|
+
"aria-hidden": "true",
|
|
1203
|
+
children: [
|
|
1204
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 3h6v6" }),
|
|
1205
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 14 21 3" }),
|
|
1206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
1207
|
+
]
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
function ArrowUpIcon() {
|
|
1211
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1212
|
+
width: "14",
|
|
1213
|
+
height: "14",
|
|
1214
|
+
viewBox: "0 0 24 24",
|
|
1215
|
+
fill: "none",
|
|
1216
|
+
stroke: "currentColor",
|
|
1217
|
+
strokeWidth: "2",
|
|
1218
|
+
strokeLinecap: "round",
|
|
1219
|
+
strokeLinejoin: "round",
|
|
1220
|
+
"aria-hidden": "true",
|
|
1221
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m5 12 7-7 7 7" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 19V5" })]
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function StopIcon() {
|
|
1225
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1226
|
+
width: "12",
|
|
1227
|
+
height: "12",
|
|
1228
|
+
viewBox: "0 0 24 24",
|
|
1229
|
+
fill: "currentColor",
|
|
1230
|
+
"aria-hidden": "true",
|
|
1231
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1232
|
+
x: "6",
|
|
1233
|
+
y: "6",
|
|
1234
|
+
width: "12",
|
|
1235
|
+
height: "12",
|
|
1236
|
+
rx: "2"
|
|
1237
|
+
})
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function chartsFrom(events) {
|
|
1241
|
+
return events.filter((ev) => ev.type === "chart_rendered").map((ev) => ev.data).filter((b) => b?.config && Array.isArray(b.columns) && Array.isArray(b.rows));
|
|
1242
|
+
}
|
|
1243
|
+
/** Rebuild a completed turn from a transcript turn's processed events —
|
|
1244
|
+
* identical to what the live dock accumulates during a run. */
|
|
1245
|
+
function turnFromEvents(question, events) {
|
|
1246
|
+
return {
|
|
1247
|
+
question,
|
|
1248
|
+
answer: extractAnswer(events) || extractClarification(events) || null,
|
|
1249
|
+
charts: chartsFrom(events),
|
|
1250
|
+
steps: buildTraceSteps(events),
|
|
1251
|
+
llm: aggregateLlmStats(events)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* The Ask Oxygen drawer. Keep it mounted and toggle `open` — the
|
|
1256
|
+
* transcript survives close/reopen, matching the web-app dock.
|
|
1257
|
+
*/
|
|
1258
|
+
function AskDock({ agentId, workspaceName, suggestions = [], threadsHref, open, onClose, className }) {
|
|
1259
|
+
const run = require_react.useAgentRun({ agentId });
|
|
1260
|
+
const { projectId, fetcher } = require_react.useOxyApp();
|
|
1261
|
+
const { threads: serverThreads, refetch: refetchHistory } = useThreadHistory();
|
|
1262
|
+
const [turns, setTurns] = react.useState([]);
|
|
1263
|
+
const [pending, setPending] = react.useState(null);
|
|
1264
|
+
const [draft, setDraft] = react.useState("");
|
|
1265
|
+
const [threadId, setThreadId] = react.useState(null);
|
|
1266
|
+
const [history, setHistory] = react.useState([]);
|
|
1267
|
+
const [historyOpen, setHistoryOpen] = react.useState(false);
|
|
1268
|
+
const [restoring, setRestoring] = react.useState(false);
|
|
1269
|
+
const scrollRef = react.useRef(null);
|
|
1270
|
+
const restoreGenRef = react.useRef(0);
|
|
1271
|
+
const busy = run.state === "running";
|
|
1272
|
+
const liveSteps = react.useMemo(() => buildTraceSteps(run.events), [run.events]);
|
|
1273
|
+
const liveLlm = react.useMemo(() => aggregateLlmStats(run.events), [run.events]);
|
|
1274
|
+
const liveCharts = react.useMemo(() => chartsFrom(run.events), [run.events]);
|
|
1275
|
+
react.useEffect(() => {
|
|
1276
|
+
if (run.threadId) setThreadId(run.threadId);
|
|
1277
|
+
}, [run.threadId]);
|
|
1278
|
+
const finalizedPendingTurn = () => pending === null ? null : {
|
|
1279
|
+
question: pending,
|
|
1280
|
+
answer: run.answer ?? run.clarification,
|
|
1281
|
+
charts: liveCharts,
|
|
1282
|
+
steps: liveSteps,
|
|
1283
|
+
llm: liveLlm
|
|
1284
|
+
};
|
|
1285
|
+
/** The current conversation as a snapshot, or null when it's empty. */
|
|
1286
|
+
const snapshotCurrent = () => {
|
|
1287
|
+
const finalized = finalizedPendingTurn();
|
|
1288
|
+
const all = finalized ? [...turns, finalized] : turns;
|
|
1289
|
+
if (all.length === 0) return null;
|
|
1290
|
+
return {
|
|
1291
|
+
id: threadId ?? all[0].question,
|
|
1292
|
+
title: all[0].question,
|
|
1293
|
+
turns: all,
|
|
1294
|
+
threadId
|
|
1295
|
+
};
|
|
1296
|
+
};
|
|
1297
|
+
const resetToEmpty = () => {
|
|
1298
|
+
restoreGenRef.current += 1;
|
|
1299
|
+
run.cancel();
|
|
1300
|
+
setTurns([]);
|
|
1301
|
+
setPending(null);
|
|
1302
|
+
setDraft("");
|
|
1303
|
+
setThreadId(null);
|
|
1304
|
+
};
|
|
1305
|
+
const newChat = () => {
|
|
1306
|
+
const snap = snapshotCurrent();
|
|
1307
|
+
if (snap) setHistory((h) => [snap, ...h.filter((c) => c.id !== snap.id)]);
|
|
1308
|
+
resetToEmpty();
|
|
1309
|
+
setHistoryOpen(false);
|
|
1310
|
+
};
|
|
1311
|
+
const openConversation = (conv) => {
|
|
1312
|
+
restoreGenRef.current += 1;
|
|
1313
|
+
const snap = snapshotCurrent();
|
|
1314
|
+
setHistory((h) => {
|
|
1315
|
+
const rest = h.filter((c) => c.id !== conv.id && (!snap || c.id !== snap.id));
|
|
1316
|
+
return snap && snap.id !== conv.id ? [snap, ...rest] : rest;
|
|
1317
|
+
});
|
|
1318
|
+
run.cancel();
|
|
1319
|
+
setTurns(conv.turns);
|
|
1320
|
+
setPending(null);
|
|
1321
|
+
setDraft("");
|
|
1322
|
+
setThreadId(conv.threadId);
|
|
1323
|
+
setHistoryOpen(false);
|
|
1324
|
+
};
|
|
1325
|
+
/** Restore a persistent (server) thread: archive the current chat, then
|
|
1326
|
+
* fetch + rebuild the transcript. Follow-ups resume the same thread. */
|
|
1327
|
+
const openServerThread = async (id) => {
|
|
1328
|
+
if (!projectId) return;
|
|
1329
|
+
const gen = restoreGenRef.current += 1;
|
|
1330
|
+
const snap = snapshotCurrent();
|
|
1331
|
+
if (snap && snap.id !== id) setHistory((h) => [snap, ...h.filter((c) => c.id !== snap.id)]);
|
|
1332
|
+
run.cancel();
|
|
1333
|
+
setTurns([]);
|
|
1334
|
+
setPending(null);
|
|
1335
|
+
setDraft("");
|
|
1336
|
+
setThreadId(id);
|
|
1337
|
+
setHistoryOpen(false);
|
|
1338
|
+
setRestoring(true);
|
|
1339
|
+
try {
|
|
1340
|
+
const transcript = await fetchThreadTranscript(fetcher, projectId, id);
|
|
1341
|
+
if (restoreGenRef.current !== gen) return;
|
|
1342
|
+
setTurns(transcript.turns.map((t) => turnFromEvents(t.question, t.events)));
|
|
1343
|
+
} catch {} finally {
|
|
1344
|
+
if (restoreGenRef.current === gen) setRestoring(false);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
const send = (raw) => {
|
|
1348
|
+
const question = raw.trim();
|
|
1349
|
+
if (!question || busy) return;
|
|
1350
|
+
restoreGenRef.current += 1;
|
|
1351
|
+
if (pending !== null) {
|
|
1352
|
+
const finalized = finalizedPendingTurn();
|
|
1353
|
+
if (finalized) setTurns((t) => [...t, finalized]);
|
|
1354
|
+
}
|
|
1355
|
+
setPending(question);
|
|
1356
|
+
setDraft("");
|
|
1357
|
+
run.ask(question, threadId ? { threadId } : void 0);
|
|
1358
|
+
};
|
|
1359
|
+
react.useEffect(() => {
|
|
1360
|
+
const el = scrollRef.current;
|
|
1361
|
+
if (!el) return;
|
|
1362
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 120) el.scrollTo({ top: el.scrollHeight });
|
|
1363
|
+
});
|
|
1364
|
+
const empty = pending === null && turns.length === 0;
|
|
1365
|
+
const canNewChat = !empty;
|
|
1366
|
+
const historyEntries = [...history.filter((c) => c.id !== threadId).map((c) => ({
|
|
1367
|
+
id: c.id,
|
|
1368
|
+
title: c.title,
|
|
1369
|
+
meta: `${c.turns.length} message${c.turns.length === 1 ? "" : "s"}`,
|
|
1370
|
+
onOpen: () => openConversation(c)
|
|
1371
|
+
})), ...serverThreads.filter((t) => t.id !== threadId && !history.some((c) => c.id === t.id)).map((t) => ({
|
|
1372
|
+
id: t.id,
|
|
1373
|
+
title: t.title,
|
|
1374
|
+
onOpen: () => void openServerThread(t.id)
|
|
1375
|
+
}))];
|
|
1376
|
+
const placeholder = run.state === "needs_clarification" ? "Reply…" : pending !== null ? "Ask a follow-up…" : `Ask Oxygen anything about ${workspaceName ?? "your workspace"}…`;
|
|
1377
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
1378
|
+
className: cx("oxy-shell-scope oxy-askdock", !open && "oxy-askdock--closed", className),
|
|
1379
|
+
"aria-label": "Ask Oxygen",
|
|
1380
|
+
"aria-hidden": !open,
|
|
1381
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1382
|
+
className: "oxy-askdock__head",
|
|
1383
|
+
children: [
|
|
1384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyMark, { className: "oxy-askdock__mark" }),
|
|
1385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1386
|
+
className: "oxy-askdock__title",
|
|
1387
|
+
children: "Ask Oxygen"
|
|
1388
|
+
}),
|
|
1389
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1390
|
+
className: "oxy-askdock__actions",
|
|
1391
|
+
children: [
|
|
1392
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1393
|
+
type: "button",
|
|
1394
|
+
className: "oxy-askdock__iconbtn",
|
|
1395
|
+
onClick: newChat,
|
|
1396
|
+
disabled: !canNewChat,
|
|
1397
|
+
"data-testid": "askdock-new-chat",
|
|
1398
|
+
"aria-label": "New chat",
|
|
1399
|
+
title: "New chat",
|
|
1400
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {})
|
|
1401
|
+
}),
|
|
1402
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1403
|
+
type: "button",
|
|
1404
|
+
className: cx("oxy-askdock__iconbtn", historyOpen && "oxy-askdock__iconbtn--active"),
|
|
1405
|
+
onClick: () => {
|
|
1406
|
+
const next = !historyOpen;
|
|
1407
|
+
setHistoryOpen(next);
|
|
1408
|
+
if (next) refetchHistory();
|
|
1409
|
+
},
|
|
1410
|
+
"data-testid": "askdock-history",
|
|
1411
|
+
"aria-label": "Chat history",
|
|
1412
|
+
"aria-expanded": historyOpen,
|
|
1413
|
+
title: "Chat history",
|
|
1414
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistoryIcon, {})
|
|
1415
|
+
}),
|
|
1416
|
+
threadsHref && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1417
|
+
className: "oxy-askdock__iconbtn",
|
|
1418
|
+
href: threadsHref,
|
|
1419
|
+
"aria-label": "Open chat in Oxygen",
|
|
1420
|
+
title: "Open chat in Oxygen",
|
|
1421
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalIcon, {})
|
|
1422
|
+
}),
|
|
1423
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1424
|
+
type: "button",
|
|
1425
|
+
className: "oxy-askdock__iconbtn",
|
|
1426
|
+
onClick: onClose,
|
|
1427
|
+
"aria-label": "Close Ask Oxygen",
|
|
1428
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
|
|
1429
|
+
})
|
|
1430
|
+
]
|
|
1431
|
+
})
|
|
1432
|
+
]
|
|
1433
|
+
}), historyOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1434
|
+
className: "oxy-askdock__history",
|
|
1435
|
+
"data-testid": "askdock-history-panel",
|
|
1436
|
+
children: historyEntries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1437
|
+
className: "oxy-askdock__history-empty",
|
|
1438
|
+
children: "No previous chats yet."
|
|
1439
|
+
}) : historyEntries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1440
|
+
type: "button",
|
|
1441
|
+
className: "oxy-askdock__history-item",
|
|
1442
|
+
onClick: entry.onOpen,
|
|
1443
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1444
|
+
className: "oxy-askdock__history-title",
|
|
1445
|
+
children: entry.title
|
|
1446
|
+
}), entry.meta && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1447
|
+
className: "oxy-askdock__history-meta",
|
|
1448
|
+
children: entry.meta
|
|
1449
|
+
})]
|
|
1450
|
+
}, entry.id))
|
|
1451
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1452
|
+
className: "oxy-askdock__scroll",
|
|
1453
|
+
ref: scrollRef,
|
|
1454
|
+
children: [
|
|
1455
|
+
restoring && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1456
|
+
className: "oxy-askdock__history-empty",
|
|
1457
|
+
children: "Loading conversation…"
|
|
1458
|
+
}),
|
|
1459
|
+
empty && !restoring && suggestions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1460
|
+
className: "oxy-askdock__chips",
|
|
1461
|
+
children: suggestions.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1462
|
+
type: "button",
|
|
1463
|
+
className: "oxy-askdock__chip",
|
|
1464
|
+
onClick: () => send(s),
|
|
1465
|
+
children: s
|
|
1466
|
+
}, s))
|
|
1467
|
+
}),
|
|
1468
|
+
turns.map((t, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1469
|
+
className: "oxy-askdock__turn",
|
|
1470
|
+
children: [
|
|
1471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1472
|
+
className: "oxy-askdock__q",
|
|
1473
|
+
children: t.question
|
|
1474
|
+
}),
|
|
1475
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningTrace, {
|
|
1476
|
+
steps: t.steps,
|
|
1477
|
+
llm: t.llm
|
|
1478
|
+
}),
|
|
1479
|
+
t.charts.map((c, ci) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnswerChart, { block: c }, `${ci}-${c.config.title ?? c.config.chart_type}`)),
|
|
1480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_react.OxyAnswer, {
|
|
1481
|
+
answer: t.answer,
|
|
1482
|
+
state: "done",
|
|
1483
|
+
threadUrl: null
|
|
1484
|
+
})
|
|
1485
|
+
]
|
|
1486
|
+
}, `${i}-${t.question}`)),
|
|
1487
|
+
pending !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1488
|
+
className: "oxy-askdock__turn",
|
|
1489
|
+
children: [
|
|
1490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1491
|
+
className: "oxy-askdock__q",
|
|
1492
|
+
children: pending
|
|
1493
|
+
}),
|
|
1494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningTrace, {
|
|
1495
|
+
steps: liveSteps,
|
|
1496
|
+
streaming: busy,
|
|
1497
|
+
llm: liveLlm
|
|
1498
|
+
}),
|
|
1499
|
+
liveCharts.map((c, ci) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnswerChart, { block: c }, `${ci}-${c.config.title ?? c.config.chart_type}`)),
|
|
1500
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_react.OxyAnswer, {
|
|
1501
|
+
answer: run.answer,
|
|
1502
|
+
state: run.state,
|
|
1503
|
+
clarification: run.clarification,
|
|
1504
|
+
error: run.error,
|
|
1505
|
+
threadUrl: null
|
|
1506
|
+
})
|
|
1507
|
+
]
|
|
1508
|
+
})
|
|
1509
|
+
]
|
|
1510
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1511
|
+
className: "oxy-askdock__composer",
|
|
1512
|
+
onSubmit: (e) => {
|
|
1513
|
+
e.preventDefault();
|
|
1514
|
+
send(draft);
|
|
1515
|
+
},
|
|
1516
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1517
|
+
className: "oxy-askdock__input",
|
|
1518
|
+
rows: 3,
|
|
1519
|
+
placeholder,
|
|
1520
|
+
value: draft,
|
|
1521
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1522
|
+
onKeyDown: (e) => {
|
|
1523
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1524
|
+
e.preventDefault();
|
|
1525
|
+
send(draft);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1529
|
+
className: "oxy-askdock__composer-row",
|
|
1530
|
+
children: busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1531
|
+
type: "button",
|
|
1532
|
+
className: "oxy-askdock__send",
|
|
1533
|
+
onClick: run.cancel,
|
|
1534
|
+
"aria-label": "Stop generating",
|
|
1535
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StopIcon, {})
|
|
1536
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1537
|
+
type: "submit",
|
|
1538
|
+
className: "oxy-askdock__send",
|
|
1539
|
+
disabled: !draft.trim(),
|
|
1540
|
+
"aria-label": "Send",
|
|
1541
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArrowUpIcon, {})
|
|
1542
|
+
})
|
|
1543
|
+
})]
|
|
1544
|
+
})] })]
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
//#endregion
|
|
1549
|
+
//#region src/shell/logoUrl.ts
|
|
1550
|
+
/** URL of the workspace logo endpoint (org-uploaded logo, falling back to
|
|
1551
|
+
* the code-first file). Consumers render an `<img>` and fall back on error.
|
|
1552
|
+
* `apiBaseUrl` is the API root (e.g. `"/api"` same-origin, or an absolute
|
|
1553
|
+
* base in dev); pass `version` (the org's `updated_at`) to bust the cache
|
|
1554
|
+
* after an upload/remove. */
|
|
1555
|
+
const workspaceLogoUrl = (apiBaseUrl, workspaceId, version) => {
|
|
1556
|
+
const base = `${apiBaseUrl.replace(/\/$/, "")}/${workspaceId}/logo`;
|
|
1557
|
+
return version ? `${base}?v=${encodeURIComponent(version)}` : base;
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
//#endregion
|
|
1561
|
+
//#region src/shell/portal.tsx
|
|
1562
|
+
/**
|
|
1563
|
+
* Floating shell content (tooltips) portals into the nearest shell scope
|
|
1564
|
+
* element instead of `document.body`, so the `oxy-shell-scope` tokens and a
|
|
1565
|
+
* host-applied `.dark` ancestor keep applying to it. Falls back to the
|
|
1566
|
+
* default body portal when no provider is mounted (standalone `ShellRail`
|
|
1567
|
+
* under a host that themes `<body>` itself).
|
|
1568
|
+
*/
|
|
1569
|
+
const ShellPortalContext = react.createContext(null);
|
|
1570
|
+
/** Track the portal container element for descendants of a shell root. */
|
|
1571
|
+
function useShellPortalContainer() {
|
|
1572
|
+
const [container, setContainer] = react.useState(null);
|
|
1573
|
+
return {
|
|
1574
|
+
container,
|
|
1575
|
+
setContainer
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
//#endregion
|
|
1580
|
+
//#region src/shell/Tooltip.tsx
|
|
1581
|
+
/**
|
|
1582
|
+
* Hover tooltip for shell controls. Mirrors the web-app's Radix tooltip
|
|
1583
|
+
* (zero delay, side arrow, `--zinc-200`/muted surface) but portals into the
|
|
1584
|
+
* shell scope so the namespaced tokens and dark-mode class reach it.
|
|
1585
|
+
*/
|
|
1586
|
+
function ShellTooltip({ content, side = "right", children }) {
|
|
1587
|
+
const container = react.useContext(ShellPortalContext);
|
|
1588
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_tooltip.Provider, {
|
|
1589
|
+
delayDuration: 0,
|
|
1590
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_radix_ui_react_tooltip.Root, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_tooltip.Trigger, {
|
|
1591
|
+
asChild: true,
|
|
1592
|
+
children
|
|
1593
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_tooltip.Portal, {
|
|
1594
|
+
container: container ?? void 0,
|
|
1595
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_radix_ui_react_tooltip.Content, {
|
|
1596
|
+
side,
|
|
1597
|
+
sideOffset: 0,
|
|
1598
|
+
className: "oxy-shell-scope oxy-shell-tooltip",
|
|
1599
|
+
children: [content, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_tooltip.Arrow, { className: "oxy-shell-tooltip__arrow" })]
|
|
1600
|
+
})
|
|
1601
|
+
})] })
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/shell/ShellRail.tsx
|
|
1607
|
+
function RailEntry({ item }) {
|
|
1608
|
+
const [failedUrl, setFailedUrl] = (0, react.useState)(null);
|
|
1609
|
+
const content = item.imageUrl && failedUrl !== item.imageUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1610
|
+
src: item.imageUrl,
|
|
1611
|
+
alt: "",
|
|
1612
|
+
onError: () => setFailedUrl(item.imageUrl ?? null),
|
|
1613
|
+
className: "oxy-rail__item-img"
|
|
1614
|
+
}) : item.icon ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1615
|
+
className: "oxy-rail__item-letter",
|
|
1616
|
+
children: item.letter
|
|
1617
|
+
});
|
|
1618
|
+
const className = cx("oxy-rail__item", item.active && "oxy-rail__item--active");
|
|
1619
|
+
const ariaCurrent = item.active ? "page" : void 0;
|
|
1620
|
+
const entry = item.href ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1621
|
+
href: item.href,
|
|
1622
|
+
"data-testid": item.testId,
|
|
1623
|
+
"aria-label": item.label,
|
|
1624
|
+
"aria-current": ariaCurrent,
|
|
1625
|
+
className,
|
|
1626
|
+
children: content
|
|
1627
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1628
|
+
type: "button",
|
|
1629
|
+
onClick: item.onSelect,
|
|
1630
|
+
"data-testid": item.testId,
|
|
1631
|
+
"aria-label": item.label,
|
|
1632
|
+
"aria-current": ariaCurrent,
|
|
1633
|
+
className,
|
|
1634
|
+
children: content
|
|
1635
|
+
});
|
|
1636
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShellTooltip, {
|
|
1637
|
+
content: item.tooltip ?? item.label,
|
|
1638
|
+
children: entry
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
/** A hairline divider between conceptual rail groups. */
|
|
1642
|
+
function RailDivider() {
|
|
1643
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "oxy-rail__divider" });
|
|
1644
|
+
}
|
|
1645
|
+
/** The icon rail. `groups` are the conceptual sections (HQ · Apps ·
|
|
1646
|
+
* Intelligence), rendered top-down with a hairline divider between them.
|
|
1647
|
+
* `footerItems` pin to the bottom of the scroll area as a distinct
|
|
1648
|
+
* "system" zone (Oxygen Factory), above the `bottom` account slot
|
|
1649
|
+
* (workspace switch · user menu — injected by the host app). */
|
|
1650
|
+
function ShellRail({ top, groups, footerItems, bottom, className }) {
|
|
1651
|
+
const { container, setContainer } = useShellPortalContainer();
|
|
1652
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShellPortalContext.Provider, {
|
|
1653
|
+
value: container,
|
|
1654
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1655
|
+
ref: setContainer,
|
|
1656
|
+
"data-testid": "shell-rail",
|
|
1657
|
+
className: cx("oxy-shell-scope oxy-rail", className),
|
|
1658
|
+
children: [
|
|
1659
|
+
top && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1660
|
+
className: "oxy-rail__top",
|
|
1661
|
+
children: top
|
|
1662
|
+
}),
|
|
1663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1664
|
+
className: "oxy-rail__nav",
|
|
1665
|
+
children: [groups.map((group, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [i > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailDivider, {}), group.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailEntry, { item }, item.key))] }, group[0]?.key ?? i)), footerItems && footerItems.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1666
|
+
className: "oxy-rail__footer",
|
|
1667
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailDivider, {}), footerItems.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailEntry, { item }, item.key))]
|
|
1668
|
+
})]
|
|
1669
|
+
}),
|
|
1670
|
+
bottom && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1671
|
+
className: "oxy-rail__bottom",
|
|
1672
|
+
children: bottom
|
|
1673
|
+
})
|
|
1674
|
+
]
|
|
1675
|
+
})
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/shell/shellContext.ts
|
|
1681
|
+
/**
|
|
1682
|
+
* Fetch the shell bootstrap payload for the current customer app. Must be
|
|
1683
|
+
* called inside `<OxyAppProvider>`. Failure is non-fatal by design: the
|
|
1684
|
+
* shell degrades to chrome-less rendering (older servers don't have the
|
|
1685
|
+
* endpoint), so errors are surfaced on the result, never thrown.
|
|
1686
|
+
*/
|
|
1687
|
+
function useShellContext() {
|
|
1688
|
+
const { projectId, fetcher } = require_react.useOxyApp();
|
|
1689
|
+
const [state, setState] = react.useState({
|
|
1690
|
+
data: null,
|
|
1691
|
+
loading: true,
|
|
1692
|
+
error: null
|
|
1693
|
+
});
|
|
1694
|
+
react.useEffect(() => {
|
|
1695
|
+
if (!projectId) {
|
|
1696
|
+
setState({
|
|
1697
|
+
data: null,
|
|
1698
|
+
loading: false,
|
|
1699
|
+
error: /* @__PURE__ */ new Error("no projectId resolved")
|
|
1700
|
+
});
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
let cancelled = false;
|
|
1704
|
+
fetcher(`/api/projects/${projectId}/shell-context`, { method: "GET" }).then(async (resp) => {
|
|
1705
|
+
if (!resp.ok) throw new Error(`shell-context failed: HTTP ${resp.status}`);
|
|
1706
|
+
const data = await resp.json();
|
|
1707
|
+
if (!cancelled) setState({
|
|
1708
|
+
data,
|
|
1709
|
+
loading: false,
|
|
1710
|
+
error: null
|
|
1711
|
+
});
|
|
1712
|
+
}).catch((e) => {
|
|
1713
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
1714
|
+
require_react.getOxyAppLogger().log("warn", "shell-context unavailable, rendering without chrome", { error: error.message });
|
|
1715
|
+
if (!cancelled) setState({
|
|
1716
|
+
data: null,
|
|
1717
|
+
loading: false,
|
|
1718
|
+
error
|
|
1719
|
+
});
|
|
1720
|
+
});
|
|
1721
|
+
return () => {
|
|
1722
|
+
cancelled = true;
|
|
1723
|
+
};
|
|
1724
|
+
}, [projectId, fetcher]);
|
|
1725
|
+
return state;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
//#endregion
|
|
1729
|
+
//#region src/shell/TopBar.tsx
|
|
1730
|
+
/** Frame only: left content + right cluster. Mirrors the rail's
|
|
1731
|
+
* `--sidebar-background` so the two read as one 48px-tall frame. */
|
|
1732
|
+
function TopBar({ left, right, className }) {
|
|
1733
|
+
const { container, setContainer } = useShellPortalContainer();
|
|
1734
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShellPortalContext.Provider, {
|
|
1735
|
+
value: container,
|
|
1736
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1737
|
+
ref: setContainer,
|
|
1738
|
+
"data-testid": "workspace-topbar",
|
|
1739
|
+
className: cx("oxy-shell-scope oxy-topbar", className),
|
|
1740
|
+
children: [left, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1741
|
+
className: "oxy-topbar__right",
|
|
1742
|
+
children: right
|
|
1743
|
+
})]
|
|
1744
|
+
})
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* "<Workspace> / <Page>" — e.g. "Poke House / HQ". The workspace name links
|
|
1749
|
+
* back to the HQ home; pass `onHomeNavigate` to intercept for SPA routing
|
|
1750
|
+
* (the anchor still carries `homeHref` for middle-click/new-tab).
|
|
1751
|
+
*/
|
|
1752
|
+
function Breadcrumb({ workspaceLabel, pageLabel, homeHref, onHomeNavigate }) {
|
|
1753
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1754
|
+
className: "oxy-breadcrumb",
|
|
1755
|
+
children: [
|
|
1756
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1757
|
+
href: homeHref,
|
|
1758
|
+
"data-testid": "topbar-workspace-link",
|
|
1759
|
+
className: "oxy-breadcrumb__workspace",
|
|
1760
|
+
onClick: onHomeNavigate ? (e) => {
|
|
1761
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
|
1762
|
+
e.preventDefault();
|
|
1763
|
+
onHomeNavigate();
|
|
1764
|
+
} : void 0,
|
|
1765
|
+
children: workspaceLabel
|
|
1766
|
+
}),
|
|
1767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1768
|
+
className: "oxy-breadcrumb__sep",
|
|
1769
|
+
children: "/"
|
|
1770
|
+
}),
|
|
1771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1772
|
+
className: "oxy-breadcrumb__page",
|
|
1773
|
+
children: pageLabel
|
|
1774
|
+
})
|
|
1775
|
+
]
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
/** `navigator.onLine` + online/offline events. */
|
|
1779
|
+
function useOnlineStatus() {
|
|
1780
|
+
const [online, setOnline] = (0, react.useState)(() => typeof navigator === "undefined" ? true : navigator.onLine);
|
|
1781
|
+
(0, react.useEffect)(() => {
|
|
1782
|
+
const up = () => setOnline(true);
|
|
1783
|
+
const down = () => setOnline(false);
|
|
1784
|
+
window.addEventListener("online", up);
|
|
1785
|
+
window.addEventListener("offline", down);
|
|
1786
|
+
return () => {
|
|
1787
|
+
window.removeEventListener("online", up);
|
|
1788
|
+
window.removeEventListener("offline", down);
|
|
1789
|
+
};
|
|
1790
|
+
}, []);
|
|
1791
|
+
return online;
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Universal "Sys: Connected" indicator — a pulsing green dot when the browser
|
|
1795
|
+
* is online, red when offline. Presentational connectivity signal; a real
|
|
1796
|
+
* backend health ping can replace `useOnlineStatus` later without touching
|
|
1797
|
+
* consumers.
|
|
1798
|
+
*/
|
|
1799
|
+
function SystemIndicator() {
|
|
1800
|
+
const online = useOnlineStatus();
|
|
1801
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1802
|
+
"data-testid": "topbar-system",
|
|
1803
|
+
className: "oxy-shell-scope oxy-sysind",
|
|
1804
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Sys:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1805
|
+
className: cx("oxy-sysind__state", online ? "oxy-sysind__state--online" : "oxy-sysind__state--offline"),
|
|
1806
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1807
|
+
className: "oxy-sysind__dot-wrap",
|
|
1808
|
+
children: [online && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "oxy-sysind__ping" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: cx("oxy-sysind__dot", online ? "oxy-sysind__dot--online" : "oxy-sysind__dot--offline") })]
|
|
1809
|
+
}), online ? "Connected" : "Offline"]
|
|
1810
|
+
})]
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Live clock in the given IANA timezone (viewer-local when omitted or
|
|
1815
|
+
* invalid). Updates each half-minute (minute precision), pausing while the
|
|
1816
|
+
* tab is hidden.
|
|
1817
|
+
*/
|
|
1818
|
+
function WorkspaceClock({ timezone }) {
|
|
1819
|
+
const [now, setNow] = (0, react.useState)(() => /* @__PURE__ */ new Date());
|
|
1820
|
+
(0, react.useEffect)(() => {
|
|
1821
|
+
const update = () => setNow(/* @__PURE__ */ new Date());
|
|
1822
|
+
const id = setInterval(() => {
|
|
1823
|
+
if (!document.hidden) update();
|
|
1824
|
+
}, 3e4);
|
|
1825
|
+
const onVisible = () => {
|
|
1826
|
+
if (!document.hidden) update();
|
|
1827
|
+
};
|
|
1828
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
1829
|
+
return () => {
|
|
1830
|
+
clearInterval(id);
|
|
1831
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
1832
|
+
};
|
|
1833
|
+
}, []);
|
|
1834
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1835
|
+
"data-testid": "topbar-clock",
|
|
1836
|
+
className: "oxy-shell-scope oxy-clock",
|
|
1837
|
+
children: (0, react.useMemo)(() => {
|
|
1838
|
+
try {
|
|
1839
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
1840
|
+
timeZone: timezone,
|
|
1841
|
+
hour: "numeric",
|
|
1842
|
+
minute: "2-digit",
|
|
1843
|
+
timeZoneName: timezone ? "short" : void 0
|
|
1844
|
+
}).format(now);
|
|
1845
|
+
} catch {
|
|
1846
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
1847
|
+
hour: "numeric",
|
|
1848
|
+
minute: "2-digit"
|
|
1849
|
+
}).format(now);
|
|
1850
|
+
}
|
|
1851
|
+
}, [now, timezone])
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
//#endregion
|
|
1856
|
+
//#region src/shell/WorkspaceTile.tsx
|
|
1857
|
+
/** Rail-top workspace identity: the org/code-first logo when present, the
|
|
1858
|
+
* name initial otherwise. Pure branding — switching workspaces is the host
|
|
1859
|
+
* app's concern (the rail `bottom` slot). */
|
|
1860
|
+
function WorkspaceTile({ name, logoUrl }) {
|
|
1861
|
+
const [failedUrl, setFailedUrl] = (0, react.useState)(null);
|
|
1862
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1863
|
+
"data-testid": "rail-workspace",
|
|
1864
|
+
title: name,
|
|
1865
|
+
className: "oxy-shell-scope oxy-workspace-tile",
|
|
1866
|
+
children: !!logoUrl && failedUrl !== logoUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1867
|
+
src: logoUrl,
|
|
1868
|
+
alt: name,
|
|
1869
|
+
onError: () => setFailedUrl(logoUrl ?? null),
|
|
1870
|
+
className: "oxy-workspace-tile__img"
|
|
1871
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1872
|
+
className: "oxy-workspace-tile__letter",
|
|
1873
|
+
children: name.slice(0, 1).toUpperCase()
|
|
1874
|
+
})
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
//#endregion
|
|
1879
|
+
//#region src/shell/OxyShell.tsx
|
|
1880
|
+
/** Home/HQ glyph (lucide "house" outline, inlined). */
|
|
1881
|
+
function HouseIcon() {
|
|
1882
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1883
|
+
width: "16",
|
|
1884
|
+
height: "16",
|
|
1885
|
+
viewBox: "0 0 24 24",
|
|
1886
|
+
fill: "none",
|
|
1887
|
+
stroke: "currentColor",
|
|
1888
|
+
strokeWidth: "2",
|
|
1889
|
+
strokeLinecap: "round",
|
|
1890
|
+
strokeLinejoin: "round",
|
|
1891
|
+
"aria-hidden": "true",
|
|
1892
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" })]
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
/** Chat glyph (lucide "messages-square" outline, inlined). */
|
|
1896
|
+
function MessagesIcon() {
|
|
1897
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1898
|
+
width: "16",
|
|
1899
|
+
height: "16",
|
|
1900
|
+
viewBox: "0 0 24 24",
|
|
1901
|
+
fill: "none",
|
|
1902
|
+
stroke: "currentColor",
|
|
1903
|
+
strokeWidth: "2",
|
|
1904
|
+
strokeLinecap: "round",
|
|
1905
|
+
strokeLinejoin: "round",
|
|
1906
|
+
"aria-hidden": "true",
|
|
1907
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 10a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 9a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" })]
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
/** Settings glyph (lucide "settings" outline, inlined). */
|
|
1911
|
+
function GearIcon() {
|
|
1912
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1913
|
+
width: "16",
|
|
1914
|
+
height: "16",
|
|
1915
|
+
viewBox: "0 0 24 24",
|
|
1916
|
+
fill: "none",
|
|
1917
|
+
stroke: "currentColor",
|
|
1918
|
+
strokeWidth: "2",
|
|
1919
|
+
strokeLinecap: "round",
|
|
1920
|
+
strokeLinejoin: "round",
|
|
1921
|
+
"aria-hidden": "true",
|
|
1922
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1923
|
+
cx: "12",
|
|
1924
|
+
cy: "12",
|
|
1925
|
+
r: "3"
|
|
1926
|
+
})]
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
/** Rewrite a shell-context relative product path to an absolute URL against
|
|
1930
|
+
* the oxy origin, when one is configured. `/api/*` paths are left relative
|
|
1931
|
+
* — they ride the host's same-origin fetch / dev proxy (which attaches
|
|
1932
|
+
* auth), so absolutizing them would drop the credentials the proxy adds. */
|
|
1933
|
+
function makeAbs(base) {
|
|
1934
|
+
const origin = base?.replace(/\/$/, "");
|
|
1935
|
+
return (url) => {
|
|
1936
|
+
if (!url) return void 0;
|
|
1937
|
+
if (!origin || !url.startsWith("/") || url.startsWith("/api/")) return url;
|
|
1938
|
+
return `${origin}${url}`;
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
function buildRailGroups(data, currentAppSlug, abs) {
|
|
1942
|
+
const hq = {
|
|
1943
|
+
key: "hq",
|
|
1944
|
+
label: "HQ",
|
|
1945
|
+
testId: "rail-hq",
|
|
1946
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HouseIcon, {}),
|
|
1947
|
+
href: abs(data.links.home)
|
|
1948
|
+
};
|
|
1949
|
+
const chat = {
|
|
1950
|
+
key: "chat",
|
|
1951
|
+
label: "Chat",
|
|
1952
|
+
testId: "rail-chat",
|
|
1953
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagesIcon, {}),
|
|
1954
|
+
href: abs(data.links.threads)
|
|
1955
|
+
};
|
|
1956
|
+
const appItems = data.apps.map((app) => ({
|
|
1957
|
+
key: app.id,
|
|
1958
|
+
label: app.name,
|
|
1959
|
+
testId: `rail-app-${app.slug}`,
|
|
1960
|
+
letter: app.name.slice(0, 1).toUpperCase(),
|
|
1961
|
+
imageUrl: abs(app.icon_url),
|
|
1962
|
+
active: app.slug === currentAppSlug,
|
|
1963
|
+
href: abs(app.url)
|
|
1964
|
+
}));
|
|
1965
|
+
return appItems.length ? [[hq, chat], appItems] : [[hq, chat]];
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* The workspace chrome around a customer app: icon rail + universal top bar
|
|
1969
|
+
* + content column — visually identical to the main web-app shell.
|
|
1970
|
+
*/
|
|
1971
|
+
function OxyShell({ children, pageLabel, topBarLeft, topBarExtra, railBottom, hideTopBar, askHotkey = true, productBaseUrl, className }) {
|
|
1972
|
+
const abs = makeAbs(productBaseUrl);
|
|
1973
|
+
const { appSlug } = require_react.useOxyApp();
|
|
1974
|
+
const { manifest } = require_react.useResolvedManifest();
|
|
1975
|
+
const { data, loading } = useShellContext();
|
|
1976
|
+
const { container, setContainer } = useShellPortalContainer();
|
|
1977
|
+
const degraded = !loading && !data;
|
|
1978
|
+
const effectiveSlug = appSlug || manifest.slug;
|
|
1979
|
+
const currentApp = data?.apps.find((app) => app.slug === effectiveSlug);
|
|
1980
|
+
const workspaceLabel = data ? data.org.name || data.workspace.name : "";
|
|
1981
|
+
const page = pageLabel ?? currentApp?.name ?? manifest.name ?? effectiveSlug ?? "";
|
|
1982
|
+
const askAgent = currentApp?.default_agent ?? manifest.ask?.agent ?? void 0;
|
|
1983
|
+
const askSuggestions = currentApp?.suggested_questions?.length ? currentApp.suggested_questions : manifest.ask?.suggestedQuestions ?? [];
|
|
1984
|
+
const [askOpen, setAskOpen] = (0, react.useState)(false);
|
|
1985
|
+
(0, react.useEffect)(() => {
|
|
1986
|
+
if (!askAgent || !askHotkey) return;
|
|
1987
|
+
const onKey = (e) => {
|
|
1988
|
+
if (e.key.toLowerCase() !== "k" || !(e.metaKey || e.ctrlKey)) return;
|
|
1989
|
+
e.preventDefault();
|
|
1990
|
+
setAskOpen((o) => !o);
|
|
1991
|
+
};
|
|
1992
|
+
window.addEventListener("keydown", onKey);
|
|
1993
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1994
|
+
}, [askAgent, askHotkey]);
|
|
1995
|
+
const settingsEntry = data?.links.settings ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShellTooltip, {
|
|
1996
|
+
content: "Settings",
|
|
1997
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1998
|
+
href: abs(data.links.settings),
|
|
1999
|
+
"aria-label": "Settings",
|
|
2000
|
+
"data-testid": "rail-settings",
|
|
2001
|
+
className: "oxy-rail__item",
|
|
2002
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GearIcon, {})
|
|
2003
|
+
})
|
|
2004
|
+
}) : null;
|
|
2005
|
+
const bottom = railBottom || settingsEntry ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [railBottom, settingsEntry] }) : void 0;
|
|
2006
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShellPortalContext.Provider, {
|
|
2007
|
+
value: container,
|
|
2008
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2009
|
+
ref: setContainer,
|
|
2010
|
+
className: cx("oxy-shell-scope oxy-shell", degraded && "oxy-shell--degraded", className),
|
|
2011
|
+
children: [
|
|
2012
|
+
!degraded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShellRail, {
|
|
2013
|
+
top: data ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceTile, {
|
|
2014
|
+
name: data.workspace.name,
|
|
2015
|
+
logoUrl: data.logo_url ?? void 0
|
|
2016
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2017
|
+
className: "oxy-workspace-tile",
|
|
2018
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyMark, { className: "oxy-rail__item-img" })
|
|
2019
|
+
}),
|
|
2020
|
+
groups: data ? buildRailGroups(data, effectiveSlug, abs) : [[]],
|
|
2021
|
+
bottom
|
|
2022
|
+
}),
|
|
2023
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2024
|
+
className: "oxy-shell__content-col",
|
|
2025
|
+
children: [!hideTopBar && (!degraded || topBarLeft || topBarExtra) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TopBar, {
|
|
2026
|
+
left: topBarLeft ?? (data ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Breadcrumb, {
|
|
2027
|
+
workspaceLabel,
|
|
2028
|
+
pageLabel: page,
|
|
2029
|
+
homeHref: abs(data.links.home) ?? data.links.home
|
|
2030
|
+
}) : void 0),
|
|
2031
|
+
right: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2032
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SystemIndicator, {}),
|
|
2033
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceClock, {}),
|
|
2034
|
+
topBarExtra,
|
|
2035
|
+
askAgent && !degraded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2036
|
+
type: "button",
|
|
2037
|
+
"data-testid": "ask-oxygen-button",
|
|
2038
|
+
onClick: () => setAskOpen((o) => !o),
|
|
2039
|
+
className: cx("oxy-askbtn", askOpen && "oxy-askbtn--open"),
|
|
2040
|
+
children: [
|
|
2041
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyMark, { className: "oxy-askbtn__mark" }),
|
|
2042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2043
|
+
className: "oxy-askbtn__label",
|
|
2044
|
+
children: "Ask Oxygen"
|
|
2045
|
+
}),
|
|
2046
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("kbd", {
|
|
2047
|
+
className: "oxy-askbtn__kbd",
|
|
2048
|
+
children: "⌘K"
|
|
2049
|
+
})
|
|
2050
|
+
]
|
|
2051
|
+
})
|
|
2052
|
+
] })
|
|
2053
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2054
|
+
className: "oxy-shell__main",
|
|
2055
|
+
children
|
|
2056
|
+
})]
|
|
2057
|
+
}),
|
|
2058
|
+
askAgent && !degraded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AskDock, {
|
|
2059
|
+
agentId: askAgent,
|
|
2060
|
+
open: askOpen,
|
|
2061
|
+
onClose: () => setAskOpen(false),
|
|
2062
|
+
workspaceName: data?.workspace.name,
|
|
2063
|
+
suggestions: askSuggestions,
|
|
2064
|
+
threadsHref: abs(data?.links.threads)
|
|
2065
|
+
})
|
|
2066
|
+
]
|
|
2067
|
+
})
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
//#endregion
|
|
2072
|
+
exports.AnswerChart = AnswerChart;
|
|
2073
|
+
exports.AskDock = AskDock;
|
|
2074
|
+
exports.Breadcrumb = Breadcrumb;
|
|
2075
|
+
exports.OXY_MARK_PATH = OXY_MARK_PATH;
|
|
2076
|
+
exports.OxyMark = OxyMark;
|
|
2077
|
+
exports.OxyShell = OxyShell;
|
|
2078
|
+
exports.OxygenFactoryMark = OxygenFactoryMark;
|
|
2079
|
+
exports.ReasoningTrace = ReasoningTrace;
|
|
2080
|
+
exports.ShellRail = ShellRail;
|
|
2081
|
+
exports.ShellTooltip = ShellTooltip;
|
|
2082
|
+
exports.SystemIndicator = SystemIndicator;
|
|
2083
|
+
exports.TopBar = TopBar;
|
|
2084
|
+
exports.WorkspaceClock = WorkspaceClock;
|
|
2085
|
+
exports.WorkspaceTile = WorkspaceTile;
|
|
2086
|
+
exports.buildTraceSteps = buildTraceSteps;
|
|
2087
|
+
exports.fetchThreadTranscript = fetchThreadTranscript;
|
|
2088
|
+
exports.useShellContext = useShellContext;
|
|
2089
|
+
exports.useThreadHistory = useThreadHistory;
|
|
2090
|
+
exports.workspaceLogoUrl = workspaceLogoUrl;
|
|
2091
|
+
//# sourceMappingURL=shell.cjs.map
|