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,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = ContactsPage;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
function ContactsPage() {
|
|
7
|
+
const [contacts, setContacts] = (0, react_1.useState)([]);
|
|
8
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
9
|
+
const [filter, setFilter] = (0, react_1.useState)('all');
|
|
10
|
+
const [editingContact, setEditingContact] = (0, react_1.useState)(null);
|
|
11
|
+
const [editName, setEditName] = (0, react_1.useState)('');
|
|
12
|
+
const [editPhone, setEditPhone] = (0, react_1.useState)('');
|
|
13
|
+
const [msg, setMsg] = (0, react_1.useState)(null);
|
|
14
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
15
|
+
setLoading(true);
|
|
16
|
+
const result = await api_1.dashboardAPI.getContacts();
|
|
17
|
+
if (result.ok && result.contacts) {
|
|
18
|
+
setContacts(result.contacts);
|
|
19
|
+
}
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}, []);
|
|
22
|
+
(0, react_1.useEffect)(() => { void refresh(); }, [refresh]);
|
|
23
|
+
const filtered = contacts.filter((c) => filter === 'all' || c.status === filter);
|
|
24
|
+
const handleApprove = async (c) => {
|
|
25
|
+
setMsg(null);
|
|
26
|
+
const r = await api_1.dashboardAPI.approveContact(c.platform, c.name);
|
|
27
|
+
setMsg(r.ok ? { kind: 'ok', text: `✅ ${c.name} approved.` } : { kind: 'err', text: r.error || 'Failed.' });
|
|
28
|
+
void refresh();
|
|
29
|
+
};
|
|
30
|
+
const handleReject = async (c) => {
|
|
31
|
+
setMsg(null);
|
|
32
|
+
const r = await api_1.dashboardAPI.rejectContact(c.platform, c.name);
|
|
33
|
+
setMsg(r.ok ? { kind: 'ok', text: `🚫 ${c.name} rejected.` } : { kind: 'err', text: r.error || 'Failed.' });
|
|
34
|
+
void refresh();
|
|
35
|
+
};
|
|
36
|
+
const handleDelete = async (c) => {
|
|
37
|
+
setMsg(null);
|
|
38
|
+
const r = await api_1.dashboardAPI.deleteContact(c.platform, c.name);
|
|
39
|
+
setMsg(r.ok ? { kind: 'ok', text: `🗑️ ${c.name} deleted.` } : { kind: 'err', text: r.error || 'Failed.' });
|
|
40
|
+
void refresh();
|
|
41
|
+
};
|
|
42
|
+
const handleSaveEdit = async () => {
|
|
43
|
+
if (!editingContact)
|
|
44
|
+
return;
|
|
45
|
+
setMsg(null);
|
|
46
|
+
const r = await api_1.dashboardAPI.updateContact(editingContact.platform, editingContact.id, { name: editName, phone: editPhone });
|
|
47
|
+
setMsg(r.ok ? { kind: 'ok', text: `✅ ${editName} updated.` } : { kind: 'err', text: r.error || 'Failed.' });
|
|
48
|
+
setEditingContact(null);
|
|
49
|
+
void refresh();
|
|
50
|
+
};
|
|
51
|
+
const statusBadge = (status) => {
|
|
52
|
+
if (status === 'approved')
|
|
53
|
+
return <span className="contact-status-badge approved">✅ Approved</span>;
|
|
54
|
+
if (status === 'pending')
|
|
55
|
+
return <span className="contact-status-badge pending">⏳ Pending</span>;
|
|
56
|
+
return <span className="contact-status-badge rejected">🚫 Rejected</span>;
|
|
57
|
+
};
|
|
58
|
+
const platformIcon = (p) => {
|
|
59
|
+
const icons = { telegram: '✈️', whatsapp: '📱', whatsapp_cloud: '📱', email: '📧', slack: '💬', discord: '🎮' };
|
|
60
|
+
return icons[p] || '🔌';
|
|
61
|
+
};
|
|
62
|
+
const formatDate = (ms) => ms ? new Date(ms).toLocaleString() : '—';
|
|
63
|
+
return (<div className="panel contacts-page">
|
|
64
|
+
<div className="panel-header">
|
|
65
|
+
<h2>📇 Contacts — Outbound Messaging Directory</h2>
|
|
66
|
+
<p className="admin-hint">Manage who the bot can send messages to. Users auto-register on first Telegram message.</p>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{msg && (<div className={`admin-row-msg ${msg.kind === 'err' ? 'admin-row-msg-err' : ''}`}>
|
|
70
|
+
{msg.text}
|
|
71
|
+
<button className="admin-mini-btn" onClick={() => setMsg(null)}>✕</button>
|
|
72
|
+
</div>)}
|
|
73
|
+
|
|
74
|
+
<div className="contacts-filters">
|
|
75
|
+
{['all', 'approved', 'pending', 'rejected'].map((f) => (<button key={f} type="button" className={filter === f ? 'platforms-filter-btn active' : 'platforms-filter-btn'} onClick={() => setFilter(f)}>
|
|
76
|
+
{f === 'all' ? `All (${contacts.length})` : f === 'approved' ? `✅ Approved (${contacts.filter((c) => c.status === 'approved').length})` : f === 'pending' ? `⏳ Pending (${contacts.filter((c) => c.status === 'pending').length})` : `🚫 Rejected (${contacts.filter((c) => c.status === 'rejected').length})`}
|
|
77
|
+
</button>))}
|
|
78
|
+
<button type="button" className="admin-refresh-btn" onClick={() => void refresh()}>🔄 Refresh</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{loading ? (<div className="empty-state">Loading contacts...</div>) : filtered.length === 0 ? (<div className="empty-state">
|
|
82
|
+
<p>No contacts{filter !== 'all' ? ` with status "${filter}"` : ''}.</p>
|
|
83
|
+
<p className="admin-hint">Contacts auto-register when users message the bot on Telegram. They appear here as "Pending" until you approve them.</p>
|
|
84
|
+
</div>) : (<div className="contacts-table-wrapper">
|
|
85
|
+
<table className="contacts-table">
|
|
86
|
+
<thead>
|
|
87
|
+
<tr>
|
|
88
|
+
<th>Platform</th>
|
|
89
|
+
<th>Name</th>
|
|
90
|
+
<th>ID</th>
|
|
91
|
+
<th>Phone</th>
|
|
92
|
+
<th>Status</th>
|
|
93
|
+
<th>Registered</th>
|
|
94
|
+
<th>Actions</th>
|
|
95
|
+
</tr>
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
{filtered.map((c) => (<tr key={`${c.platform}:${c.id}`} className={`contact-row status-${c.status}`}>
|
|
99
|
+
<td>{platformIcon(c.platform)} {c.platform}</td>
|
|
100
|
+
<td className="contact-name">{c.name}</td>
|
|
101
|
+
<td className="contact-id"><code>{c.id}</code></td>
|
|
102
|
+
<td className="contact-phone">{c.phone || '—'}</td>
|
|
103
|
+
<td>{statusBadge(c.status)}</td>
|
|
104
|
+
<td className="contact-date">{formatDate(c.registeredAt || c.addedAt)}</td>
|
|
105
|
+
<td className="contact-actions">
|
|
106
|
+
{c.status !== 'approved' && (<button className="admin-mini-btn contact-approve-btn" onClick={() => void handleApprove(c)}>✅ Approve</button>)}
|
|
107
|
+
{c.status !== 'rejected' && (<button className="admin-mini-btn contact-reject-btn" onClick={() => void handleReject(c)}>🚫 Reject</button>)}
|
|
108
|
+
<button className="admin-mini-btn" onClick={() => { setEditingContact(c); setEditName(c.name); setEditPhone(c.phone || ''); }}>✏️ Edit</button>
|
|
109
|
+
<button className="admin-mini-btn contact-delete-btn" onClick={() => void handleDelete(c)}>🗑️</button>
|
|
110
|
+
</td>
|
|
111
|
+
</tr>))}
|
|
112
|
+
</tbody>
|
|
113
|
+
</table>
|
|
114
|
+
</div>)}
|
|
115
|
+
|
|
116
|
+
{editingContact && (<div className="wizard-overlay" onClick={() => setEditingContact(null)}>
|
|
117
|
+
<div className="wizard-modal" onClick={(e) => e.stopPropagation()}>
|
|
118
|
+
<div className="wizard-header">
|
|
119
|
+
<span className="wizard-icon">✏️</span>
|
|
120
|
+
<span className="wizard-title">Edit Contact: {editingContact.name}</span>
|
|
121
|
+
<button type="button" className="admin-mini-btn" onClick={() => setEditingContact(null)}>✕</button>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="wizard-body">
|
|
124
|
+
<label className="hub-send-target">
|
|
125
|
+
<span className="admin-hint">Name</span>
|
|
126
|
+
<input type="text" value={editName} onChange={(e) => setEditName(e.target.value)}/>
|
|
127
|
+
</label>
|
|
128
|
+
<label className="hub-send-target">
|
|
129
|
+
<span className="admin-hint">Phone (optional, for flexible lookup)</span>
|
|
130
|
+
<input type="text" value={editPhone} onChange={(e) => setEditPhone(e.target.value)} placeholder="+918800123456"/>
|
|
131
|
+
</label>
|
|
132
|
+
<p className="admin-hint">Platform: <strong>{editingContact.platform}</strong> | ID: <code>{editingContact.id}</code></p>
|
|
133
|
+
<div className="wizard-nav">
|
|
134
|
+
<button type="button" className="admin-mini-btn" onClick={() => setEditingContact(null)}>Cancel</button>
|
|
135
|
+
<button type="button" className="admin-refresh-btn" onClick={() => void handleSaveEdit()}>Save</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>)}
|
|
140
|
+
</div>);
|
|
141
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = CostDashboard;
|
|
4
|
+
function formatCost(usd) {
|
|
5
|
+
if (usd === undefined || usd === null)
|
|
6
|
+
return '$0.00';
|
|
7
|
+
if (usd < 0.00001)
|
|
8
|
+
return '$0.00';
|
|
9
|
+
if (usd < 0.01)
|
|
10
|
+
return '$' + usd.toFixed(6);
|
|
11
|
+
return '$' + usd.toFixed(4);
|
|
12
|
+
}
|
|
13
|
+
function formatNumber(n) {
|
|
14
|
+
if (n === undefined || n === null)
|
|
15
|
+
return '0';
|
|
16
|
+
if (n >= 1_000_000)
|
|
17
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
18
|
+
if (n >= 1_000)
|
|
19
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
20
|
+
return n.toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
function formatTime(ts) {
|
|
23
|
+
const d = new Date(ts);
|
|
24
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
25
|
+
}
|
|
26
|
+
function CostDashboard({ data }) {
|
|
27
|
+
if (!data) {
|
|
28
|
+
return <div className="loading-state"><p>Loading cost data...</p></div>;
|
|
29
|
+
}
|
|
30
|
+
const { cost } = data;
|
|
31
|
+
const byProvider = Object.entries(cost.byProvider || {}).sort(([, a], [, b]) => b - a);
|
|
32
|
+
const byModel = Object.entries(cost.byModel || {}).sort(([, a], [, b]) => b - a).slice(0, 10);
|
|
33
|
+
const providerTotal = byProvider.reduce((s, [, v]) => s + v, 0);
|
|
34
|
+
return (<>
|
|
35
|
+
<h2 className="section-title">💰 Cost Tracking</h2>
|
|
36
|
+
|
|
37
|
+
<div className="cost-summary">
|
|
38
|
+
<div className="cost-total">
|
|
39
|
+
<span className="cost-label">Total Spent</span>
|
|
40
|
+
<span className="cost-amount">{formatCost(cost.totalCost)}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="cost-total">
|
|
43
|
+
<span className="cost-label">Total Tokens</span>
|
|
44
|
+
<span className="cost-amount">{formatNumber(cost.totalTokens)}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="cost-total">
|
|
47
|
+
<span className="cost-label">Total Requests</span>
|
|
48
|
+
<span className="cost-amount">{formatNumber(cost.totalRequests)}</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className="cost-summary cost-summary-sub">
|
|
53
|
+
<div className="cost-total">
|
|
54
|
+
<span className="cost-label">📏 Measured (exact wire tokens)</span>
|
|
55
|
+
<span className="cost-amount">{formatCost(cost.measuredCost)}</span>
|
|
56
|
+
<span className="cost-label">{formatNumber(cost.measuredCalls)} calls</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="cost-total">
|
|
59
|
+
<span className="cost-label">📐 Estimated (length-based)</span>
|
|
60
|
+
<span className="cost-amount">{formatCost(cost.estimatedCost)}</span>
|
|
61
|
+
<span className="cost-label">{formatNumber(cost.estimatedCalls)} calls</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<h3 className="section-subtitle">By Provider</h3>
|
|
66
|
+
<div className="cost-list">
|
|
67
|
+
{byProvider.length === 0 ? (<div className="empty-state">No costs recorded yet.</div>) : (byProvider.map(([provider, amount]) => {
|
|
68
|
+
const pct = providerTotal > 0 ? ((amount / providerTotal) * 100).toFixed(1) : '0';
|
|
69
|
+
return (<div className="cost-row" key={provider}>
|
|
70
|
+
<span className="cost-row-name">{provider}</span>
|
|
71
|
+
<div className="cost-row-bar">
|
|
72
|
+
<div className="cost-row-fill" style={{ width: `${pct}%` }}/>
|
|
73
|
+
</div>
|
|
74
|
+
<span className="cost-row-value">{formatCost(amount)}</span>
|
|
75
|
+
</div>);
|
|
76
|
+
}))}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<h3 className="section-subtitle">By Model</h3>
|
|
80
|
+
<div className="cost-list">
|
|
81
|
+
{byModel.length === 0 ? (<div className="empty-state">No model costs recorded yet.</div>) : (byModel.map(([model, amount]) => {
|
|
82
|
+
const pct = providerTotal > 0 ? ((amount / providerTotal) * 100).toFixed(1) : '0';
|
|
83
|
+
const displayModel = model.length > 30 ? model.slice(0, 28) + '..' : model;
|
|
84
|
+
return (<div className="cost-row" key={model}>
|
|
85
|
+
<span className="cost-row-name">{displayModel}</span>
|
|
86
|
+
<div className="cost-row-bar">
|
|
87
|
+
<div className="cost-row-fill" style={{ width: `${pct}%` }}/>
|
|
88
|
+
</div>
|
|
89
|
+
<span className="cost-row-value">{formatCost(amount)}</span>
|
|
90
|
+
</div>);
|
|
91
|
+
}))}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<h3 className="section-subtitle">Recent Requests</h3>
|
|
95
|
+
<div className="request-list">
|
|
96
|
+
{(!cost.recent || cost.recent.length === 0) ? (<div className="empty-state">No requests recorded yet.</div>) : (cost.recent.slice(0, 30).map((req, i) => (<div className="request-row" key={i}>
|
|
97
|
+
<span className="request-time">{formatTime(req.timestamp)}</span>
|
|
98
|
+
<span className="request-provider">{req.provider || '--'}</span>
|
|
99
|
+
<span className="request-model">{req.model || '--'}</span>
|
|
100
|
+
<span className="request-cost">{formatCost(req.costUsd)}</span>
|
|
101
|
+
<span className="request-tokens">{formatNumber(req.totalTokens)} tok</span>
|
|
102
|
+
</div>)))}
|
|
103
|
+
</div>
|
|
104
|
+
</>);
|
|
105
|
+
}
|