@arpproject/vibearp-mcp 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +202 -0
- package/README.md +158 -0
- package/lib/dashboard/static/dashboard.css +1611 -0
- package/lib/dashboard/static/dashboard.js +2402 -0
- package/lib/dashboard/static/index.html +569 -0
- package/lib/server.js +70099 -0
- package/lib/server.js.map +7 -0
- package/lib/shutdown.js +345 -0
- package/lib/shutdown.js.map +7 -0
- package/node_modules/cedar-template-converter/LICENSE.md +202 -0
- package/node_modules/cedar-template-converter/dist/cedar-converter.d.ts +139 -0
- package/node_modules/cedar-template-converter/dist/cedar-converter.d.ts.map +1 -0
- package/node_modules/cedar-template-converter/dist/cedar-converter.js +765 -0
- package/node_modules/cedar-template-converter/dist/cedar-converter.js.map +1 -0
- package/node_modules/cedar-template-converter/package.json +33 -0
- package/package.json +53 -0
|
@@ -0,0 +1,2402 @@
|
|
|
1
|
+
// ******************************************************************************************
|
|
2
|
+
// Copyright (C) 2025-2026 SZTAKI, Department of Distributed Systems (https://dsd.sztaki.hu).
|
|
3
|
+
//
|
|
4
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
// ******************************************************************************************
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RO-Crate MCP Dashboard Client
|
|
9
|
+
* Handles API communication and UI updates
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// API base URL (same host)
|
|
13
|
+
const API_BASE = window.location.origin;
|
|
14
|
+
|
|
15
|
+
let dashboardLocale = 'en';
|
|
16
|
+
const MASKED_SECRET_PLACEHOLDER = '••••••••';
|
|
17
|
+
|
|
18
|
+
const huTranslations = {
|
|
19
|
+
'RO-Crate MCP Dashboard': 'RO-Crate MCP-vezérlőpult',
|
|
20
|
+
'All time': 'Teljes időszak',
|
|
21
|
+
'15 minutes': '15 perc',
|
|
22
|
+
'1 hour': '1 óra',
|
|
23
|
+
'24 hours': '24 óra',
|
|
24
|
+
'Refresh': 'Frissítés',
|
|
25
|
+
'Metadata Profiles': 'Metadataprofilok',
|
|
26
|
+
'Settings': 'Beállítások',
|
|
27
|
+
'Shut down MCP': 'MCP leállítása',
|
|
28
|
+
'Shut down the RO-Crate MCP server? Active MCP connections will be closed.': 'Leállítja a RO-Crate MCP-szervert? Az aktív MCP-kapcsolatok bezáródnak.',
|
|
29
|
+
'Shutting down MCP...': 'MCP leállítása…',
|
|
30
|
+
'MCP shutdown requested.': 'Az MCP leállítási kérése elküldve.',
|
|
31
|
+
'Failed to shut down MCP: {0}': 'Nem sikerült leállítani az MCP-t: {0}',
|
|
32
|
+
'Total Calls': 'Összes hívás',
|
|
33
|
+
'Error Rate': 'Hibaarány',
|
|
34
|
+
'Active Sessions': 'Aktív munkamenetek',
|
|
35
|
+
'P95 Latency': 'P95 késleltetés',
|
|
36
|
+
'Avg Latency': 'Átlagos késleltetés',
|
|
37
|
+
'Uptime': 'Üzemidő',
|
|
38
|
+
'Tool Statistics': 'Eszközstatisztika',
|
|
39
|
+
'Tool': 'Eszköz',
|
|
40
|
+
'Calls': 'Hívások',
|
|
41
|
+
'Fail %': 'Hiba %',
|
|
42
|
+
'Last Call': 'Utolsó hívás',
|
|
43
|
+
'Loading...': 'Betöltés…',
|
|
44
|
+
'Recent Errors': 'Legutóbbi hibák',
|
|
45
|
+
'Test Tavily Search': 'Tavily-keresés tesztelése',
|
|
46
|
+
'Search Query': 'Keresőkifejezés',
|
|
47
|
+
'Enter a test query to verify Tavily search functionality': 'Adjon meg egy tesztkifejezést a Tavily-keresés ellenőrzéséhez',
|
|
48
|
+
'Max Results': 'Találatok maximális száma',
|
|
49
|
+
'Number of results to return (1-10)': 'A visszaadott találatok száma (1–10)',
|
|
50
|
+
'Search Depth': 'Keresési mélység',
|
|
51
|
+
'Basic or advanced search': 'Alap- vagy speciális keresés',
|
|
52
|
+
'Basic': 'Alap',
|
|
53
|
+
'Advanced': 'Speciális',
|
|
54
|
+
'The test uses the currently saved Tavily configuration. Save Settings first if you changed the API key.': 'A teszt a jelenleg mentett Tavily-beállításokat használja. Ha módosította az API-kulcsot, először mentse a beállításokat.',
|
|
55
|
+
'Test Search': 'Keresés tesztelése',
|
|
56
|
+
'Clear Results': 'Találatok törlése',
|
|
57
|
+
'Sessions': 'Munkamenetek',
|
|
58
|
+
'Session ID': 'Munkamenet-azonosító',
|
|
59
|
+
'Started': 'Elindítva',
|
|
60
|
+
'Last Activity': 'Utolsó tevékenység',
|
|
61
|
+
'Mode': 'Mód',
|
|
62
|
+
'Requests': 'Kérések',
|
|
63
|
+
'Errors': 'Hibák',
|
|
64
|
+
'Status': 'Állapot',
|
|
65
|
+
'Actions': 'Műveletek',
|
|
66
|
+
'Dependency Usage': 'Függőségek használata',
|
|
67
|
+
'Dependency': 'Függőség',
|
|
68
|
+
'Success Rate': 'Sikerességi arány',
|
|
69
|
+
'Session Details': 'Munkamenet részletei',
|
|
70
|
+
'Tool Call Details': 'Eszközhívás részletei',
|
|
71
|
+
'Dashboard Settings': 'Vezérlőpult beállításai',
|
|
72
|
+
'Telemetry': 'Telemetria',
|
|
73
|
+
'Detailed Tool Call Logging': 'Részletes eszközhívás-naplózás',
|
|
74
|
+
'Store tool parameters and results (uses more memory)': 'Az eszközparaméterek és eredmények tárolása (több memóriát használ)',
|
|
75
|
+
'Data Retention': 'Adatmegőrzés',
|
|
76
|
+
'How long to keep telemetry data (hours)': 'A telemetriai adatok megőrzési ideje (óra)',
|
|
77
|
+
'hours': 'óra',
|
|
78
|
+
'Dataverse Upload Tool': 'Dataverse-feltöltő eszköz',
|
|
79
|
+
'Environment variables are used by default. Dashboard values are saved in the RocKIT storage root (default: ~/.rockit/rocrate-mcp-settings.json) and override them for this and future MCP processes.': 'Alapértelmezés szerint a környezeti változók használatosak. A vezérlőpult értékei a RocKIT tárolási gyökerében (alapértelmezés: ~/.rockit/rocrate-mcp-settings.json) kerülnek mentésre, és felülírják ezeket a jelenlegi és a későbbi MCP-folyamatokban.',
|
|
80
|
+
'Select “Use environment/default value” to clear a dashboard override.': 'A vezérlőpult felülírásának törléséhez válassza a „Környezeti/alapértelmezett érték használata” lehetőséget.',
|
|
81
|
+
'Saved API-key values stay masked; click the eye to view them after reload.': 'A mentett API-kulcsok rejtve maradnak; újratöltés után a megtekintésükhöz kattintson a szem ikonra.',
|
|
82
|
+
'Use environment/default value': 'Környezeti/alapértelmezett érték használata',
|
|
83
|
+
'Use environment value': 'Környezeti érték használata',
|
|
84
|
+
'Enter a new dashboard override': 'Új vezérlőpult-felülírás megadása',
|
|
85
|
+
'Show API key': 'API-kulcs megjelenítése',
|
|
86
|
+
'Hide API key': 'API-kulcs elrejtése',
|
|
87
|
+
'Failed to reveal API key: {0}': 'Nem sikerült megjeleníteni az API-kulcsot: {0}',
|
|
88
|
+
'Web Search Tool': 'Webes keresőeszköz',
|
|
89
|
+
'Configure the Tavily API key used by the': 'A Tavily API-kulcs beállítása, amelyet a',
|
|
90
|
+
'tool.': 'eszköz.',
|
|
91
|
+
'Saved values stay masked; click the eye to view them.': 'A mentett értékek rejtve maradnak; megtekintésükhöz kattintson a szem ikonra.',
|
|
92
|
+
'Dashboard override active': 'Aktív vezérlőpult-felülírás',
|
|
93
|
+
'Using environment variable: {0}': 'Környezeti változó használata: {0}',
|
|
94
|
+
'Using built-in default': 'Beépített alapérték használata',
|
|
95
|
+
'Not configured': 'Nincs beállítva',
|
|
96
|
+
'Keep RO-Crate ZIPs': 'RO-Crate ZIP-fájlok megtartása',
|
|
97
|
+
'Keep generated ZIP files after successful Dataverse uploads': 'A létrehozott ZIP-fájlok megtartása sikeres Dataverse-feltöltés után',
|
|
98
|
+
'Save Settings': 'Beállítások mentése',
|
|
99
|
+
'Cancel': 'Mégse',
|
|
100
|
+
'Name': 'Név',
|
|
101
|
+
'Reload': 'Újratöltés',
|
|
102
|
+
'CEDAR Schema Profiles': 'CEDAR-sémaprofilok',
|
|
103
|
+
'Browse remote CEDAR repositories and manage locally converted recrate profiles.': 'Távoli CEDAR-tárolók böngészése és a helyben átalakított ReCrate-profilok kezelése.',
|
|
104
|
+
'Browse Remote': 'Távoli sémák böngészése',
|
|
105
|
+
'Storage': 'Tárolási hely',
|
|
106
|
+
'Version': 'Verzió',
|
|
107
|
+
'Source': 'Forrás',
|
|
108
|
+
'Conforms To': 'Megfelel ennek',
|
|
109
|
+
'Ref (@id)': 'Hivatkozás (@id)',
|
|
110
|
+
'Import URL': 'Importálás URL-ről',
|
|
111
|
+
'Select Remote Provider': 'Távoli szolgáltató kiválasztása',
|
|
112
|
+
'Choose Repository': 'Tároló kiválasztása',
|
|
113
|
+
'Select a remote provider to browse schemas.': 'Válasszon távoli szolgáltatót a sémák böngészéséhez.',
|
|
114
|
+
'Manage Providers...': 'Szolgáltatók kezelése…',
|
|
115
|
+
'Manage Remote Providers': 'Távoli szolgáltatók kezelése',
|
|
116
|
+
'Configured Providers': 'Beállított szolgáltatók',
|
|
117
|
+
'Manage connections to remote schema repositories.': 'Távoli sématárolók kapcsolatainak kezelése.',
|
|
118
|
+
'Add Provider': 'Szolgáltató hozzáadása',
|
|
119
|
+
'Dataverse proxy (read-only)': 'Dataverse proxy (csak olvasható)',
|
|
120
|
+
'CEDAR API key': 'CEDAR API-kulcs',
|
|
121
|
+
'Save Provider': 'Szolgáltató mentése',
|
|
122
|
+
'Close': 'Bezárás',
|
|
123
|
+
'Search': 'Keresés',
|
|
124
|
+
'Expand All': 'Összes kibontása',
|
|
125
|
+
'Collapse All': 'Összes összecsukása',
|
|
126
|
+
'Locate in Tree': 'Megkeresés a fában',
|
|
127
|
+
'Deselect': 'Kijelölés megszüntetése',
|
|
128
|
+
'Select a template to import...': 'Válasszon importálandó sémát…',
|
|
129
|
+
'Add': 'Hozzáadás',
|
|
130
|
+
'Previous page': 'Előző oldal',
|
|
131
|
+
'Next page': 'Következő oldal',
|
|
132
|
+
'Rows per page': 'Sorok oldalanként',
|
|
133
|
+
'10 / page': '10 / oldal',
|
|
134
|
+
'25 / page': '25 / oldal',
|
|
135
|
+
'50 / page': '50 / oldal',
|
|
136
|
+
'100 / page': '100 / oldal',
|
|
137
|
+
'e.g., RO-Crate metadata specification': 'pl. RO-Crate metaadat-specifikáció',
|
|
138
|
+
'Provider id': 'Szolgáltató azonosítója',
|
|
139
|
+
'Display name': 'Megjelenítendő név',
|
|
140
|
+
'API key (stored in keytar when available)': 'API-kulcs (ha elérhető, a keytar tárolja)',
|
|
141
|
+
'Search folders and templates': 'Mappák és sablonok keresése',
|
|
142
|
+
'No tool calls in this time range': 'Ebben az időszakban nem volt eszközhívás',
|
|
143
|
+
'Never': 'Soha',
|
|
144
|
+
'Failed to load: {0}': 'Nem sikerült betölteni: {0}',
|
|
145
|
+
'No errors recorded': 'Nincsenek rögzített hibák',
|
|
146
|
+
'Failed to load errors: {0}': 'Nem sikerült betölteni a hibákat: {0}',
|
|
147
|
+
'No sessions recorded': 'Nincsenek rögzített munkamenetek',
|
|
148
|
+
'Active': 'Aktív',
|
|
149
|
+
'Inactive': 'Inaktív',
|
|
150
|
+
'View': 'Megtekintés',
|
|
151
|
+
'Failed to load sessions: {0}': 'Nem sikerült betölteni a munkameneteket: {0}',
|
|
152
|
+
'Session: {0}…': 'Munkamenet: {0}…',
|
|
153
|
+
'Session Info': 'Munkamenet adatai',
|
|
154
|
+
'Transport': 'Kapcsolat',
|
|
155
|
+
'Call Statistics': 'Hívási statisztika',
|
|
156
|
+
'Successful': 'Sikeres',
|
|
157
|
+
'Failed': 'Sikertelen',
|
|
158
|
+
'Timeout': 'Időtúllépés',
|
|
159
|
+
'Tool Calls': 'Eszközhívások',
|
|
160
|
+
'Duration': 'Időtartam',
|
|
161
|
+
'Time': 'Időpont',
|
|
162
|
+
'View Details': 'Részletek',
|
|
163
|
+
'No tool calls in this session': 'Ebben a munkamenetben nem volt eszközhívás',
|
|
164
|
+
'Failed to load session details: {0}': 'Nem sikerült betölteni a munkamenet részleteit: {0}',
|
|
165
|
+
'Call Info': 'Hívás adatai',
|
|
166
|
+
'In progress': 'Folyamatban',
|
|
167
|
+
'Finished': 'Befejezve',
|
|
168
|
+
'Artifacts': 'Melléktermékek',
|
|
169
|
+
'Artifact': 'Melléktermék',
|
|
170
|
+
'Parameters': 'Paraméterek',
|
|
171
|
+
'Parameters not available (enable ROCRATE_DASHBOARD_DETAILED_LOGGING=true to capture parameters)': 'A paraméterek nem érhetők el (a rögzítésükhöz engedélyezze a ROCRATE_DASHBOARD_DETAILED_LOGGING=true beállítást)',
|
|
172
|
+
'Result': 'Eredmény',
|
|
173
|
+
'Error': 'Hiba',
|
|
174
|
+
'Unknown': 'Ismeretlen',
|
|
175
|
+
'No message': 'Nincs üzenet',
|
|
176
|
+
'HTTP Communication': 'HTTP-kommunikáció',
|
|
177
|
+
'Timestamp': 'Időbélyeg',
|
|
178
|
+
'Request': 'Kérés',
|
|
179
|
+
'Response': 'Válasz',
|
|
180
|
+
'Request Headers': 'Kérés fejlécei',
|
|
181
|
+
'Request Body': 'Kérés törzse',
|
|
182
|
+
'Response Headers': 'Válasz fejlécei',
|
|
183
|
+
'Response Body': 'Válasz törzse',
|
|
184
|
+
'Metadata': 'Metaadatok',
|
|
185
|
+
'Args Size': 'Argumentumok mérete',
|
|
186
|
+
'Result Size': 'Eredmény mérete',
|
|
187
|
+
'Failed to load tool call details: {0}': 'Nem sikerült betölteni az eszközhívás részleteit: {0}',
|
|
188
|
+
'No headers logged': 'Nincsenek naplózott fejlécek',
|
|
189
|
+
'No body logged': 'Nincs naplózott törzs',
|
|
190
|
+
'Failed to load settings: {0}': 'Nem sikerült betölteni a beállításokat: {0}',
|
|
191
|
+
'(not set)': '(nincs beállítva)',
|
|
192
|
+
'Set from DATAVERSE_BASE_URL': 'A DATAVERSE_BASE_URL alapján beállítva',
|
|
193
|
+
'Using upload tool default': 'A feltöltőeszköz alapértékét használja',
|
|
194
|
+
'Set from DATAVERSE_API_KEY': 'A DATAVERSE_API_KEY alapján beállítva',
|
|
195
|
+
'DATAVERSE_API_KEY is not set': 'A DATAVERSE_API_KEY nincs beállítva',
|
|
196
|
+
'Retention hours must be between 1 and 168': 'A megőrzési időnek 1 és 168 óra között kell lennie',
|
|
197
|
+
'Settings saved successfully!': 'A beállítások mentése sikerült!',
|
|
198
|
+
'Failed to save settings: {0}': 'Nem sikerült menteni a beállításokat: {0}',
|
|
199
|
+
'Edit': 'Szerkesztés',
|
|
200
|
+
'Delete': 'Törlés',
|
|
201
|
+
'Failed to load metadata profiles: {0}': 'Nem sikerült betölteni a metadataprofilokat: {0}',
|
|
202
|
+
'No metadata profiles imported': 'Nincsenek importált metadataprofilok',
|
|
203
|
+
'Ready': 'Kész',
|
|
204
|
+
'No CEDAR providers are configured': 'Nincsenek beállított CEDAR-szolgáltatók',
|
|
205
|
+
'No CEDAR providers are configured.': 'Nincsenek beállított CEDAR-szolgáltatók.',
|
|
206
|
+
'Provider load failed': 'A szolgáltatók betöltése sikertelen',
|
|
207
|
+
'Failed to load CEDAR providers: {0}': 'Nem sikerült betölteni a CEDAR-szolgáltatókat: {0}',
|
|
208
|
+
'Dataverse proxy': 'Dataverse proxy',
|
|
209
|
+
'proxy': 'proxy',
|
|
210
|
+
'key configured': 'kulcs beállítva',
|
|
211
|
+
'key': 'kulcs',
|
|
212
|
+
'API key': 'API-kulcs',
|
|
213
|
+
'Browse {0}': '{0} böngészése',
|
|
214
|
+
'Remote': 'Távoli tár',
|
|
215
|
+
'Loading repository...': 'Tároló betöltése…',
|
|
216
|
+
'Loading providers...': 'Szolgáltatók betöltése…',
|
|
217
|
+
'Failed to load repository: {0}': 'Nem sikerült betölteni a tárolót: {0}',
|
|
218
|
+
'No results found.': 'Nincs találat.',
|
|
219
|
+
'No templates found.': 'Nem találhatók sablonok.',
|
|
220
|
+
'Imported': 'Importálva',
|
|
221
|
+
'Remote metadata profile imported.': 'A távoli metadataprofil importálása sikerült.',
|
|
222
|
+
'Failed to import remote profile: {0}': 'Nem sikerült importálni a távoli profilt: {0}',
|
|
223
|
+
'Provider id, title, base URL and domain base are required.': 'A szolgáltató azonosítója, neve, alap-URL-je és tartományalapja kötelező.',
|
|
224
|
+
'Provider saved.': 'A szolgáltató mentése sikerült.',
|
|
225
|
+
'Failed to save provider: {0}': 'Nem sikerült menteni a szolgáltatót: {0}',
|
|
226
|
+
"Delete remote provider '{0}'?": "Törli a(z) „{0}” távoli szolgáltatót?",
|
|
227
|
+
'Provider deleted.': 'A szolgáltató törlése sikerült.',
|
|
228
|
+
'Failed to delete provider: {0}': 'Nem sikerült törölni a szolgáltatót: {0}',
|
|
229
|
+
'Profile URL is required.': 'A profil URL-címe kötelező.',
|
|
230
|
+
'Metadata profile imported.': 'A metadataprofil importálása sikerült.',
|
|
231
|
+
'Failed to import profile: {0}': 'Nem sikerült importálni a profilt: {0}',
|
|
232
|
+
"Delete metadata profile '{0}' and its files?": "Törli a(z) „{0}” metadataprofilt és a fájljait?",
|
|
233
|
+
'Metadata profile deleted.': 'A metadataprofil törlése sikerült.',
|
|
234
|
+
'Failed to delete metadata profile: {0}': 'Nem sikerült törölni a metadataprofilt: {0}',
|
|
235
|
+
'id, displayName, schemaUrl and matchesUrls are required.': 'Az id, displayName, schemaUrl és matchesUrls mezők kötelezők.',
|
|
236
|
+
'Schema updated.': 'A séma frissítése sikerült.',
|
|
237
|
+
'Schema added.': 'A séma hozzáadása sikerült.',
|
|
238
|
+
'Failed to save schema: {0}': 'Nem sikerült menteni a sémát: {0}',
|
|
239
|
+
'Schema not found: {0}': 'A séma nem található: {0}',
|
|
240
|
+
'Failed to load schema for edit: {0}': 'Nem sikerült betölteni a sémát szerkesztésre: {0}',
|
|
241
|
+
"Delete schema '{0}'?": "Törli a(z) „{0}” sémát?",
|
|
242
|
+
'Schema deleted.': 'A séma törlése sikerült.',
|
|
243
|
+
'Failed to delete schema: {0}': 'Nem sikerült törölni a sémát: {0}',
|
|
244
|
+
'No dependency usage recorded': 'Nincs rögzített függőséghasználat',
|
|
245
|
+
'Please enter a search query': 'Adjon meg egy keresőkifejezést',
|
|
246
|
+
'Testing...': 'Tesztelés…',
|
|
247
|
+
'Running test search...': 'Tesztkeresés futtatása…',
|
|
248
|
+
'Search successful!': 'A keresés sikerült!',
|
|
249
|
+
'Query': 'Lekérdezés',
|
|
250
|
+
'Latency': 'Késleltetés',
|
|
251
|
+
'Answer': 'Válasz',
|
|
252
|
+
'Results': 'Találatok',
|
|
253
|
+
'Untitled': 'Névtelen',
|
|
254
|
+
'Score': 'Pontszám',
|
|
255
|
+
'Search failed': 'A keresés sikertelen',
|
|
256
|
+
'TAVILY_API_KEY environment variable is not set on the server.': 'A TAVILY_API_KEY környezeti változó nincs beállítva a szerveren.',
|
|
257
|
+
'Please set the environment variable and restart the server.': 'Állítsa be a környezeti változót, majd indítsa újra a szervert.',
|
|
258
|
+
'Unknown error': 'Ismeretlen hiba',
|
|
259
|
+
'Request failed: {0}': 'A kérés sikertelen: {0}',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
function t(message, ...values) {
|
|
263
|
+
let result = dashboardLocale === 'hu' ? (huTranslations[message] || message) : message;
|
|
264
|
+
values.forEach((value, index) => {
|
|
265
|
+
result = result.replace(`{${index}}`, String(value));
|
|
266
|
+
});
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function applyStaticTranslations() {
|
|
271
|
+
document.documentElement.lang = dashboardLocale;
|
|
272
|
+
document.title = t('RO-Crate MCP Dashboard');
|
|
273
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
274
|
+
let node;
|
|
275
|
+
while ((node = walker.nextNode())) {
|
|
276
|
+
const trimmed = node.nodeValue.trim();
|
|
277
|
+
if (trimmed && huTranslations[trimmed]) {
|
|
278
|
+
node.nodeValue = node.nodeValue.replace(trimmed, t(trimmed));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
document.querySelectorAll('[placeholder], [title], [aria-label]').forEach((element) => {
|
|
282
|
+
['placeholder', 'title', 'aria-label'].forEach((attribute) => {
|
|
283
|
+
const value = element.getAttribute(attribute);
|
|
284
|
+
if (value && huTranslations[value]) element.setAttribute(attribute, t(value));
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// State
|
|
290
|
+
let currentMinutes = 15;
|
|
291
|
+
let autoRefreshInterval = null;
|
|
292
|
+
let metadataProfileProviders = [];
|
|
293
|
+
let currentMetadataProfileProviderId = '';
|
|
294
|
+
let metadataProfilesState = {
|
|
295
|
+
profiles: [],
|
|
296
|
+
page: 1,
|
|
297
|
+
pageSize: 10,
|
|
298
|
+
};
|
|
299
|
+
let cedarBrowserState = {
|
|
300
|
+
providerId: '',
|
|
301
|
+
selectedTemplateId: '',
|
|
302
|
+
selectedTemplateName: '',
|
|
303
|
+
nodes: [],
|
|
304
|
+
expanded: new Set(),
|
|
305
|
+
loading: new Set(),
|
|
306
|
+
query: '',
|
|
307
|
+
};
|
|
308
|
+
let serviceSettingsState = {
|
|
309
|
+
dataverseBaseUrl: '',
|
|
310
|
+
dataverseBaseUrlSource: 'default',
|
|
311
|
+
dataverseApiKeySource: 'unset',
|
|
312
|
+
tavilyApiKeySource: 'unset',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// DOM Elements
|
|
316
|
+
const elements = {
|
|
317
|
+
totalCalls: document.getElementById('totalCalls'),
|
|
318
|
+
errorRate: document.getElementById('errorRate'),
|
|
319
|
+
activeSessions: document.getElementById('activeSessions'),
|
|
320
|
+
p95Latency: document.getElementById('p95Latency'),
|
|
321
|
+
avgLatency: document.getElementById('avgLatency'),
|
|
322
|
+
uptime: document.getElementById('uptime'),
|
|
323
|
+
toolsTableBody: document.querySelector('#toolsTable tbody'),
|
|
324
|
+
errorsList: document.getElementById('errorsList'),
|
|
325
|
+
sessionsTableBody: document.querySelector('#sessionsTable tbody'),
|
|
326
|
+
dependenciesTableBody: document.querySelector('#dependenciesTable tbody'),
|
|
327
|
+
timeRange: document.getElementById('timeRange'),
|
|
328
|
+
refreshBtn: document.getElementById('refreshBtn'),
|
|
329
|
+
metadataProfilesBtn: document.getElementById('metadataProfilesBtn'),
|
|
330
|
+
settingsBtn: document.getElementById('settingsBtn'),
|
|
331
|
+
shutdownBtn: document.getElementById('shutdownBtn'),
|
|
332
|
+
errorBanner: document.getElementById('errorBanner'),
|
|
333
|
+
sessionModal: document.getElementById('sessionModal'),
|
|
334
|
+
sessionModalTitle: document.getElementById('sessionModalTitle'),
|
|
335
|
+
sessionModalBody: document.getElementById('sessionModalBody'),
|
|
336
|
+
closeSessionModal: document.getElementById('closeSessionModal'),
|
|
337
|
+
toolCallModal: document.getElementById('toolCallModal'),
|
|
338
|
+
toolCallModalTitle: document.getElementById('toolCallModalTitle'),
|
|
339
|
+
toolCallModalBody: document.getElementById('toolCallModalBody'),
|
|
340
|
+
closeToolCallModal: document.getElementById('closeToolCallModal'),
|
|
341
|
+
settingsModal: document.getElementById('settingsModal'),
|
|
342
|
+
settingsForm: document.getElementById('settingsForm'),
|
|
343
|
+
tavilyTestModal: document.getElementById('tavilyTestModal'),
|
|
344
|
+
openTavilyTestBtn: document.getElementById('openTavilyTestBtn'),
|
|
345
|
+
closeTavilyTestModal: document.getElementById('closeTavilyTestModal'),
|
|
346
|
+
detailedLoggingToggle: document.getElementById('detailedLoggingToggle'),
|
|
347
|
+
keepDataverseUploadZipsToggle: document.getElementById('keepDataverseUploadZipsToggle'),
|
|
348
|
+
retentionHoursInput: document.getElementById('retentionHoursInput'),
|
|
349
|
+
cancelSettingsBtn: document.getElementById('cancelSettingsBtn'),
|
|
350
|
+
closeSettingsModal: document.getElementById('closeSettingsModal'),
|
|
351
|
+
metadataProfilesModal: document.getElementById('metadataProfilesModal'),
|
|
352
|
+
closeMetadataProfilesModal: document.getElementById('closeMetadataProfilesModal'),
|
|
353
|
+
settingsMessage: document.getElementById('settingsMessage'),
|
|
354
|
+
dataverseBaseUrlInput: document.getElementById('dataverseBaseUrlInput'),
|
|
355
|
+
dataverseBaseUrlUseEnv: document.getElementById('dataverseBaseUrlUseEnv'),
|
|
356
|
+
dataverseBaseUrlSource: document.getElementById('dataverseBaseUrlSource'),
|
|
357
|
+
dataverseApiKeyInput: document.getElementById('dataverseApiKeyInput'),
|
|
358
|
+
dataverseApiKeyVisibilityBtn: document.getElementById('dataverseApiKeyVisibilityBtn'),
|
|
359
|
+
dataverseApiKeyUseEnv: document.getElementById('dataverseApiKeyUseEnv'),
|
|
360
|
+
dataverseApiKeySource: document.getElementById('dataverseApiKeySource'),
|
|
361
|
+
tavilyApiKeyInput: document.getElementById('tavilyApiKeyInput'),
|
|
362
|
+
tavilyApiKeyVisibilityBtn: document.getElementById('tavilyApiKeyVisibilityBtn'),
|
|
363
|
+
tavilyApiKeyUseEnv: document.getElementById('tavilyApiKeyUseEnv'),
|
|
364
|
+
tavilyApiKeySource: document.getElementById('tavilyApiKeySource'),
|
|
365
|
+
metadataProfileStorageValue: document.getElementById('metadataProfileStorageValue'),
|
|
366
|
+
metadataProfilesTableBody: document.querySelector('#metadataProfilesTable tbody'),
|
|
367
|
+
metadataProfilesPagination: document.getElementById('metadataProfilesPagination'),
|
|
368
|
+
metadataProfilesPrevPageBtn: document.getElementById('metadataProfilesPrevPageBtn'),
|
|
369
|
+
metadataProfilesNextPageBtn: document.getElementById('metadataProfilesNextPageBtn'),
|
|
370
|
+
metadataProfilesPageValue: document.getElementById('metadataProfilesPageValue'),
|
|
371
|
+
metadataProfilesPageSizeSelect: document.getElementById('metadataProfilesPageSizeSelect'),
|
|
372
|
+
metadataProfileUrlInput: document.getElementById('metadataProfileUrlInput'),
|
|
373
|
+
importMetadataProfileBtn: document.getElementById('importMetadataProfileBtn'),
|
|
374
|
+
browseMetadataProfilesBtn: document.getElementById('browseMetadataProfilesBtn'),
|
|
375
|
+
manageMetadataProvidersBtn: document.getElementById('manageMetadataProvidersBtn'),
|
|
376
|
+
reloadMetadataProfilesBtn: document.getElementById('reloadMetadataProfilesBtn'),
|
|
377
|
+
metadataProfilesMessage: document.getElementById('metadataProfilesMessage'),
|
|
378
|
+
remoteProviderSelectModal: document.getElementById('remoteProviderSelectModal'),
|
|
379
|
+
closeRemoteProviderSelectModal: document.getElementById('closeRemoteProviderSelectModal'),
|
|
380
|
+
cancelRemoteProviderSelectBtn: document.getElementById('cancelRemoteProviderSelectBtn'),
|
|
381
|
+
openManageProvidersFromSelectBtn: document.getElementById('openManageProvidersFromSelectBtn'),
|
|
382
|
+
remoteProviderSelectList: document.getElementById('remoteProviderSelectList'),
|
|
383
|
+
manageRemoteProvidersModal: document.getElementById('manageRemoteProvidersModal'),
|
|
384
|
+
closeManageRemoteProvidersModal: document.getElementById('closeManageRemoteProvidersModal'),
|
|
385
|
+
closeManageRemoteProvidersBtn: document.getElementById('closeManageRemoteProvidersBtn'),
|
|
386
|
+
addRemoteProviderBtn: document.getElementById('addRemoteProviderBtn'),
|
|
387
|
+
remoteProviderManageList: document.getElementById('remoteProviderManageList'),
|
|
388
|
+
remoteProviderForm: document.getElementById('remoteProviderForm'),
|
|
389
|
+
remoteProviderOriginalId: document.getElementById('remoteProviderOriginalId'),
|
|
390
|
+
remoteProviderIdInput: document.getElementById('remoteProviderIdInput'),
|
|
391
|
+
remoteProviderTitleInput: document.getElementById('remoteProviderTitleInput'),
|
|
392
|
+
remoteProviderBaseUrlInput: document.getElementById('remoteProviderBaseUrlInput'),
|
|
393
|
+
remoteProviderDomainInput: document.getElementById('remoteProviderDomainInput'),
|
|
394
|
+
remoteProviderAccessModeInput: document.getElementById('remoteProviderAccessModeInput'),
|
|
395
|
+
remoteProviderProxyBaseUrlInput: document.getElementById('remoteProviderProxyBaseUrlInput'),
|
|
396
|
+
remoteProviderApiKeyInput: document.getElementById('remoteProviderApiKeyInput'),
|
|
397
|
+
remoteProviderApiKeyControl: document.getElementById('remoteProviderApiKeyControl'),
|
|
398
|
+
remoteProviderApiKeyVisibilityBtn: document.getElementById('remoteProviderApiKeyVisibilityBtn'),
|
|
399
|
+
cancelRemoteProviderFormBtn: document.getElementById('cancelRemoteProviderFormBtn'),
|
|
400
|
+
remoteProviderManageMessage: document.getElementById('remoteProviderManageMessage'),
|
|
401
|
+
cedarBrowserModal: document.getElementById('cedarBrowserModal'),
|
|
402
|
+
cedarBrowserTitle: document.getElementById('cedarBrowserTitle'),
|
|
403
|
+
closeCedarBrowserModal: document.getElementById('closeCedarBrowserModal'),
|
|
404
|
+
cedarSearchToggleBtn: document.getElementById('cedarSearchToggleBtn'),
|
|
405
|
+
cedarSearchInput: document.getElementById('cedarSearchInput'),
|
|
406
|
+
cedarExpandAllBtn: document.getElementById('cedarExpandAllBtn'),
|
|
407
|
+
cedarCollapseAllBtn: document.getElementById('cedarCollapseAllBtn'),
|
|
408
|
+
cedarBrowserTree: document.getElementById('cedarBrowserTree'),
|
|
409
|
+
cedarLocateSelectedBtn: document.getElementById('cedarLocateSelectedBtn'),
|
|
410
|
+
cedarClearSelectionBtn: document.getElementById('cedarClearSelectionBtn'),
|
|
411
|
+
cedarBrowserSelectionText: document.getElementById('cedarBrowserSelectionText'),
|
|
412
|
+
cancelCedarBrowserBtn: document.getElementById('cancelCedarBrowserBtn'),
|
|
413
|
+
addCedarTemplateBtn: document.getElementById('addCedarTemplateBtn'),
|
|
414
|
+
tavilyTestForm: document.getElementById('tavilyTestForm'),
|
|
415
|
+
tavilyQueryInput: document.getElementById('tavilyQueryInput'),
|
|
416
|
+
tavilyMaxResults: document.getElementById('tavilyMaxResults'),
|
|
417
|
+
tavilySearchDepth: document.getElementById('tavilySearchDepth'),
|
|
418
|
+
tavilyTestBtn: document.getElementById('tavilyTestBtn'),
|
|
419
|
+
tavilyClearBtn: document.getElementById('tavilyClearBtn'),
|
|
420
|
+
tavilyTestResult: document.getElementById('tavilyTestResult'),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// API Helpers
|
|
424
|
+
async function fetchAPI(endpoint, options = {}) {
|
|
425
|
+
const url = `${API_BASE}${endpoint}`;
|
|
426
|
+
const response = await fetch(url, {
|
|
427
|
+
...options,
|
|
428
|
+
headers: {
|
|
429
|
+
'Accept': 'application/json',
|
|
430
|
+
...options.headers,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return response.json();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function shutdownMcp() {
|
|
442
|
+
if (!window.confirm(t('Shut down the RO-Crate MCP server? Active MCP connections will be closed.'))) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const button = elements.shutdownBtn;
|
|
447
|
+
if (!button) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const originalContent = button.innerHTML;
|
|
452
|
+
button.disabled = true;
|
|
453
|
+
button.innerHTML = `<span class="icon">…</span> ${t('Shutting down MCP...')}`;
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
await fetchAPI('/daemon/shutdown', { method: 'POST' });
|
|
457
|
+
button.innerHTML = `<span class="icon">✓</span> ${t('MCP shutdown requested.')}`;
|
|
458
|
+
} catch (err) {
|
|
459
|
+
button.disabled = false;
|
|
460
|
+
button.innerHTML = originalContent;
|
|
461
|
+
showError(t('Failed to shut down MCP: {0}', err.message));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function postAPI(endpoint, data) {
|
|
466
|
+
const url = `${API_BASE}${endpoint}`;
|
|
467
|
+
const response = await fetch(url, {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: {
|
|
470
|
+
'Accept': 'application/json',
|
|
471
|
+
'Content-Type': 'application/json',
|
|
472
|
+
},
|
|
473
|
+
body: JSON.stringify(data),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
if (!response.ok) {
|
|
477
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return response.json();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function putAPI(endpoint, data) {
|
|
484
|
+
const url = `${API_BASE}${endpoint}`;
|
|
485
|
+
const response = await fetch(url, {
|
|
486
|
+
method: 'PUT',
|
|
487
|
+
headers: {
|
|
488
|
+
'Accept': 'application/json',
|
|
489
|
+
'Content-Type': 'application/json',
|
|
490
|
+
},
|
|
491
|
+
body: JSON.stringify(data),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
const text = await response.text();
|
|
496
|
+
throw new Error(text || `HTTP ${response.status}: ${response.statusText}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return response.json();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function deleteAPI(endpoint) {
|
|
503
|
+
const url = `${API_BASE}${endpoint}`;
|
|
504
|
+
const response = await fetch(url, {
|
|
505
|
+
method: 'DELETE',
|
|
506
|
+
headers: {
|
|
507
|
+
'Accept': 'application/json',
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (!response.ok) {
|
|
512
|
+
const text = await response.text();
|
|
513
|
+
throw new Error(text || `HTTP ${response.status}: ${response.statusText}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return response.json();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Show error banner
|
|
520
|
+
function showError(message) {
|
|
521
|
+
elements.errorBanner.textContent = message;
|
|
522
|
+
elements.errorBanner.classList.remove('hidden');
|
|
523
|
+
setTimeout(() => {
|
|
524
|
+
elements.errorBanner.classList.add('hidden');
|
|
525
|
+
}, 5000);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Format session ID (show first 8 chars)
|
|
529
|
+
function formatSessionId(id) {
|
|
530
|
+
if (!id) return '-';
|
|
531
|
+
const short = id.slice(0, 8);
|
|
532
|
+
return `<code class="session-id"><span class="truncated">${short}</span></code>`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Get color class for error rate
|
|
536
|
+
function getErrorRateColor(rate) {
|
|
537
|
+
if (rate === 0) return 'success';
|
|
538
|
+
if (rate < 1) return 'success';
|
|
539
|
+
if (rate < 5) return 'warning';
|
|
540
|
+
return 'danger';
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Get badge class for status
|
|
544
|
+
function getStatusBadge(status) {
|
|
545
|
+
const classes = {
|
|
546
|
+
active: 'badge-success',
|
|
547
|
+
inactive: 'badge-neutral',
|
|
548
|
+
success: 'badge-success',
|
|
549
|
+
error: 'badge-danger',
|
|
550
|
+
started: 'badge-info',
|
|
551
|
+
timeout: 'badge-warning',
|
|
552
|
+
};
|
|
553
|
+
return classes[status] || 'badge-neutral';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function translateStatus(status) {
|
|
557
|
+
const labels = {
|
|
558
|
+
active: 'Active',
|
|
559
|
+
inactive: 'Inactive',
|
|
560
|
+
success: 'Successful',
|
|
561
|
+
error: 'Error',
|
|
562
|
+
started: 'Started',
|
|
563
|
+
timeout: 'Timeout',
|
|
564
|
+
};
|
|
565
|
+
return t(labels[status] || status);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Update overview cards
|
|
569
|
+
async function updateOverview() {
|
|
570
|
+
try {
|
|
571
|
+
const params = currentMinutes === 'all' ? '' : `?minutes=${currentMinutes}`;
|
|
572
|
+
const data = await fetchAPI(`/metrics/summary${params}`);
|
|
573
|
+
|
|
574
|
+
elements.totalCalls.textContent = data.totalCalls.toLocaleString();
|
|
575
|
+
elements.totalCalls.className = 'card-value';
|
|
576
|
+
|
|
577
|
+
const errorRate = data.errorRate || 0;
|
|
578
|
+
elements.errorRate.textContent = errorRate.toFixed(2) + '%';
|
|
579
|
+
elements.errorRate.className = `card-value ${getErrorRateColor(errorRate)}`;
|
|
580
|
+
|
|
581
|
+
elements.activeSessions.textContent = data.activeSessions;
|
|
582
|
+
elements.activeSessions.className = 'card-value';
|
|
583
|
+
|
|
584
|
+
const p95 = data.p95LatencyMs;
|
|
585
|
+
elements.p95Latency.textContent = p95 !== null ? formatLatency(p95) : 'N/A';
|
|
586
|
+
elements.p95Latency.className = 'card-value';
|
|
587
|
+
|
|
588
|
+
const avg = data.avgLatencyMs;
|
|
589
|
+
elements.avgLatency.textContent = formatLatency(avg);
|
|
590
|
+
elements.avgLatency.className = 'card-value';
|
|
591
|
+
|
|
592
|
+
elements.uptime.textContent = data.uptimeFormatted || formatDuration(data.uptimeSeconds);
|
|
593
|
+
elements.uptime.className = 'card-value neutral';
|
|
594
|
+
} catch (err) {
|
|
595
|
+
console.error('Failed to update overview:', err);
|
|
596
|
+
// Silently fail - individual errors are shown elsewhere
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Format latency
|
|
601
|
+
function formatLatency(ms) {
|
|
602
|
+
if (ms === null || ms === undefined) return 'N/A';
|
|
603
|
+
if (ms < 1000) return `${ms}ms`;
|
|
604
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Format duration
|
|
608
|
+
function formatDuration(seconds) {
|
|
609
|
+
if (seconds < 60) return `${seconds}s`;
|
|
610
|
+
const mins = Math.floor(seconds / 60);
|
|
611
|
+
if (mins < 60) return `${mins}m`;
|
|
612
|
+
const hours = Math.floor(mins / 60);
|
|
613
|
+
const remainingMins = mins % 60;
|
|
614
|
+
return `${hours}h ${remainingMins}m`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function localizeRelativeTime(value) {
|
|
618
|
+
if (dashboardLocale !== 'hu' || !value) return value;
|
|
619
|
+
if (value === 'Never') return t('Never');
|
|
620
|
+
const match = String(value).match(/^(\d+)([smhd]) ago$/);
|
|
621
|
+
if (!match) return value;
|
|
622
|
+
const amount = match[1];
|
|
623
|
+
const suffix = { s: 'másodperce', m: 'perce', h: 'órája', d: 'napja' }[match[2]];
|
|
624
|
+
return `${amount} ${suffix}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Update tools table
|
|
628
|
+
async function updateTools() {
|
|
629
|
+
try {
|
|
630
|
+
const params = currentMinutes === 'all' ? '' : `?minutes=${currentMinutes}`;
|
|
631
|
+
const data = await fetchAPI(`/metrics/tools${params}`);
|
|
632
|
+
|
|
633
|
+
if (data.tools.length === 0) {
|
|
634
|
+
elements.toolsTableBody.innerHTML = `<tr><td colspan="6" class="empty">${t('No tool calls in this time range')}</td></tr>`;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
elements.toolsTableBody.innerHTML = data.tools.map(tool => `
|
|
639
|
+
<tr>
|
|
640
|
+
<td><code>${escapeHtml(tool.toolName)}</code></td>
|
|
641
|
+
<td>${tool.callCount.toLocaleString()}</td>
|
|
642
|
+
<td>
|
|
643
|
+
<span class="badge ${getErrorRateColor(tool.failureRate)}">
|
|
644
|
+
${tool.failureRate.toFixed(2)}%
|
|
645
|
+
</span>
|
|
646
|
+
${tool.failedCalls > 0 ? ` (${tool.failedCalls})` : ''}
|
|
647
|
+
</td>
|
|
648
|
+
<td>${tool.avgLatencyFormatted}</td>
|
|
649
|
+
<td>${tool.p95LatencyFormatted}</td>
|
|
650
|
+
<td>${localizeRelativeTime(tool.lastCallFormatted || 'Never')}</td>
|
|
651
|
+
</tr>
|
|
652
|
+
`).join('');
|
|
653
|
+
} catch (err) {
|
|
654
|
+
elements.toolsTableBody.innerHTML = `<tr><td colspan="6" class="text-danger">${t('Failed to load: {0}', err.message)}</td></tr>`;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Update errors list
|
|
659
|
+
async function updateErrors() {
|
|
660
|
+
try {
|
|
661
|
+
const data = await fetchAPI('/errors/recent?limit=20');
|
|
662
|
+
|
|
663
|
+
if (data.errors.length === 0) {
|
|
664
|
+
elements.errorsList.innerHTML = `<div class="text-muted" style="padding: 1rem; text-align: center;">${t('No errors recorded')}</div>`;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
elements.errorsList.innerHTML = data.errors.map(error => `
|
|
669
|
+
<div class="error-item">
|
|
670
|
+
<div class="error-header">
|
|
671
|
+
<span class="error-code">${escapeHtml(error.errorCode)}</span>
|
|
672
|
+
<span class="error-time">${localizeRelativeTime(error.timestampFormatted || error.timestamp)}</span>
|
|
673
|
+
</div>
|
|
674
|
+
${error.toolName ? `<div class="error-tool">${t('Tool')}: <code>${escapeHtml(error.toolName)}</code></div>` : ''}
|
|
675
|
+
<div class="error-message">${escapeHtml(error.message)}</div>
|
|
676
|
+
</div>
|
|
677
|
+
`).join('');
|
|
678
|
+
} catch (err) {
|
|
679
|
+
elements.errorsList.innerHTML = `<div class="text-danger">${t('Failed to load errors: {0}', err.message)}</div>`;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Update sessions table
|
|
684
|
+
async function updateSessions() {
|
|
685
|
+
try {
|
|
686
|
+
const data = await fetchAPI('/sessions');
|
|
687
|
+
|
|
688
|
+
if (data.sessions.length === 0) {
|
|
689
|
+
elements.sessionsTableBody.innerHTML = `<tr><td colspan="8" class="empty">${t('No sessions recorded')}</td></tr>`;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
elements.sessionsTableBody.innerHTML = data.sessions.map(session => `
|
|
694
|
+
<tr>
|
|
695
|
+
<td>${formatSessionId(session.id)}</td>
|
|
696
|
+
<td>${localizeRelativeTime(session.startedAtFormatted || session.startedAt)}</td>
|
|
697
|
+
<td>${localizeRelativeTime(session.lastActivityFormatted || session.lastActivityAt)}</td>
|
|
698
|
+
<td><span class="badge badge-neutral">${escapeHtml(session.transportMode)}</span></td>
|
|
699
|
+
<td>${session.requestCount}</td>
|
|
700
|
+
<td>${session.errorCount > 0 ? `<span class="text-danger">${session.errorCount}</span>` : '0'}</td>
|
|
701
|
+
<td><span class="badge ${session.active ? 'badge-success' : 'badge-neutral'}">${session.active ? t('Active') : t('Inactive')}</span></td>
|
|
702
|
+
<td>
|
|
703
|
+
<button class="btn btn-sm" onclick="viewSession('${session.id}')">${t('View')}</button>
|
|
704
|
+
</td>
|
|
705
|
+
</tr>
|
|
706
|
+
`).join('');
|
|
707
|
+
} catch (err) {
|
|
708
|
+
elements.sessionsTableBody.innerHTML = `<tr><td colspan="8" class="text-danger">${t('Failed to load sessions: {0}', err.message)}</td></tr>`;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// View session details
|
|
713
|
+
async function viewSession(sessionId) {
|
|
714
|
+
try {
|
|
715
|
+
const data = await fetchAPI(`/sessions/${sessionId}`);
|
|
716
|
+
const session = data.session;
|
|
717
|
+
const stats = data.stats;
|
|
718
|
+
|
|
719
|
+
elements.sessionModalTitle.textContent = t('Session: {0}…', sessionId.slice(0, 8));
|
|
720
|
+
|
|
721
|
+
elements.sessionModalBody.innerHTML = `
|
|
722
|
+
<div class="session-detail-section">
|
|
723
|
+
<h3>${t('Session Info')}</h3>
|
|
724
|
+
<div class="session-stats">
|
|
725
|
+
<div class="session-stat">
|
|
726
|
+
<div class="session-stat-label">${t('Status')}</div>
|
|
727
|
+
<div class="session-stat-value">
|
|
728
|
+
<span class="badge ${session.active ? 'badge-success' : 'badge-neutral'}">
|
|
729
|
+
${session.active ? t('Active') : t('Inactive')}
|
|
730
|
+
</span>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
<div class="session-stat">
|
|
734
|
+
<div class="session-stat-label">${t('Transport')}</div>
|
|
735
|
+
<div class="session-stat-value">${escapeHtml(session.transportMode)}</div>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="session-stat">
|
|
738
|
+
<div class="session-stat-label">${t('Requests')}</div>
|
|
739
|
+
<div class="session-stat-value">${session.requestCount}</div>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="session-stat">
|
|
742
|
+
<div class="session-stat-label">${t('Errors')}</div>
|
|
743
|
+
<div class="session-stat-value ${session.errorCount > 0 ? 'text-danger' : ''}">${session.errorCount}</div>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
|
|
748
|
+
<div class="session-detail-section">
|
|
749
|
+
<h3>${t('Call Statistics')}</h3>
|
|
750
|
+
<div class="session-stats">
|
|
751
|
+
<div class="session-stat">
|
|
752
|
+
<div class="session-stat-label">${t('Total Calls')}</div>
|
|
753
|
+
<div class="session-stat-value">${stats.toolCallCount}</div>
|
|
754
|
+
</div>
|
|
755
|
+
<div class="session-stat">
|
|
756
|
+
<div class="session-stat-label">${t('Successful')}</div>
|
|
757
|
+
<div class="session-stat-value text-success">${stats.successfulCalls}</div>
|
|
758
|
+
</div>
|
|
759
|
+
<div class="session-stat">
|
|
760
|
+
<div class="session-stat-label">${t('Failed')}</div>
|
|
761
|
+
<div class="session-stat-value ${stats.failedCalls > 0 ? 'text-danger' : ''}">${stats.failedCalls}</div>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
<div class="session-detail-section">
|
|
767
|
+
<h3>${t('Tool Calls')} (${data.toolCalls.length})</h3>
|
|
768
|
+
${data.toolCalls.length > 0 ? `
|
|
769
|
+
<table class="data-table">
|
|
770
|
+
<thead>
|
|
771
|
+
<tr>
|
|
772
|
+
<th>${t('Tool')}</th>
|
|
773
|
+
<th>${t('Status')}</th>
|
|
774
|
+
<th>${t('Duration')}</th>
|
|
775
|
+
<th>${t('Time')}</th>
|
|
776
|
+
<th>${t('Actions')}</th>
|
|
777
|
+
</tr>
|
|
778
|
+
</thead>
|
|
779
|
+
<tbody>
|
|
780
|
+
${data.toolCalls.map(call => `
|
|
781
|
+
<tr>
|
|
782
|
+
<td><code>${escapeHtml(call.toolName)}</code></td>
|
|
783
|
+
<td><span class="badge ${getStatusBadge(call.status)}">${translateStatus(call.status)}</span></td>
|
|
784
|
+
<td>${call.durationMs !== null ? formatLatency(call.durationMs) : '-'}</td>
|
|
785
|
+
<td style="font-size: 0.75rem; color: var(--color-text-muted);">${new Date(call.startedAt).toLocaleTimeString()}</td>
|
|
786
|
+
<td><button class="btn btn-sm" onclick="viewToolCall('${call.id}')">${t('View Details')}</button></td>
|
|
787
|
+
</tr>
|
|
788
|
+
`).join('')}
|
|
789
|
+
</tbody>
|
|
790
|
+
</table>
|
|
791
|
+
` : `<p class="text-muted">${t('No tool calls in this session')}</p>`}
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
${data.errors.length > 0 ? `
|
|
795
|
+
<div class="session-detail-section">
|
|
796
|
+
<h3>${t('Errors')} (${data.errors.length})</h3>
|
|
797
|
+
<div class="errors-list">
|
|
798
|
+
${data.errors.map(error => `
|
|
799
|
+
<div class="error-item">
|
|
800
|
+
<div class="error-header">
|
|
801
|
+
<span class="error-code">${escapeHtml(error.errorCode)}</span>
|
|
802
|
+
<span class="error-time">${localizeRelativeTime(error.timestampFormatted || error.timestamp)}</span>
|
|
803
|
+
</div>
|
|
804
|
+
${error.toolName ? `<div class="error-tool">${t('Tool')}: <code>${escapeHtml(error.toolName)}</code></div>` : ''}
|
|
805
|
+
<div class="error-message">${escapeHtml(error.message)}</div>
|
|
806
|
+
</div>
|
|
807
|
+
`).join('')}
|
|
808
|
+
</div>
|
|
809
|
+
</div>
|
|
810
|
+
` : ''}
|
|
811
|
+
`;
|
|
812
|
+
|
|
813
|
+
elements.sessionModal.classList.remove('hidden');
|
|
814
|
+
} catch (err) {
|
|
815
|
+
showError(t('Failed to load session details: {0}', err.message));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// View tool call details
|
|
820
|
+
async function viewToolCall(toolCallId) {
|
|
821
|
+
try {
|
|
822
|
+
const data = await fetchAPI(`/tool-calls/${toolCallId}`);
|
|
823
|
+
const toolCall = data.toolCall;
|
|
824
|
+
|
|
825
|
+
elements.toolCallModalTitle.textContent = `${toolCall.toolName} - ${toolCall.id.slice(0, 8)}...`;
|
|
826
|
+
|
|
827
|
+
let detailsHtml = '';
|
|
828
|
+
|
|
829
|
+
// Basic info
|
|
830
|
+
detailsHtml += `
|
|
831
|
+
<div class="session-detail-section">
|
|
832
|
+
<h3>${t('Call Info')}</h3>
|
|
833
|
+
<div class="session-stats">
|
|
834
|
+
<div class="session-stat">
|
|
835
|
+
<div class="session-stat-label">${t('Status')}</div>
|
|
836
|
+
<div class="session-stat-value">
|
|
837
|
+
<span class="badge ${getStatusBadge(toolCall.status)}">${translateStatus(toolCall.status)}</span>
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
<div class="session-stat">
|
|
841
|
+
<div class="session-stat-label">${t('Duration')}</div>
|
|
842
|
+
<div class="session-stat-value">${toolCall.durationFormatted || t('In progress')}</div>
|
|
843
|
+
</div>
|
|
844
|
+
<div class="session-stat">
|
|
845
|
+
<div class="session-stat-label">${t('Started')}</div>
|
|
846
|
+
<div class="session-stat-value">${localizeRelativeTime(toolCall.startedAtFormatted)}</div>
|
|
847
|
+
</div>
|
|
848
|
+
${toolCall.finishedAtFormatted ? `
|
|
849
|
+
<div class="session-stat">
|
|
850
|
+
<div class="session-stat-label">${t('Finished')}</div>
|
|
851
|
+
<div class="session-stat-value">${localizeRelativeTime(toolCall.finishedAtFormatted)}</div>
|
|
852
|
+
</div>
|
|
853
|
+
` : ''}
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
`;
|
|
857
|
+
|
|
858
|
+
// Artifacts
|
|
859
|
+
if (Array.isArray(toolCall.artifacts) && toolCall.artifacts.length > 0) {
|
|
860
|
+
detailsHtml += `
|
|
861
|
+
<div class="session-detail-section">
|
|
862
|
+
<h3>${t('Artifacts')}</h3>
|
|
863
|
+
${toolCall.artifacts.map(artifact => `
|
|
864
|
+
<div class="artifact-row">
|
|
865
|
+
<div class="artifact-label">${escapeHtml(artifact.label || t('Artifact'))}</div>
|
|
866
|
+
<pre class="code-block artifact-path">${escapeHtml(artifact.path || '')}</pre>
|
|
867
|
+
</div>
|
|
868
|
+
`).join('')}
|
|
869
|
+
</div>
|
|
870
|
+
`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Parameters
|
|
874
|
+
if (toolCall.params !== undefined && toolCall.params !== null) {
|
|
875
|
+
detailsHtml += `
|
|
876
|
+
<div class="session-detail-section">
|
|
877
|
+
<h3>${t('Parameters')}</h3>
|
|
878
|
+
<pre class="code-block">${escapeHtml(typeof toolCall.params === 'string' ? toolCall.params : JSON.stringify(toolCall.params, null, 2))}</pre>
|
|
879
|
+
</div>
|
|
880
|
+
`;
|
|
881
|
+
} else {
|
|
882
|
+
detailsHtml += `
|
|
883
|
+
<div class="session-detail-section">
|
|
884
|
+
<h3>${t('Parameters')}</h3>
|
|
885
|
+
<p class="text-muted">${t('Parameters not available (enable ROCRATE_DASHBOARD_DETAILED_LOGGING=true to capture parameters)')}</p>
|
|
886
|
+
</div>
|
|
887
|
+
`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Result
|
|
891
|
+
if (toolCall.status === 'success' && toolCall.result !== undefined && toolCall.result !== null) {
|
|
892
|
+
detailsHtml += `
|
|
893
|
+
<div class="session-detail-section">
|
|
894
|
+
<h3>${t('Result')}</h3>
|
|
895
|
+
<pre class="code-block">${escapeHtml(typeof toolCall.result === 'string' ? toolCall.result : JSON.stringify(toolCall.result, null, 2))}</pre>
|
|
896
|
+
</div>
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Error
|
|
901
|
+
if (toolCall.status === 'error') {
|
|
902
|
+
detailsHtml += `
|
|
903
|
+
<div class="session-detail-section">
|
|
904
|
+
<h3>${t('Error')}</h3>
|
|
905
|
+
<div class="error-item">
|
|
906
|
+
<div class="error-header">
|
|
907
|
+
<span class="error-code">${escapeHtml(toolCall.errorCode || t('Unknown'))}</span>
|
|
908
|
+
</div>
|
|
909
|
+
<div class="error-message">${escapeHtml(toolCall.errorMessage || toolCall.errorMessageFull || t('No message'))}</div>
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (Array.isArray(toolCall.httpLogs) && toolCall.httpLogs.length > 0) {
|
|
916
|
+
detailsHtml += `
|
|
917
|
+
<div class="session-detail-section">
|
|
918
|
+
<h3>${t('HTTP Communication')}</h3>
|
|
919
|
+
${toolCall.httpLogs.map((log, index) => `
|
|
920
|
+
<div class="session-detail-section">
|
|
921
|
+
<h4>${escapeHtml(log.dependency || 'http')} #${index + 1}</h4>
|
|
922
|
+
<div class="session-stats">
|
|
923
|
+
<div class="session-stat">
|
|
924
|
+
<div class="session-stat-label">${t('Timestamp')}</div>
|
|
925
|
+
<div class="session-stat-value">${escapeHtml(log.timestamp || '')}</div>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="session-stat">
|
|
928
|
+
<div class="session-stat-label">${t('Request')}</div>
|
|
929
|
+
<div class="session-stat-value">${escapeHtml((log.request?.method || 'GET') + ' ' + (log.request?.url || ''))}</div>
|
|
930
|
+
</div>
|
|
931
|
+
${log.response ? `
|
|
932
|
+
<div class="session-stat">
|
|
933
|
+
<div class="session-stat-label">${t('Response')}</div>
|
|
934
|
+
<div class="session-stat-value">${escapeHtml(String(log.response.status))}</div>
|
|
935
|
+
</div>
|
|
936
|
+
` : ''}
|
|
937
|
+
</div>
|
|
938
|
+
<h4>${t('Request Headers')}</h4>
|
|
939
|
+
${formatHttpHeaders(log.request?.headers)}
|
|
940
|
+
<h4>${t('Request Body')}</h4>
|
|
941
|
+
${formatHttpBody(log.request?.body)}
|
|
942
|
+
${log.response ? `
|
|
943
|
+
<h4>${t('Response Headers')}</h4>
|
|
944
|
+
${formatHttpHeaders(log.response.headers)}
|
|
945
|
+
<h4>${t('Response Body')}</h4>
|
|
946
|
+
${formatHttpBody(log.response.body)}
|
|
947
|
+
` : ''}
|
|
948
|
+
${log.error ? `
|
|
949
|
+
<h4>${t('Error')}</h4>
|
|
950
|
+
<pre class="code-block">${escapeHtml(log.error)}</pre>
|
|
951
|
+
` : ''}
|
|
952
|
+
</div>
|
|
953
|
+
`).join('')}
|
|
954
|
+
</div>
|
|
955
|
+
`;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Sizes
|
|
959
|
+
detailsHtml += `
|
|
960
|
+
<div class="session-detail-section">
|
|
961
|
+
<h3>${t('Metadata')}</h3>
|
|
962
|
+
<div class="session-stats">
|
|
963
|
+
<div class="session-stat">
|
|
964
|
+
<div class="session-stat-label">${t('Args Size')}</div>
|
|
965
|
+
<div class="session-stat-value">${formatBytes(toolCall.argsSizeBytes)}</div>
|
|
966
|
+
</div>
|
|
967
|
+
${toolCall.resultSizeBytes !== undefined ? `
|
|
968
|
+
<div class="session-stat">
|
|
969
|
+
<div class="session-stat-label">${t('Result Size')}</div>
|
|
970
|
+
<div class="session-stat-value">${formatBytes(toolCall.resultSizeBytes)}</div>
|
|
971
|
+
</div>
|
|
972
|
+
` : ''}
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
`;
|
|
976
|
+
|
|
977
|
+
elements.toolCallModalBody.innerHTML = detailsHtml;
|
|
978
|
+
elements.toolCallModal.classList.remove('hidden');
|
|
979
|
+
} catch (err) {
|
|
980
|
+
showError(t('Failed to load tool call details: {0}', err.message));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Format bytes to human readable
|
|
985
|
+
function formatBytes(bytes) {
|
|
986
|
+
if (bytes === 0) return '0 B';
|
|
987
|
+
const k = 1024;
|
|
988
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
989
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
990
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function formatHttpHeaders(headers) {
|
|
994
|
+
if (!headers || Object.keys(headers).length === 0) {
|
|
995
|
+
return `<p class="text-muted">${t('No headers logged')}</p>`;
|
|
996
|
+
}
|
|
997
|
+
return `<pre class="code-block">${escapeHtml(JSON.stringify(headers, null, 2))}</pre>`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function formatHttpBody(body) {
|
|
1001
|
+
if (body === undefined || body === null || body === '') {
|
|
1002
|
+
return `<p class="text-muted">${t('No body logged')}</p>`;
|
|
1003
|
+
}
|
|
1004
|
+
return `<pre class="code-block">${escapeHtml(body)}</pre>`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Settings functions
|
|
1008
|
+
async function loadSettings() {
|
|
1009
|
+
try {
|
|
1010
|
+
const config = await fetchAPI('/config');
|
|
1011
|
+
elements.detailedLoggingToggle.checked = config.detailedToolCallLogging;
|
|
1012
|
+
elements.keepDataverseUploadZipsToggle.checked = config.keepDataverseUploadZips === true;
|
|
1013
|
+
elements.retentionHoursInput.value = config.retentionHours;
|
|
1014
|
+
renderDataverseSettings(config.dataverse);
|
|
1015
|
+
renderTavilySettings(config.tavily);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
showError(t('Failed to load settings: {0}', err.message));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function setSecretVisibility(input, button, visible) {
|
|
1022
|
+
if (!input || !button) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
input.type = visible ? 'text' : 'password';
|
|
1027
|
+
const label = t(visible ? 'Hide API key' : 'Show API key');
|
|
1028
|
+
button.setAttribute('title', label);
|
|
1029
|
+
button.setAttribute('aria-label', label);
|
|
1030
|
+
button.classList.toggle('is-visible', visible);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function revealConfiguredSecret(input, button, environmentName) {
|
|
1034
|
+
if (
|
|
1035
|
+
input.value.trim() === '' &&
|
|
1036
|
+
input.dataset.secretConfigured === 'true' &&
|
|
1037
|
+
environmentName
|
|
1038
|
+
) {
|
|
1039
|
+
button.disabled = true;
|
|
1040
|
+
try {
|
|
1041
|
+
const result = await fetchAPI(
|
|
1042
|
+
'/config/secrets',
|
|
1043
|
+
{
|
|
1044
|
+
method: 'POST',
|
|
1045
|
+
headers: {
|
|
1046
|
+
'Content-Type': 'application/json',
|
|
1047
|
+
'X-RoCrate-Dashboard-Intent': 'reveal-secret',
|
|
1048
|
+
},
|
|
1049
|
+
body: JSON.stringify({ key: environmentName }),
|
|
1050
|
+
},
|
|
1051
|
+
);
|
|
1052
|
+
if (typeof result.value === 'string' && result.value !== '') {
|
|
1053
|
+
input.value = result.value;
|
|
1054
|
+
}
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
showSettingsMessage(t('Failed to reveal API key: {0}', err.message), 'error');
|
|
1057
|
+
return;
|
|
1058
|
+
} finally {
|
|
1059
|
+
button.disabled = false;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
setSecretVisibility(input, button, input.type === 'password');
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function bindSecretVisibility(input, button, environmentName) {
|
|
1067
|
+
if (!input || !button) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
setSecretVisibility(input, button, false);
|
|
1072
|
+
button.addEventListener('click', () => {
|
|
1073
|
+
void revealConfiguredSecret(input, button, environmentName);
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function renderDataverseSettings(dataverse) {
|
|
1078
|
+
const config = dataverse || {};
|
|
1079
|
+
const baseUrl = config.baseUrl || '';
|
|
1080
|
+
const baseUrlSource = config.baseUrlSource || 'default';
|
|
1081
|
+
const apiKeySource = config.apiKeySource || 'unset';
|
|
1082
|
+
|
|
1083
|
+
serviceSettingsState.dataverseBaseUrl = baseUrl;
|
|
1084
|
+
serviceSettingsState.dataverseBaseUrlSource = baseUrlSource;
|
|
1085
|
+
serviceSettingsState.dataverseApiKeySource = apiKeySource;
|
|
1086
|
+
|
|
1087
|
+
elements.dataverseBaseUrlInput.value = baseUrl;
|
|
1088
|
+
elements.dataverseBaseUrlUseEnv.checked = baseUrlSource !== 'dashboard';
|
|
1089
|
+
elements.dataverseBaseUrlSource.textContent = formatRuntimeSource(
|
|
1090
|
+
baseUrlSource,
|
|
1091
|
+
'DATAVERSE_BASE_URL',
|
|
1092
|
+
true,
|
|
1093
|
+
);
|
|
1094
|
+
setSecretVisibility(
|
|
1095
|
+
elements.dataverseApiKeyInput,
|
|
1096
|
+
elements.dataverseApiKeyVisibilityBtn,
|
|
1097
|
+
false,
|
|
1098
|
+
);
|
|
1099
|
+
elements.dataverseApiKeyInput.value = '';
|
|
1100
|
+
elements.dataverseApiKeyInput.dataset.secretConfigured = config.apiKeyPresent
|
|
1101
|
+
? 'true'
|
|
1102
|
+
: 'false';
|
|
1103
|
+
elements.dataverseApiKeyUseEnv.checked = apiKeySource !== 'dashboard';
|
|
1104
|
+
elements.dataverseApiKeyInput.placeholder = config.apiKeyPresent
|
|
1105
|
+
? MASKED_SECRET_PLACEHOLDER
|
|
1106
|
+
: t('Enter a new dashboard override');
|
|
1107
|
+
elements.dataverseApiKeySource.textContent = formatRuntimeSource(
|
|
1108
|
+
apiKeySource,
|
|
1109
|
+
'DATAVERSE_API_KEY',
|
|
1110
|
+
false,
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function renderTavilySettings(tavily) {
|
|
1115
|
+
const config = tavily || {};
|
|
1116
|
+
const apiKeySource = config.apiKeySource || 'unset';
|
|
1117
|
+
|
|
1118
|
+
serviceSettingsState.tavilyApiKeySource = apiKeySource;
|
|
1119
|
+
setSecretVisibility(
|
|
1120
|
+
elements.tavilyApiKeyInput,
|
|
1121
|
+
elements.tavilyApiKeyVisibilityBtn,
|
|
1122
|
+
false,
|
|
1123
|
+
);
|
|
1124
|
+
elements.tavilyApiKeyInput.value = '';
|
|
1125
|
+
elements.tavilyApiKeyInput.dataset.secretConfigured = config.apiKeyPresent
|
|
1126
|
+
? 'true'
|
|
1127
|
+
: 'false';
|
|
1128
|
+
elements.tavilyApiKeyUseEnv.checked = apiKeySource !== 'dashboard';
|
|
1129
|
+
elements.tavilyApiKeyInput.placeholder = config.apiKeyPresent
|
|
1130
|
+
? MASKED_SECRET_PLACEHOLDER
|
|
1131
|
+
: t('Enter a new dashboard override');
|
|
1132
|
+
elements.tavilyApiKeySource.textContent = formatRuntimeSource(
|
|
1133
|
+
apiKeySource,
|
|
1134
|
+
'TAVILY_API_KEY',
|
|
1135
|
+
false,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function formatRuntimeSource(source, environmentName, hasDefault) {
|
|
1140
|
+
if (source === 'dashboard') {
|
|
1141
|
+
return t('Dashboard override active');
|
|
1142
|
+
}
|
|
1143
|
+
if (source === 'env') {
|
|
1144
|
+
return t('Using environment variable: {0}', environmentName);
|
|
1145
|
+
}
|
|
1146
|
+
return hasDefault ? t('Using built-in default') : t('Not configured');
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function addRuntimeSettingUpdate(updates, key, input, useEnvironment, initialValue) {
|
|
1150
|
+
if (useEnvironment.checked) {
|
|
1151
|
+
updates[key] = null;
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const value = input.value.trim();
|
|
1156
|
+
if (value === '') {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (key === 'DATAVERSE_BASE_URL') {
|
|
1161
|
+
const normalizedValue = value.replace(/\/+$/, '');
|
|
1162
|
+
if (normalizedValue === initialValue) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
updates[key] = value;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function saveSettings(e) {
|
|
1171
|
+
e.preventDefault();
|
|
1172
|
+
|
|
1173
|
+
const detailedLogging = elements.detailedLoggingToggle.checked;
|
|
1174
|
+
const keepDataverseUploadZips = elements.keepDataverseUploadZipsToggle.checked;
|
|
1175
|
+
const retentionHours = parseInt(elements.retentionHoursInput.value, 10);
|
|
1176
|
+
|
|
1177
|
+
if (isNaN(retentionHours) || retentionHours < 1 || retentionHours > 168) {
|
|
1178
|
+
showSettingsMessage(t('Retention hours must be between 1 and 168'), 'error');
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const updates = {
|
|
1183
|
+
detailedToolCallLogging: detailedLogging,
|
|
1184
|
+
keepDataverseUploadZips: keepDataverseUploadZips,
|
|
1185
|
+
retentionHours: retentionHours,
|
|
1186
|
+
};
|
|
1187
|
+
addRuntimeSettingUpdate(
|
|
1188
|
+
updates,
|
|
1189
|
+
'DATAVERSE_BASE_URL',
|
|
1190
|
+
elements.dataverseBaseUrlInput,
|
|
1191
|
+
elements.dataverseBaseUrlUseEnv,
|
|
1192
|
+
serviceSettingsState.dataverseBaseUrl,
|
|
1193
|
+
);
|
|
1194
|
+
addRuntimeSettingUpdate(
|
|
1195
|
+
updates,
|
|
1196
|
+
'DATAVERSE_API_KEY',
|
|
1197
|
+
elements.dataverseApiKeyInput,
|
|
1198
|
+
elements.dataverseApiKeyUseEnv,
|
|
1199
|
+
'',
|
|
1200
|
+
);
|
|
1201
|
+
addRuntimeSettingUpdate(
|
|
1202
|
+
updates,
|
|
1203
|
+
'TAVILY_API_KEY',
|
|
1204
|
+
elements.tavilyApiKeyInput,
|
|
1205
|
+
elements.tavilyApiKeyUseEnv,
|
|
1206
|
+
'',
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
try {
|
|
1210
|
+
const result = await postAPI('/config', updates);
|
|
1211
|
+
if (result.config) {
|
|
1212
|
+
renderDataverseSettings(result.config.dataverse);
|
|
1213
|
+
renderTavilySettings(result.config.tavily);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
showSettingsMessage(t('Settings saved successfully!'), 'success');
|
|
1217
|
+
|
|
1218
|
+
// Refresh the data to reflect any changes
|
|
1219
|
+
setTimeout(() => refreshAll(), 500);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
showSettingsMessage(t('Failed to save settings: {0}', err.message), 'error');
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function showSettingsMessage(message, type) {
|
|
1226
|
+
elements.settingsMessage.textContent = message;
|
|
1227
|
+
elements.settingsMessage.className = `settings-message ${type}`;
|
|
1228
|
+
elements.settingsMessage.classList.remove('hidden');
|
|
1229
|
+
|
|
1230
|
+
setTimeout(() => {
|
|
1231
|
+
elements.settingsMessage.classList.add('hidden');
|
|
1232
|
+
}, 3000);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function bindRuntimeSettingInput(input, useEnvironment) {
|
|
1236
|
+
if (!input || !useEnvironment) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
input.addEventListener('input', () => {
|
|
1241
|
+
if (input.value.trim() !== '') {
|
|
1242
|
+
useEnvironment.checked = false;
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function showMetadataProfilesMessage(message, type) {
|
|
1248
|
+
elements.metadataProfilesMessage.textContent = message;
|
|
1249
|
+
elements.metadataProfilesMessage.className = `settings-message ${type}`;
|
|
1250
|
+
elements.metadataProfilesMessage.classList.remove('hidden');
|
|
1251
|
+
|
|
1252
|
+
setTimeout(() => {
|
|
1253
|
+
elements.metadataProfilesMessage.classList.add('hidden');
|
|
1254
|
+
}, 3000);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async function loadMetadataProfiles() {
|
|
1258
|
+
if (!elements.metadataProfilesTableBody) return;
|
|
1259
|
+
try {
|
|
1260
|
+
const data = await fetchAPI('/metadata-profiles');
|
|
1261
|
+
const storage = data.storage || {};
|
|
1262
|
+
elements.metadataProfileStorageValue.textContent = `${storage.rootPath || ''} (${storage.indexPath || ''})`;
|
|
1263
|
+
metadataProfilesState.profiles = data.profiles || [];
|
|
1264
|
+
renderMetadataProfilesTable();
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
elements.metadataProfilesTableBody.innerHTML = `<tr><td colspan="7" class="text-danger">${t('Failed to load metadata profiles: {0}', escapeHtml(err.message))}</td></tr>`;
|
|
1267
|
+
updateMetadataProfilesPagination(0);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function renderUrlCell(value) {
|
|
1272
|
+
const text = value || '';
|
|
1273
|
+
if (!text) {
|
|
1274
|
+
return '<span class="text-muted">-</span>';
|
|
1275
|
+
}
|
|
1276
|
+
const escaped = escapeHtml(text);
|
|
1277
|
+
if (/^https?:\/\//i.test(text)) {
|
|
1278
|
+
return `<a class="url-cell" href="${escaped}" target="_blank" rel="noopener noreferrer" title="${escaped}">${escaped}</a>`;
|
|
1279
|
+
}
|
|
1280
|
+
return `<code class="url-cell" title="${escaped}">${escaped}</code>`;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function renderMetadataProfilesTable() {
|
|
1284
|
+
if (!elements.metadataProfilesTableBody) return;
|
|
1285
|
+
const profiles = metadataProfilesState.profiles || [];
|
|
1286
|
+
const totalPages = Math.max(1, Math.ceil(profiles.length / metadataProfilesState.pageSize));
|
|
1287
|
+
metadataProfilesState.page = Math.min(Math.max(1, metadataProfilesState.page), totalPages);
|
|
1288
|
+
|
|
1289
|
+
if (profiles.length === 0) {
|
|
1290
|
+
elements.metadataProfilesTableBody.innerHTML = `<tr><td colspan="7" class="empty">${t('No metadata profiles imported')}</td></tr>`;
|
|
1291
|
+
updateMetadataProfilesPagination(0);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const start = (metadataProfilesState.page - 1) * metadataProfilesState.pageSize;
|
|
1296
|
+
const pageProfiles = profiles.slice(start, start + metadataProfilesState.pageSize);
|
|
1297
|
+
elements.metadataProfilesTableBody.innerHTML = pageProfiles.map((profile) => {
|
|
1298
|
+
const reference = profile.aux?.reference || profile.downloadUrl || '';
|
|
1299
|
+
const conformsTo = profile.conformsTo || '';
|
|
1300
|
+
return `
|
|
1301
|
+
<tr>
|
|
1302
|
+
<td><span class="badge badge-success">${t('Ready')}</span></td>
|
|
1303
|
+
<td>${escapeHtml(profile.name)}</td>
|
|
1304
|
+
<td>${escapeHtml(profile.version || '')}</td>
|
|
1305
|
+
<td><span class="badge badge-neutral">${escapeHtml(profile.source || '')}</span></td>
|
|
1306
|
+
<td>${renderUrlCell(reference)}</td>
|
|
1307
|
+
<td>${renderUrlCell(conformsTo)}</td>
|
|
1308
|
+
<td>
|
|
1309
|
+
<button class="btn btn-sm" onclick='deleteMetadataProfile(${JSON.stringify(profile.id)})'>${t('Delete')}</button>
|
|
1310
|
+
</td>
|
|
1311
|
+
</tr>
|
|
1312
|
+
`;
|
|
1313
|
+
}).join('');
|
|
1314
|
+
updateMetadataProfilesPagination(profiles.length);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function updateMetadataProfilesPagination(total) {
|
|
1318
|
+
if (!elements.metadataProfilesPagination) return;
|
|
1319
|
+
const totalPages = Math.max(1, Math.ceil(total / metadataProfilesState.pageSize));
|
|
1320
|
+
elements.metadataProfilesPageValue.textContent = String(metadataProfilesState.page);
|
|
1321
|
+
elements.metadataProfilesPrevPageBtn.disabled = metadataProfilesState.page <= 1;
|
|
1322
|
+
elements.metadataProfilesNextPageBtn.disabled = metadataProfilesState.page >= totalPages || total === 0;
|
|
1323
|
+
elements.metadataProfilesPageSizeSelect.value = String(metadataProfilesState.pageSize);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async function loadMetadataProfileProviders() {
|
|
1327
|
+
try {
|
|
1328
|
+
const data = await fetchAPI('/metadata-profiles/providers');
|
|
1329
|
+
metadataProfileProviders = data.providers || [];
|
|
1330
|
+
const providers = metadataProfileProviders;
|
|
1331
|
+
if (providers.length === 0) {
|
|
1332
|
+
if (elements.remoteProviderSelectList) {
|
|
1333
|
+
elements.remoteProviderSelectList.innerHTML = `<div class="empty">${t('No CEDAR providers are configured')}</div>`;
|
|
1334
|
+
}
|
|
1335
|
+
if (elements.remoteProviderManageList) {
|
|
1336
|
+
elements.remoteProviderManageList.innerHTML = `<div class="empty">${t('No CEDAR providers are configured')}</div>`;
|
|
1337
|
+
}
|
|
1338
|
+
showMetadataProfilesMessage(t('No CEDAR providers are configured.'), 'error');
|
|
1339
|
+
return providers;
|
|
1340
|
+
}
|
|
1341
|
+
const proxyProvider = providers.find((provider) => provider.accessMode === 'dataverseProxy');
|
|
1342
|
+
const keyedProvider = providers.find((provider) => provider.apiKeyPresent);
|
|
1343
|
+
currentMetadataProfileProviderId = proxyProvider?.id || keyedProvider?.id || providers[0]?.id || '';
|
|
1344
|
+
renderRemoteProviderSelectList();
|
|
1345
|
+
renderRemoteProviderManageList();
|
|
1346
|
+
if (data.warnings && data.warnings.length > 0) {
|
|
1347
|
+
showMetadataProfilesMessage(data.warnings.join(' '), 'error');
|
|
1348
|
+
}
|
|
1349
|
+
return providers;
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
if (elements.remoteProviderSelectList) {
|
|
1352
|
+
elements.remoteProviderSelectList.innerHTML = `<div class="text-danger">${t('Provider load failed')}</div>`;
|
|
1353
|
+
}
|
|
1354
|
+
if (elements.remoteProviderManageList) {
|
|
1355
|
+
elements.remoteProviderManageList.innerHTML = `<div class="text-danger">${t('Provider load failed')}</div>`;
|
|
1356
|
+
}
|
|
1357
|
+
showMetadataProfilesMessage(t('Failed to load CEDAR providers: {0}', err.message), 'error');
|
|
1358
|
+
return [];
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function renderRemoteProviderSelectList() {
|
|
1363
|
+
if (!elements.remoteProviderSelectList) return;
|
|
1364
|
+
if (metadataProfileProviders.length === 0) {
|
|
1365
|
+
elements.remoteProviderSelectList.innerHTML = `<div class="empty">${t('No CEDAR providers are configured')}</div>`;
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
elements.remoteProviderSelectList.innerHTML = metadataProfileProviders.map((provider) => `
|
|
1369
|
+
<div class="provider-row clickable" onclick='selectRemoteProvider(${JSON.stringify(provider.id || '')})'>
|
|
1370
|
+
<div class="provider-row-icon provider-server-icon"></div>
|
|
1371
|
+
<div>
|
|
1372
|
+
<div class="provider-row-title">${escapeHtml(provider.title || provider.id || 'CEDAR Provider')}${providerAccessBadge(provider)}</div>
|
|
1373
|
+
<div class="provider-row-url">${escapeHtml(provider.displayUrl || provider.baseUrl || provider.domainBase || '')}</div>
|
|
1374
|
+
</div>
|
|
1375
|
+
<div class="provider-arrow">›</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
`).join('');
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function renderRemoteProviderManageList() {
|
|
1381
|
+
if (!elements.remoteProviderManageList) return;
|
|
1382
|
+
if (metadataProfileProviders.length === 0) {
|
|
1383
|
+
elements.remoteProviderManageList.innerHTML = `<div class="empty">${t('No CEDAR providers are configured')}</div>`;
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
elements.remoteProviderManageList.innerHTML = metadataProfileProviders.map((provider) => `
|
|
1387
|
+
<div class="provider-row">
|
|
1388
|
+
<div class="provider-row-icon provider-server-icon"></div>
|
|
1389
|
+
<div>
|
|
1390
|
+
<div class="provider-row-title">${escapeHtml(provider.title || provider.id || 'CEDAR Provider')}${providerAccessBadge(provider, true)}</div>
|
|
1391
|
+
<div class="provider-row-url">${escapeHtml(provider.displayUrl || provider.baseUrl || provider.domainBase || '')}</div>
|
|
1392
|
+
</div>
|
|
1393
|
+
<div class="provider-row-actions">
|
|
1394
|
+
<button type="button" class="btn btn-sm" onclick='editRemoteProvider(${JSON.stringify(provider.id || '')})'>${t('Edit')}</button>
|
|
1395
|
+
<button type="button" class="btn btn-sm" onclick='deleteRemoteProvider(${JSON.stringify(provider.id || '')})'>${t('Delete')}</button>
|
|
1396
|
+
</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
`).join('');
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function showRemoteProviderManageMessage(message, type = 'success') {
|
|
1402
|
+
if (!elements.remoteProviderManageMessage) return;
|
|
1403
|
+
elements.remoteProviderManageMessage.textContent = message;
|
|
1404
|
+
elements.remoteProviderManageMessage.className = `settings-message ${type}`;
|
|
1405
|
+
elements.remoteProviderManageMessage.classList.remove('hidden');
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function hideRemoteProviderManageMessage() {
|
|
1409
|
+
if (elements.remoteProviderManageMessage) {
|
|
1410
|
+
elements.remoteProviderManageMessage.classList.add('hidden');
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function providerAccessBadge(provider, verbose = false) {
|
|
1415
|
+
const mode = provider.accessMode || (provider.apiKeyPresent ? 'apiKey' : 'dataverseProxy');
|
|
1416
|
+
if (mode === 'dataverseProxy') {
|
|
1417
|
+
return ` <span class="badge badge-success">${verbose ? t('Dataverse proxy') : t('proxy')}</span>`;
|
|
1418
|
+
}
|
|
1419
|
+
if (provider.apiKeyPresent) {
|
|
1420
|
+
return ` <span class="badge badge-success">${verbose ? t('key configured') : t('key')}</span>`;
|
|
1421
|
+
}
|
|
1422
|
+
return ` <span class="badge">${verbose ? t('API key') : t('key')}</span>`;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
async function openRemoteProviderSelect() {
|
|
1426
|
+
await loadMetadataProfileProviders();
|
|
1427
|
+
elements.remoteProviderSelectModal.classList.remove('hidden');
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function closeRemoteProviderSelect() {
|
|
1431
|
+
elements.remoteProviderSelectModal.classList.add('hidden');
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function openManageRemoteProviders() {
|
|
1435
|
+
closeRemoteProviderSelect();
|
|
1436
|
+
await loadMetadataProfileProviders();
|
|
1437
|
+
hideRemoteProviderForm();
|
|
1438
|
+
hideRemoteProviderManageMessage();
|
|
1439
|
+
elements.manageRemoteProvidersModal.classList.remove('hidden');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function closeManageRemoteProviders() {
|
|
1443
|
+
elements.manageRemoteProvidersModal.classList.add('hidden');
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function selectRemoteProvider(providerId) {
|
|
1447
|
+
currentMetadataProfileProviderId = providerId || '';
|
|
1448
|
+
closeRemoteProviderSelect();
|
|
1449
|
+
await openCedarBrowser(providerId);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async function openCedarBrowser(providerId) {
|
|
1453
|
+
const provider = metadataProfileProviders.find((item) => item.id === providerId) || {};
|
|
1454
|
+
cedarBrowserState = {
|
|
1455
|
+
providerId: providerId || '',
|
|
1456
|
+
selectedTemplateId: '',
|
|
1457
|
+
selectedTemplateName: '',
|
|
1458
|
+
nodes: [],
|
|
1459
|
+
expanded: new Set(),
|
|
1460
|
+
loading: new Set(),
|
|
1461
|
+
query: '',
|
|
1462
|
+
};
|
|
1463
|
+
elements.cedarBrowserTitle.textContent = t('Browse {0}', provider.title || provider.id || t('Remote'));
|
|
1464
|
+
elements.cedarSearchInput.value = '';
|
|
1465
|
+
elements.cedarSearchInput.classList.add('hidden');
|
|
1466
|
+
elements.cedarBrowserModal.classList.remove('hidden');
|
|
1467
|
+
updateCedarSelection();
|
|
1468
|
+
elements.cedarBrowserTree.innerHTML = `<div class="loading">${t('Loading repository...')}</div>`;
|
|
1469
|
+
try {
|
|
1470
|
+
cedarBrowserState.nodes = await fetchCedarFolder('');
|
|
1471
|
+
renderCedarTree();
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
elements.cedarBrowserTree.innerHTML = `<div class="text-danger">${t('Failed to load repository: {0}', escapeHtml(err.message))}</div>`;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function closeCedarBrowser() {
|
|
1478
|
+
elements.cedarBrowserModal.classList.add('hidden');
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async function fetchCedarFolder(folderId) {
|
|
1482
|
+
const params = new URLSearchParams();
|
|
1483
|
+
if (cedarBrowserState.providerId) params.set('providerId', cedarBrowserState.providerId);
|
|
1484
|
+
if (folderId) params.set('folderId', folderId);
|
|
1485
|
+
const data = await fetchAPI(`/metadata-profiles/remote-folder?${params.toString()}`);
|
|
1486
|
+
return (data.resources || []).map((resource) => ({
|
|
1487
|
+
id: resource.id,
|
|
1488
|
+
name: resource.name,
|
|
1489
|
+
resourceType: resource.resourceType,
|
|
1490
|
+
isFolder: resource.resourceType === 'folder',
|
|
1491
|
+
conformsTo: resource.conformsTo || '',
|
|
1492
|
+
alreadyImported: Boolean(resource.alreadyImported),
|
|
1493
|
+
children: [],
|
|
1494
|
+
childrenLoaded: false,
|
|
1495
|
+
error: '',
|
|
1496
|
+
}));
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function renderCedarTree() {
|
|
1500
|
+
const query = cedarBrowserState.query.trim().toLowerCase();
|
|
1501
|
+
const nodes = query ? filterCedarNodes(cedarBrowserState.nodes, query) : cedarBrowserState.nodes;
|
|
1502
|
+
if (nodes.length === 0) {
|
|
1503
|
+
elements.cedarBrowserTree.innerHTML = `<div class="empty">${query ? t('No results found.') : t('No templates found.')}</div>`;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
elements.cedarBrowserTree.innerHTML = `<ul class="cedar-tree-list">${renderCedarNodes(nodes)}</ul>`;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function renderCedarNodes(nodes) {
|
|
1510
|
+
return nodes.map((node) => {
|
|
1511
|
+
const expanded = cedarBrowserState.expanded.has(node.id);
|
|
1512
|
+
const loading = cedarBrowserState.loading.has(node.id);
|
|
1513
|
+
const selected = cedarBrowserState.selectedTemplateId === node.id;
|
|
1514
|
+
const disabled = node.alreadyImported && !node.isFolder;
|
|
1515
|
+
const childrenHtml = node.isFolder && expanded
|
|
1516
|
+
? `<ul class="cedar-tree-children">${loading ? `<li class="cedar-tree-loading">${t('Loading...')}</li>` : node.error ? `<li class="cedar-tree-error">${escapeHtml(node.error)}</li>` : renderCedarNodes(node.children)}</ul>`
|
|
1517
|
+
: '';
|
|
1518
|
+
return `
|
|
1519
|
+
<li class="cedar-tree-node" id="cedar-node-${escapeAttr(node.id)}">
|
|
1520
|
+
<div class="cedar-tree-row ${selected ? 'selected' : ''} ${disabled ? 'disabled' : ''}" onclick='handleCedarNodeClick(${JSON.stringify(node.id)})'>
|
|
1521
|
+
<button type="button" class="cedar-tree-toggle ${node.isFolder ? '' : 'placeholder'}" onclick='handleCedarToggle(event, ${JSON.stringify(node.id)})'>${node.isFolder ? (expanded ? '⌄' : '›') : ''}</button>
|
|
1522
|
+
<span class="cedar-tree-icon ${node.isFolder ? 'folder' : 'template'}"></span>
|
|
1523
|
+
<span class="cedar-tree-name">${escapeHtml(node.name)}${node.alreadyImported ? ` <span class="badge badge-success">${t('Imported')}</span>` : ''}</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
${childrenHtml}
|
|
1526
|
+
</li>
|
|
1527
|
+
`;
|
|
1528
|
+
}).join('');
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function filterCedarNodes(nodes, query) {
|
|
1532
|
+
return nodes.map((node) => {
|
|
1533
|
+
const ownMatch = node.name.toLowerCase().includes(query);
|
|
1534
|
+
const childMatches = filterCedarNodes(node.children || [], query);
|
|
1535
|
+
if (ownMatch || childMatches.length > 0) {
|
|
1536
|
+
return { ...node, children: childMatches };
|
|
1537
|
+
}
|
|
1538
|
+
return null;
|
|
1539
|
+
}).filter(Boolean);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function findCedarNode(nodes, id) {
|
|
1543
|
+
for (const node of nodes) {
|
|
1544
|
+
if (node.id === id) return node;
|
|
1545
|
+
const found = findCedarNode(node.children || [], id);
|
|
1546
|
+
if (found) return found;
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
async function handleCedarToggle(event, nodeId) {
|
|
1552
|
+
event.stopPropagation();
|
|
1553
|
+
const node = findCedarNode(cedarBrowserState.nodes, nodeId);
|
|
1554
|
+
if (!node || !node.isFolder) return;
|
|
1555
|
+
if (cedarBrowserState.expanded.has(nodeId)) {
|
|
1556
|
+
cedarBrowserState.expanded.delete(nodeId);
|
|
1557
|
+
renderCedarTree();
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
cedarBrowserState.expanded.add(nodeId);
|
|
1561
|
+
if (!node.childrenLoaded) {
|
|
1562
|
+
cedarBrowserState.loading.add(nodeId);
|
|
1563
|
+
renderCedarTree();
|
|
1564
|
+
try {
|
|
1565
|
+
node.children = await fetchCedarFolder(node.id);
|
|
1566
|
+
node.childrenLoaded = true;
|
|
1567
|
+
node.error = '';
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
node.error = err.message;
|
|
1570
|
+
} finally {
|
|
1571
|
+
cedarBrowserState.loading.delete(nodeId);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
renderCedarTree();
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async function handleCedarNodeClick(nodeId) {
|
|
1578
|
+
const node = findCedarNode(cedarBrowserState.nodes, nodeId);
|
|
1579
|
+
if (!node) return;
|
|
1580
|
+
if (node.isFolder) {
|
|
1581
|
+
cedarBrowserState.selectedTemplateId = '';
|
|
1582
|
+
cedarBrowserState.selectedTemplateName = '';
|
|
1583
|
+
updateCedarSelection();
|
|
1584
|
+
await handleCedarToggle({ stopPropagation() {} }, nodeId);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (node.alreadyImported) return;
|
|
1588
|
+
cedarBrowserState.selectedTemplateId = node.id;
|
|
1589
|
+
cedarBrowserState.selectedTemplateName = node.name;
|
|
1590
|
+
updateCedarSelection();
|
|
1591
|
+
renderCedarTree();
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function updateCedarSelection() {
|
|
1595
|
+
const hasSelection = Boolean(cedarBrowserState.selectedTemplateId);
|
|
1596
|
+
elements.addCedarTemplateBtn.disabled = !hasSelection;
|
|
1597
|
+
elements.cedarBrowserSelectionText.textContent = hasSelection
|
|
1598
|
+
? cedarBrowserState.selectedTemplateName
|
|
1599
|
+
: t('Select a template to import...');
|
|
1600
|
+
elements.cedarBrowserSelectionText.classList.toggle('cedar-browser-placeholder', !hasSelection);
|
|
1601
|
+
elements.cedarLocateSelectedBtn.classList.toggle('hidden', !hasSelection);
|
|
1602
|
+
elements.cedarClearSelectionBtn.classList.toggle('hidden', !hasSelection);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function clearCedarSelection() {
|
|
1606
|
+
cedarBrowserState.selectedTemplateId = '';
|
|
1607
|
+
cedarBrowserState.selectedTemplateName = '';
|
|
1608
|
+
updateCedarSelection();
|
|
1609
|
+
renderCedarTree();
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function locateCedarSelection() {
|
|
1613
|
+
if (!cedarBrowserState.selectedTemplateId) return;
|
|
1614
|
+
document.getElementById(`cedar-node-${cedarBrowserState.selectedTemplateId}`)?.scrollIntoView({
|
|
1615
|
+
behavior: 'smooth',
|
|
1616
|
+
block: 'center',
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
async function expandAllCedarNodes(nodes = cedarBrowserState.nodes) {
|
|
1621
|
+
for (const node of nodes) {
|
|
1622
|
+
if (!node.isFolder) continue;
|
|
1623
|
+
cedarBrowserState.expanded.add(node.id);
|
|
1624
|
+
if (!node.childrenLoaded) {
|
|
1625
|
+
cedarBrowserState.loading.add(node.id);
|
|
1626
|
+
renderCedarTree();
|
|
1627
|
+
try {
|
|
1628
|
+
node.children = await fetchCedarFolder(node.id);
|
|
1629
|
+
node.childrenLoaded = true;
|
|
1630
|
+
node.error = '';
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
node.error = err.message;
|
|
1633
|
+
} finally {
|
|
1634
|
+
cedarBrowserState.loading.delete(node.id);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
await expandAllCedarNodes(node.children);
|
|
1638
|
+
}
|
|
1639
|
+
renderCedarTree();
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function collapseAllCedarNodes() {
|
|
1643
|
+
cedarBrowserState.expanded = new Set();
|
|
1644
|
+
renderCedarTree();
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
async function addSelectedCedarTemplate() {
|
|
1648
|
+
if (!cedarBrowserState.selectedTemplateId) return;
|
|
1649
|
+
try {
|
|
1650
|
+
elements.addCedarTemplateBtn.disabled = true;
|
|
1651
|
+
await postAPI('/metadata-profiles/import-known', {
|
|
1652
|
+
templateIdOrUrl: cedarBrowserState.selectedTemplateId,
|
|
1653
|
+
providerId: cedarBrowserState.providerId,
|
|
1654
|
+
});
|
|
1655
|
+
closeCedarBrowser();
|
|
1656
|
+
showMetadataProfilesMessage(t('Remote metadata profile imported.'), 'success');
|
|
1657
|
+
await loadMetadataProfiles();
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
showMetadataProfilesMessage(t('Failed to import remote profile: {0}', err.message), 'error');
|
|
1660
|
+
elements.addCedarTemplateBtn.disabled = false;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function showRemoteProviderForm(provider = null) {
|
|
1665
|
+
elements.remoteProviderForm.classList.remove('hidden');
|
|
1666
|
+
elements.remoteProviderOriginalId.value = provider?.id || '';
|
|
1667
|
+
elements.remoteProviderIdInput.value = provider?.id || '';
|
|
1668
|
+
elements.remoteProviderTitleInput.value = provider?.title || '';
|
|
1669
|
+
elements.remoteProviderBaseUrlInput.value = provider?.displayUrl || provider?.baseUrl || '';
|
|
1670
|
+
elements.remoteProviderDomainInput.value = provider?.domainBase || '';
|
|
1671
|
+
elements.remoteProviderAccessModeInput.value = provider?.accessMode || (provider?.apiKeyPresent ? 'apiKey' : 'dataverseProxy');
|
|
1672
|
+
elements.remoteProviderProxyBaseUrlInput.value = provider?.dataverseProxyBaseUrl || deriveDataverseProxyBaseUrl(provider?.domainBase || provider?.displayUrl || provider?.baseUrl || '');
|
|
1673
|
+
setSecretVisibility(
|
|
1674
|
+
elements.remoteProviderApiKeyInput,
|
|
1675
|
+
elements.remoteProviderApiKeyVisibilityBtn,
|
|
1676
|
+
false,
|
|
1677
|
+
);
|
|
1678
|
+
elements.remoteProviderApiKeyInput.value = '';
|
|
1679
|
+
updateRemoteProviderAccessFields();
|
|
1680
|
+
hideRemoteProviderManageMessage();
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function hideRemoteProviderForm() {
|
|
1684
|
+
elements.remoteProviderForm.classList.add('hidden');
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function updateRemoteProviderAccessFields() {
|
|
1688
|
+
const mode = elements.remoteProviderAccessModeInput.value || 'dataverseProxy';
|
|
1689
|
+
elements.remoteProviderProxyBaseUrlInput.classList.toggle('hidden', mode !== 'dataverseProxy');
|
|
1690
|
+
elements.remoteProviderApiKeyControl.classList.toggle('hidden', mode !== 'apiKey');
|
|
1691
|
+
if (mode === 'dataverseProxy' && !elements.remoteProviderProxyBaseUrlInput.value) {
|
|
1692
|
+
elements.remoteProviderProxyBaseUrlInput.value = deriveDataverseProxyBaseUrl(
|
|
1693
|
+
elements.remoteProviderDomainInput.value || elements.remoteProviderBaseUrlInput.value,
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function deriveDataverseProxyBaseUrl(value) {
|
|
1699
|
+
const trimmed = String(value || '').trim();
|
|
1700
|
+
if (!trimmed) return 'https://repo.researchdata.hu';
|
|
1701
|
+
try {
|
|
1702
|
+
const url = new URL(/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`);
|
|
1703
|
+
const parts = url.hostname.split('.');
|
|
1704
|
+
const first = (parts[0] || '').toLowerCase();
|
|
1705
|
+
if (first === 'schema') {
|
|
1706
|
+
parts[0] = 'repo';
|
|
1707
|
+
} else if (['cedar', 'resource', 'open', 'openview'].includes(first)) {
|
|
1708
|
+
parts.shift();
|
|
1709
|
+
if ((parts[0] || '').toLowerCase() === 'schema') {
|
|
1710
|
+
parts[0] = 'repo';
|
|
1711
|
+
} else {
|
|
1712
|
+
parts.unshift('repo');
|
|
1713
|
+
}
|
|
1714
|
+
} else if (first !== 'repo') {
|
|
1715
|
+
parts.unshift('repo');
|
|
1716
|
+
}
|
|
1717
|
+
url.hostname = parts.join('.');
|
|
1718
|
+
url.pathname = '';
|
|
1719
|
+
url.search = '';
|
|
1720
|
+
url.hash = '';
|
|
1721
|
+
return url.origin;
|
|
1722
|
+
} catch {
|
|
1723
|
+
return trimmed;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function editRemoteProvider(providerId) {
|
|
1728
|
+
const provider = metadataProfileProviders.find((item) => item.id === providerId);
|
|
1729
|
+
if (provider) {
|
|
1730
|
+
showRemoteProviderForm(provider);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async function saveRemoteProvider(event) {
|
|
1735
|
+
event.preventDefault();
|
|
1736
|
+
const payload = {
|
|
1737
|
+
id: (elements.remoteProviderIdInput.value || '').trim(),
|
|
1738
|
+
title: (elements.remoteProviderTitleInput.value || '').trim(),
|
|
1739
|
+
baseUrl: (elements.remoteProviderBaseUrlInput.value || '').trim(),
|
|
1740
|
+
domainBase: (elements.remoteProviderDomainInput.value || '').trim(),
|
|
1741
|
+
accessMode: elements.remoteProviderAccessModeInput.value,
|
|
1742
|
+
dataverseProxyBaseUrl: (elements.remoteProviderProxyBaseUrlInput.value || '').trim(),
|
|
1743
|
+
apiKey: elements.remoteProviderAccessModeInput.value === 'apiKey'
|
|
1744
|
+
? (elements.remoteProviderApiKeyInput.value || '').trim()
|
|
1745
|
+
: '',
|
|
1746
|
+
};
|
|
1747
|
+
if (!payload.id || !payload.title || !payload.baseUrl || !payload.domainBase) {
|
|
1748
|
+
showRemoteProviderManageMessage(t('Provider id, title, base URL and domain base are required.'), 'error');
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (payload.accessMode === 'dataverseProxy' && !payload.dataverseProxyBaseUrl) {
|
|
1752
|
+
payload.dataverseProxyBaseUrl = deriveDataverseProxyBaseUrl(payload.domainBase || payload.baseUrl);
|
|
1753
|
+
}
|
|
1754
|
+
try {
|
|
1755
|
+
const originalId = elements.remoteProviderOriginalId.value;
|
|
1756
|
+
if (originalId && originalId !== payload.id) {
|
|
1757
|
+
await deleteAPI(`/metadata-profiles/providers/${encodeURIComponent(originalId)}`);
|
|
1758
|
+
}
|
|
1759
|
+
const data = await postAPI('/metadata-profiles/providers', payload);
|
|
1760
|
+
metadataProfileProviders = data.providers || [];
|
|
1761
|
+
renderRemoteProviderManageList();
|
|
1762
|
+
renderRemoteProviderSelectList();
|
|
1763
|
+
hideRemoteProviderForm();
|
|
1764
|
+
if (data.warnings && data.warnings.length > 0) {
|
|
1765
|
+
showRemoteProviderManageMessage(data.warnings.join(' '), 'error');
|
|
1766
|
+
} else {
|
|
1767
|
+
showRemoteProviderManageMessage(t('Provider saved.'), 'success');
|
|
1768
|
+
}
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
showRemoteProviderManageMessage(t('Failed to save provider: {0}', err.message), 'error');
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
async function deleteRemoteProvider(providerId) {
|
|
1775
|
+
if (!window.confirm(t("Delete remote provider '{0}'?", providerId))) {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
try {
|
|
1779
|
+
const data = await deleteAPI(`/metadata-profiles/providers/${encodeURIComponent(providerId)}`);
|
|
1780
|
+
metadataProfileProviders = data.providers || [];
|
|
1781
|
+
renderRemoteProviderManageList();
|
|
1782
|
+
renderRemoteProviderSelectList();
|
|
1783
|
+
showRemoteProviderManageMessage(t('Provider deleted.'), 'success');
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
showRemoteProviderManageMessage(t('Failed to delete provider: {0}', err.message), 'error');
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function selectedMetadataProfileProviderId() {
|
|
1790
|
+
return currentMetadataProfileProviderId || metadataProfileProviders[0]?.id || '';
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
async function importMetadataProfileUrl() {
|
|
1794
|
+
const url = (elements.metadataProfileUrlInput.value || '').trim();
|
|
1795
|
+
if (!url) {
|
|
1796
|
+
showMetadataProfilesMessage(t('Profile URL is required.'), 'error');
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
try {
|
|
1800
|
+
await postAPI('/metadata-profiles/import-url', {
|
|
1801
|
+
url,
|
|
1802
|
+
providerId: selectedMetadataProfileProviderId(),
|
|
1803
|
+
});
|
|
1804
|
+
elements.metadataProfileUrlInput.value = '';
|
|
1805
|
+
showMetadataProfilesMessage(t('Metadata profile imported.'), 'success');
|
|
1806
|
+
await loadMetadataProfiles();
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
showMetadataProfilesMessage(t('Failed to import profile: {0}', err.message), 'error');
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async function importKnownMetadataProfile(templateIdOrUrl, conformsTo) {
|
|
1813
|
+
try {
|
|
1814
|
+
await postAPI('/metadata-profiles/import-known', {
|
|
1815
|
+
templateIdOrUrl,
|
|
1816
|
+
conformsTo,
|
|
1817
|
+
providerId: selectedMetadataProfileProviderId(),
|
|
1818
|
+
});
|
|
1819
|
+
showMetadataProfilesMessage(t('Remote metadata profile imported.'), 'success');
|
|
1820
|
+
await loadMetadataProfiles();
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
showMetadataProfilesMessage(t('Failed to import remote profile: {0}', err.message), 'error');
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
async function deleteMetadataProfile(id) {
|
|
1827
|
+
if (!window.confirm(t("Delete metadata profile '{0}' and its files?", id))) {
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
await deleteAPI(`/metadata-profiles/${encodeURIComponent(id)}`);
|
|
1832
|
+
showMetadataProfilesMessage(t('Metadata profile deleted.'), 'success');
|
|
1833
|
+
await loadMetadataProfiles();
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
showMetadataProfilesMessage(t('Failed to delete metadata profile: {0}', err.message), 'error');
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function openSettings() {
|
|
1840
|
+
loadSettings();
|
|
1841
|
+
elements.settingsModal.classList.remove('hidden');
|
|
1842
|
+
elements.settingsMessage.classList.add('hidden');
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function closeSettings() {
|
|
1846
|
+
elements.settingsModal.classList.add('hidden');
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function openTavilyTest() {
|
|
1850
|
+
clearTavilyTestResult();
|
|
1851
|
+
elements.tavilyTestModal.classList.remove('hidden');
|
|
1852
|
+
elements.tavilyQueryInput.focus();
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function closeTavilyTest() {
|
|
1856
|
+
elements.tavilyTestModal.classList.add('hidden');
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function openMetadataProfiles() {
|
|
1860
|
+
loadMetadataProfileProviders();
|
|
1861
|
+
loadMetadataProfiles();
|
|
1862
|
+
elements.metadataProfilesModal.classList.remove('hidden');
|
|
1863
|
+
elements.metadataProfilesMessage.classList.add('hidden');
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function closeMetadataProfiles() {
|
|
1867
|
+
elements.metadataProfilesModal.classList.add('hidden');
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function closeOpenModals() {
|
|
1871
|
+
if (elements.tavilyTestModal && !elements.tavilyTestModal.classList.contains('hidden')) {
|
|
1872
|
+
closeTavilyTest();
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (elements.sessionModal && !elements.sessionModal.classList.contains('hidden')) {
|
|
1876
|
+
elements.sessionModal.classList.add('hidden');
|
|
1877
|
+
}
|
|
1878
|
+
if (elements.toolCallModal && !elements.toolCallModal.classList.contains('hidden')) {
|
|
1879
|
+
elements.toolCallModal.classList.add('hidden');
|
|
1880
|
+
}
|
|
1881
|
+
if (elements.settingsModal && !elements.settingsModal.classList.contains('hidden')) {
|
|
1882
|
+
elements.settingsModal.classList.add('hidden');
|
|
1883
|
+
}
|
|
1884
|
+
if (elements.metadataProfilesModal && !elements.metadataProfilesModal.classList.contains('hidden')) {
|
|
1885
|
+
elements.metadataProfilesModal.classList.add('hidden');
|
|
1886
|
+
}
|
|
1887
|
+
if (elements.remoteProviderSelectModal && !elements.remoteProviderSelectModal.classList.contains('hidden')) {
|
|
1888
|
+
elements.remoteProviderSelectModal.classList.add('hidden');
|
|
1889
|
+
}
|
|
1890
|
+
if (elements.manageRemoteProvidersModal && !elements.manageRemoteProvidersModal.classList.contains('hidden')) {
|
|
1891
|
+
elements.manageRemoteProvidersModal.classList.add('hidden');
|
|
1892
|
+
}
|
|
1893
|
+
if (elements.cedarBrowserModal && !elements.cedarBrowserModal.classList.contains('hidden')) {
|
|
1894
|
+
elements.cedarBrowserModal.classList.add('hidden');
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Update dependencies table
|
|
1899
|
+
async function updateDependencies() {
|
|
1900
|
+
try {
|
|
1901
|
+
const data = await fetchAPI('/dependencies');
|
|
1902
|
+
|
|
1903
|
+
if (data.dependencies.length === 0) {
|
|
1904
|
+
elements.dependenciesTableBody.innerHTML = `<tr><td colspan="5" class="empty">${t('No dependency usage recorded')}</td></tr>`;
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
elements.dependenciesTableBody.innerHTML = data.dependencies.map(dep => `
|
|
1909
|
+
<tr>
|
|
1910
|
+
<td><code>${escapeHtml(dep.dependency)}</code></td>
|
|
1911
|
+
<td>${dep.callCount.toLocaleString()}</td>
|
|
1912
|
+
<td>${dep.successRate}</td>
|
|
1913
|
+
<td>${dep.avgLatency}</td>
|
|
1914
|
+
<td>${localizeRelativeTime(dep.lastCall)}</td>
|
|
1915
|
+
</tr>
|
|
1916
|
+
`).join('');
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
elements.dependenciesTableBody.innerHTML = `<tr><td colspan="5" class="text-danger">${t('Failed to load: {0}', err.message)}</td></tr>`;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Escape HTML to prevent XSS
|
|
1923
|
+
function escapeHtml(str) {
|
|
1924
|
+
if (!str) return '';
|
|
1925
|
+
const div = document.createElement('div');
|
|
1926
|
+
div.textContent = str;
|
|
1927
|
+
return div.innerHTML;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function escapeAttr(str) {
|
|
1931
|
+
return String(str || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// Refresh all data
|
|
1935
|
+
async function refreshAll() {
|
|
1936
|
+
const icon = elements.refreshBtn.querySelector('.icon');
|
|
1937
|
+
icon.classList.add('spinning');
|
|
1938
|
+
|
|
1939
|
+
try {
|
|
1940
|
+
await Promise.all([
|
|
1941
|
+
updateOverview(),
|
|
1942
|
+
updateTools(),
|
|
1943
|
+
updateErrors(),
|
|
1944
|
+
updateSessions(),
|
|
1945
|
+
updateDependencies(),
|
|
1946
|
+
]);
|
|
1947
|
+
} finally {
|
|
1948
|
+
icon.classList.remove('spinning');
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Event Listeners
|
|
1953
|
+
elements.timeRange.addEventListener('change', (e) => {
|
|
1954
|
+
currentMinutes = e.target.value === 'all' ? 'all' : parseInt(e.target.value, 10);
|
|
1955
|
+
refreshAll();
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
elements.refreshBtn.addEventListener('click', refreshAll);
|
|
1959
|
+
|
|
1960
|
+
elements.closeSessionModal.addEventListener('click', () => {
|
|
1961
|
+
elements.sessionModal.classList.add('hidden');
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
elements.sessionModal.addEventListener('click', (e) => {
|
|
1965
|
+
if (e.target === elements.sessionModal) {
|
|
1966
|
+
elements.sessionModal.classList.add('hidden');
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
// Tool call modal event listeners
|
|
1971
|
+
if (elements.closeToolCallModal) {
|
|
1972
|
+
elements.closeToolCallModal.addEventListener('click', () => {
|
|
1973
|
+
elements.toolCallModal.classList.add('hidden');
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
elements.toolCallModal.addEventListener('click', (e) => {
|
|
1977
|
+
if (e.target === elements.toolCallModal) {
|
|
1978
|
+
elements.toolCallModal.classList.add('hidden');
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Settings modal event listeners
|
|
1984
|
+
if (elements.settingsBtn) {
|
|
1985
|
+
elements.settingsBtn.addEventListener('click', openSettings);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (elements.metadataProfilesBtn) {
|
|
1989
|
+
elements.metadataProfilesBtn.addEventListener('click', openMetadataProfiles);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
if (elements.shutdownBtn) {
|
|
1993
|
+
elements.shutdownBtn.addEventListener('click', shutdownMcp);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (elements.closeSettingsModal) {
|
|
1997
|
+
elements.closeSettingsModal.addEventListener('click', closeSettings);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (elements.cancelSettingsBtn) {
|
|
2001
|
+
elements.cancelSettingsBtn.addEventListener('click', closeSettings);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
if (elements.closeMetadataProfilesModal) {
|
|
2005
|
+
elements.closeMetadataProfilesModal.addEventListener('click', closeMetadataProfiles);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
if (elements.settingsForm) {
|
|
2009
|
+
elements.settingsForm.addEventListener('submit', saveSettings);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (elements.openTavilyTestBtn) {
|
|
2013
|
+
elements.openTavilyTestBtn.addEventListener('click', openTavilyTest);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
if (elements.closeTavilyTestModal) {
|
|
2017
|
+
elements.closeTavilyTestModal.addEventListener('click', closeTavilyTest);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
bindRuntimeSettingInput(
|
|
2021
|
+
elements.dataverseBaseUrlInput,
|
|
2022
|
+
elements.dataverseBaseUrlUseEnv,
|
|
2023
|
+
);
|
|
2024
|
+
bindRuntimeSettingInput(
|
|
2025
|
+
elements.dataverseApiKeyInput,
|
|
2026
|
+
elements.dataverseApiKeyUseEnv,
|
|
2027
|
+
);
|
|
2028
|
+
bindRuntimeSettingInput(
|
|
2029
|
+
elements.tavilyApiKeyInput,
|
|
2030
|
+
elements.tavilyApiKeyUseEnv,
|
|
2031
|
+
);
|
|
2032
|
+
bindSecretVisibility(
|
|
2033
|
+
elements.dataverseApiKeyInput,
|
|
2034
|
+
elements.dataverseApiKeyVisibilityBtn,
|
|
2035
|
+
'DATAVERSE_API_KEY',
|
|
2036
|
+
);
|
|
2037
|
+
bindSecretVisibility(
|
|
2038
|
+
elements.tavilyApiKeyInput,
|
|
2039
|
+
elements.tavilyApiKeyVisibilityBtn,
|
|
2040
|
+
'TAVILY_API_KEY',
|
|
2041
|
+
);
|
|
2042
|
+
bindSecretVisibility(
|
|
2043
|
+
elements.remoteProviderApiKeyInput,
|
|
2044
|
+
elements.remoteProviderApiKeyVisibilityBtn,
|
|
2045
|
+
);
|
|
2046
|
+
|
|
2047
|
+
if (elements.settingsModal) {
|
|
2048
|
+
elements.settingsModal.addEventListener('click', (e) => {
|
|
2049
|
+
if (e.target === elements.settingsModal) {
|
|
2050
|
+
closeSettings();
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
if (elements.tavilyTestModal) {
|
|
2056
|
+
elements.tavilyTestModal.addEventListener('click', (e) => {
|
|
2057
|
+
if (e.target === elements.tavilyTestModal) {
|
|
2058
|
+
closeTavilyTest();
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
if (elements.metadataProfilesModal) {
|
|
2064
|
+
elements.metadataProfilesModal.addEventListener('click', (e) => {
|
|
2065
|
+
if (e.target === elements.metadataProfilesModal) {
|
|
2066
|
+
closeMetadataProfiles();
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (elements.remoteProviderSelectModal) {
|
|
2072
|
+
elements.remoteProviderSelectModal.addEventListener('click', (e) => {
|
|
2073
|
+
if (e.target === elements.remoteProviderSelectModal) {
|
|
2074
|
+
closeRemoteProviderSelect();
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
if (elements.manageRemoteProvidersModal) {
|
|
2080
|
+
elements.manageRemoteProvidersModal.addEventListener('click', (e) => {
|
|
2081
|
+
if (e.target === elements.manageRemoteProvidersModal) {
|
|
2082
|
+
closeManageRemoteProviders();
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (elements.cedarBrowserModal) {
|
|
2088
|
+
elements.cedarBrowserModal.addEventListener('click', (e) => {
|
|
2089
|
+
if (e.target === elements.cedarBrowserModal) {
|
|
2090
|
+
closeCedarBrowser();
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
if (elements.importMetadataProfileBtn) {
|
|
2096
|
+
elements.importMetadataProfileBtn.addEventListener('click', importMetadataProfileUrl);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
if (elements.browseMetadataProfilesBtn) {
|
|
2100
|
+
elements.browseMetadataProfilesBtn.addEventListener('click', openRemoteProviderSelect);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (elements.manageMetadataProvidersBtn) {
|
|
2104
|
+
elements.manageMetadataProvidersBtn.addEventListener('click', openManageRemoteProviders);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (elements.reloadMetadataProfilesBtn) {
|
|
2108
|
+
elements.reloadMetadataProfilesBtn.addEventListener('click', loadMetadataProfiles);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
if (elements.metadataProfilesPrevPageBtn) {
|
|
2112
|
+
elements.metadataProfilesPrevPageBtn.addEventListener('click', () => {
|
|
2113
|
+
metadataProfilesState.page = Math.max(1, metadataProfilesState.page - 1);
|
|
2114
|
+
renderMetadataProfilesTable();
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
if (elements.metadataProfilesNextPageBtn) {
|
|
2119
|
+
elements.metadataProfilesNextPageBtn.addEventListener('click', () => {
|
|
2120
|
+
metadataProfilesState.page += 1;
|
|
2121
|
+
renderMetadataProfilesTable();
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
if (elements.metadataProfilesPageSizeSelect) {
|
|
2126
|
+
elements.metadataProfilesPageSizeSelect.addEventListener('change', (event) => {
|
|
2127
|
+
metadataProfilesState.pageSize = Number(event.target.value) || 10;
|
|
2128
|
+
metadataProfilesState.page = 1;
|
|
2129
|
+
renderMetadataProfilesTable();
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
if (elements.closeRemoteProviderSelectModal) {
|
|
2134
|
+
elements.closeRemoteProviderSelectModal.addEventListener('click', closeRemoteProviderSelect);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
if (elements.cancelRemoteProviderSelectBtn) {
|
|
2138
|
+
elements.cancelRemoteProviderSelectBtn.addEventListener('click', closeRemoteProviderSelect);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
if (elements.openManageProvidersFromSelectBtn) {
|
|
2142
|
+
elements.openManageProvidersFromSelectBtn.addEventListener('click', openManageRemoteProviders);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
if (elements.closeManageRemoteProvidersModal) {
|
|
2146
|
+
elements.closeManageRemoteProvidersModal.addEventListener('click', closeManageRemoteProviders);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
if (elements.closeManageRemoteProvidersBtn) {
|
|
2150
|
+
elements.closeManageRemoteProvidersBtn.addEventListener('click', closeManageRemoteProviders);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if (elements.addRemoteProviderBtn) {
|
|
2154
|
+
elements.addRemoteProviderBtn.addEventListener('click', () => showRemoteProviderForm());
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
if (elements.cancelRemoteProviderFormBtn) {
|
|
2158
|
+
elements.cancelRemoteProviderFormBtn.addEventListener('click', hideRemoteProviderForm);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (elements.remoteProviderAccessModeInput) {
|
|
2162
|
+
elements.remoteProviderAccessModeInput.addEventListener('change', updateRemoteProviderAccessFields);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (elements.remoteProviderBaseUrlInput) {
|
|
2166
|
+
elements.remoteProviderBaseUrlInput.addEventListener('input', () => {
|
|
2167
|
+
if (elements.remoteProviderAccessModeInput.value === 'dataverseProxy') {
|
|
2168
|
+
elements.remoteProviderProxyBaseUrlInput.value = deriveDataverseProxyBaseUrl(
|
|
2169
|
+
elements.remoteProviderDomainInput.value || elements.remoteProviderBaseUrlInput.value,
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
if (elements.remoteProviderDomainInput) {
|
|
2176
|
+
elements.remoteProviderDomainInput.addEventListener('input', () => {
|
|
2177
|
+
if (elements.remoteProviderAccessModeInput.value === 'dataverseProxy') {
|
|
2178
|
+
elements.remoteProviderProxyBaseUrlInput.value = deriveDataverseProxyBaseUrl(
|
|
2179
|
+
elements.remoteProviderDomainInput.value || elements.remoteProviderBaseUrlInput.value,
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
if (elements.remoteProviderForm) {
|
|
2186
|
+
elements.remoteProviderForm.addEventListener('submit', saveRemoteProvider);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if (elements.closeCedarBrowserModal) {
|
|
2190
|
+
elements.closeCedarBrowserModal.addEventListener('click', closeCedarBrowser);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (elements.cancelCedarBrowserBtn) {
|
|
2194
|
+
elements.cancelCedarBrowserBtn.addEventListener('click', closeCedarBrowser);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
if (elements.cedarSearchToggleBtn) {
|
|
2198
|
+
elements.cedarSearchToggleBtn.addEventListener('click', () => {
|
|
2199
|
+
elements.cedarSearchInput.classList.toggle('hidden');
|
|
2200
|
+
if (!elements.cedarSearchInput.classList.contains('hidden')) {
|
|
2201
|
+
elements.cedarSearchInput.focus();
|
|
2202
|
+
} else {
|
|
2203
|
+
elements.cedarSearchInput.value = '';
|
|
2204
|
+
cedarBrowserState.query = '';
|
|
2205
|
+
renderCedarTree();
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
if (elements.cedarSearchInput) {
|
|
2211
|
+
elements.cedarSearchInput.addEventListener('input', () => {
|
|
2212
|
+
cedarBrowserState.query = elements.cedarSearchInput.value || '';
|
|
2213
|
+
renderCedarTree();
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (elements.cedarExpandAllBtn) {
|
|
2218
|
+
elements.cedarExpandAllBtn.addEventListener('click', () => {
|
|
2219
|
+
expandAllCedarNodes();
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
if (elements.cedarCollapseAllBtn) {
|
|
2224
|
+
elements.cedarCollapseAllBtn.addEventListener('click', collapseAllCedarNodes);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if (elements.cedarLocateSelectedBtn) {
|
|
2228
|
+
elements.cedarLocateSelectedBtn.addEventListener('click', locateCedarSelection);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (elements.cedarClearSelectionBtn) {
|
|
2232
|
+
elements.cedarClearSelectionBtn.addEventListener('click', clearCedarSelection);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (elements.addCedarTemplateBtn) {
|
|
2236
|
+
elements.addCedarTemplateBtn.addEventListener('click', addSelectedCedarTemplate);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// Tavily test form event listeners
|
|
2240
|
+
if (elements.tavilyTestForm) {
|
|
2241
|
+
elements.tavilyTestForm.addEventListener('submit', testTavilySearch);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (elements.tavilyClearBtn) {
|
|
2245
|
+
elements.tavilyClearBtn.addEventListener('click', clearTavilyTestResult);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
document.addEventListener('keydown', (e) => {
|
|
2249
|
+
if (e.key === 'Escape') {
|
|
2250
|
+
closeOpenModals();
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
// Make viewSession and viewToolCall available globally
|
|
2255
|
+
window.viewSession = viewSession;
|
|
2256
|
+
window.viewToolCall = viewToolCall;
|
|
2257
|
+
window.deleteMetadataProfile = deleteMetadataProfile;
|
|
2258
|
+
window.importKnownMetadataProfile = importKnownMetadataProfile;
|
|
2259
|
+
window.selectRemoteProvider = selectRemoteProvider;
|
|
2260
|
+
window.editRemoteProvider = editRemoteProvider;
|
|
2261
|
+
window.deleteRemoteProvider = deleteRemoteProvider;
|
|
2262
|
+
window.handleCedarToggle = handleCedarToggle;
|
|
2263
|
+
window.handleCedarNodeClick = handleCedarNodeClick;
|
|
2264
|
+
|
|
2265
|
+
// Tavily search test functions
|
|
2266
|
+
async function testTavilySearch(e) {
|
|
2267
|
+
e.preventDefault();
|
|
2268
|
+
|
|
2269
|
+
const query = elements.tavilyQueryInput.value.trim();
|
|
2270
|
+
const maxResults = parseInt(elements.tavilyMaxResults.value, 10);
|
|
2271
|
+
const searchDepth = elements.tavilySearchDepth.value;
|
|
2272
|
+
|
|
2273
|
+
if (!query) {
|
|
2274
|
+
showTavilyTestResult(t('Please enter a search query'), 'error');
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// Show loading state
|
|
2279
|
+
elements.tavilyTestBtn.disabled = true;
|
|
2280
|
+
const originalBtnContent = elements.tavilyTestBtn.innerHTML;
|
|
2281
|
+
elements.tavilyTestBtn.innerHTML = `<span class="icon spinning">↻</span> ${t('Testing...')}`;
|
|
2282
|
+
|
|
2283
|
+
showTavilyTestResult(t('Running test search...'), 'info');
|
|
2284
|
+
|
|
2285
|
+
try {
|
|
2286
|
+
const result = await postAPI('/test/tavily-search', {
|
|
2287
|
+
query,
|
|
2288
|
+
max_results: maxResults,
|
|
2289
|
+
search_depth: searchDepth,
|
|
2290
|
+
});
|
|
2291
|
+
|
|
2292
|
+
if (result.success) {
|
|
2293
|
+
// Display successful results
|
|
2294
|
+
let resultHtml = `
|
|
2295
|
+
<div class="settings-message success" style="margin-top: 1rem;">
|
|
2296
|
+
<strong>✓ ${t('Search successful!')}</strong><br>
|
|
2297
|
+
${t('Query')}: ${escapeHtml(result.query)}<br>
|
|
2298
|
+
${t('Latency')}: ${result.latencyMs}ms
|
|
2299
|
+
</div>
|
|
2300
|
+
`;
|
|
2301
|
+
|
|
2302
|
+
if (result.result && result.result.answer) {
|
|
2303
|
+
resultHtml += `
|
|
2304
|
+
<div style="margin-top: 1rem; padding: 1rem; background: var(--color-bg-tertiary); border-radius: var(--border-radius);">
|
|
2305
|
+
<h4 style="margin-bottom: 0.5rem;">${t('Answer')}:</h4>
|
|
2306
|
+
<p style="color: var(--color-text); line-height: 1.5;">${escapeHtml(result.result.answer)}</p>
|
|
2307
|
+
</div>
|
|
2308
|
+
`;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (result.result && result.result.results && Array.isArray(result.result.results)) {
|
|
2312
|
+
resultHtml += `
|
|
2313
|
+
<div style="margin-top: 1rem;">
|
|
2314
|
+
<h4 style="margin-bottom: 0.5rem;">${t('Results')} (${result.result.results.length}):</h4>
|
|
2315
|
+
<div style="max-height: 300px; overflow-y: auto;">
|
|
2316
|
+
`;
|
|
2317
|
+
|
|
2318
|
+
result.result.results.forEach((item, index) => {
|
|
2319
|
+
resultHtml += `
|
|
2320
|
+
<div style="padding: 0.75rem; margin-bottom: 0.5rem; background: var(--color-bg-tertiary); border-radius: var(--border-radius);">
|
|
2321
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
2322
|
+
<strong style="color: var(--color-primary);">${index + 1}. ${escapeHtml(item.title || t('Untitled'))}</strong>
|
|
2323
|
+
${item.score ? `<span class="badge badge-neutral">${t('Score')}: ${item.score.toFixed(2)}</span>` : ''}
|
|
2324
|
+
</div>
|
|
2325
|
+
${item.url ? `<div style="margin-top: 0.25rem;"><a href="${escapeHtml(item.url)}" target="_blank" style="color: var(--color-info); font-size: 0.875rem;">${escapeHtml(item.url)}</a></div>` : ''}
|
|
2326
|
+
${item.content ? `<div style="margin-top: 0.5rem; font-size: 0.875rem; color: var(--color-text-secondary);">${escapeHtml(item.content.slice(0, 200))}${item.content.length > 200 ? '...' : ''}</div>` : ''}
|
|
2327
|
+
</div>
|
|
2328
|
+
`;
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
resultHtml += `
|
|
2332
|
+
</div>
|
|
2333
|
+
</div>
|
|
2334
|
+
`;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
elements.tavilyTestResult.innerHTML = resultHtml;
|
|
2338
|
+
} else {
|
|
2339
|
+
// Display error
|
|
2340
|
+
let errorHtml = `
|
|
2341
|
+
<div class="settings-message error" style="margin-top: 1rem;">
|
|
2342
|
+
<strong>✗ ${t('Search failed')}</strong><br>
|
|
2343
|
+
`;
|
|
2344
|
+
|
|
2345
|
+
if (!result.apiKeyPresent) {
|
|
2346
|
+
errorHtml += `${t('TAVILY_API_KEY environment variable is not set on the server.')}<br>`;
|
|
2347
|
+
errorHtml += t('Please set the environment variable and restart the server.');
|
|
2348
|
+
} else {
|
|
2349
|
+
errorHtml += `${escapeHtml(result.error || t('Unknown error'))}`;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
errorHtml += `</div>`;
|
|
2353
|
+
elements.tavilyTestResult.innerHTML = errorHtml;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
elements.tavilyTestResult.classList.remove('hidden');
|
|
2357
|
+
} catch (err) {
|
|
2358
|
+
showTavilyTestResult(t('Request failed: {0}', escapeHtml(err.message)), 'error');
|
|
2359
|
+
} finally {
|
|
2360
|
+
elements.tavilyTestBtn.disabled = false;
|
|
2361
|
+
elements.tavilyTestBtn.innerHTML = originalBtnContent;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function showTavilyTestResult(message, type) {
|
|
2366
|
+
const colorClass = type === 'error' ? 'error' : type === 'success' ? 'success' : 'info';
|
|
2367
|
+
const bgColor = type === 'error' ? 'var(--color-danger-bg)' : type === 'success' ? 'var(--color-success-bg)' : 'var(--color-info-bg)';
|
|
2368
|
+
const textColor = type === 'error' ? '#ffa198' : type === 'success' ? '#7ee787' : '#79c0ff';
|
|
2369
|
+
|
|
2370
|
+
elements.tavilyTestResult.innerHTML = `
|
|
2371
|
+
<div class="settings-message ${colorClass}" style="margin-top: 1rem;">
|
|
2372
|
+
${escapeHtml(message)}
|
|
2373
|
+
</div>
|
|
2374
|
+
`;
|
|
2375
|
+
elements.tavilyTestResult.classList.remove('hidden');
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function clearTavilyTestResult() {
|
|
2379
|
+
elements.tavilyQueryInput.value = '';
|
|
2380
|
+
elements.tavilyTestResult.classList.add('hidden');
|
|
2381
|
+
elements.tavilyTestResult.innerHTML = '';
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
async function initializeDashboard() {
|
|
2385
|
+
const requestedLocale = new URLSearchParams(window.location.search).get('lang');
|
|
2386
|
+
try {
|
|
2387
|
+
const config = await fetchAPI('/config');
|
|
2388
|
+
const locale = requestedLocale || config.locale || navigator.language || 'en';
|
|
2389
|
+
dashboardLocale = locale.toLowerCase().startsWith('hu') ? 'hu' : 'en';
|
|
2390
|
+
} catch {
|
|
2391
|
+
const locale = requestedLocale || navigator.language || 'en';
|
|
2392
|
+
dashboardLocale = locale.toLowerCase().startsWith('hu') ? 'hu' : 'en';
|
|
2393
|
+
}
|
|
2394
|
+
applyStaticTranslations();
|
|
2395
|
+
await refreshAll();
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Initial load
|
|
2399
|
+
void initializeDashboard();
|
|
2400
|
+
|
|
2401
|
+
// Auto-refresh every 30 seconds
|
|
2402
|
+
setInterval(refreshAll, 30000);
|