agent-nuvira 2.7.0 → 2.7.2
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/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +17 -3
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/app.js +26 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +108 -2
- package/dist/cli/chat.js.map +1 -1
- package/dist/config/auth.js +18 -0
- package/dist/config/jwt.js +24 -0
- package/dist/config/keys.js +14 -0
- package/dist/gateway/chat-store.d.ts +32 -5
- package/dist/gateway/chat-store.d.ts.map +1 -1
- package/dist/gateway/chat-store.js +52 -6
- package/dist/gateway/chat-store.js.map +1 -1
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +54 -10
- package/dist/gateway/registry.js.map +1 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +11 -1
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts +31 -0
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +41 -0
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +5 -3
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +2 -2
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/middleware/auth.js +30 -0
- package/dist/nlu/conversation-gate.js +1 -1
- package/dist/nlu/conversation-gate.js.map +1 -1
- package/dist/nlu/entities.d.ts.map +1 -1
- package/dist/nlu/entities.js +12 -0
- package/dist/nlu/entities.js.map +1 -1
- package/dist/nlu/intent.d.ts.map +1 -1
- package/dist/nlu/intent.js +83 -15
- package/dist/nlu/intent.js.map +1 -1
- package/dist/passport.js +34 -0
- package/dist/routes/auth.js +42 -0
- package/dist/routes/user.js +31 -0
- package/dist/server.js +17 -0
- package/dist/tools/pipeline-tool.d.ts.map +1 -1
- package/dist/tools/pipeline-tool.js +14 -0
- package/dist/tools/pipeline-tool.js.map +1 -1
- package/dist/tools/release-sync.d.ts +52 -16
- package/dist/tools/release-sync.d.ts.map +1 -1
- package/dist/tools/release-sync.js +143 -79
- package/dist/tools/release-sync.js.map +1 -1
- package/dist/web-dashboard/src/App.js +85 -0
- package/dist/web-dashboard/src/admin-auth.test.js +186 -0
- package/dist/web-dashboard/src/ansi.js +23 -0
- package/dist/web-dashboard/src/ansi.test.js +31 -0
- package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
- package/dist/web-dashboard/src/api-admin.test.js +65 -0
- package/dist/web-dashboard/src/api-hub.test.js +117 -0
- package/dist/web-dashboard/src/api.js +1421 -0
- package/dist/web-dashboard/src/api.test.js +51 -0
- package/dist/web-dashboard/src/artifacts.js +128 -0
- package/dist/web-dashboard/src/artifacts.test.js +139 -0
- package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
- package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
- package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
- package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
- package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
- package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
- package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
- package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
- package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
- package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
- package/dist/web-dashboard/src/components/DAGView.js +477 -0
- package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
- package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
- package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
- package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
- package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
- package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
- package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
- package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
- package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
- package/dist/web-dashboard/src/components/Layout.js +50 -0
- package/dist/web-dashboard/src/components/Markdown.js +152 -0
- package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
- package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
- package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
- package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
- package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
- package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
- package/dist/web-dashboard/src/components/Overview.js +123 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
- package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
- package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
- package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
- package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
- package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
- package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
- package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
- package/dist/web-dashboard/src/components/TasksPage.js +256 -0
- package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
- package/dist/web-dashboard/src/components/TracePanel.js +306 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
- package/dist/web-dashboard/src/jsonOrNull.js +37 -0
- package/dist/web-dashboard/src/main.js +15 -0
- package/dist/web-dashboard/src/mask.js +11 -0
- package/dist/web-dashboard/vite.config.js +23 -0
- package/dist/web-dashboard/vitest.config.js +15 -0
- package/package.json +5 -3
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MarkdownZeroDep — the ZERO-DEPENDENCY markdown fallback (Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* The primary renderer is `Markdown.tsx` (react-markdown + remark-gfm +
|
|
6
|
+
* rehype-highlight — spec-complete, syntax-highlighted). This module is the
|
|
7
|
+
* original hand-rolled renderer, preserved so a dependency-free build is one
|
|
8
|
+
* import away: change ChatPage's `import Markdown from './Markdown.js'` to
|
|
9
|
+
* `import Markdown from './MarkdownZeroDep.js'` and no markdown packages are
|
|
10
|
+
* needed. It covers the common shapes (fenced code + copy, headings, lists,
|
|
11
|
+
* bold/italic, inline code, links, tables, blockquotes, hr) but is NOT
|
|
12
|
+
* CommonMark-complete (nested lists, ***bold italic***, autolinks,
|
|
13
|
+
* strikethrough, task lists) and has no syntax highlighting — the tradeoffs
|
|
14
|
+
* accepted for zero deps. Never uses dangerouslySetInnerHTML (React escaping
|
|
15
|
+
* + an href allowlist are the XSS guards).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.default = MarkdownZeroDep;
|
|
19
|
+
const react_1 = require("react");
|
|
20
|
+
/** Split on ``` fences; everything outside becomes prose lines. */
|
|
21
|
+
function splitBlocks(text) {
|
|
22
|
+
const blocks = [];
|
|
23
|
+
const fenceRe = /^```([\w+-]*)\s*$/;
|
|
24
|
+
const lines = text.split('\n');
|
|
25
|
+
let i = 0;
|
|
26
|
+
let prose = [];
|
|
27
|
+
const flushProse = () => {
|
|
28
|
+
if (prose.length > 0) {
|
|
29
|
+
blocks.push({ type: 'prose', lines: prose });
|
|
30
|
+
prose = [];
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
while (i < lines.length) {
|
|
34
|
+
const m = fenceRe.exec(lines[i].trim());
|
|
35
|
+
if (m) {
|
|
36
|
+
flushProse();
|
|
37
|
+
const lang = m[1] || 'text';
|
|
38
|
+
const code = [];
|
|
39
|
+
i += 1;
|
|
40
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
|
|
41
|
+
code.push(lines[i]);
|
|
42
|
+
i += 1;
|
|
43
|
+
}
|
|
44
|
+
i += 1; // closing fence
|
|
45
|
+
blocks.push({ type: 'code', lang, code: code.join('\n') });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
prose.push(lines[i]);
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
flushProse();
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
55
|
+
const SAFE_HREF = /^(https?:\/\/|mailto:)/i;
|
|
56
|
+
/**
|
|
57
|
+
* Inline tokenizer: `code` first (so ** inside code stays literal), then
|
|
58
|
+
* **bold**, *italic*, [text](url). Everything else is plain text. Returns
|
|
59
|
+
* React nodes — no HTML strings, so no injection surface.
|
|
60
|
+
*/
|
|
61
|
+
function inline(text, keyPrefix) {
|
|
62
|
+
const nodes = [];
|
|
63
|
+
const re = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\n]+\*)|(\[([^\]]+)\]\(([^)\s]+)\))/g;
|
|
64
|
+
let last = 0;
|
|
65
|
+
let m;
|
|
66
|
+
let k = 0;
|
|
67
|
+
while ((m = re.exec(text)) !== null) {
|
|
68
|
+
if (m.index > last)
|
|
69
|
+
nodes.push(text.slice(last, m.index));
|
|
70
|
+
const [, code, bold, ital, , linkText, linkHref] = m;
|
|
71
|
+
if (code !== undefined) {
|
|
72
|
+
nodes.push(<code key={`${keyPrefix}-c${k}`} className="md-inline-code">
|
|
73
|
+
{code.slice(1, -1)}
|
|
74
|
+
</code>);
|
|
75
|
+
}
|
|
76
|
+
else if (bold !== undefined) {
|
|
77
|
+
nodes.push(<strong key={`${keyPrefix}-b${k}`}>{bold.slice(2, -2)}</strong>);
|
|
78
|
+
}
|
|
79
|
+
else if (ital !== undefined) {
|
|
80
|
+
nodes.push(<em key={`${keyPrefix}-i${k}`}>{ital.slice(1, -1)}</em>);
|
|
81
|
+
}
|
|
82
|
+
else if (linkHref !== undefined && SAFE_HREF.test(linkHref)) {
|
|
83
|
+
nodes.push(<a key={`${keyPrefix}-l${k}`} href={linkHref} target="_blank" rel="noreferrer">
|
|
84
|
+
{linkText}
|
|
85
|
+
</a>);
|
|
86
|
+
}
|
|
87
|
+
else if (linkHref !== undefined) {
|
|
88
|
+
// Unsafe scheme — render the text, never the href.
|
|
89
|
+
nodes.push(<span key={`${keyPrefix}-l${k}`}>{linkText}</span>);
|
|
90
|
+
}
|
|
91
|
+
last = m.index + m[0].length;
|
|
92
|
+
k += 1;
|
|
93
|
+
}
|
|
94
|
+
if (last < text.length)
|
|
95
|
+
nodes.push(text.slice(last));
|
|
96
|
+
return nodes;
|
|
97
|
+
}
|
|
98
|
+
/** Render one prose line; list items and tables accumulate into groups. */
|
|
99
|
+
function Prose({ lines }) {
|
|
100
|
+
const rows = [];
|
|
101
|
+
let listRun = null;
|
|
102
|
+
let tableRun = null;
|
|
103
|
+
let key = 0;
|
|
104
|
+
const flushList = () => {
|
|
105
|
+
if (!listRun)
|
|
106
|
+
return;
|
|
107
|
+
const items = listRun;
|
|
108
|
+
const Tag = items[0].type === 'ol' ? 'ol' : 'ul';
|
|
109
|
+
rows.push(<Tag key={`md-l${key++}`} className={`md-list md-${items[0].type}`}>
|
|
110
|
+
{items.map((it, i) => (<li key={i}>{inline(it.text, `md-li${key}-${i}`)}</li>))}
|
|
111
|
+
</Tag>);
|
|
112
|
+
listRun = null;
|
|
113
|
+
};
|
|
114
|
+
const flushTable = () => {
|
|
115
|
+
if (!tableRun)
|
|
116
|
+
return;
|
|
117
|
+
const t = tableRun;
|
|
118
|
+
rows.push(<table key={`md-t${key++}`} className="md-table">
|
|
119
|
+
<thead>
|
|
120
|
+
<tr>
|
|
121
|
+
{t[0].map((h, i) => (<th key={i}>{inline(h, `md-th${i}`)}</th>))}
|
|
122
|
+
</tr>
|
|
123
|
+
</thead>
|
|
124
|
+
<tbody>
|
|
125
|
+
{t.slice(2).map((r, ri) => (<tr key={ri}>
|
|
126
|
+
{r.map((c, ci) => (<td key={ci}>{inline(c, `md-td${ri}-${ci}`)}</td>))}
|
|
127
|
+
</tr>))}
|
|
128
|
+
</tbody>
|
|
129
|
+
</table>);
|
|
130
|
+
tableRun = null;
|
|
131
|
+
};
|
|
132
|
+
for (const raw of lines) {
|
|
133
|
+
const line = raw;
|
|
134
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
135
|
+
if (h) {
|
|
136
|
+
flushList();
|
|
137
|
+
flushTable();
|
|
138
|
+
const level = h[1].length;
|
|
139
|
+
const Tag = (`h${Math.min(level, 6)}`);
|
|
140
|
+
rows.push(<Tag key={`md-h${key++}`} className={`md-heading md-h${level}`}>{inline(h[2], `md-h${key}`)}</Tag>);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const hr = /^\s*(---+|\*\*\*+|___+)\s*$/.exec(line);
|
|
144
|
+
if (hr) {
|
|
145
|
+
flushList();
|
|
146
|
+
flushTable();
|
|
147
|
+
rows.push(<hr key={`md-r${key++}`} className="md-hr"/>);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const quote = /^>\s?(.*)$/.exec(line);
|
|
151
|
+
if (quote) {
|
|
152
|
+
flushList();
|
|
153
|
+
flushTable();
|
|
154
|
+
rows.push(<blockquote key={`md-q${key++}`} className="md-quote">{inline(quote[1], `md-q${key}`)}</blockquote>);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const ul = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
158
|
+
if (ul) {
|
|
159
|
+
flushTable();
|
|
160
|
+
if (listRun && listRun[0].type === 'ul')
|
|
161
|
+
listRun.push({ type: 'ul', text: ul[1] });
|
|
162
|
+
else {
|
|
163
|
+
flushList();
|
|
164
|
+
listRun = [{ type: 'ul', text: ul[1] }];
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const ol = /^\s*\d+[.)]\s+(.*)$/.exec(line);
|
|
169
|
+
if (ol) {
|
|
170
|
+
flushTable();
|
|
171
|
+
if (listRun && listRun[0].type === 'ol')
|
|
172
|
+
listRun.push({ type: 'ol', text: ol[1] });
|
|
173
|
+
else {
|
|
174
|
+
flushList();
|
|
175
|
+
listRun = [{ type: 'ol', text: ol[1] }];
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const tbl = /^\s*\|.*\|\s*$/.exec(line);
|
|
180
|
+
if (tbl) {
|
|
181
|
+
flushList();
|
|
182
|
+
const cells = line
|
|
183
|
+
.trim()
|
|
184
|
+
.replace(/^\||\|$/g, '')
|
|
185
|
+
.split('|')
|
|
186
|
+
.map((c) => c.trim());
|
|
187
|
+
if (!tableRun)
|
|
188
|
+
tableRun = [cells];
|
|
189
|
+
else
|
|
190
|
+
tableRun.push(cells);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
flushList();
|
|
194
|
+
flushTable();
|
|
195
|
+
if (line.trim() === '') {
|
|
196
|
+
rows.push(<div key={`md-p${key++}`} className="md-blank"/>);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
rows.push(<p key={`md-p${key++}`} className="md-para">{inline(line, `md-p${key}`)}</p>);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
flushList();
|
|
203
|
+
flushTable();
|
|
204
|
+
return <>{rows}</>;
|
|
205
|
+
}
|
|
206
|
+
/** A fenced code block with a language label and a copy button. */
|
|
207
|
+
function CodeBlockView({ lang, code }) {
|
|
208
|
+
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
209
|
+
const copy = async () => {
|
|
210
|
+
try {
|
|
211
|
+
await navigator.clipboard?.writeText(code);
|
|
212
|
+
setCopied(true);
|
|
213
|
+
setTimeout(() => setCopied(false), 1500);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* clipboard unavailable (non-secure context / jsdom) — button no-ops */
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return (<div className="md-code-block">
|
|
220
|
+
<div className="md-code-head">
|
|
221
|
+
<span className="md-code-lang">{lang}</span>
|
|
222
|
+
<button type="button" className="md-code-copy" onClick={() => void copy()}>
|
|
223
|
+
{copied ? '✓ Copied' : '⧉ Copy'}
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
<pre className="md-code-pre">
|
|
227
|
+
<code>{code}</code>
|
|
228
|
+
</pre>
|
|
229
|
+
</div>);
|
|
230
|
+
}
|
|
231
|
+
/** Top-level renderer: split into code/prose blocks, render each. */
|
|
232
|
+
function MarkdownZeroDep({ text }) {
|
|
233
|
+
const blocks = (0, react_1.useMemo)(() => splitBlocks(text ?? ''), [text]);
|
|
234
|
+
return (<div className="md-root">
|
|
235
|
+
{blocks.map((b, i) => b.type === 'code' ? <CodeBlockView key={`b${i}`} lang={b.lang} code={b.code}/> : <Prose key={`b${i}`} lines={b.lines}/>)}
|
|
236
|
+
</div>);
|
|
237
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = MemoryPanel;
|
|
4
|
+
function formatNumber(n) {
|
|
5
|
+
if (n === undefined || n === null)
|
|
6
|
+
return '0';
|
|
7
|
+
if (n >= 1_000_000)
|
|
8
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
9
|
+
if (n >= 1_000)
|
|
10
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
11
|
+
return n.toLocaleString();
|
|
12
|
+
}
|
|
13
|
+
function formatPercent(value) {
|
|
14
|
+
if (value === undefined || value === null)
|
|
15
|
+
return '0%';
|
|
16
|
+
return (value * 100).toFixed(1) + '%';
|
|
17
|
+
}
|
|
18
|
+
function MemoryPanel({ data }) {
|
|
19
|
+
if (!data) {
|
|
20
|
+
return <div className="loading-state"><p>Loading memory data...</p></div>;
|
|
21
|
+
}
|
|
22
|
+
const { memory, health } = data;
|
|
23
|
+
const entries = Object.entries(memory.byFingerprint || {}).sort(([, a], [, b]) => b - a);
|
|
24
|
+
const factEntries = Object.entries(memory.facts?.byProject || {}).sort(([, a], [, b]) => b - a);
|
|
25
|
+
const recall = memory.recall || { total: 0, today: 0, last7d: 0 };
|
|
26
|
+
return (<>
|
|
27
|
+
<h2 className="section-title">💾 Memory Store</h2>
|
|
28
|
+
|
|
29
|
+
<div className="stats-grid mini">
|
|
30
|
+
<div className="stat-card">
|
|
31
|
+
<div className="stat-value">{formatNumber(memory.total)}</div>
|
|
32
|
+
<div className="stat-label">Trajectories</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="stat-card">
|
|
35
|
+
<div className="stat-value">{formatPercent(memory.avgScore)}</div>
|
|
36
|
+
<div className="stat-label">Avg Score</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="stat-card">
|
|
39
|
+
<div className="stat-value">{formatNumber(memory.facts?.total)}</div>
|
|
40
|
+
<div className="stat-label">Facts</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="stat-card">
|
|
43
|
+
<div className="stat-value">{formatNumber(recall.total)}</div>
|
|
44
|
+
<div className="stat-label">Recall Hits</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="stat-card">
|
|
47
|
+
<div className="stat-value">{formatNumber(health?.patterns)}</div>
|
|
48
|
+
<div className="stat-label">Coding Patterns</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="stat-card">
|
|
51
|
+
<div className="stat-value">{formatNumber(health?.feedback)}</div>
|
|
52
|
+
<div className="stat-label">Feedback Ratings</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<p className="memory-backend">
|
|
57
|
+
Backend: <code>{memory.backend || 'local'}</code>
|
|
58
|
+
{recall.last7d > 0 ? ` · ${formatNumber(recall.last7d)} recall(s) this week, ${formatNumber(recall.today)} today` : ' · no recalls yet'}
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
{entries.length > 0 && (<>
|
|
62
|
+
<h3 className="section-subtitle">By Project Type</h3>
|
|
63
|
+
<div className="memory-list">
|
|
64
|
+
{entries.map(([project, count]) => (<div className="memory-item" key={project}>
|
|
65
|
+
<span className="memory-project">{project}</span>
|
|
66
|
+
<span className="memory-count">{count} trajectory(ies)</span>
|
|
67
|
+
</div>))}
|
|
68
|
+
</div>
|
|
69
|
+
</>)}
|
|
70
|
+
|
|
71
|
+
{factEntries.length > 0 && (<>
|
|
72
|
+
<h3 className="section-subtitle">Facts by Project</h3>
|
|
73
|
+
<div className="memory-list">
|
|
74
|
+
{factEntries.map(([project, count]) => (<div className="memory-item" key={project}>
|
|
75
|
+
<span className="memory-project">{project}</span>
|
|
76
|
+
<span className="memory-count">{count} fact(s)</span>
|
|
77
|
+
</div>))}
|
|
78
|
+
</div>
|
|
79
|
+
</>)}
|
|
80
|
+
</>);
|
|
81
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = ModelTimeline;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
function formatTimeAgo(ms) {
|
|
6
|
+
if (ms <= 0)
|
|
7
|
+
return 'never';
|
|
8
|
+
const seconds = Math.floor(ms / 1000);
|
|
9
|
+
const minutes = Math.floor(seconds / 60);
|
|
10
|
+
const hours = Math.floor(minutes / 60);
|
|
11
|
+
const days = Math.floor(hours / 24);
|
|
12
|
+
if (days > 30)
|
|
13
|
+
return `${days}d ago`;
|
|
14
|
+
if (days > 0)
|
|
15
|
+
return `${days}d ${hours % 24}h ago`;
|
|
16
|
+
if (hours > 0)
|
|
17
|
+
return `${hours}h ${minutes % 60}m ago`;
|
|
18
|
+
if (minutes > 0)
|
|
19
|
+
return `${minutes}m ago`;
|
|
20
|
+
return `${seconds}s ago`;
|
|
21
|
+
}
|
|
22
|
+
function getStatusColor(status, lastProbedAt, errorRate) {
|
|
23
|
+
if (status === 'unavailable')
|
|
24
|
+
return '#f85149';
|
|
25
|
+
if (status === 'unverified')
|
|
26
|
+
return '#d29922';
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const daysSinceProbe = (now - lastProbedAt) / (24 * 60 * 60 * 1000);
|
|
29
|
+
if (daysSinceProbe > 30 && errorRate > 0.5)
|
|
30
|
+
return '#f85149';
|
|
31
|
+
if (daysSinceProbe > 7)
|
|
32
|
+
return '#d29922';
|
|
33
|
+
return '#3fb950';
|
|
34
|
+
}
|
|
35
|
+
function getStatusLabel(status, lastProbedAt, errorRate) {
|
|
36
|
+
if (status === 'unavailable')
|
|
37
|
+
return 'Unavailable';
|
|
38
|
+
if (status === 'unverified')
|
|
39
|
+
return 'Unverified';
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const daysSinceProbe = (now - lastProbedAt) / (24 * 60 * 60 * 1000);
|
|
42
|
+
if (daysSinceProbe > 30 && errorRate > 0.5)
|
|
43
|
+
return 'Likely Removed';
|
|
44
|
+
if (daysSinceProbe > 7)
|
|
45
|
+
return 'Stale';
|
|
46
|
+
return 'Fresh';
|
|
47
|
+
}
|
|
48
|
+
function TimelineBar({ entry }) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const probeAge = now - entry.lastProbedAt;
|
|
51
|
+
const verifyAge = now - entry.lastVerifiedAt;
|
|
52
|
+
const useAge = now - entry.lastUsedAt;
|
|
53
|
+
// Normalize to 30-day scale
|
|
54
|
+
const probePercent = Math.min(100, (probeAge / (30 * 24 * 60 * 60 * 1000)) * 100);
|
|
55
|
+
const verifyPercent = entry.lastVerifiedAt > 0 ? Math.min(100, (verifyAge / (30 * 24 * 60 * 60 * 1000)) * 100) : -1;
|
|
56
|
+
const usePercent = entry.lastUsedAt > 0 ? Math.min(100, (useAge / (30 * 24 * 60 * 60 * 1000)) * 100) : -1;
|
|
57
|
+
return (<div className="timeline-bar-container">
|
|
58
|
+
<div className="timeline-bar" title={`Last probed: ${formatTimeAgo(probeAge)}`}>
|
|
59
|
+
<div className="timeline-bar-fill probe" style={{ width: `${probePercent}%` }}/>
|
|
60
|
+
</div>
|
|
61
|
+
{verifyPercent >= 0 && (<div className="timeline-bar small" title={`Last verified: ${formatTimeAgo(verifyAge)}`}>
|
|
62
|
+
<div className="timeline-bar-fill verify" style={{ width: `${verifyPercent}%` }}/>
|
|
63
|
+
</div>)}
|
|
64
|
+
{usePercent >= 0 && (<div className="timeline-bar small" title={`Last used: ${formatTimeAgo(useAge)}`}>
|
|
65
|
+
<div className="timeline-bar-fill use" style={{ width: `${usePercent}%` }}/>
|
|
66
|
+
</div>)}
|
|
67
|
+
</div>);
|
|
68
|
+
}
|
|
69
|
+
function ModelTimeline() {
|
|
70
|
+
const [data, setData] = (0, react_1.useState)(null);
|
|
71
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
72
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
73
|
+
const [filter, setFilter] = (0, react_1.useState)('all');
|
|
74
|
+
const [sortBy, setSortBy] = (0, react_1.useState)('provider');
|
|
75
|
+
const mountedRef = (0, react_1.useRef)(true);
|
|
76
|
+
const fetchData = (0, react_1.useCallback)(async () => {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch('/api/model-timeline');
|
|
79
|
+
if (!response.ok)
|
|
80
|
+
throw new Error('Failed to fetch timeline data');
|
|
81
|
+
const result = await response.json();
|
|
82
|
+
if (mountedRef.current) {
|
|
83
|
+
setData(result);
|
|
84
|
+
setError(null);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (mountedRef.current) {
|
|
89
|
+
setError(err instanceof Error ? err.message : 'Failed to load timeline');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (mountedRef.current)
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
(0, react_1.useEffect)(() => {
|
|
98
|
+
mountedRef.current = true;
|
|
99
|
+
fetchData();
|
|
100
|
+
const interval = setInterval(fetchData, 30000); // Refresh every 30s
|
|
101
|
+
return () => {
|
|
102
|
+
mountedRef.current = false;
|
|
103
|
+
clearInterval(interval);
|
|
104
|
+
};
|
|
105
|
+
}, [fetchData]);
|
|
106
|
+
if (loading) {
|
|
107
|
+
return (<div className="admin-header">
|
|
108
|
+
<h2 className="section-title">📅 Model Discovery Timeline</h2>
|
|
109
|
+
<div className="loading-state">
|
|
110
|
+
<div className="loading-spinner"/>
|
|
111
|
+
<p>Loading timeline...</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>);
|
|
114
|
+
}
|
|
115
|
+
if (error) {
|
|
116
|
+
return (<div className="admin-header">
|
|
117
|
+
<h2 className="section-title">📅 Model Discovery Timeline</h2>
|
|
118
|
+
<div className="admin-row-msg admin-row-msg-err">{error}</div>
|
|
119
|
+
</div>);
|
|
120
|
+
}
|
|
121
|
+
const entries = data?.entries ?? [];
|
|
122
|
+
// Filter entries
|
|
123
|
+
const filteredEntries = entries.filter((e) => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const daysSinceProbe = (now - e.lastProbedAt) / (24 * 60 * 60 * 1000);
|
|
126
|
+
const isStale = daysSinceProbe > 7;
|
|
127
|
+
const isRemoved = daysSinceProbe > 30 && e.errorRate > 0.5;
|
|
128
|
+
const isFresh = !isStale && !isRemoved;
|
|
129
|
+
if (filter === 'fresh')
|
|
130
|
+
return isFresh;
|
|
131
|
+
if (filter === 'stale')
|
|
132
|
+
return isStale && !isRemoved;
|
|
133
|
+
if (filter === 'removed')
|
|
134
|
+
return isRemoved;
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
// Sort entries
|
|
138
|
+
const sortedEntries = [...filteredEntries].sort((a, b) => {
|
|
139
|
+
if (sortBy === 'provider')
|
|
140
|
+
return a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model);
|
|
141
|
+
if (sortBy === 'lastProbed')
|
|
142
|
+
return b.lastProbedAt - a.lastProbedAt;
|
|
143
|
+
if (sortBy === 'status') {
|
|
144
|
+
const statusOrder = { unavailable: 0, unverified: 1, verified: 2 };
|
|
145
|
+
return (statusOrder[b.status] ?? 0) - (statusOrder[a.status] ?? 0);
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
});
|
|
149
|
+
// Group by provider
|
|
150
|
+
const byProvider = new Map();
|
|
151
|
+
for (const e of sortedEntries) {
|
|
152
|
+
if (!byProvider.has(e.provider))
|
|
153
|
+
byProvider.set(e.provider, []);
|
|
154
|
+
byProvider.get(e.provider).push(e);
|
|
155
|
+
}
|
|
156
|
+
return (<div className="admin-header">
|
|
157
|
+
<h2 className="section-title">📅 Model Discovery Timeline</h2>
|
|
158
|
+
|
|
159
|
+
{/* Summary cards */}
|
|
160
|
+
<div className="stats-grid" style={{ marginBottom: 16 }}>
|
|
161
|
+
<div className="stat-card">
|
|
162
|
+
<div className="stat-value">{data?.totalModels ?? 0}</div>
|
|
163
|
+
<div className="stat-label">Total Models</div>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="stat-card" style={{ borderColor: '#3fb950' }}>
|
|
166
|
+
<div className="stat-value" style={{ color: '#3fb950' }}>{data?.freshCount ?? 0}</div>
|
|
167
|
+
<div className="stat-label">Fresh (<7d)</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="stat-card" style={{ borderColor: '#d29922' }}>
|
|
170
|
+
<div className="stat-value" style={{ color: '#d29922' }}>{data?.staleCount ?? 0}</div>
|
|
171
|
+
<div className="stat-label">Stale (7-30d)</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="stat-card" style={{ borderColor: '#f85149' }}>
|
|
174
|
+
<div className="stat-value" style={{ color: '#f85149' }}>{data?.removedCount ?? 0}</div>
|
|
175
|
+
<div className="stat-label">Likely Removed (>30d)</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Filters */}
|
|
180
|
+
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
|
|
181
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
182
|
+
{['all', 'fresh', 'stale', 'removed'].map((f) => (<button key={f} className={`admin-refresh-btn ${filter === f ? 'active' : ''}`} onClick={() => setFilter(f)} style={{
|
|
183
|
+
background: filter === f ? '#238636' : '#21262d',
|
|
184
|
+
border: `1px solid ${filter === f ? '#3fb950' : '#30363d'}`,
|
|
185
|
+
padding: '4px 12px',
|
|
186
|
+
fontSize: 12,
|
|
187
|
+
cursor: 'pointer',
|
|
188
|
+
}}>
|
|
189
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
190
|
+
</button>))}
|
|
191
|
+
</div>
|
|
192
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
193
|
+
{['provider', 'lastProbed', 'status'].map((s) => (<button key={s} className={`admin-refresh-btn ${sortBy === s ? 'active' : ''}`} onClick={() => setSortBy(s)} style={{
|
|
194
|
+
background: sortBy === s ? '#238636' : '#21262d',
|
|
195
|
+
border: `1px solid ${sortBy === s ? '#3fb950' : '#30363d'}`,
|
|
196
|
+
padding: '4px 12px',
|
|
197
|
+
fontSize: 12,
|
|
198
|
+
cursor: 'pointer',
|
|
199
|
+
}}>
|
|
200
|
+
Sort: {s === 'lastProbed' ? 'Last Seen' : s.charAt(0).toUpperCase() + s.slice(1)}
|
|
201
|
+
</button>))}
|
|
202
|
+
</div>
|
|
203
|
+
<button className="admin-refresh-btn" onClick={fetchData} style={{
|
|
204
|
+
background: '#21262d',
|
|
205
|
+
border: '1px solid #30363d',
|
|
206
|
+
padding: '4px 12px',
|
|
207
|
+
fontSize: 12,
|
|
208
|
+
cursor: 'pointer',
|
|
209
|
+
}}>
|
|
210
|
+
🔄 Refresh
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Legend */}
|
|
215
|
+
<div style={{ display: 'flex', gap: 16, marginBottom: 12, fontSize: 11, color: '#8b949e' }}>
|
|
216
|
+
<span><span style={{ color: '#58a6ff' }}>■</span> Probe (last seen)</span>
|
|
217
|
+
<span><span style={{ color: '#3fb950' }}>■</span> Verified (last success)</span>
|
|
218
|
+
<span><span style={{ color: '#d29922' }}>■</span> Used (last invocation)</span>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Timeline entries */}
|
|
222
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
223
|
+
{Array.from(byProvider.entries()).map(([provider, models]) => (<div key={provider} style={{
|
|
224
|
+
background: '#161b22',
|
|
225
|
+
borderRadius: 8,
|
|
226
|
+
border: '1px solid #30363d',
|
|
227
|
+
overflow: 'hidden',
|
|
228
|
+
}}>
|
|
229
|
+
<div style={{
|
|
230
|
+
padding: '10px 14px',
|
|
231
|
+
borderBottom: '1px solid #30363d',
|
|
232
|
+
display: 'flex',
|
|
233
|
+
alignItems: 'center',
|
|
234
|
+
gap: 8,
|
|
235
|
+
}}>
|
|
236
|
+
<span style={{ fontSize: 16 }}>
|
|
237
|
+
{provider === 'groq' ? '⚡' : provider === 'gemini' ? '🌀' : provider === 'local' ? '💻' : '🤖'}
|
|
238
|
+
</span>
|
|
239
|
+
<span style={{ fontWeight: 600, color: '#e6edf3' }}>{provider}</span>
|
|
240
|
+
<span style={{ fontSize: 12, color: '#8b949e' }}>({models.length} models)</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div style={{ padding: '8px 14px' }}>
|
|
243
|
+
{models.map((entry) => {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const daysSinceProbe = (now - entry.lastProbedAt) / (24 * 60 * 60 * 1000);
|
|
246
|
+
const statusColor = getStatusColor(entry.status, entry.lastProbedAt, entry.errorRate);
|
|
247
|
+
const statusLabel = getStatusLabel(entry.status, entry.lastProbedAt, entry.errorRate);
|
|
248
|
+
return (<div key={entry.model} style={{
|
|
249
|
+
display: 'flex',
|
|
250
|
+
alignItems: 'center',
|
|
251
|
+
gap: 12,
|
|
252
|
+
padding: '6px 0',
|
|
253
|
+
borderBottom: '1px solid #21262d',
|
|
254
|
+
}}>
|
|
255
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
256
|
+
<div style={{
|
|
257
|
+
fontSize: 13,
|
|
258
|
+
color: '#e6edf3',
|
|
259
|
+
overflow: 'hidden',
|
|
260
|
+
textOverflow: 'ellipsis',
|
|
261
|
+
whiteSpace: 'nowrap',
|
|
262
|
+
}}>
|
|
263
|
+
{entry.model}
|
|
264
|
+
</div>
|
|
265
|
+
<div style={{ fontSize: 11, color: '#8b949e', display: 'flex', gap: 12 }}>
|
|
266
|
+
<span>Probed: {formatTimeAgo(now - entry.lastProbedAt)}</span>
|
|
267
|
+
{entry.lastVerifiedAt > 0 && (<span>Verified: {formatTimeAgo(now - entry.lastVerifiedAt)}</span>)}
|
|
268
|
+
{entry.lastUsedAt > 0 && (<span>Used: {formatTimeAgo(now - entry.lastUsedAt)}</span>)}
|
|
269
|
+
{entry.latencyMs !== undefined && (<span>{entry.latencyMs}ms</span>)}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<TimelineBar entry={entry}/>
|
|
273
|
+
<div style={{
|
|
274
|
+
fontSize: 11,
|
|
275
|
+
padding: '2px 8px',
|
|
276
|
+
borderRadius: 4,
|
|
277
|
+
background: `${statusColor}20`,
|
|
278
|
+
color: statusColor,
|
|
279
|
+
border: `1px solid ${statusColor}40`,
|
|
280
|
+
whiteSpace: 'nowrap',
|
|
281
|
+
}}>
|
|
282
|
+
{statusLabel}
|
|
283
|
+
</div>
|
|
284
|
+
</div>);
|
|
285
|
+
})}
|
|
286
|
+
</div>
|
|
287
|
+
</div>))}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{filteredEntries.length === 0 && (<div style={{
|
|
291
|
+
textAlign: 'center',
|
|
292
|
+
padding: '40px 20px',
|
|
293
|
+
color: '#8b949e',
|
|
294
|
+
}}>
|
|
295
|
+
No models match the current filter
|
|
296
|
+
</div>)}
|
|
297
|
+
</div>);
|
|
298
|
+
}
|