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,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = PlatformsPage;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const GUIDES = {
|
|
7
|
+
telegram: { icon: 'โ๏ธ', category: 'Chat', steps: ['Open Telegram and search for @BotFather', 'Send /newbot and follow the prompts', 'Give your bot a name (e.g. "My Agent Bot")', 'Give your bot a username ending in "bot"', 'BotFather gives you a token โ paste it in Step 2', 'Open your bot in Telegram and send /start'], testTarget: 'telegram:' },
|
|
8
|
+
discord: { icon: '๐ฎ', category: 'Chat', steps: ['Go to discord.com/developers/applications', 'Click "New Application" and give it a name', 'Go to Bot tab and click "Add Bot"', 'Copy the bot token', 'Enable "Message Content Intent"', 'Invite bot to your server (OAuth2 URL generator)'], testTarget: 'discord:' },
|
|
9
|
+
slack: { icon: '๐ฌ', category: 'Chat', steps: ['Go to api.slack.com/apps', 'Create New App from scratch', 'Add Bot Token Scopes: chat:write, im:read, im:write', 'Install to workspace', 'Copy the Bot User OAuth Token (xoxb-...)'], testTarget: 'slack:' },
|
|
10
|
+
whatsapp: { icon: '๐ฑ', category: 'Chat', steps: ['Run: buff whatsapp pair (or from Gateway tab)', 'Scan the QR code with WhatsApp', 'Pairing completes automatically'], testTarget: '' },
|
|
11
|
+
whatsapp_cloud: { icon: 'โ๏ธ', category: 'Chat', steps: ['Go to developers.facebook.com', 'Set up WhatsApp Cloud API', 'Copy token + Phone ID'], testTarget: 'whatsapp_cloud:' },
|
|
12
|
+
email: { icon: '๐ง', category: 'Chat', steps: ['Get SMTP credentials (Gmail, SendGrid, etc.)', 'For Gmail: use App Password at smtp.gmail.com:587', 'Paste SMTP host + user below'], testTarget: '' },
|
|
13
|
+
signal: { icon: '๐', category: 'Chat', steps: ['Run signal-cli-rest-api Docker container', 'Register a Signal account', 'Paste account number below'], testTarget: '' },
|
|
14
|
+
dingtalk: { icon: '๐', category: 'Webhooks', steps: ['Open DingTalk and create a group', 'Add custom robot and copy webhook URL', 'Paste URL below'], testTarget: '' },
|
|
15
|
+
feishu: { icon: '๐ฆ', category: 'Webhooks', steps: ['Open Feishu and create a bot', 'Copy webhook URL and paste below'], testTarget: '' },
|
|
16
|
+
wecom: { icon: '๐ข', category: 'Webhooks', steps: ['Open WeCom and create a group bot', 'Copy webhook URL and paste below'], testTarget: '' },
|
|
17
|
+
mattermost: { icon: '๐', category: 'Webhooks', steps: ['Open Mattermost, go to Integrations', 'Create incoming webhook and copy URL'], testTarget: '' },
|
|
18
|
+
matrix: { icon: '๐', category: 'Chat', steps: ['Get your Matrix homeserver URL', 'Create a bot user and get access token', 'Paste URL + token below'], testTarget: '' },
|
|
19
|
+
webhook: { icon: '๐ช', category: 'Webhooks', steps: ['Enter your webhook URL', 'The agent sends outbound webhooks to this URL'], testTarget: '' },
|
|
20
|
+
bluebubbles: { icon: '๐', category: 'Chat', steps: ['Set up BlueBubbles server', 'Copy server URL + password'], testTarget: '' },
|
|
21
|
+
ntfy: { icon: '๐ข', category: 'Notifications', steps: ['Choose an ntfy topic (or use ntfy.sh)', 'Paste the topic below'], testTarget: '' },
|
|
22
|
+
teams: { icon: '๐ฅ', category: 'Webhooks', steps: ['Open Microsoft Teams, go to channel', 'Connectors, Incoming Webhook, create', 'Copy webhook URL'], testTarget: '' },
|
|
23
|
+
google_chat: { icon: '๐ฌ', category: 'Webhooks', steps: ['Open Google Chat, space settings', 'Create webhook and copy URL'], testTarget: '' },
|
|
24
|
+
weixin: { icon: '๐ด', category: 'Chat', steps: ['Get Weixin iLink bot token', 'Paste token below'], testTarget: '' },
|
|
25
|
+
sms: { icon: '๐ฒ', category: 'Chat', steps: ['Get Twilio account SID + auth token', 'Register a phone number', 'Paste credentials below'], testTarget: '' },
|
|
26
|
+
irc: { icon: '๐ป', category: 'Chat', steps: ['Choose an IRC server (e.g. irc.libera.chat)', 'Set nickname + channel and paste below'], testTarget: '' },
|
|
27
|
+
simplex: { icon: '๐จ', category: 'Chat', steps: ['Run local simplex-chat daemon', 'Paste WebSocket URL (ws://127.0.0.1:5225)'], testTarget: '' },
|
|
28
|
+
homeassistant: { icon: '๐ ', category: 'IoT', steps: ['Open Home Assistant Profile', 'Create long-lived access token and paste below'], testTarget: '' },
|
|
29
|
+
};
|
|
30
|
+
function PlatformsPage() {
|
|
31
|
+
const [configs, setConfigs] = (0, react_1.useState)(null);
|
|
32
|
+
const [filter, setFilter] = (0, react_1.useState)('all');
|
|
33
|
+
const [wizardPlatform, setWizardPlatform] = (0, react_1.useState)(null);
|
|
34
|
+
const [wizardStep, setWizardStep] = (0, react_1.useState)(0);
|
|
35
|
+
const [wizardValues, setWizardValues] = (0, react_1.useState)({});
|
|
36
|
+
const [wizardSaving, setWizardSaving] = (0, react_1.useState)(false);
|
|
37
|
+
const [wizardResult, setWizardResult] = (0, react_1.useState)(null);
|
|
38
|
+
const [wizardVerify, setWizardVerify] = (0, react_1.useState)({ status: 'idle' });
|
|
39
|
+
const [wizardTestTarget, setWizardTestTarget] = (0, react_1.useState)('');
|
|
40
|
+
const [wizardTestMsg, setWizardTestMsg] = (0, react_1.useState)('Hello from agent-nuvira!');
|
|
41
|
+
const [wizardTestSending, setWizardTestSending] = (0, react_1.useState)(false);
|
|
42
|
+
const [wizardTestResult, setWizardTestResult] = (0, react_1.useState)(null);
|
|
43
|
+
const mounted = (0, react_1.useRef)(true);
|
|
44
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
45
|
+
const list = await api_1.dashboardAPI.getPlatformConfigs();
|
|
46
|
+
if (!mounted.current)
|
|
47
|
+
return;
|
|
48
|
+
setConfigs(list);
|
|
49
|
+
}, []);
|
|
50
|
+
(0, react_1.useEffect)(() => {
|
|
51
|
+
void refresh();
|
|
52
|
+
return () => { mounted.current = false; };
|
|
53
|
+
}, [refresh]);
|
|
54
|
+
const startWizard = (entry) => {
|
|
55
|
+
setWizardPlatform(entry);
|
|
56
|
+
setWizardStep(0);
|
|
57
|
+
setWizardValues(Object.fromEntries(entry.envVars.map((v) => [v.varName, v.value])));
|
|
58
|
+
setWizardResult(null);
|
|
59
|
+
setWizardVerify({ status: 'idle' });
|
|
60
|
+
const g = GUIDES[entry.platform];
|
|
61
|
+
setWizardTestTarget(g?.testTarget ?? '');
|
|
62
|
+
setWizardTestMsg('Hello from agent-nuvira!');
|
|
63
|
+
setWizardTestResult(null);
|
|
64
|
+
};
|
|
65
|
+
const verifyWizard = async () => {
|
|
66
|
+
if (!wizardPlatform)
|
|
67
|
+
return;
|
|
68
|
+
setWizardVerify({ status: 'checking' });
|
|
69
|
+
const result = await api_1.dashboardAPI.verifyPlatformConfig(wizardPlatform.platform, wizardValues);
|
|
70
|
+
if (result.ok) {
|
|
71
|
+
setWizardVerify({ status: 'ok', info: result.info });
|
|
72
|
+
setWizardResult({ kind: 'ok', text: 'Verified! ' + (result.info ? 'Connected as ' + result.info + '.' : 'Token is valid.') });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
setWizardVerify({ status: 'err', error: result.error });
|
|
76
|
+
setWizardResult({ kind: 'err', text: result.error || 'Verification failed.' });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const saveWizard = async () => {
|
|
80
|
+
if (!wizardPlatform)
|
|
81
|
+
return;
|
|
82
|
+
setWizardSaving(true);
|
|
83
|
+
setWizardResult(null);
|
|
84
|
+
const result = await api_1.dashboardAPI.setPlatformConfig(wizardPlatform.platform, wizardValues);
|
|
85
|
+
setWizardSaving(false);
|
|
86
|
+
if (result.ok) {
|
|
87
|
+
setWizardResult({ kind: 'ok', text: 'Token saved! Verifying...' });
|
|
88
|
+
void refresh();
|
|
89
|
+
setWizardStep(2);
|
|
90
|
+
void verifyWizard();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setWizardResult({ kind: 'err', text: result.error || 'Save failed.' });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const sendTestMessage = async () => {
|
|
97
|
+
if (!wizardPlatform || !wizardTestTarget)
|
|
98
|
+
return;
|
|
99
|
+
setWizardTestSending(true);
|
|
100
|
+
setWizardTestResult(null);
|
|
101
|
+
const result = await api_1.dashboardAPI.sendChannelMessage(wizardTestTarget, wizardTestMsg);
|
|
102
|
+
setWizardTestSending(false);
|
|
103
|
+
if (result.ok) {
|
|
104
|
+
setWizardTestResult({ kind: 'ok', text: 'Message sent via ' + result.platform + '! Check your ' + wizardPlatform.label + ' app.' });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
setWizardTestResult({ kind: 'err', text: result.error || 'Send failed. Make sure the gateway is running (Gateway tab).' });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
if (configs === null) {
|
|
111
|
+
return <div className="empty-state">Loading platforms...</div>;
|
|
112
|
+
}
|
|
113
|
+
const configuredCount = configs.filter((c) => c.configured).length;
|
|
114
|
+
const filtered = configs.filter((c) => {
|
|
115
|
+
if (filter === 'configured')
|
|
116
|
+
return c.configured;
|
|
117
|
+
if (filter === 'unconfigured')
|
|
118
|
+
return !c.configured;
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
const guide = wizardPlatform ? GUIDES[wizardPlatform.platform] : null;
|
|
122
|
+
function stepDotClass(step) {
|
|
123
|
+
return wizardStep === step ? 'wizard-step-dot active' : 'wizard-step-dot';
|
|
124
|
+
}
|
|
125
|
+
return (<div className="panel">
|
|
126
|
+
<div className="panel-header">
|
|
127
|
+
<h2>Platforms โ Messaging Gateway Onboarding</h2>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="platforms-summary">
|
|
131
|
+
<div className="platforms-summary-stat">
|
|
132
|
+
<span className="platforms-summary-number">{configuredCount}</span>
|
|
133
|
+
<span className="platforms-summary-label">configured</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="platforms-summary-stat">
|
|
136
|
+
<span className="platforms-summary-number">{configs.length - configuredCount}</span>
|
|
137
|
+
<span className="platforms-summary-label">available</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="platforms-summary-filters">
|
|
140
|
+
{['all', 'configured', 'unconfigured'].map((f) => (<button key={f} type="button" className={filter === f ? 'platforms-filter-btn active' : 'platforms-filter-btn'} onClick={() => setFilter(f)}>
|
|
141
|
+
{f === 'all' ? 'All' : f === 'configured' ? 'Configured' : 'Not set up'}
|
|
142
|
+
</button>))}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="platforms-grid">
|
|
147
|
+
{filtered.map((p) => {
|
|
148
|
+
const g = GUIDES[p.platform];
|
|
149
|
+
return (<div key={p.platform} className={p.configured ? 'platform-card configured' : 'platform-card'}>
|
|
150
|
+
<div className="platform-card-head">
|
|
151
|
+
<span className="platform-card-icon">{g?.icon ?? '๐'}</span>
|
|
152
|
+
<div className="platform-card-info">
|
|
153
|
+
<span className="platform-card-name">{p.label}</span>
|
|
154
|
+
<span className="platform-card-category">{g?.category ?? 'Platform'}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<span className={p.configured ? 'status-dot connected' : 'status-dot reconnecting'}/>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="platform-card-vars">
|
|
159
|
+
{p.envVars.map((v) => (<span key={v.varName} className={v.set ? 'platform-var-chip set' : 'platform-var-chip'}>
|
|
160
|
+
{v.varName} {v.set ? 'done' : '--'}
|
|
161
|
+
</span>))}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="platform-card-actions">
|
|
164
|
+
{!p.configured ? (<button type="button" className="wizard-start-btn" onClick={() => startWizard(p)}>Setup</button>) : (<span className="platform-card-badge">Active</span>)}
|
|
165
|
+
{p.setupUrl ? (<a href={p.setupUrl} target="_blank" rel="noopener noreferrer" className="admin-mini-btn platform-docs-link">Docs</a>) : null}
|
|
166
|
+
</div>
|
|
167
|
+
{!p.configured && p.setupHint ? (<div className="platform-card-hint">{p.setupHint}</div>) : null}
|
|
168
|
+
</div>);
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="platforms-cli-hint">
|
|
173
|
+
<span className="admin-hint">Prefer the terminal? </span>
|
|
174
|
+
<code>buff gateway setup telegram</code>
|
|
175
|
+
<span className="admin-hint"> โ same wizard from your CLI</span>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{wizardPlatform && guide ? (<div className="wizard-overlay" onClick={() => setWizardPlatform(null)}>
|
|
179
|
+
<div className="wizard-modal" onClick={(e) => e.stopPropagation()}>
|
|
180
|
+
<div className="wizard-header">
|
|
181
|
+
<span className="wizard-icon">{guide.icon}</span>
|
|
182
|
+
<span className="wizard-title">Getting Started: {wizardPlatform.label}</span>
|
|
183
|
+
<div className="wizard-steps-indicator">
|
|
184
|
+
<span className={stepDotClass(0)}>1</span>
|
|
185
|
+
<span className={stepDotClass(1)}>2</span>
|
|
186
|
+
<span className={stepDotClass(2)}>3</span>
|
|
187
|
+
</div>
|
|
188
|
+
<button type="button" className="admin-mini-btn" onClick={() => setWizardPlatform(null)}>X</button>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="wizard-body">
|
|
191
|
+
{wizardStep === 0 && (<>
|
|
192
|
+
<p className="wizard-section-title">Step 1: Create your bot on {wizardPlatform.label}</p>
|
|
193
|
+
<ol className="wizard-steps">
|
|
194
|
+
{guide.steps.map((s, i) => <li key={i}>{s}</li>)}
|
|
195
|
+
</ol>
|
|
196
|
+
<div className="wizard-nav">
|
|
197
|
+
<button type="button" className="admin-refresh-btn" onClick={() => setWizardStep(1)}>I have my token โ Step 2</button>
|
|
198
|
+
{wizardPlatform.setupUrl ? (<a href={wizardPlatform.setupUrl} target="_blank" rel="noopener noreferrer" className="admin-mini-btn platform-docs-link">Open docs</a>) : null}
|
|
199
|
+
</div>
|
|
200
|
+
</>)}
|
|
201
|
+
|
|
202
|
+
{wizardStep === 1 && (<>
|
|
203
|
+
<p className="wizard-section-title">Step 2: Paste your token</p>
|
|
204
|
+
{wizardPlatform.envVars.map((v) => (<label className="hub-send-target" key={v.varName}>
|
|
205
|
+
<span className="admin-hint">{v.prompt}</span>
|
|
206
|
+
<input type={v.secret ? 'password' : 'text'} value={wizardValues[v.varName] ?? ''} onChange={(e) => setWizardValues((prev) => ({ ...prev, [v.varName]: e.target.value }))} placeholder={v.secret ? 'paste your token here' : 'value'} autoComplete="off" spellCheck={false} autoFocus/>
|
|
207
|
+
</label>))}
|
|
208
|
+
{wizardResult ? (<div className={wizardResult.kind === 'ok' ? 'admin-row-msg' : 'admin-row-msg admin-row-msg-err'}>{wizardResult.text}</div>) : null}
|
|
209
|
+
<div className="wizard-nav">
|
|
210
|
+
<button type="button" className="admin-mini-btn" onClick={() => setWizardStep(0)}>Back</button>
|
|
211
|
+
<button type="button" className="admin-refresh-btn" onClick={() => void saveWizard()} disabled={wizardSaving}>
|
|
212
|
+
{wizardSaving ? 'Saving...' : 'Save & Verify'}
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
</>)}
|
|
216
|
+
|
|
217
|
+
{wizardStep === 2 && (<>
|
|
218
|
+
<p className="wizard-section-title">Step 3: Verify and test</p>
|
|
219
|
+
|
|
220
|
+
{wizardVerify.status === 'checking' && (<div className="wizard-verify-status">Verifying token...</div>)}
|
|
221
|
+
{wizardVerify.status === 'ok' && (<div className="wizard-verify-status ok">Connected as {wizardVerify.info}</div>)}
|
|
222
|
+
{wizardVerify.status === 'err' && (<div className="wizard-verify-status err">{wizardVerify.error}</div>)}
|
|
223
|
+
{wizardVerify.status === 'idle' && wizardResult?.kind === 'ok' && (<div className="wizard-verify-status ok">{wizardResult.text}</div>)}
|
|
224
|
+
|
|
225
|
+
{wizardTestTarget && (<div className="wizard-test-section">
|
|
226
|
+
<p className="wizard-section-title">Send a test message:</p>
|
|
227
|
+
<div className="wizard-test-row">
|
|
228
|
+
<input className="wizard-test-input" value={wizardTestTarget} onChange={(e) => setWizardTestTarget(e.target.value)} placeholder="telegram:YOUR_CHAT_ID"/>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="wizard-test-row">
|
|
231
|
+
<input className="wizard-test-input" value={wizardTestMsg} onChange={(e) => setWizardTestMsg(e.target.value)} placeholder="Hello from agent-nuvira!"/>
|
|
232
|
+
<button type="button" className="admin-refresh-btn" onClick={() => void sendTestMessage()} disabled={wizardTestSending || !wizardTestTarget}>
|
|
233
|
+
{wizardTestSending ? 'Sending...' : 'Send test'}
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
{wizardTestResult ? (<div className={wizardTestResult.kind === 'ok' ? 'admin-row-msg' : 'admin-row-msg admin-row-msg-err'}>{wizardTestResult.text}</div>) : null}
|
|
237
|
+
</div>)}
|
|
238
|
+
|
|
239
|
+
<div className="wizard-nav">
|
|
240
|
+
<button type="button" className="admin-mini-btn" onClick={() => setWizardStep(1)}>Edit token</button>
|
|
241
|
+
<button type="button" className="admin-refresh-btn" onClick={() => setWizardPlatform(null)}>Done</button>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{!wizardTestTarget && (<p className="wizard-hint">Token saved! Start the gateway from the Gateway tab, then send a message from your {wizardPlatform.label} app.</p>)}
|
|
245
|
+
</>)}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>) : null}
|
|
249
|
+
</div>);
|
|
250
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = QuotaPanel;
|
|
4
|
+
/**
|
|
5
|
+
* QuotaPanel โ Session 36: the user-declared daily budget editor.
|
|
6
|
+
*
|
|
7
|
+
* The provider's TPD (tokens-per-day) allowance is invisible to us โ only the
|
|
8
|
+
* user knows their plan. This panel lets them declare it (tokens / requests
|
|
9
|
+
* per reset window + the admin cost cap) in the SAME config the CLI writes
|
|
10
|
+
* (`buff model quota set` โ `routing.quota.*` + `routing.governance.maxCostUsd`),
|
|
11
|
+
* so the quota ledger + auto-router pace around it BEFORE requests go out.
|
|
12
|
+
*
|
|
13
|
+
* Semantics (Design Decision 21): advisory + pacing, never a product hard cap.
|
|
14
|
+
* Unset = current behavior; set = the ledger parks the provider at the cap and
|
|
15
|
+
* auto-re-enables on window rollover. Clearing a field (blank) returns to
|
|
16
|
+
* unset. Role gates: budget fields โ routing.operate (admin + operator); cost
|
|
17
|
+
* cap โ policy.write (admin); viewer sees a read-only table.
|
|
18
|
+
*/
|
|
19
|
+
const react_1 = require("react");
|
|
20
|
+
const api_1 = require("../api");
|
|
21
|
+
function toField(value) {
|
|
22
|
+
if (value.trim() === '')
|
|
23
|
+
return null;
|
|
24
|
+
const n = Number(value);
|
|
25
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
26
|
+
}
|
|
27
|
+
function QuotaPanel({ authed, role }) {
|
|
28
|
+
const [data, setData] = (0, react_1.useState)(null);
|
|
29
|
+
const [drafts, setDrafts] = (0, react_1.useState)({});
|
|
30
|
+
const [costDraft, setCostDraft] = (0, react_1.useState)('');
|
|
31
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
32
|
+
const [msg, setMsg] = (0, react_1.useState)(null);
|
|
33
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
34
|
+
const canEditBudget = authed && (role === 'admin' || role === 'operator');
|
|
35
|
+
const canEditCost = authed && role === 'admin';
|
|
36
|
+
const load = (0, react_1.useCallback)(async () => {
|
|
37
|
+
const d = await api_1.dashboardAPI.fetchAdminQuota();
|
|
38
|
+
if (!d) {
|
|
39
|
+
setError('Could not load budget config.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setError(null);
|
|
43
|
+
setData(d);
|
|
44
|
+
const rows = {};
|
|
45
|
+
for (const p of d.providers) {
|
|
46
|
+
const limit = d.quota[p] || {};
|
|
47
|
+
rows[p] = {
|
|
48
|
+
provider: p,
|
|
49
|
+
tokens: limit.tokensPerWindow?.toString() ?? '',
|
|
50
|
+
requests: limit.requestsPerWindow?.toString() ?? '',
|
|
51
|
+
windowMs: limit.windowMs?.toString() ?? '',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
setDrafts(rows);
|
|
55
|
+
setCostDraft(d.costUsd === null ? '' : String(d.costUsd));
|
|
56
|
+
}, []);
|
|
57
|
+
(0, react_1.useEffect)(() => {
|
|
58
|
+
void load();
|
|
59
|
+
}, [load]);
|
|
60
|
+
const setDraft = (provider, field, value) => {
|
|
61
|
+
setDrafts((d) => ({ ...d, [provider]: { ...(d[provider] || { provider }), [field]: value } }));
|
|
62
|
+
};
|
|
63
|
+
const save = async () => {
|
|
64
|
+
setBusy(true);
|
|
65
|
+
setMsg(null);
|
|
66
|
+
const quota = {};
|
|
67
|
+
for (const row of Object.values(drafts)) {
|
|
68
|
+
const limit = {};
|
|
69
|
+
const t = toField(row.tokens);
|
|
70
|
+
const r = toField(row.requests);
|
|
71
|
+
const w = toField(row.windowMs);
|
|
72
|
+
if (t !== null)
|
|
73
|
+
limit.tokensPerWindow = t;
|
|
74
|
+
if (r !== null)
|
|
75
|
+
limit.requestsPerWindow = r;
|
|
76
|
+
if (w !== null)
|
|
77
|
+
limit.windowMs = w;
|
|
78
|
+
quota[row.provider] = Object.keys(limit).length > 0 ? limit : null;
|
|
79
|
+
}
|
|
80
|
+
const payload = { quota };
|
|
81
|
+
// The cost cap is gated on policy.write (admin) SERVER-side โ an operator
|
|
82
|
+
// must never send costUsd or every save would 403. Omit it entirely unless
|
|
83
|
+
// this session can edit the cap.
|
|
84
|
+
if (canEditCost) {
|
|
85
|
+
if (costDraft.trim() !== '') {
|
|
86
|
+
const n = Number(costDraft);
|
|
87
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
88
|
+
setMsg({ ok: false, text: 'Cost cap must be a non-negative number.' });
|
|
89
|
+
setBusy(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
payload.costUsd = n;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
payload.costUsd = null; // clears the cap
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const res = await api_1.dashboardAPI.saveAdminQuota(payload);
|
|
99
|
+
setBusy(false);
|
|
100
|
+
if (res.ok) {
|
|
101
|
+
setMsg({ ok: true, text: 'Budget saved โ the quota ledger + auto-router will enforce it.' });
|
|
102
|
+
void load();
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
setMsg({ ok: false, text: res.error || 'Save failed.' });
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const configured = (p) => {
|
|
109
|
+
const limit = data?.quota[p];
|
|
110
|
+
return !!limit && (limit.tokensPerWindow !== undefined || limit.requestsPerWindow !== undefined || limit.windowMs !== undefined);
|
|
111
|
+
};
|
|
112
|
+
const input = (disabled) => disabled ? 'admin-input' : 'admin-input';
|
|
113
|
+
return (<div className="admin-quota-panel">
|
|
114
|
+
<h3 className="section-subtitle">๐ฐ Daily Budget (routing.quota โ your plan, your say)</h3>
|
|
115
|
+
<p className="admin-quota-note">
|
|
116
|
+
Free-tier providers enforce tokens-per-day caps the API doesn't advertise โ only you
|
|
117
|
+
know your plan's allowance. Declare it here and the agent paces around it (parks the provider
|
|
118
|
+
at the cap, auto-resumes at window rollover) instead of exhausting your budget mid-task.
|
|
119
|
+
Unset = current behavior. Same config as <code>buff model quota set</code>.
|
|
120
|
+
</p>
|
|
121
|
+
{error ? <div className="admin-error">{error}</div> : null}
|
|
122
|
+
{data ? (<>
|
|
123
|
+
<div className="admin-quota-table">
|
|
124
|
+
<div className="admin-quota-row admin-quota-head">
|
|
125
|
+
<span>Provider</span>
|
|
126
|
+
<span>Tokens / window</span>
|
|
127
|
+
<span>Requests / window</span>
|
|
128
|
+
<span>Window (ms)</span>
|
|
129
|
+
<span>Status</span>
|
|
130
|
+
</div>
|
|
131
|
+
{data.providers.map((p) => (<div className="admin-quota-row" key={p}>
|
|
132
|
+
<span className="admin-provider-type">{p}</span>
|
|
133
|
+
<input className={input(!canEditBudget)} type="number" min={0} placeholder="unset" disabled={!canEditBudget} value={drafts[p]?.tokens ?? ''} onChange={(e) => setDraft(p, 'tokens', e.target.value)}/>
|
|
134
|
+
<input className={input(!canEditBudget)} type="number" min={0} placeholder="unset" disabled={!canEditBudget} value={drafts[p]?.requests ?? ''} onChange={(e) => setDraft(p, 'requests', e.target.value)}/>
|
|
135
|
+
<input className={input(!canEditBudget)} type="number" min={0} placeholder="86400000 (24h)" disabled={!canEditBudget} value={drafts[p]?.windowMs ?? ''} onChange={(e) => setDraft(p, 'windowMs', e.target.value)}/>
|
|
136
|
+
<span className={`admin-role-badge ${configured(p) ? 'admin-role-admin' : 'admin-role-viewer'}`}>
|
|
137
|
+
{configured(p) ? 'declared' : 'unset'}
|
|
138
|
+
</span>
|
|
139
|
+
</div>))}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="admin-quota-row admin-quota-cost">
|
|
143
|
+
<span className="admin-provider-type">Max cost / call (USD, admin)</span>
|
|
144
|
+
<input className={input(!canEditCost)} type="number" min={0} step={0.01} placeholder="unset" disabled={!canEditCost} value={costDraft} onChange={(e) => setCostDraft(e.target.value)}/>
|
|
145
|
+
<span className="admin-quota-cost-hint">governance.maxCostUsd โ hard admin cap</span>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{canEditBudget || canEditCost ? (<div className="admin-quota-actions">
|
|
149
|
+
<button className="admin-mini-btn" onClick={() => void save()} disabled={busy}>
|
|
150
|
+
{busy ? 'โณ Savingโฆ' : '๐พ Save budget'}
|
|
151
|
+
</button>
|
|
152
|
+
{msg ? (<span className={msg.ok ? 'admin-quota-ok' : 'admin-quota-err'}>{msg.text}</span>) : null}
|
|
153
|
+
</div>) : (<div className="admin-readonly-note">
|
|
154
|
+
๐ Read-only for <code>{role}</code> โ budget limits need <code>admin</code> or{' '}
|
|
155
|
+
<code>operator</code>; the cost cap needs <code>admin</code>.
|
|
156
|
+
</div>)}
|
|
157
|
+
</>) : (<div className="empty-state">Loading budgetโฆ</div>)}
|
|
158
|
+
</div>);
|
|
159
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* QuotaPanel tests (Session 36 โ user-declared daily budget).
|
|
8
|
+
*
|
|
9
|
+
* Covers: provider rows + the TPD callout, pre-fill of declared limits,
|
|
10
|
+
* viewer read-only gating, admin save payload (routing.quota + cost cap),
|
|
11
|
+
* and the operator partial permission (budget editable, cost cap locked).
|
|
12
|
+
*/
|
|
13
|
+
const vitest_1 = require("vitest");
|
|
14
|
+
const react_1 = require("@testing-library/react");
|
|
15
|
+
const QuotaPanel_1 = __importDefault(require("./QuotaPanel"));
|
|
16
|
+
const api_1 = require("../api");
|
|
17
|
+
const QUOTA = {
|
|
18
|
+
ok: true,
|
|
19
|
+
quota: { groq: { tokensPerWindow: 12000 } },
|
|
20
|
+
costUsd: 0.1,
|
|
21
|
+
providers: ['groq', 'gemini', 'local'],
|
|
22
|
+
};
|
|
23
|
+
function mockFetch(config = QUOTA) {
|
|
24
|
+
return vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminQuota').mockResolvedValue(config);
|
|
25
|
+
}
|
|
26
|
+
(0, vitest_1.describe)('QuotaPanel', () => {
|
|
27
|
+
(0, vitest_1.afterEach)(() => {
|
|
28
|
+
(0, react_1.cleanup)();
|
|
29
|
+
vitest_1.vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)('renders every provider row + the TPD callout', async () => {
|
|
32
|
+
mockFetch();
|
|
33
|
+
(0, react_1.render)(<QuotaPanel_1.default authed={false} role="viewer"/>);
|
|
34
|
+
(0, vitest_1.expect)(await react_1.screen.findByText(/Daily Budget/)).toBeTruthy();
|
|
35
|
+
(0, vitest_1.expect)(react_1.screen.getByText('groq')).toBeTruthy();
|
|
36
|
+
(0, vitest_1.expect)(react_1.screen.getByText('gemini')).toBeTruthy();
|
|
37
|
+
(0, vitest_1.expect)(react_1.screen.getByText('local')).toBeTruthy();
|
|
38
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/tokens-per.*day/)).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)('pre-fills declared limits and the cost cap', async () => {
|
|
41
|
+
mockFetch();
|
|
42
|
+
(0, react_1.render)(<QuotaPanel_1.default authed={false} role="viewer"/>);
|
|
43
|
+
(0, vitest_1.expect)((await react_1.screen.findAllByDisplayValue('12000')).length).toBeGreaterThan(0);
|
|
44
|
+
(0, vitest_1.expect)(react_1.screen.getByDisplayValue('0.1')).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)('viewer sees read-only inputs and the read-only note', async () => {
|
|
47
|
+
mockFetch();
|
|
48
|
+
(0, react_1.render)(<QuotaPanel_1.default authed={true} role="viewer"/>);
|
|
49
|
+
const firstInput = (await react_1.screen.findAllByPlaceholderText('unset'))[0];
|
|
50
|
+
(0, vitest_1.expect)(firstInput.disabled).toBe(true);
|
|
51
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/Read-only for/)).toBeTruthy();
|
|
52
|
+
(0, vitest_1.expect)(react_1.screen.queryByText('๐พ Save budget')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.it)('admin can save โ saveAdminQuota receives the declared budget', async () => {
|
|
55
|
+
mockFetch();
|
|
56
|
+
const save = vitest_1.vi.spyOn(api_1.dashboardAPI, 'saveAdminQuota').mockResolvedValue({ ok: true });
|
|
57
|
+
(0, react_1.render)(<QuotaPanel_1.default authed={true} role="admin"/>);
|
|
58
|
+
react_1.fireEvent.change(await react_1.screen.findByDisplayValue('12000'), { target: { value: '20000' } });
|
|
59
|
+
react_1.fireEvent.click(react_1.screen.getByText('๐พ Save budget'));
|
|
60
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(save).toHaveBeenCalled());
|
|
61
|
+
const payload = save.mock.calls[0][0];
|
|
62
|
+
(0, vitest_1.expect)(payload.quota.groq.tokensPerWindow).toBe(20000);
|
|
63
|
+
(0, vitest_1.expect)(payload.costUsd).toBe(0.1);
|
|
64
|
+
});
|
|
65
|
+
(0, vitest_1.it)('operator can edit budget fields but the cost cap input stays locked', async () => {
|
|
66
|
+
mockFetch();
|
|
67
|
+
(0, react_1.render)(<QuotaPanel_1.default authed={true} role="operator"/>);
|
|
68
|
+
await react_1.screen.findByDisplayValue('12000');
|
|
69
|
+
(0, vitest_1.expect)(react_1.screen.getByText('๐พ Save budget')).toBeTruthy();
|
|
70
|
+
const costInput = react_1.screen.getByDisplayValue('0.1');
|
|
71
|
+
(0, vitest_1.expect)(costInput.disabled).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('operator save payload OMITS costUsd โ the server gates it on policy.write (admin)', async () => {
|
|
74
|
+
mockFetch();
|
|
75
|
+
const save = vitest_1.vi.spyOn(api_1.dashboardAPI, 'saveAdminQuota').mockResolvedValue({ ok: true });
|
|
76
|
+
(0, react_1.render)(<QuotaPanel_1.default authed={true} role="operator"/>);
|
|
77
|
+
react_1.fireEvent.change(await react_1.screen.findByDisplayValue('12000'), { target: { value: '9000' } });
|
|
78
|
+
react_1.fireEvent.click(react_1.screen.getByText('๐พ Save budget'));
|
|
79
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(save).toHaveBeenCalled());
|
|
80
|
+
const payload = save.mock.calls[0][0];
|
|
81
|
+
(0, vitest_1.expect)(payload.costUsd).toBeUndefined(); // operator must never send the cap
|
|
82
|
+
const quota = payload.quota;
|
|
83
|
+
(0, vitest_1.expect)(quota.groq.tokensPerWindow).toBe(9000);
|
|
84
|
+
});
|
|
85
|
+
});
|