artbot 0.1.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 +32 -0
- package/bin/artbot.cjs +11 -0
- package/dist/chunks/chunk-KKAF45ZB.js +667 -0
- package/dist/chunks/chunk-KKAF45ZB.js.map +7 -0
- package/dist/chunks/interactive-DQDPPJBS.js +994 -0
- package/dist/chunks/interactive-DQDPPJBS.js.map +7 -0
- package/dist/index.js +892 -0
- package/dist/index.js.map +7 -0
- package/package.json +57 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assessLocalSetup,
|
|
3
|
+
pathExists,
|
|
4
|
+
runSetupWizard
|
|
5
|
+
} from "./chunk-KKAF45ZB.js";
|
|
6
|
+
|
|
7
|
+
// src/interactive-app.tsx
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { Box as Box2, Text as Text2, render, useApp, useInput } from "ink";
|
|
10
|
+
import TextInput from "ink-text-input";
|
|
11
|
+
|
|
12
|
+
// src/tui/helpers.ts
|
|
13
|
+
function text(text2, tone = "neutral", weight = "normal") {
|
|
14
|
+
return { kind: "text", text: text2, tone, weight };
|
|
15
|
+
}
|
|
16
|
+
function spacer(size = 1) {
|
|
17
|
+
return { kind: "spacer", size };
|
|
18
|
+
}
|
|
19
|
+
function divider(label, tone = "muted") {
|
|
20
|
+
return { kind: "divider", label, tone };
|
|
21
|
+
}
|
|
22
|
+
function panel(title, children, options = {}) {
|
|
23
|
+
return {
|
|
24
|
+
kind: "panel",
|
|
25
|
+
title,
|
|
26
|
+
subtitle: options.subtitle,
|
|
27
|
+
accent: options.accent,
|
|
28
|
+
width: options.width,
|
|
29
|
+
children
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function stack(direction, children, gap = 1) {
|
|
33
|
+
return {
|
|
34
|
+
kind: "stack",
|
|
35
|
+
direction,
|
|
36
|
+
gap,
|
|
37
|
+
children
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function commandHint(command, description, tone = "accent") {
|
|
41
|
+
return { command, description, tone };
|
|
42
|
+
}
|
|
43
|
+
function table(columns, rows) {
|
|
44
|
+
return { kind: "table", columns, rows };
|
|
45
|
+
}
|
|
46
|
+
function progressBar(value, tone = "accent", label, width) {
|
|
47
|
+
return { kind: "progress-bar", value: Math.min(1, Math.max(0, value)), tone, label, width };
|
|
48
|
+
}
|
|
49
|
+
function formatElapsed(seconds) {
|
|
50
|
+
if (seconds < 60) return `${seconds}s`;
|
|
51
|
+
const m = Math.floor(seconds / 60);
|
|
52
|
+
const s = seconds % 60;
|
|
53
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/tui/status-rail.ts
|
|
57
|
+
function statusDot(state) {
|
|
58
|
+
switch (state) {
|
|
59
|
+
case "healthy":
|
|
60
|
+
return { symbol: "\u25CF", tone: "success" };
|
|
61
|
+
case "degraded":
|
|
62
|
+
return { symbol: "\u25CF", tone: "warning" };
|
|
63
|
+
case "offline":
|
|
64
|
+
return { symbol: "\u25CF", tone: "danger" };
|
|
65
|
+
default:
|
|
66
|
+
return { symbol: "\u25CB", tone: "muted" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function statusEntry(name, status) {
|
|
70
|
+
const dot = statusDot(status.state);
|
|
71
|
+
const detail = status.detail ? `: ${status.detail}` : "";
|
|
72
|
+
return text(`${dot.symbol} ${name}${detail}`, dot.tone);
|
|
73
|
+
}
|
|
74
|
+
var StatusRail = (props) => stack("column", [
|
|
75
|
+
stack("row", [statusEntry("LM Studio", props.llm), statusEntry("API", props.api), statusEntry("Worker", props.worker)], 3),
|
|
76
|
+
stack("row", [statusEntry("Auth", props.auth), statusEntry("Licensed", props.licensed)], 3)
|
|
77
|
+
]);
|
|
78
|
+
StatusRail.displayName = "StatusRail";
|
|
79
|
+
|
|
80
|
+
// src/tui/command-bar.ts
|
|
81
|
+
var CommandBar = ({ command }) => text(
|
|
82
|
+
command.mode === "running" ? "Running pipeline..." : command.mode === "setup" ? "Setup mode \u2014 run /setup to configure" : "Type /research <artist> or artist name. /help for all commands.",
|
|
83
|
+
command.mode === "running" ? "accent" : "muted",
|
|
84
|
+
"dim"
|
|
85
|
+
);
|
|
86
|
+
CommandBar.displayName = "CommandBar";
|
|
87
|
+
function buildDefaultCommandHints() {
|
|
88
|
+
return [
|
|
89
|
+
commandHint("/research <artist>", "Start artist price research"),
|
|
90
|
+
commandHint("/work <artist> --title <title>", "Start work-specific research"),
|
|
91
|
+
commandHint("/setup", "Verify LM Studio, API, worker, and auth"),
|
|
92
|
+
commandHint("/auth", "Inspect or capture browser session state"),
|
|
93
|
+
commandHint("/doctor", "Run a local environment health check"),
|
|
94
|
+
commandHint("/runs", "Inspect recent or active runs")
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/tui/run-progress-view.ts
|
|
99
|
+
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
100
|
+
function stageIcon(state, tick) {
|
|
101
|
+
switch (state) {
|
|
102
|
+
case "done":
|
|
103
|
+
return { symbol: "\u2713", tone: "success" };
|
|
104
|
+
case "running":
|
|
105
|
+
return { symbol: SPINNER[tick % SPINNER.length], tone: "accent" };
|
|
106
|
+
case "blocked":
|
|
107
|
+
return { symbol: "!", tone: "warning" };
|
|
108
|
+
case "failed":
|
|
109
|
+
return { symbol: "\u2717", tone: "danger" };
|
|
110
|
+
default:
|
|
111
|
+
return { symbol: "\u25CB", tone: "muted" };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
var RunProgressView = ({ progress }) => {
|
|
115
|
+
const tick = progress.tick ?? 0;
|
|
116
|
+
if (progress.status === "idle") {
|
|
117
|
+
return text(" No active research. Type /research <artist> to begin.", "muted", "dim");
|
|
118
|
+
}
|
|
119
|
+
const isActive = progress.status === "running" || progress.status === "queued";
|
|
120
|
+
const stateTone = progress.status === "running" ? "accent" : progress.status === "completed" ? "success" : progress.status === "failed" ? "danger" : "muted";
|
|
121
|
+
const statusIcon = progress.status === "completed" ? "\u2713" : progress.status === "failed" ? "\u2717" : isActive ? SPINNER[tick % SPINNER.length] : "\u25B6";
|
|
122
|
+
const headerParts = [
|
|
123
|
+
text(`${statusIcon} ${progress.artistName ?? "n/a"}`, stateTone, "strong"),
|
|
124
|
+
text(progress.status.toUpperCase(), stateTone, "strong")
|
|
125
|
+
];
|
|
126
|
+
if (progress.elapsed != null) {
|
|
127
|
+
headerParts.push(text(formatElapsed(progress.elapsed), "muted", "dim"));
|
|
128
|
+
}
|
|
129
|
+
const lines = [
|
|
130
|
+
stack("row", headerParts, 2),
|
|
131
|
+
text(` ${progress.runId ?? "n/a"}`, "muted", "dim")
|
|
132
|
+
];
|
|
133
|
+
const doneCount = progress.stages.filter((s) => s.state === "done").length;
|
|
134
|
+
const total = progress.stages.length;
|
|
135
|
+
const ratio = total > 0 ? doneCount / total : 0;
|
|
136
|
+
const barLabel = isActive && progress.elapsed != null ? formatElapsed(progress.elapsed) + " elapsed" : void 0;
|
|
137
|
+
lines.push(spacer(1));
|
|
138
|
+
lines.push(
|
|
139
|
+
progressBar(
|
|
140
|
+
progress.status === "completed" ? 1 : progress.status === "failed" ? ratio : ratio,
|
|
141
|
+
progress.status === "completed" ? "success" : progress.status === "failed" ? "danger" : "accent",
|
|
142
|
+
barLabel
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
lines.push(spacer(1));
|
|
146
|
+
if (isActive) {
|
|
147
|
+
for (const stage of progress.stages) {
|
|
148
|
+
const icon = stageIcon(stage.state, tick);
|
|
149
|
+
const stateLabel = stage.state === "running" ? "running..." : stage.state;
|
|
150
|
+
lines.push(stack("row", [text(` ${icon.symbol} ${stage.label}`, icon.tone), text(stateLabel, icon.tone, "dim")], 1));
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const stages = [];
|
|
154
|
+
for (let i = 0; i < progress.stages.length; i++) {
|
|
155
|
+
const stage = progress.stages[i];
|
|
156
|
+
const icon = stageIcon(stage.state, tick);
|
|
157
|
+
stages.push(text(`${icon.symbol} ${stage.label}`, icon.tone));
|
|
158
|
+
if (i < progress.stages.length - 1) {
|
|
159
|
+
stages.push(text("\u2192", "muted", "dim"));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
lines.push(stack("row", stages, 1));
|
|
163
|
+
}
|
|
164
|
+
if (progress.summaryLines.length > 0) {
|
|
165
|
+
lines.push(spacer(1));
|
|
166
|
+
lines.push(text(` ${progress.summaryLines.join(" \xB7 ")}`, "muted"));
|
|
167
|
+
}
|
|
168
|
+
if (progress.blockerSummary) {
|
|
169
|
+
lines.push(text(` \u26A0 ${progress.blockerSummary}`, "warning"));
|
|
170
|
+
}
|
|
171
|
+
return stack("column", lines, 0);
|
|
172
|
+
};
|
|
173
|
+
RunProgressView.displayName = "RunProgressView";
|
|
174
|
+
|
|
175
|
+
// src/tui/report-workspace.ts
|
|
176
|
+
function recordTone(priceType) {
|
|
177
|
+
switch (priceType) {
|
|
178
|
+
case "hammer_price":
|
|
179
|
+
case "realized_price":
|
|
180
|
+
case "realized_with_buyers_premium":
|
|
181
|
+
return "success";
|
|
182
|
+
case "asking_price":
|
|
183
|
+
return "accent";
|
|
184
|
+
case "estimate":
|
|
185
|
+
return "warning";
|
|
186
|
+
default:
|
|
187
|
+
return "muted";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
var ReportWorkspace = ({ report }) => {
|
|
191
|
+
const lines = [];
|
|
192
|
+
const coverageText = report.sourceCoverage.map((e) => `${e.label}: ${e.value}`).join(" \xB7 ");
|
|
193
|
+
lines.push(text(` Sources ${coverageText}`, "muted"));
|
|
194
|
+
const valuationText = report.valuation.map((e) => `${e.label}: ${e.value}`).join(" \xB7 ");
|
|
195
|
+
lines.push(text(` Values ${valuationText}`, "muted"));
|
|
196
|
+
if (report.acceptedRecords.length > 0) {
|
|
197
|
+
lines.push(spacer(1));
|
|
198
|
+
const columns = [
|
|
199
|
+
{ key: "price", label: "Price", width: 14, tone: "neutral" },
|
|
200
|
+
{ key: "title", label: "Title", width: 34, tone: "neutral" },
|
|
201
|
+
{ key: "source", label: "Source", width: 22, tone: "muted" },
|
|
202
|
+
{ key: "detail", label: "Date / Info", width: 20, tone: "muted" }
|
|
203
|
+
];
|
|
204
|
+
const rows = report.acceptedRecords.map((record) => ({
|
|
205
|
+
price: { text: record.price, tone: recordTone(record.priceType) },
|
|
206
|
+
title: { text: record.workTitle, tone: "neutral" },
|
|
207
|
+
source: { text: record.sourceName, tone: "muted" },
|
|
208
|
+
detail: { text: record.detail ?? "", tone: "muted" }
|
|
209
|
+
}));
|
|
210
|
+
lines.push(table(columns, rows));
|
|
211
|
+
}
|
|
212
|
+
if (lines.length === 0) {
|
|
213
|
+
return text(" No results yet.", "muted", "dim");
|
|
214
|
+
}
|
|
215
|
+
return stack("column", lines, 0);
|
|
216
|
+
};
|
|
217
|
+
ReportWorkspace.displayName = "ReportWorkspace";
|
|
218
|
+
|
|
219
|
+
// src/tui/side-detail-pane.ts
|
|
220
|
+
var SideDetailPane = ({ detail }) => {
|
|
221
|
+
const lines = [];
|
|
222
|
+
for (const blocker of detail.blockers) {
|
|
223
|
+
if (blocker && blocker !== "No blockers recorded.") {
|
|
224
|
+
lines.push(text(`\u26A0 ${blocker}`, "warning"));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const url of detail.evidence) {
|
|
228
|
+
lines.push(text(url, "muted", "dim"));
|
|
229
|
+
}
|
|
230
|
+
if (lines.length === 0) {
|
|
231
|
+
return text("No evidence collected yet.", "muted", "dim");
|
|
232
|
+
}
|
|
233
|
+
return stack("column", lines, 0);
|
|
234
|
+
};
|
|
235
|
+
SideDetailPane.displayName = "SideDetailPane";
|
|
236
|
+
|
|
237
|
+
// src/tui/shell.ts
|
|
238
|
+
var ArtbotTuiShell = ({ model }) => {
|
|
239
|
+
const children = [];
|
|
240
|
+
children.push(StatusRail(model.status));
|
|
241
|
+
children.push(divider("Pipeline"));
|
|
242
|
+
children.push(RunProgressView({ progress: model.progress }));
|
|
243
|
+
const hasRun = model.progress.status !== "idle";
|
|
244
|
+
if (hasRun) {
|
|
245
|
+
children.push(divider("Results"));
|
|
246
|
+
children.push(ReportWorkspace({ report: model.report }));
|
|
247
|
+
}
|
|
248
|
+
if (model.detail.evidence.length > 0) {
|
|
249
|
+
children.push(divider("Evidence"));
|
|
250
|
+
for (const url of model.detail.evidence) {
|
|
251
|
+
children.push(text(` ${url}`, "muted", "dim"));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const section of model.report.diagnostics) {
|
|
255
|
+
const skip = /* @__PURE__ */ new Set(["No detail available.", "No additional diagnostics.", "Slash command ready."]);
|
|
256
|
+
const meaningful = section.lines.filter((l) => !skip.has(l));
|
|
257
|
+
if (meaningful.length > 0) {
|
|
258
|
+
children.push(divider(section.title));
|
|
259
|
+
for (const line of meaningful) {
|
|
260
|
+
const tone = section.tone === "warning" ? "warning" : "muted";
|
|
261
|
+
children.push(text(` ${line}`, tone));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
children.push(spacer(1));
|
|
266
|
+
children.push(CommandBar({ command: model.command }));
|
|
267
|
+
return panel(model.title, children, { accent: "accent", subtitle: model.subtitle });
|
|
268
|
+
};
|
|
269
|
+
ArtbotTuiShell.displayName = "ArtbotTuiShell";
|
|
270
|
+
|
|
271
|
+
// src/tui/ink-renderer.tsx
|
|
272
|
+
import { Fragment } from "react";
|
|
273
|
+
import { Box, Text } from "ink";
|
|
274
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
275
|
+
function toneColor(tone) {
|
|
276
|
+
switch (tone) {
|
|
277
|
+
case "accent":
|
|
278
|
+
return "cyan";
|
|
279
|
+
case "success":
|
|
280
|
+
return "green";
|
|
281
|
+
case "warning":
|
|
282
|
+
return "yellow";
|
|
283
|
+
case "danger":
|
|
284
|
+
return "red";
|
|
285
|
+
case "muted":
|
|
286
|
+
return "gray";
|
|
287
|
+
case "inverse":
|
|
288
|
+
return "black";
|
|
289
|
+
default:
|
|
290
|
+
return "white";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function renderText(node, key) {
|
|
294
|
+
return /* @__PURE__ */ jsx(Text, { color: toneColor(node.tone), dimColor: node.weight === "dim", bold: node.weight === "strong", children: node.text }, key);
|
|
295
|
+
}
|
|
296
|
+
function renderSpacer(node, key) {
|
|
297
|
+
return /* @__PURE__ */ jsx(Text, { children: "\n".repeat(Math.max(1, node.size)) }, key);
|
|
298
|
+
}
|
|
299
|
+
function renderDivider(node, key) {
|
|
300
|
+
return /* @__PURE__ */ jsx(Text, { color: toneColor(node.tone), dimColor: true, children: node.label ? `\u2500\u2500 ${node.label} ${"\u2500".repeat(Math.max(8, 28 - node.label.length))}` : "\u2500".repeat(32) }, key);
|
|
301
|
+
}
|
|
302
|
+
function renderMetric(node, key) {
|
|
303
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: toneColor(node.tone), paddingX: 1, marginRight: 1, children: [
|
|
304
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: node.label }),
|
|
305
|
+
/* @__PURE__ */ jsx(Text, { color: toneColor(node.tone), bold: true, children: node.value }),
|
|
306
|
+
node.hint ? /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: node.hint }) : null
|
|
307
|
+
] }, key);
|
|
308
|
+
}
|
|
309
|
+
function renderList(node, key) {
|
|
310
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginRight: 1, children: [
|
|
311
|
+
node.title ? /* @__PURE__ */ jsx(Text, { color: "gray", bold: true, children: node.title }) : null,
|
|
312
|
+
node.items.map((item, index) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: item.detail ? 1 : 0, children: [
|
|
313
|
+
/* @__PURE__ */ jsxs(Text, { color: toneColor(item.tone), children: [
|
|
314
|
+
item.label,
|
|
315
|
+
item.value ? /* @__PURE__ */ jsxs(Text, { color: "white", children: [
|
|
316
|
+
": ",
|
|
317
|
+
item.value
|
|
318
|
+
] }) : null
|
|
319
|
+
] }),
|
|
320
|
+
item.detail ? /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: item.detail }) : null
|
|
321
|
+
] }, `${key}-item-${index}`))
|
|
322
|
+
] }, key);
|
|
323
|
+
}
|
|
324
|
+
function renderStack(node, key) {
|
|
325
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: node.direction, children: node.children.map((child, index) => /* @__PURE__ */ jsx(
|
|
326
|
+
Box,
|
|
327
|
+
{
|
|
328
|
+
marginRight: node.direction === "row" && index < node.children.length - 1 ? node.gap ?? 1 : 0,
|
|
329
|
+
marginBottom: node.direction === "column" && index < node.children.length - 1 ? node.gap ?? 1 : 0,
|
|
330
|
+
children: /* @__PURE__ */ jsx(RenderTuiNode, { node: child })
|
|
331
|
+
},
|
|
332
|
+
`${key}-child-${index}`
|
|
333
|
+
)) }, key);
|
|
334
|
+
}
|
|
335
|
+
function renderSplit(node, key) {
|
|
336
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: node.direction, children: node.children.map((child, index) => /* @__PURE__ */ jsx(Box, { flexGrow: node.ratios?.[index] ?? 1, marginRight: node.direction === "row" && index === 0 ? 1 : 0, children: /* @__PURE__ */ jsx(RenderTuiNode, { node: child }) }, `${key}-split-${index}`)) }, key);
|
|
337
|
+
}
|
|
338
|
+
function renderPanel(node, key) {
|
|
339
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: toneColor(node.accent), paddingX: 1, paddingY: 0, width: node.width, children: [
|
|
340
|
+
node.title ? /* @__PURE__ */ jsx(Text, { color: toneColor(node.accent), bold: true, children: node.title }) : null,
|
|
341
|
+
node.subtitle ? /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: node.subtitle }) : null,
|
|
342
|
+
node.children.map((child, index) => /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(RenderTuiNode, { node: child }) }, `${key}-panel-${index}`))
|
|
343
|
+
] }, key);
|
|
344
|
+
}
|
|
345
|
+
function renderTable(node, key) {
|
|
346
|
+
if (node.rows.length === 0) return null;
|
|
347
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
348
|
+
/* @__PURE__ */ jsx(Box, { children: node.columns.map((col, i) => /* @__PURE__ */ jsx(Box, { width: col.width, marginRight: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", bold: true, children: col.label }) }, `${key}-hdr-${i}`)) }),
|
|
349
|
+
/* @__PURE__ */ jsx(Box, { children: node.columns.map((col, i) => /* @__PURE__ */ jsx(Box, { width: col.width, marginRight: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2500".repeat(Math.min(col.width, col.label.length + 2)) }) }, `${key}-sep-${i}`)) }),
|
|
350
|
+
node.rows.map((row, ri) => /* @__PURE__ */ jsx(Box, { children: node.columns.map((col, ci) => {
|
|
351
|
+
const cell = row[col.key];
|
|
352
|
+
return /* @__PURE__ */ jsx(Box, { width: col.width, marginRight: 1, children: /* @__PURE__ */ jsx(Text, { color: toneColor(cell?.tone ?? col.tone), wrap: "truncate", children: cell?.text ?? "" }) }, `${key}-cell-${ri}-${ci}`);
|
|
353
|
+
}) }, `${key}-row-${ri}`))
|
|
354
|
+
] }, key);
|
|
355
|
+
}
|
|
356
|
+
function renderProgressBar(node, key) {
|
|
357
|
+
const width = node.width ?? 24;
|
|
358
|
+
const filled = Math.round(node.value * width);
|
|
359
|
+
const empty = width - filled;
|
|
360
|
+
const pct = Math.round(node.value * 100);
|
|
361
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
362
|
+
/* @__PURE__ */ jsx(Text, { color: toneColor(node.tone), children: "\u2588".repeat(filled) }),
|
|
363
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2591".repeat(empty) }),
|
|
364
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
365
|
+
" ",
|
|
366
|
+
pct,
|
|
367
|
+
"%"
|
|
368
|
+
] }),
|
|
369
|
+
node.label ? /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
|
|
370
|
+
" ",
|
|
371
|
+
node.label
|
|
372
|
+
] }) : null
|
|
373
|
+
] }, key);
|
|
374
|
+
}
|
|
375
|
+
function renderKeyHint(node, key) {
|
|
376
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "row", gap: 2, children: node.keys.map((k, i) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
377
|
+
/* @__PURE__ */ jsxs(Text, { color: toneColor(k.tone ?? "accent"), bold: true, children: [
|
|
378
|
+
"[",
|
|
379
|
+
k.key,
|
|
380
|
+
"]"
|
|
381
|
+
] }),
|
|
382
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
383
|
+
" ",
|
|
384
|
+
k.label
|
|
385
|
+
] })
|
|
386
|
+
] }, `${key}-kh-${i}`)) }, key);
|
|
387
|
+
}
|
|
388
|
+
function RenderTuiNode({ node }) {
|
|
389
|
+
switch (node.kind) {
|
|
390
|
+
case "text":
|
|
391
|
+
return renderText(node, "text");
|
|
392
|
+
case "spacer":
|
|
393
|
+
return renderSpacer(node, "spacer");
|
|
394
|
+
case "divider":
|
|
395
|
+
return renderDivider(node, "divider");
|
|
396
|
+
case "metric":
|
|
397
|
+
return renderMetric(node, "metric");
|
|
398
|
+
case "list":
|
|
399
|
+
return renderList(node, "list");
|
|
400
|
+
case "stack":
|
|
401
|
+
return renderStack(node, "stack");
|
|
402
|
+
case "split":
|
|
403
|
+
return renderSplit(node, "split");
|
|
404
|
+
case "panel":
|
|
405
|
+
return renderPanel(node, "panel");
|
|
406
|
+
case "table":
|
|
407
|
+
return renderTable(node, "table");
|
|
408
|
+
case "progress-bar":
|
|
409
|
+
return renderProgressBar(node, "progress-bar");
|
|
410
|
+
case "key-hint":
|
|
411
|
+
return renderKeyHint(node, "key-hint");
|
|
412
|
+
default:
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/interactive-app.tsx
|
|
418
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
419
|
+
function fmtCurrency(amount, currency) {
|
|
420
|
+
try {
|
|
421
|
+
return new Intl.NumberFormat("en-US", {
|
|
422
|
+
style: "currency",
|
|
423
|
+
currency,
|
|
424
|
+
maximumFractionDigits: 0
|
|
425
|
+
}).format(amount);
|
|
426
|
+
} catch {
|
|
427
|
+
return `${amount.toLocaleString("en-US")} ${currency}`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function asRuntimeStatus(label, ok, detail, tone) {
|
|
431
|
+
return {
|
|
432
|
+
label,
|
|
433
|
+
state: ok ? "healthy" : "offline",
|
|
434
|
+
detail,
|
|
435
|
+
tone: tone ?? (ok ? "success" : "danger")
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function blockerSummary(details) {
|
|
439
|
+
if (!details?.summary?.acceptance_reason_breakdown) return void 0;
|
|
440
|
+
const entries = Object.entries(details.summary.acceptance_reason_breakdown).filter(([, count]) => count > 0).sort((a, b) => b[1] - a[1]);
|
|
441
|
+
if (entries.length === 0) return void 0;
|
|
442
|
+
return `Top issue: ${entries[0][0]} (${entries[0][1]})`;
|
|
443
|
+
}
|
|
444
|
+
function priceLabel(record) {
|
|
445
|
+
if (typeof record.normalized_price_usd_nominal === "number") return fmtCurrency(record.normalized_price_usd_nominal, "USD");
|
|
446
|
+
if (typeof record.normalized_price_usd === "number") return fmtCurrency(record.normalized_price_usd, "USD");
|
|
447
|
+
if (typeof record.price_amount === "number") return fmtCurrency(record.price_amount, record.currency ?? "TRY");
|
|
448
|
+
if (typeof record.estimate_low === "number" && typeof record.estimate_high === "number") {
|
|
449
|
+
return `${fmtCurrency(record.estimate_low, record.currency ?? "TRY")}\u2013${fmtCurrency(record.estimate_high, record.currency ?? "TRY")}`;
|
|
450
|
+
}
|
|
451
|
+
if (record.price_type === "inquiry_only" || record.price_hidden) return "Inquiry only";
|
|
452
|
+
return "n/a";
|
|
453
|
+
}
|
|
454
|
+
function rangeLabel(range) {
|
|
455
|
+
if (!range) return "n/a";
|
|
456
|
+
return `${fmtCurrency(range.low, "TRY")} \u2013 ${fmtCurrency(range.high, "TRY")}`;
|
|
457
|
+
}
|
|
458
|
+
function buildModel(params) {
|
|
459
|
+
const { assessment, details, recentRuns, activeArtist, input, history, detailMode, busy, message, context, tick, runStartedAt } = params;
|
|
460
|
+
const summary = details?.summary;
|
|
461
|
+
const records = details?.records ?? [];
|
|
462
|
+
const runId = details?.run?.id ?? "n/a";
|
|
463
|
+
const valuation = details?.valuation;
|
|
464
|
+
const issueLines = assessment?.issues.map((issue) => `${issue.severity}: ${issue.message}${issue.detail ? ` (${issue.detail})` : ""}`) ?? [];
|
|
465
|
+
const recentRunLines = recentRuns.slice(0, 5).map((run) => `${run.id} \xB7 ${run.status} \xB7 ${run.query.artist}`);
|
|
466
|
+
const authLines = assessment?.sessionStates.map((session) => `${session.profileId}: ${session.exists ? session.expired ? "expired" : "ready" : "missing"}`) ?? [];
|
|
467
|
+
const diagnosticsLines = detailMode === "runs" ? recentRunLines : detailMode === "auth" ? authLines : detailMode === "setup" ? issueLines : [message || "No additional diagnostics."];
|
|
468
|
+
return {
|
|
469
|
+
title: "ArtBot",
|
|
470
|
+
subtitle: "Market operations console",
|
|
471
|
+
command: {
|
|
472
|
+
mode: busy ? "running" : detailMode === "setup" ? "setup" : "idle",
|
|
473
|
+
input,
|
|
474
|
+
placeholder: "Type /research <artist> or plain artist text. /help for commands.",
|
|
475
|
+
hints: buildDefaultCommandHints(),
|
|
476
|
+
history
|
|
477
|
+
},
|
|
478
|
+
status: {
|
|
479
|
+
llm: assessment ? asRuntimeStatus("LM Studio", assessment.llmHealth.ok, assessment.llmHealth.modelId ?? assessment.llmHealth.reason) : asRuntimeStatus("LM Studio", false, "checking"),
|
|
480
|
+
api: assessment ? asRuntimeStatus("ArtBot API", assessment.apiHealth.ok, assessment.apiHealth.reason ?? assessment.apiBaseUrl) : asRuntimeStatus("ArtBot API", false, "checking"),
|
|
481
|
+
worker: assessment && assessment.apiHealth.ok ? { label: "Worker", state: "healthy", detail: "assumed with local backend", tone: "success" } : { label: "Worker", state: "unknown", detail: "verify with /doctor", tone: "muted" },
|
|
482
|
+
auth: assessment ? {
|
|
483
|
+
label: "Auth",
|
|
484
|
+
state: assessment.authProfilesError ? "offline" : assessment.sessionStates.some((session) => session.exists && !session.expired) ? "healthy" : "degraded",
|
|
485
|
+
detail: assessment.authProfilesError?.message ?? `${assessment.profiles.length} profiles`,
|
|
486
|
+
tone: assessment.authProfilesError ? "danger" : assessment.sessionStates.some((session) => session.exists && !session.expired) ? "success" : "warning"
|
|
487
|
+
} : { label: "Auth", state: "unknown", detail: "checking", tone: "muted" },
|
|
488
|
+
licensed: {
|
|
489
|
+
label: "Licensed",
|
|
490
|
+
state: context.defaults.allowLicensed ? "healthy" : "degraded",
|
|
491
|
+
detail: context.defaults.licensedIntegrations.length > 0 ? context.defaults.licensedIntegrations.join(", ") : "none",
|
|
492
|
+
tone: context.defaults.allowLicensed ? "success" : "warning"
|
|
493
|
+
},
|
|
494
|
+
model: assessment?.llmHealth.modelId,
|
|
495
|
+
apiBaseUrl: context.apiBaseUrl,
|
|
496
|
+
llmBaseUrl: assessment?.llmBaseUrl
|
|
497
|
+
},
|
|
498
|
+
progress: {
|
|
499
|
+
runId,
|
|
500
|
+
artistName: activeArtist || "n/a",
|
|
501
|
+
status: details?.run?.status === "completed" ? "completed" : details?.run?.status === "failed" ? "failed" : details?.run?.status === "running" ? "running" : details?.run?.status === "pending" ? "queued" : "idle",
|
|
502
|
+
stages: [
|
|
503
|
+
{
|
|
504
|
+
id: "queue",
|
|
505
|
+
label: "Queue",
|
|
506
|
+
state: details?.run?.status ? details.run.status === "pending" ? "running" : "done" : "pending"
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
id: "scan",
|
|
510
|
+
label: "Scan sources",
|
|
511
|
+
state: details?.run?.status === "running" ? "running" : summary ? "done" : "pending"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "analyze",
|
|
515
|
+
label: "Analyze",
|
|
516
|
+
state: summary ? details?.run?.status === "completed" ? "done" : "running" : "pending"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: "report",
|
|
520
|
+
label: "Report",
|
|
521
|
+
state: details?.run?.status === "completed" ? "done" : details?.run?.status === "failed" ? "failed" : "pending"
|
|
522
|
+
}
|
|
523
|
+
],
|
|
524
|
+
summaryLines: [
|
|
525
|
+
summary ? `Accepted: ${summary.accepted_records}` : "Accepted: n/a",
|
|
526
|
+
summary ? `Valuation eligible: ${summary.valuation_eligible_records ?? 0}` : "Valuation eligible: n/a",
|
|
527
|
+
summary ? `Priced coverage: ${Math.round((summary.priced_crawled_source_coverage_ratio ?? summary.priced_source_coverage_ratio ?? 0) * 100)}%` : "Priced coverage: n/a"
|
|
528
|
+
],
|
|
529
|
+
blockerSummary: blockerSummary(details),
|
|
530
|
+
tick,
|
|
531
|
+
elapsed: runStartedAt ? Math.floor((Date.now() - runStartedAt) / 1e3) : void 0
|
|
532
|
+
},
|
|
533
|
+
report: {
|
|
534
|
+
artistName: activeArtist || "No active research",
|
|
535
|
+
runId,
|
|
536
|
+
overview: [
|
|
537
|
+
{ label: "Accepted", value: String(summary?.accepted_records ?? 0), tone: "success" },
|
|
538
|
+
{ label: "URLs Crawled", value: String(summary?.total_attempts ?? 0), tone: "muted" },
|
|
539
|
+
{
|
|
540
|
+
label: "Coverage",
|
|
541
|
+
value: `${Math.round((summary?.priced_crawled_source_coverage_ratio ?? summary?.priced_source_coverage_ratio ?? 0) * 100)}%`,
|
|
542
|
+
tone: (summary?.priced_crawled_source_coverage_ratio ?? 0) >= 0.7 ? "success" : "warning"
|
|
543
|
+
}
|
|
544
|
+
],
|
|
545
|
+
sourceCoverage: [
|
|
546
|
+
{ label: "Platforms", value: String(Object.keys(summary?.source_candidate_breakdown ?? {}).length), tone: "accent" },
|
|
547
|
+
{ label: "Public", value: String(summary?.source_status_breakdown?.public_access ?? 0), tone: "success" },
|
|
548
|
+
{ label: "Blocked", value: String(summary?.source_status_breakdown?.blocked ?? 0), tone: "danger" }
|
|
549
|
+
],
|
|
550
|
+
valuation: [
|
|
551
|
+
{ label: "Blended", value: rangeLabel(valuation?.blendedRange), tone: valuation?.generated ? "success" : "warning" },
|
|
552
|
+
{ label: "Turkey", value: rangeLabel(valuation?.turkeyRange), tone: "accent" },
|
|
553
|
+
{ label: "Intl", value: rangeLabel(valuation?.internationalRange), tone: "muted" }
|
|
554
|
+
],
|
|
555
|
+
acceptedRecords: records.slice(0, 8).map((record) => ({
|
|
556
|
+
price: priceLabel(record),
|
|
557
|
+
priceType: record.price_type,
|
|
558
|
+
workTitle: record.work_title ?? "Untitled",
|
|
559
|
+
sourceName: record.source_name,
|
|
560
|
+
detail: [record.sale_or_listing_date, record.dimensions_text, record.year].filter(Boolean).join(" \xB7 ")
|
|
561
|
+
})),
|
|
562
|
+
diagnostics: [
|
|
563
|
+
{
|
|
564
|
+
title: detailMode === "help" ? "Commands" : detailMode === "setup" ? "Setup Issues" : detailMode === "auth" ? "Auth State" : detailMode === "runs" ? "Recent Runs" : "Run Notes",
|
|
565
|
+
tone: detailMode === "setup" ? "warning" : "muted",
|
|
566
|
+
lines: detailMode === "help" ? [
|
|
567
|
+
"/research <artist>",
|
|
568
|
+
"/work <artist> --title <title>",
|
|
569
|
+
"/setup",
|
|
570
|
+
"/auth",
|
|
571
|
+
"/doctor",
|
|
572
|
+
"/status",
|
|
573
|
+
"/runs",
|
|
574
|
+
"/help",
|
|
575
|
+
"/exit"
|
|
576
|
+
] : diagnosticsLines.length > 0 ? diagnosticsLines : ["No detail available."]
|
|
577
|
+
}
|
|
578
|
+
]
|
|
579
|
+
},
|
|
580
|
+
detail: {
|
|
581
|
+
title: detailMode === "auth" ? "Auth" : detailMode === "runs" ? "Runs" : detailMode === "setup" ? "Setup" : detailMode === "help" ? "Help" : "Context",
|
|
582
|
+
subtitle: detailMode === "setup" ? "Run `artbot setup` for the guided wizard and `artbot auth capture <profile>` for login capture." : message,
|
|
583
|
+
status: [
|
|
584
|
+
assessment ? asRuntimeStatus("LM Studio", assessment.llmHealth.ok, assessment.llmHealth.modelId ?? assessment.llmHealth.reason) : asRuntimeStatus("LM Studio", false, "checking"),
|
|
585
|
+
assessment ? asRuntimeStatus("ArtBot API", assessment.apiHealth.ok, assessment.apiHealth.reason ?? assessment.apiBaseUrl) : asRuntimeStatus("ArtBot API", false, "checking")
|
|
586
|
+
],
|
|
587
|
+
details: detailMode === "auth" ? assessment?.sessionStates.map((session) => ({
|
|
588
|
+
label: session.profileId,
|
|
589
|
+
value: session.exists ? session.expired ? "expired" : "ready" : "missing",
|
|
590
|
+
tone: session.exists ? session.expired ? "warning" : "success" : "danger",
|
|
591
|
+
detail: session.storageStatePath
|
|
592
|
+
})) ?? [] : detailMode === "runs" ? recentRuns.slice(0, 6).map((run) => ({
|
|
593
|
+
label: run.id,
|
|
594
|
+
value: run.status,
|
|
595
|
+
tone: run.status === "completed" ? "success" : run.status === "failed" ? "danger" : "accent",
|
|
596
|
+
detail: run.query.artist
|
|
597
|
+
})) : [
|
|
598
|
+
{
|
|
599
|
+
label: "Mode",
|
|
600
|
+
value: detailMode,
|
|
601
|
+
tone: detailMode === "setup" ? "warning" : "accent"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
label: "Artist",
|
|
605
|
+
value: activeArtist || "n/a",
|
|
606
|
+
tone: "neutral"
|
|
607
|
+
}
|
|
608
|
+
],
|
|
609
|
+
blockers: detailMode === "setup" ? issueLines : [blockerSummary(details) ?? "No blockers recorded."],
|
|
610
|
+
evidence: details?.attempts?.slice(0, 4).map((attempt) => attempt.source_url) ?? []
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function parseWorkCommand(value) {
|
|
615
|
+
const match = value.match(/^\/work\s+(.+?)\s+--title\s+(.+)$/i);
|
|
616
|
+
if (!match) return null;
|
|
617
|
+
return {
|
|
618
|
+
artist: match[1].trim(),
|
|
619
|
+
title: match[2].trim()
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function runInteractiveTui(props) {
|
|
623
|
+
return new Promise((resolve) => {
|
|
624
|
+
let settled = false;
|
|
625
|
+
const instance = render(
|
|
626
|
+
/* @__PURE__ */ jsx2(
|
|
627
|
+
InteractiveApp,
|
|
628
|
+
{
|
|
629
|
+
...props,
|
|
630
|
+
onExit: (code) => {
|
|
631
|
+
if (settled) return;
|
|
632
|
+
settled = true;
|
|
633
|
+
instance.unmount();
|
|
634
|
+
resolve(code);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function InteractiveApp({ context, initialAssessment, onExit }) {
|
|
642
|
+
const { exit } = useApp();
|
|
643
|
+
const [assessment, setAssessment] = useState(initialAssessment);
|
|
644
|
+
const [details, setDetails] = useState(null);
|
|
645
|
+
const [recentRuns, setRecentRuns] = useState([]);
|
|
646
|
+
const [input, setInput] = useState("");
|
|
647
|
+
const [history, setHistory] = useState([]);
|
|
648
|
+
const [detailMode, setDetailMode] = useState(initialAssessment?.issues.length ? "setup" : "help");
|
|
649
|
+
const [busy, setBusy] = useState(false);
|
|
650
|
+
const [activeArtist, setActiveArtist] = useState("");
|
|
651
|
+
const [message, setMessage] = useState("Slash command ready.");
|
|
652
|
+
const [tick, setTick] = useState(0);
|
|
653
|
+
const [runStartedAt, setRunStartedAt] = useState(null);
|
|
654
|
+
const cancelPollingRef = useRef(false);
|
|
655
|
+
useInput((_input, key) => {
|
|
656
|
+
if (key.ctrl && _input === "c") {
|
|
657
|
+
exit();
|
|
658
|
+
onExit(0);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
if (!busy) return;
|
|
663
|
+
const interval = setInterval(() => setTick((t) => t + 1), 150);
|
|
664
|
+
return () => clearInterval(interval);
|
|
665
|
+
}, [busy]);
|
|
666
|
+
const refreshAssessment = useCallback(async () => {
|
|
667
|
+
const next = await assessLocalSetup();
|
|
668
|
+
setAssessment(next);
|
|
669
|
+
return next;
|
|
670
|
+
}, []);
|
|
671
|
+
const fetchRecentRuns = useCallback(async () => {
|
|
672
|
+
const headers = {};
|
|
673
|
+
if (context.apiKey) headers["x-api-key"] = context.apiKey;
|
|
674
|
+
const response = await fetch(`${context.apiBaseUrl}/runs?limit=8`, { headers });
|
|
675
|
+
if (!response.ok) {
|
|
676
|
+
throw new Error(`Failed to load runs (${response.status})`);
|
|
677
|
+
}
|
|
678
|
+
const payload = await response.json();
|
|
679
|
+
setRecentRuns(payload.runs);
|
|
680
|
+
}, [context.apiBaseUrl, context.apiKey]);
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
void (async () => {
|
|
683
|
+
try {
|
|
684
|
+
await refreshAssessment();
|
|
685
|
+
await fetchRecentRuns();
|
|
686
|
+
} catch (error) {
|
|
687
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
688
|
+
}
|
|
689
|
+
})();
|
|
690
|
+
}, [fetchRecentRuns, refreshAssessment]);
|
|
691
|
+
const startResearch = useCallback(
|
|
692
|
+
async (kind, artist, title) => {
|
|
693
|
+
setBusy(true);
|
|
694
|
+
setDetailMode("report");
|
|
695
|
+
setActiveArtist(artist);
|
|
696
|
+
setMessage(`Launching ${kind} research for ${artist}...`);
|
|
697
|
+
setRunStartedAt(Date.now());
|
|
698
|
+
cancelPollingRef.current = false;
|
|
699
|
+
try {
|
|
700
|
+
const nextAssessment = await refreshAssessment();
|
|
701
|
+
if (!nextAssessment.apiHealth.ok) {
|
|
702
|
+
setDetailMode("setup");
|
|
703
|
+
setMessage(`ArtBot API offline at ${nextAssessment.apiBaseUrl}. Run /setup or artbot setup.`);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const headers = {
|
|
707
|
+
"content-type": "application/json"
|
|
708
|
+
};
|
|
709
|
+
if (context.apiKey) headers["x-api-key"] = context.apiKey;
|
|
710
|
+
const response = await fetch(`${context.apiBaseUrl}/research/${kind}`, {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers,
|
|
713
|
+
body: JSON.stringify({
|
|
714
|
+
query: {
|
|
715
|
+
artist,
|
|
716
|
+
title,
|
|
717
|
+
scope: "turkey_plus_international",
|
|
718
|
+
turkeyFirst: true,
|
|
719
|
+
analysisMode: context.defaults.analysisMode,
|
|
720
|
+
priceNormalization: context.defaults.priceNormalization,
|
|
721
|
+
authProfileId: context.defaults.authProfileId,
|
|
722
|
+
manualLoginCheckpoint: false,
|
|
723
|
+
allowLicensed: context.defaults.allowLicensed,
|
|
724
|
+
licensedIntegrations: context.defaults.licensedIntegrations
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
});
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
const text2 = await response.text();
|
|
730
|
+
throw new Error(`Research request failed (${response.status}): ${text2.slice(0, 200)}`);
|
|
731
|
+
}
|
|
732
|
+
const created = await response.json();
|
|
733
|
+
setDetails({
|
|
734
|
+
run: {
|
|
735
|
+
id: created.runId,
|
|
736
|
+
status: created.status
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
setMessage(`Run created: ${created.runId}`);
|
|
740
|
+
while (!cancelPollingRef.current) {
|
|
741
|
+
const detailResponse = await fetch(`${context.apiBaseUrl}/runs/${created.runId}`, {
|
|
742
|
+
headers: context.apiKey ? { "x-api-key": context.apiKey } : void 0
|
|
743
|
+
});
|
|
744
|
+
if (!detailResponse.ok) {
|
|
745
|
+
throw new Error(`Failed to poll run ${created.runId} (${detailResponse.status})`);
|
|
746
|
+
}
|
|
747
|
+
const nextDetails = await detailResponse.json();
|
|
748
|
+
setDetails(nextDetails);
|
|
749
|
+
const status = nextDetails.run?.status;
|
|
750
|
+
if (status === "completed" || status === "failed") {
|
|
751
|
+
if (status === "completed") {
|
|
752
|
+
const s = nextDetails.summary;
|
|
753
|
+
const accepted = s?.accepted_records ?? 0;
|
|
754
|
+
const coverage = Math.round((s?.priced_crawled_source_coverage_ratio ?? s?.priced_source_coverage_ratio ?? 0) * 100);
|
|
755
|
+
setMessage(`\u2713 Run completed \u2014 ${accepted} accepted, ${coverage}% coverage`);
|
|
756
|
+
} else {
|
|
757
|
+
setMessage(`\u2717 Run failed: ${created.runId}`);
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
762
|
+
}
|
|
763
|
+
await fetchRecentRuns();
|
|
764
|
+
} finally {
|
|
765
|
+
setBusy(false);
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
[context.apiBaseUrl, context.apiKey, context.defaults, fetchRecentRuns, refreshAssessment]
|
|
769
|
+
);
|
|
770
|
+
const handleSubmit = useCallback(
|
|
771
|
+
async (value) => {
|
|
772
|
+
const trimmed = value.trim();
|
|
773
|
+
if (!trimmed) return;
|
|
774
|
+
setHistory((current) => [trimmed, ...current].slice(0, 8));
|
|
775
|
+
setInput("");
|
|
776
|
+
try {
|
|
777
|
+
if (!trimmed.startsWith("/")) {
|
|
778
|
+
await startResearch("artist", trimmed);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (trimmed === "/exit") {
|
|
782
|
+
cancelPollingRef.current = true;
|
|
783
|
+
exit();
|
|
784
|
+
onExit(0);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (trimmed === "/help") {
|
|
788
|
+
setDetailMode("help");
|
|
789
|
+
setMessage("Command reference loaded.");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (trimmed === "/status" || trimmed === "/doctor") {
|
|
793
|
+
setDetailMode(trimmed === "/status" ? "status" : "setup");
|
|
794
|
+
await refreshAssessment();
|
|
795
|
+
setMessage("Environment status refreshed.");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (trimmed === "/setup") {
|
|
799
|
+
setDetailMode("setup");
|
|
800
|
+
await refreshAssessment();
|
|
801
|
+
setMessage("Setup diagnostics loaded. Run `artbot setup` for the guided wizard.");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (trimmed === "/auth") {
|
|
805
|
+
setDetailMode("auth");
|
|
806
|
+
await refreshAssessment();
|
|
807
|
+
setMessage("Auth profile status loaded.");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (trimmed === "/runs") {
|
|
811
|
+
setDetailMode("runs");
|
|
812
|
+
await fetchRecentRuns();
|
|
813
|
+
setMessage("Recent runs loaded.");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (trimmed.startsWith("/research ")) {
|
|
817
|
+
await startResearch("artist", trimmed.slice("/research ".length).trim());
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const workCommand = parseWorkCommand(trimmed);
|
|
821
|
+
if (workCommand) {
|
|
822
|
+
await startResearch("work", workCommand.artist, workCommand.title);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
setMessage(`Unknown command: ${trimmed}`);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
setMessage(error instanceof Error ? error.message : String(error));
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
[exit, fetchRecentRuns, onExit, refreshAssessment, startResearch]
|
|
831
|
+
);
|
|
832
|
+
const model = useMemo(
|
|
833
|
+
() => buildModel({
|
|
834
|
+
assessment,
|
|
835
|
+
details,
|
|
836
|
+
recentRuns,
|
|
837
|
+
activeArtist,
|
|
838
|
+
input,
|
|
839
|
+
history,
|
|
840
|
+
detailMode,
|
|
841
|
+
busy,
|
|
842
|
+
message,
|
|
843
|
+
context,
|
|
844
|
+
tick,
|
|
845
|
+
runStartedAt
|
|
846
|
+
}),
|
|
847
|
+
[activeArtist, assessment, busy, context, detailMode, details, history, input, message, recentRuns, tick, runStartedAt]
|
|
848
|
+
);
|
|
849
|
+
const messageColor = message.startsWith("\u2717") || message.startsWith("Failed") || message.includes("error") ? "red" : message.startsWith("\u2713") ? "green" : "gray";
|
|
850
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
851
|
+
/* @__PURE__ */ jsx2(RenderTuiNode, { node: ArtbotTuiShell({ model }) }),
|
|
852
|
+
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
853
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "\u276F" }),
|
|
854
|
+
/* @__PURE__ */ jsx2(Box2, { marginLeft: 1, flexGrow: 1, children: /* @__PURE__ */ jsx2(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "/research <artist>" }) })
|
|
855
|
+
] }),
|
|
856
|
+
/* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
|
|
857
|
+
message && message !== "Slash command ready." ? /* @__PURE__ */ jsx2(Text2, { color: messageColor, dimColor: true, children: message }) : /* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
858
|
+
/* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
|
|
859
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
860
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "[/r]" }),
|
|
861
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: " research" })
|
|
862
|
+
] }),
|
|
863
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
864
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "[/s]" }),
|
|
865
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: " setup" })
|
|
866
|
+
] }),
|
|
867
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
868
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "[/h]" }),
|
|
869
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: " help" })
|
|
870
|
+
] }),
|
|
871
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
872
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "[^c]" }),
|
|
873
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: " quit" })
|
|
874
|
+
] })
|
|
875
|
+
] })
|
|
876
|
+
] })
|
|
877
|
+
] });
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/interactive.ts
|
|
881
|
+
function resolveContext() {
|
|
882
|
+
return {
|
|
883
|
+
apiBaseUrl: process.env.API_BASE_URL ?? "http://localhost:4000",
|
|
884
|
+
apiKey: process.env.ARTBOT_API_KEY
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function toPositiveInt(value, fallback) {
|
|
888
|
+
const parsed = Number(value ?? fallback);
|
|
889
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
890
|
+
return Math.floor(parsed);
|
|
891
|
+
}
|
|
892
|
+
function parseBoolean(value, fallback) {
|
|
893
|
+
if (value === void 0) return fallback;
|
|
894
|
+
return value.trim().toLowerCase() === "true";
|
|
895
|
+
}
|
|
896
|
+
function resolvePipelineDefaultsFromEnv() {
|
|
897
|
+
const rawLicensed = process.env.DEFAULT_LICENSED_INTEGRATIONS ?? "";
|
|
898
|
+
const licensedIntegrations = rawLicensed.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
899
|
+
return {
|
|
900
|
+
analysisMode: process.env.DEFAULT_ANALYSIS_MODE ?? "comprehensive",
|
|
901
|
+
priceNormalization: process.env.DEFAULT_PRICE_NORMALIZATION ?? "usd_dual",
|
|
902
|
+
authProfileId: process.env.DEFAULT_AUTH_PROFILE?.trim() || void 0,
|
|
903
|
+
allowLicensed: parseBoolean(process.env.ENABLE_LICENSED_INTEGRATIONS, false),
|
|
904
|
+
licensedIntegrations,
|
|
905
|
+
transportMaxAttempts: toPositiveInt(process.env.TRANSPORT_MAX_ATTEMPTS, 3),
|
|
906
|
+
transportRequestTimeoutMs: toPositiveInt(process.env.TRANSPORT_REQUEST_TIMEOUT_MS, 15e3),
|
|
907
|
+
transportCurlFallback: parseBoolean(process.env.TRANSPORT_CURL_FALLBACK, true),
|
|
908
|
+
pipelineConcurrency: {
|
|
909
|
+
healthy: toPositiveInt(process.env.PIPELINE_MAX_CONCURRENCY, 6),
|
|
910
|
+
degraded: toPositiveInt(process.env.PIPELINE_DEGRADED_CONCURRENCY, 3),
|
|
911
|
+
suspected: toPositiveInt(process.env.PIPELINE_SUSPECTED_CONCURRENCY, 1)
|
|
912
|
+
},
|
|
913
|
+
pipelineCandidateTimeoutMs: toPositiveInt(process.env.PIPELINE_CANDIDATE_TIMEOUT_MS, 45e3)
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function hostFromUrl(url) {
|
|
917
|
+
if (!url) return null;
|
|
918
|
+
try {
|
|
919
|
+
return new URL(url).hostname.toLowerCase();
|
|
920
|
+
} catch {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function classifyBlocker(attempt) {
|
|
925
|
+
const blocker = (attempt.blocker_reason ?? "").toLowerCase();
|
|
926
|
+
const transport = attempt.extracted_fields?.transport;
|
|
927
|
+
const transportKind = transport?.kind?.toUpperCase();
|
|
928
|
+
if (blocker.includes("target_unreachable") || blocker.includes("transport:dns_failed") || blocker.includes("transport:tcp_timeout") || blocker.includes("transport:tcp_refused") || blocker.includes("transport:tls_failed") || blocker.includes("transport:unknown_network")) {
|
|
929
|
+
return "transport_outage";
|
|
930
|
+
}
|
|
931
|
+
if (transportKind === "RATE_LIMITED") return "rate_limited";
|
|
932
|
+
if (transportKind === "AUTH_INVALID" || attempt.source_access_status === "auth_required") return "auth_required";
|
|
933
|
+
if (transportKind === "LEGAL_BLOCK") return "legal_block";
|
|
934
|
+
if (transportKind === "WAF_BLOCK") return "waf_block";
|
|
935
|
+
if (attempt.source_access_status === "price_hidden") return "price_hidden";
|
|
936
|
+
if (attempt.source_access_status === "blocked") return "blocked";
|
|
937
|
+
return "other";
|
|
938
|
+
}
|
|
939
|
+
function summarizeAttemptBlockers(attempts) {
|
|
940
|
+
if (!attempts || attempts.length === 0) return null;
|
|
941
|
+
const counters = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const attempt of attempts) {
|
|
943
|
+
const category = classifyBlocker(attempt);
|
|
944
|
+
const host = hostFromUrl(attempt.source_url) ?? "unknown-host";
|
|
945
|
+
const current = counters.get(category) ?? { count: 0, hosts: /* @__PURE__ */ new Map() };
|
|
946
|
+
current.count += 1;
|
|
947
|
+
current.hosts.set(host, (current.hosts.get(host) ?? 0) + 1);
|
|
948
|
+
counters.set(category, current);
|
|
949
|
+
}
|
|
950
|
+
const top = [...counters.entries()].sort((a, b) => b[1].count - a[1].count)[0];
|
|
951
|
+
if (!top) return null;
|
|
952
|
+
const topHosts = [...top[1].hosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([host]) => host);
|
|
953
|
+
return {
|
|
954
|
+
category: top[0],
|
|
955
|
+
count: top[1].count,
|
|
956
|
+
hosts: topHosts
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function shouldRunSetupWizard(assessment) {
|
|
960
|
+
return !pathExists(assessment.envPath) || Boolean(assessment.authProfilesError);
|
|
961
|
+
}
|
|
962
|
+
async function startInteractive() {
|
|
963
|
+
let initialAssessment = await assessLocalSetup();
|
|
964
|
+
if (shouldRunSetupWizard(initialAssessment)) {
|
|
965
|
+
try {
|
|
966
|
+
const setup = await runSetupWizard();
|
|
967
|
+
initialAssessment = setup.assessment;
|
|
968
|
+
} catch {
|
|
969
|
+
return 0;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const ctx = resolveContext();
|
|
973
|
+
const pipelineDefaults = resolvePipelineDefaultsFromEnv();
|
|
974
|
+
return runInteractiveTui({
|
|
975
|
+
context: {
|
|
976
|
+
apiBaseUrl: ctx.apiBaseUrl,
|
|
977
|
+
apiKey: ctx.apiKey,
|
|
978
|
+
defaults: {
|
|
979
|
+
analysisMode: pipelineDefaults.analysisMode,
|
|
980
|
+
priceNormalization: pipelineDefaults.priceNormalization,
|
|
981
|
+
authProfileId: pipelineDefaults.authProfileId,
|
|
982
|
+
allowLicensed: pipelineDefaults.allowLicensed,
|
|
983
|
+
licensedIntegrations: pipelineDefaults.licensedIntegrations
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
initialAssessment
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
export {
|
|
990
|
+
resolvePipelineDefaultsFromEnv,
|
|
991
|
+
startInteractive,
|
|
992
|
+
summarizeAttemptBlockers
|
|
993
|
+
};
|
|
994
|
+
//# sourceMappingURL=interactive-DQDPPJBS.js.map
|