nyxora 1.6.2 → 1.6.3
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 +22 -12
- package/SECURITY.md +25 -21
- package/assets/raw-diagram.png +0 -0
- package/assets/security-flow.png +0 -0
- package/bin/nyxora.mjs +236 -0
- package/launcher.js +8 -3
- package/launcher.ts +28 -1
- package/package.json +11 -7
- package/packages/core/package.json +4 -3
- package/packages/core/src/agent/reasoning.ts +10 -8
- package/packages/core/src/config/parser.ts +2 -1
- package/packages/core/src/gateway/cli.ts +2 -64
- package/packages/core/src/gateway/server.ts +89 -8
- package/packages/core/src/gateway/setup-cli.ts +7 -0
- package/packages/core/src/gateway/setup.ts +51 -28
- package/packages/core/src/gateway/telegram.ts +147 -89
- package/packages/core/src/memory/logger.ts +63 -7
- package/packages/core/src/system/pluginManager.ts +48 -34
- package/packages/core/src/utils/state.ts +15 -2
- package/packages/core/src/web3/config.ts +18 -3
- package/packages/core/src/web3/skills/marketAnalysis.ts +43 -17
- package/packages/core/src/web3/skills/swapToken.ts +9 -1
- package/packages/dashboard/dist/assets/index-BTP1WrFj.js +194 -0
- package/packages/dashboard/dist/assets/index-POJM-7Fd.css +1 -0
- package/packages/dashboard/dist/favicon.svg +1 -1
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package-lock.json +2 -2
- package/packages/dashboard/package.json +1 -1
- package/packages/dashboard/public/favicon.svg +1 -1
- package/packages/dashboard/src/App.tsx +224 -167
- package/packages/dashboard/src/Settings.tsx +55 -0
- package/packages/dashboard/src/Skills.tsx +8 -1
- package/packages/dashboard/src/index.css +146 -35
- package/packages/policy/package.json +1 -1
- package/packages/policy/src/server.ts +21 -28
- package/packages/signer/package.json +1 -1
- package/packages/signer/src/server.ts +39 -13
- package/bin/nyxora.js +0 -13
- package/packages/dashboard/dist/assets/index-BK4qmIy6.js +0 -200
- package/packages/dashboard/dist/assets/index-C1m4ohce.css +0 -1
- package/packages/dashboard/src/Memory.tsx +0 -110
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { apiFetch } from './utils/api';
|
|
2
2
|
import { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { Send, Bot, Activity, MessageSquare, LayoutDashboard, Settings as SettingsIcon,
|
|
3
|
+
import { Send, Bot, Activity, MessageSquare, LayoutDashboard, Settings as SettingsIcon, Zap, Database, Mic, Copy, Check, Plus, Trash2, Search, Edit2 } from 'lucide-react';
|
|
4
4
|
import Overview from './Overview';
|
|
5
|
-
import Memory from './Memory';
|
|
6
5
|
import Settings from './Settings';
|
|
7
6
|
import Skills from './Skills';
|
|
8
7
|
import PendingTransactions from './PendingTransactions';
|
|
@@ -25,8 +24,13 @@ interface Config {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
function App() {
|
|
28
|
-
const [currentView, setCurrentView] = useState<'chat' | 'overview' | '
|
|
27
|
+
const [currentView, setCurrentView] = useState<'chat' | 'overview' | 'settings' | 'skills'>('chat');
|
|
28
|
+
const [trendingTokens, setTrendingTokens] = useState<string[]>(['$BTC', '$ETH', '$SOL', '$SUI']);
|
|
29
29
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
30
|
+
const [chatSessions, setChatSessions] = useState<any[]>([]);
|
|
31
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
32
|
+
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
|
33
|
+
const [editSessionTitle, setEditSessionTitle] = useState<string>('');
|
|
30
34
|
const [input, setInput] = useState('');
|
|
31
35
|
const [isLoading, setIsLoading] = useState(false);
|
|
32
36
|
const [isListening, setIsListening] = useState(false);
|
|
@@ -35,52 +39,14 @@ function App() {
|
|
|
35
39
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
36
40
|
const [config, setConfig] = useState<Config | null>(null);
|
|
37
41
|
const [chatWidth, setChatWidth] = useState(70);
|
|
42
|
+
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
|
38
43
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
39
44
|
const recognitionRef = useRef<any>(null);
|
|
40
|
-
const workspaceRef = useRef<HTMLDivElement>(null);
|
|
41
|
-
const isDragging = useRef(false);
|
|
42
45
|
|
|
43
46
|
useEffect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
|
|
48
|
-
if (newWidth > 20 && newWidth < 80) {
|
|
49
|
-
setChatWidth(newWidth);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const handleMouseUp = () => {
|
|
54
|
-
isDragging.current = false;
|
|
55
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
56
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Attach them to document only when dragging is active
|
|
60
|
-
// We will attach them in handleMouseDown
|
|
61
|
-
}, []);
|
|
62
|
-
|
|
63
|
-
const handleMouseDown = () => {
|
|
64
|
-
isDragging.current = true;
|
|
65
|
-
|
|
66
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
67
|
-
if (!isDragging.current || !workspaceRef.current) return;
|
|
68
|
-
const rect = workspaceRef.current.getBoundingClientRect();
|
|
69
|
-
const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
|
|
70
|
-
if (newWidth > 25 && newWidth < 75) {
|
|
71
|
-
setChatWidth(newWidth);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const handleMouseUp = () => {
|
|
76
|
-
isDragging.current = false;
|
|
77
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
78
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
82
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
83
|
-
};
|
|
47
|
+
// Scroll to bottom on new message
|
|
48
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
49
|
+
}, [messages, isLoading]);
|
|
84
50
|
|
|
85
51
|
useEffect(() => {
|
|
86
52
|
// Initialize Speech Recognition
|
|
@@ -151,22 +117,86 @@ function App() {
|
|
|
151
117
|
};
|
|
152
118
|
|
|
153
119
|
const fetchHistory = async () => {
|
|
120
|
+
if (!activeSessionId) {
|
|
121
|
+
setMessages([]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
154
124
|
try {
|
|
155
|
-
const
|
|
125
|
+
const url = `http://localhost:3000/api/history?session_id=${activeSessionId}`;
|
|
126
|
+
const res = await apiFetch(url);
|
|
156
127
|
if (res.ok) {
|
|
157
128
|
const data = await res.json();
|
|
158
|
-
setMessages(
|
|
159
|
-
if (prev.length !== data.length || (prev.length > 0 && data.length > 0 && prev[prev.length - 1].content !== data[data.length - 1].content)) {
|
|
160
|
-
return data;
|
|
161
|
-
}
|
|
162
|
-
return prev;
|
|
163
|
-
});
|
|
129
|
+
setMessages(data);
|
|
164
130
|
}
|
|
165
131
|
} catch (err) {
|
|
166
132
|
console.warn('Backend not ready, retrying history fetch in 2s...');
|
|
167
133
|
}
|
|
168
134
|
};
|
|
169
135
|
|
|
136
|
+
const fetchSessions = async () => {
|
|
137
|
+
try {
|
|
138
|
+
const res = await apiFetch('http://localhost:3000/api/sessions');
|
|
139
|
+
if (res.ok) {
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
setChatSessions(data);
|
|
142
|
+
// On first load, if no active session, auto-select the most recent one
|
|
143
|
+
if (data.length > 0 && !activeSessionId) {
|
|
144
|
+
setActiveSessionId(data[0].id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const fetchTrendingTokens = async () => {
|
|
151
|
+
try {
|
|
152
|
+
const res = await apiFetch('http://localhost:3000/api/trending');
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
setTrendingTokens(await res.json());
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const createNewSession = async () => {
|
|
160
|
+
try {
|
|
161
|
+
const res = await apiFetch('http://localhost:3000/api/sessions', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({ title: 'New Chat' })
|
|
165
|
+
});
|
|
166
|
+
if (res.ok) {
|
|
167
|
+
const { id } = await res.json();
|
|
168
|
+
setActiveSessionId(id);
|
|
169
|
+
setMessages([]);
|
|
170
|
+
await fetchSessions();
|
|
171
|
+
setCurrentView('chat');
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const renameSession = async (id: string, newTitle: string) => {
|
|
177
|
+
try {
|
|
178
|
+
await apiFetch(`http://localhost:3000/api/sessions/${id}`, {
|
|
179
|
+
method: 'PUT',
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
body: JSON.stringify({ title: newTitle })
|
|
182
|
+
});
|
|
183
|
+
setEditingSessionId(null);
|
|
184
|
+
await fetchSessions();
|
|
185
|
+
} catch (err) {}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const deleteSession = async (id: string, e: React.MouseEvent) => {
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
try {
|
|
191
|
+
await apiFetch(`http://localhost:3000/api/sessions/${id}`, { method: 'DELETE' });
|
|
192
|
+
if (activeSessionId === id) {
|
|
193
|
+
setActiveSessionId(null);
|
|
194
|
+
setMessages([]);
|
|
195
|
+
}
|
|
196
|
+
await fetchSessions();
|
|
197
|
+
} catch (err) {}
|
|
198
|
+
};
|
|
199
|
+
|
|
170
200
|
const fetchConfig = async () => {
|
|
171
201
|
try {
|
|
172
202
|
const res = await apiFetch('http://localhost:3000/api/config');
|
|
@@ -198,9 +228,15 @@ function App() {
|
|
|
198
228
|
useEffect(() => {
|
|
199
229
|
fetchHistory();
|
|
200
230
|
fetchConfig();
|
|
201
|
-
|
|
231
|
+
fetchSessions();
|
|
232
|
+
fetchTrendingTokens();
|
|
233
|
+
const interval = setInterval(() => {
|
|
234
|
+
fetchHistory();
|
|
235
|
+
fetchSessions();
|
|
236
|
+
fetchTrendingTokens();
|
|
237
|
+
}, 2000);
|
|
202
238
|
return () => clearInterval(interval);
|
|
203
|
-
}, []);
|
|
239
|
+
}, [activeSessionId]);
|
|
204
240
|
|
|
205
241
|
useEffect(() => {
|
|
206
242
|
// Adding a slight timeout to ensure DOM is fully rendered before scrolling
|
|
@@ -217,17 +253,45 @@ function App() {
|
|
|
217
253
|
setInput('');
|
|
218
254
|
setIsLoading(true);
|
|
219
255
|
|
|
256
|
+
let currentSessionId = activeSessionId;
|
|
257
|
+
|
|
258
|
+
// Auto-create session if null
|
|
259
|
+
if (!currentSessionId) {
|
|
260
|
+
try {
|
|
261
|
+
const title = userMsg.length > 25 ? userMsg.substring(0, 25) + '...' : userMsg;
|
|
262
|
+
const res = await apiFetch('http://localhost:3000/api/sessions', {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ title })
|
|
266
|
+
});
|
|
267
|
+
if (res.ok) {
|
|
268
|
+
const { id } = await res.json();
|
|
269
|
+
currentSessionId = id;
|
|
270
|
+
setActiveSessionId(id);
|
|
271
|
+
await fetchSessions();
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error("Failed to auto-create session", err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
220
278
|
setMessages(prev => [...prev, { role: 'user', content: userMsg }]);
|
|
221
279
|
|
|
222
280
|
try {
|
|
223
281
|
const res = await apiFetch('http://localhost:3000/api/chat', {
|
|
224
282
|
method: 'POST',
|
|
225
283
|
headers: { 'Content-Type': 'application/json' },
|
|
226
|
-
body: JSON.stringify({ message: userMsg }),
|
|
284
|
+
body: JSON.stringify({ message: userMsg, session_id: currentSessionId }),
|
|
227
285
|
});
|
|
228
286
|
const data = await res.json();
|
|
229
287
|
await fetchHistory();
|
|
230
288
|
|
|
289
|
+
// Auto-rename on first prompt
|
|
290
|
+
if (messages.length === 0 && currentSessionId) {
|
|
291
|
+
const autoTitle = userMsg.length > 25 ? userMsg.substring(0, 25) + '...' : userMsg;
|
|
292
|
+
renameSession(currentSessionId, autoTitle);
|
|
293
|
+
}
|
|
294
|
+
|
|
231
295
|
// Trigger TTS if in voice mode
|
|
232
296
|
if (isVoiceModeRef.current && data.response) {
|
|
233
297
|
speak(data.response);
|
|
@@ -269,7 +333,7 @@ function App() {
|
|
|
269
333
|
<aside className="sidebar">
|
|
270
334
|
<div className="agent-identity-card">
|
|
271
335
|
<div className="agent-avatar">
|
|
272
|
-
<Bot size={28} color="#
|
|
336
|
+
<Bot size={28} color="#88c0d0" />
|
|
273
337
|
</div>
|
|
274
338
|
<div className="agent-info">
|
|
275
339
|
<div className="agent-name">Nyxora AI</div>
|
|
@@ -280,47 +344,63 @@ function App() {
|
|
|
280
344
|
</div>
|
|
281
345
|
|
|
282
346
|
<div className="sidebar-scroll-area">
|
|
283
|
-
<
|
|
284
|
-
<nav className="sidebar-nav">
|
|
347
|
+
<nav className="sidebar-nav" style={{ paddingTop: '16px' }}>
|
|
285
348
|
<div
|
|
286
|
-
className=
|
|
287
|
-
onClick={
|
|
349
|
+
className="nav-item"
|
|
350
|
+
onClick={createNewSession}
|
|
288
351
|
>
|
|
289
|
-
<
|
|
352
|
+
<Plus size={15} className="nav-icon" /> New Chat
|
|
290
353
|
</div>
|
|
291
354
|
<div
|
|
292
355
|
className={`nav-item ${currentView === 'overview' ? 'active' : ''}`}
|
|
293
356
|
onClick={() => setCurrentView('overview')}
|
|
294
357
|
>
|
|
295
|
-
<LayoutDashboard size={
|
|
358
|
+
<LayoutDashboard size={15} className="nav-icon" /> Overview
|
|
296
359
|
</div>
|
|
297
|
-
</nav>
|
|
298
|
-
|
|
299
|
-
<div className="sidebar-section">KNOWLEDGE</div>
|
|
300
|
-
<nav className="sidebar-nav">
|
|
301
360
|
<div
|
|
302
361
|
className={`nav-item ${currentView === 'skills' ? 'active' : ''}`}
|
|
303
362
|
onClick={() => setCurrentView('skills')}
|
|
304
363
|
>
|
|
305
|
-
<
|
|
306
|
-
</div>
|
|
307
|
-
<div
|
|
308
|
-
className={`nav-item ${currentView === 'memory' ? 'active' : ''}`}
|
|
309
|
-
onClick={() => setCurrentView('memory')}
|
|
310
|
-
>
|
|
311
|
-
<Database size={18} className="nav-icon" /> Memory
|
|
364
|
+
<Zap size={15} className="nav-icon" /> Web3 Skills
|
|
312
365
|
</div>
|
|
313
|
-
</nav>
|
|
314
|
-
|
|
315
|
-
<div className="sidebar-section">SYSTEM</div>
|
|
316
|
-
<nav className="sidebar-nav">
|
|
317
366
|
<div
|
|
318
367
|
className={`nav-item ${currentView === 'settings' ? 'active' : ''}`}
|
|
319
368
|
onClick={() => setCurrentView('settings')}
|
|
320
369
|
>
|
|
321
|
-
<SettingsIcon size={
|
|
370
|
+
<SettingsIcon size={15} className="nav-icon" /> Settings
|
|
322
371
|
</div>
|
|
323
372
|
</nav>
|
|
373
|
+
|
|
374
|
+
<div className="sidebar-section">
|
|
375
|
+
<span>Recent</span>
|
|
376
|
+
</div>
|
|
377
|
+
<nav className="sidebar-nav sessions-list">
|
|
378
|
+
{chatSessions.map((session) => (
|
|
379
|
+
<div
|
|
380
|
+
key={session.id}
|
|
381
|
+
className={`nav-item session-item ${activeSessionId === session.id && currentView === 'chat' ? 'active' : ''}`}
|
|
382
|
+
onClick={() => {
|
|
383
|
+
setActiveSessionId(session.id);
|
|
384
|
+
setCurrentView('chat');
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', overflow: 'hidden', flex: 1 }}>
|
|
388
|
+
<MessageSquare size={14} className="nav-icon" />
|
|
389
|
+
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: '0.85rem' }}>
|
|
390
|
+
{session.title}
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<div style={{ display: 'flex', gap: '4px' }}>
|
|
394
|
+
<button className="delete-session-btn" onClick={(e) => { e.stopPropagation(); setEditingSessionId(session.id); setEditSessionTitle(session.title); }} title="Rename Session">
|
|
395
|
+
<Edit2 size={12} />
|
|
396
|
+
</button>
|
|
397
|
+
<button className="delete-session-btn" onClick={(e) => deleteSession(session.id, e)} title="Delete Session">
|
|
398
|
+
<Trash2 size={14} />
|
|
399
|
+
</button>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
))}
|
|
403
|
+
</nav>
|
|
324
404
|
</div>
|
|
325
405
|
</aside>
|
|
326
406
|
|
|
@@ -350,64 +430,6 @@ function App() {
|
|
|
350
430
|
<option value="bsc">BNB Smart Chain</option>
|
|
351
431
|
<option value="arbitrum">Arbitrum</option>
|
|
352
432
|
</select>
|
|
353
|
-
|
|
354
|
-
<select
|
|
355
|
-
className="config-dropdown"
|
|
356
|
-
value={config.llm.provider}
|
|
357
|
-
onChange={(e) => updateConfig({ ...config, llm: { ...config.llm, provider: e.target.value }})}
|
|
358
|
-
>
|
|
359
|
-
<option value="gemini">Google Gemini</option>
|
|
360
|
-
<option value="openai">OpenAI</option>
|
|
361
|
-
<option value="openrouter">OpenRouter</option>
|
|
362
|
-
<option value="ollama">Local Ollama</option>
|
|
363
|
-
</select>
|
|
364
|
-
|
|
365
|
-
<input
|
|
366
|
-
type="text"
|
|
367
|
-
list="model-suggestions"
|
|
368
|
-
className="config-dropdown"
|
|
369
|
-
value={config.llm.model}
|
|
370
|
-
onChange={(e) => updateConfig({ ...config, llm: { ...config.llm, model: e.target.value }})}
|
|
371
|
-
placeholder="Enter model name..."
|
|
372
|
-
style={{ width: '200px' }}
|
|
373
|
-
/>
|
|
374
|
-
<datalist id="model-suggestions">
|
|
375
|
-
{config.llm.provider === 'gemini' && (
|
|
376
|
-
<>
|
|
377
|
-
<option value="gemini-2.5-flash" />
|
|
378
|
-
<option value="gemini-2.5-pro" />
|
|
379
|
-
<option value="gemini-1.5-flash" />
|
|
380
|
-
<option value="gemini-1.5-pro" />
|
|
381
|
-
</>
|
|
382
|
-
)}
|
|
383
|
-
{config.llm.provider === 'openai' && (
|
|
384
|
-
<>
|
|
385
|
-
<option value="gpt-4o" />
|
|
386
|
-
<option value="gpt-4o-mini" />
|
|
387
|
-
<option value="gpt-4-turbo" />
|
|
388
|
-
<option value="o1-mini" />
|
|
389
|
-
</>
|
|
390
|
-
)}
|
|
391
|
-
{config.llm.provider === 'openrouter' && (
|
|
392
|
-
<>
|
|
393
|
-
<option value="anthropic/claude-3.5-sonnet" />
|
|
394
|
-
<option value="anthropic/claude-3-opus" />
|
|
395
|
-
<option value="meta-llama/llama-3.1-70b-instruct" />
|
|
396
|
-
<option value="google/gemini-1.5-pro" />
|
|
397
|
-
<option value="x-ai/grok-2" />
|
|
398
|
-
<option value="mistralai/mixtral-8x7b-instruct" />
|
|
399
|
-
<option value="deepseek/deepseek-coder" />
|
|
400
|
-
</>
|
|
401
|
-
)}
|
|
402
|
-
{config.llm.provider === 'ollama' && (
|
|
403
|
-
<>
|
|
404
|
-
<option value="llama3" />
|
|
405
|
-
<option value="llama3.1" />
|
|
406
|
-
<option value="mistral" />
|
|
407
|
-
<option value="qwen2" />
|
|
408
|
-
</>
|
|
409
|
-
)}
|
|
410
|
-
</datalist>
|
|
411
433
|
</>
|
|
412
434
|
)}
|
|
413
435
|
</div>
|
|
@@ -417,19 +439,26 @@ function App() {
|
|
|
417
439
|
<Overview config={config} />
|
|
418
440
|
) : currentView === 'skills' ? (
|
|
419
441
|
<Skills />
|
|
420
|
-
) : currentView === 'memory' ? (
|
|
421
|
-
<Memory />
|
|
422
442
|
) : currentView === 'settings' ? (
|
|
423
443
|
<Settings config={config} onConfigChange={setConfig} />
|
|
424
444
|
) : (
|
|
425
|
-
<div className="workspace-container"
|
|
426
|
-
<div className="chat-wrapper" style={{ width:
|
|
445
|
+
<div className="workspace-container">
|
|
446
|
+
<div className="chat-wrapper" style={{ width: '100%', margin: '0 auto', maxWidth: '1000px' }}>
|
|
427
447
|
<div className="chat-container">
|
|
428
448
|
{messages.map((msg, idx) => {
|
|
449
|
+
const handleCopy = () => {
|
|
450
|
+
navigator.clipboard.writeText(msg.content);
|
|
451
|
+
setCopiedIndex(idx);
|
|
452
|
+
setTimeout(() => setCopiedIndex(null), 2000);
|
|
453
|
+
};
|
|
454
|
+
|
|
429
455
|
if (msg.role === 'user') {
|
|
430
456
|
return (
|
|
431
457
|
<div key={idx} className="message-wrapper user">
|
|
432
458
|
<div className="message-bubble">{msg.content}</div>
|
|
459
|
+
<button className="copy-btn" onClick={handleCopy} title="Copy message">
|
|
460
|
+
{copiedIndex === idx ? <Check size={14} color="#a3be8c" /> : <Copy size={14} />}
|
|
461
|
+
</button>
|
|
433
462
|
</div>
|
|
434
463
|
);
|
|
435
464
|
}
|
|
@@ -437,6 +466,9 @@ function App() {
|
|
|
437
466
|
return (
|
|
438
467
|
<div key={idx} className="message-wrapper agent">
|
|
439
468
|
<div className="message-bubble">{renderMessageContent(msg.content)}</div>
|
|
469
|
+
<button className="copy-btn" onClick={handleCopy} title="Copy message">
|
|
470
|
+
{copiedIndex === idx ? <Check size={14} color="#a3be8c" /> : <Copy size={14} />}
|
|
471
|
+
</button>
|
|
440
472
|
</div>
|
|
441
473
|
);
|
|
442
474
|
}
|
|
@@ -487,39 +519,64 @@ function App() {
|
|
|
487
519
|
<Send size={20} />
|
|
488
520
|
</button>
|
|
489
521
|
</form>
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
A2UI CONNECTED
|
|
522
|
+
<div className="trending-tokens">
|
|
523
|
+
<span>Trending Tokens:</span>
|
|
524
|
+
{trendingTokens.map((token, idx) => (
|
|
525
|
+
<span
|
|
526
|
+
key={idx}
|
|
527
|
+
className="token-tag"
|
|
528
|
+
onClick={() => setInput(`Tolong berikan analisis pasar terbaru untuk ${token}`)}
|
|
529
|
+
title={`Click to analyze ${token}`}
|
|
530
|
+
style={{ cursor: 'pointer' }}
|
|
531
|
+
>
|
|
532
|
+
{token}
|
|
533
|
+
</span>
|
|
534
|
+
))}
|
|
504
535
|
</div>
|
|
505
536
|
</div>
|
|
506
|
-
|
|
507
|
-
{activeWidget ? (
|
|
508
|
-
<div style={{ marginTop: '24px' }}>
|
|
509
|
-
{activeWidget}
|
|
510
|
-
</div>
|
|
511
|
-
) : (
|
|
512
|
-
<div className="canvas-empty">
|
|
513
|
-
<LayoutDashboard size={48} color="rgba(255,255,255,0.1)" />
|
|
514
|
-
<p>Awaiting agent interaction...</p>
|
|
515
|
-
<span style={{ fontSize: '0.8rem' }}>Ask the agent to check your balance or make a transfer.</span>
|
|
516
|
-
</div>
|
|
517
|
-
)}
|
|
518
|
-
<PendingTransactions />
|
|
519
537
|
</div>
|
|
520
538
|
</div>
|
|
521
539
|
)}
|
|
522
540
|
</main>
|
|
541
|
+
|
|
542
|
+
{editingSessionId && (
|
|
543
|
+
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}>
|
|
544
|
+
<div style={{ background: '#1e1e24', borderRadius: '16px', padding: '24px', width: '400px', boxShadow: '0 10px 25px rgba(0,0,0,0.5)', border: '1px solid rgba(255,255,255,0.1)' }}>
|
|
545
|
+
<h3 style={{ margin: '0 0 16px 0', fontSize: '1.2rem', color: '#e2e8f0', fontWeight: 500 }}>Rename this chat</h3>
|
|
546
|
+
<input
|
|
547
|
+
type="text"
|
|
548
|
+
value={editSessionTitle}
|
|
549
|
+
onChange={(e) => setEditSessionTitle(e.target.value)}
|
|
550
|
+
onKeyDown={(e) => {
|
|
551
|
+
if (e.key === 'Enter') renameSession(editingSessionId, editSessionTitle);
|
|
552
|
+
if (e.key === 'Escape') setEditingSessionId(null);
|
|
553
|
+
}}
|
|
554
|
+
autoFocus
|
|
555
|
+
style={{ width: '100%', background: 'transparent', color: '#fff', border: '1px solid #3f4451', borderRadius: '8px', padding: '14px 16px', fontSize: '0.95rem', outline: 'none', boxSizing: 'border-box' }}
|
|
556
|
+
onFocus={(e) => e.target.style.borderColor = '#88c0d0'}
|
|
557
|
+
onBlur={(e) => e.target.style.borderColor = '#3f4451'}
|
|
558
|
+
/>
|
|
559
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '24px' }}>
|
|
560
|
+
<button
|
|
561
|
+
onClick={() => setEditingSessionId(null)}
|
|
562
|
+
style={{ background: 'transparent', border: 'none', color: '#a0aec0', cursor: 'pointer', padding: '10px 20px', borderRadius: '24px', fontWeight: 500, fontSize: '0.9rem' }}
|
|
563
|
+
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
|
|
564
|
+
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
|
565
|
+
>
|
|
566
|
+
Cancel
|
|
567
|
+
</button>
|
|
568
|
+
<button
|
|
569
|
+
onClick={() => renameSession(editingSessionId, editSessionTitle)}
|
|
570
|
+
style={{ background: '#88c0d0', border: 'none', color: '#13131a', cursor: 'pointer', padding: '10px 20px', borderRadius: '24px', fontWeight: 600, fontSize: '0.9rem' }}
|
|
571
|
+
onMouseEnter={(e) => e.currentTarget.style.opacity = '0.9'}
|
|
572
|
+
onMouseLeave={(e) => e.currentTarget.style.opacity = '1'}
|
|
573
|
+
>
|
|
574
|
+
Rename
|
|
575
|
+
</button>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
)}
|
|
523
580
|
</>
|
|
524
581
|
);
|
|
525
582
|
}
|
|
@@ -5,6 +5,7 @@ import { Save } from 'lucide-react';
|
|
|
5
5
|
interface Config {
|
|
6
6
|
agent: { name: string; default_chain: string };
|
|
7
7
|
llm: { provider: string; model: string; temperature: number; api_keys?: string[] };
|
|
8
|
+
web3?: { rpc_urls?: Record<string, string | string[]> };
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
interface SettingsProps {
|
|
@@ -30,6 +31,9 @@ const Settings: React.FC<SettingsProps> = ({ config, onConfigChange }) => {
|
|
|
30
31
|
api_keys: Array.isArray(config.llm?.api_keys)
|
|
31
32
|
? config.llm.api_keys
|
|
32
33
|
: (config.llm?.api_keys ? [config.llm.api_keys as unknown as string] : [])
|
|
34
|
+
},
|
|
35
|
+
web3: {
|
|
36
|
+
rpc_urls: config.web3?.rpc_urls || {}
|
|
33
37
|
}
|
|
34
38
|
});
|
|
35
39
|
}
|
|
@@ -50,6 +54,30 @@ const Settings: React.FC<SettingsProps> = ({ config, onConfigChange }) => {
|
|
|
50
54
|
});
|
|
51
55
|
};
|
|
52
56
|
|
|
57
|
+
const handleWeb3Change = (chainName: string, value: string) => {
|
|
58
|
+
setFormData(prev => {
|
|
59
|
+
if (!prev) return prev;
|
|
60
|
+
const urls = value.split(',').map(s => s.trim()).filter(s => s);
|
|
61
|
+
const newRpcUrls = { ...(prev.web3?.rpc_urls || {}) };
|
|
62
|
+
|
|
63
|
+
if (urls.length === 0) {
|
|
64
|
+
delete newRpcUrls[chainName];
|
|
65
|
+
} else if (urls.length === 1) {
|
|
66
|
+
newRpcUrls[chainName] = urls[0];
|
|
67
|
+
} else {
|
|
68
|
+
newRpcUrls[chainName] = urls;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...prev,
|
|
73
|
+
web3: {
|
|
74
|
+
...prev.web3,
|
|
75
|
+
rpc_urls: newRpcUrls
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
53
81
|
const handleAddApiKey = () => {
|
|
54
82
|
setFormData(prev => {
|
|
55
83
|
if (!prev) return prev;
|
|
@@ -217,6 +245,33 @@ const Settings: React.FC<SettingsProps> = ({ config, onConfigChange }) => {
|
|
|
217
245
|
</div>
|
|
218
246
|
</div>
|
|
219
247
|
|
|
248
|
+
<div className="panel">
|
|
249
|
+
<div className="panel-header">
|
|
250
|
+
<h3>Web3 & RPC Settings</h3>
|
|
251
|
+
<p style={{ fontSize: '0.85rem', color: '#64748b', marginTop: '4px' }}>
|
|
252
|
+
Override the default public RPCs with your own Premium endpoints (Alchemy/Infura).
|
|
253
|
+
Separate multiple URLs with a comma for Fallback High-Availability.
|
|
254
|
+
</p>
|
|
255
|
+
</div>
|
|
256
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginTop: '16px' }}>
|
|
257
|
+
{['ethereum', 'base', 'bsc', 'arbitrum', 'optimism', 'sepolia'].map(chain => {
|
|
258
|
+
const rpcVal = formData.web3?.rpc_urls?.[chain];
|
|
259
|
+
const displayVal = Array.isArray(rpcVal) ? rpcVal.join(', ') : (rpcVal || '');
|
|
260
|
+
return (
|
|
261
|
+
<div key={chain} className="form-group">
|
|
262
|
+
<label style={{ textTransform: 'capitalize' }}>{chain} RPC</label>
|
|
263
|
+
<input
|
|
264
|
+
type="text"
|
|
265
|
+
placeholder="https://..."
|
|
266
|
+
value={displayVal}
|
|
267
|
+
onChange={(e) => handleWeb3Change(chain, e.target.value)}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
220
275
|
<div className="form-actions" style={{ justifyContent: 'flex-end', marginTop: '32px' }}>
|
|
221
276
|
<button className="btn-primary" onClick={handleSave} disabled={isSaving}>
|
|
222
277
|
<Save size={16} style={{ marginRight: '8px', display: 'inline' }} />
|
|
@@ -21,6 +21,13 @@ interface SkillDefinition {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const formatSkillName = (name: string) => {
|
|
25
|
+
return name
|
|
26
|
+
.split('_')
|
|
27
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
28
|
+
.join(' ');
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
const Skills: React.FC = () => {
|
|
25
32
|
const [skills, setSkills] = useState<SkillDefinition[]>([]);
|
|
26
33
|
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -55,7 +62,7 @@ const Skills: React.FC = () => {
|
|
|
55
62
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
56
63
|
<Compass size={18} color="#3b82f6" />
|
|
57
64
|
<h3 style={{ margin: 0, fontFamily: 'monospace', fontSize: '1.1rem', color: '#60a5fa' }}>
|
|
58
|
-
{skill.function.name}
|
|
65
|
+
{formatSkillName(skill.function.name)}
|
|
59
66
|
</h3>
|
|
60
67
|
</div>
|
|
61
68
|
<span className="badge" style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#4ade80' }}>Active</span>
|