stratifyai 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- api/__init__.py +3 -0
- api/main.py +763 -0
- api/static/index.html +1126 -0
- api/static/models.html +567 -0
- api/static/stratifyai_trans_logo.png +0 -0
- api/static/stratifyai_wide_logo.png +0 -0
- api/static/stratum_logo.png +0 -0
- {stratifyai-0.1.2.dist-info → stratifyai-0.1.3.dist-info}/METADATA +4 -1
- {stratifyai-0.1.2.dist-info → stratifyai-0.1.3.dist-info}/RECORD +13 -6
- {stratifyai-0.1.2.dist-info → stratifyai-0.1.3.dist-info}/top_level.txt +1 -0
- {stratifyai-0.1.2.dist-info → stratifyai-0.1.3.dist-info}/WHEEL +0 -0
- {stratifyai-0.1.2.dist-info → stratifyai-0.1.3.dist-info}/entry_points.txt +0 -0
- {stratifyai-0.1.2.dist-info → stratifyai-0.1.3.dist-info}/licenses/LICENSE +0 -0
api/static/index.html
ADDED
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>StratifyAI - Multi-Provider LLM Interface</title>
|
|
7
|
+
<!-- Markdown rendering -->
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
9
|
+
<!-- Syntax highlighting -->
|
|
10
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
11
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
* {
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
21
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
padding: 20px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.container {
|
|
27
|
+
max-width: 1200px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.header {
|
|
32
|
+
background: white;
|
|
33
|
+
padding: 20px 30px;
|
|
34
|
+
border-radius: 10px;
|
|
35
|
+
margin-bottom: 20px;
|
|
36
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.header h1 {
|
|
40
|
+
color: #667eea;
|
|
41
|
+
margin-bottom: 5px;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 15px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.header h1 img {
|
|
48
|
+
height: 50px;
|
|
49
|
+
width: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.header p {
|
|
53
|
+
color: #666;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.main-grid {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: 300px 1fr;
|
|
59
|
+
gap: 20px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.sidebar {
|
|
63
|
+
background: white;
|
|
64
|
+
padding: 20px;
|
|
65
|
+
border-radius: 10px;
|
|
66
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
67
|
+
height: fit-content;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.sidebar h3 {
|
|
71
|
+
color: #333;
|
|
72
|
+
margin-bottom: 15px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.form-group {
|
|
76
|
+
margin-bottom: 15px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
label {
|
|
80
|
+
display: block;
|
|
81
|
+
color: #666;
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
margin-bottom: 5px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
select, textarea, input, button {
|
|
87
|
+
width: 100%;
|
|
88
|
+
padding: 10px;
|
|
89
|
+
border: 1px solid #ddd;
|
|
90
|
+
border-radius: 5px;
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
textarea {
|
|
95
|
+
min-height: 100px;
|
|
96
|
+
resize: vertical;
|
|
97
|
+
font-family: inherit;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
button {
|
|
101
|
+
background: #667eea;
|
|
102
|
+
color: white;
|
|
103
|
+
border: none;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
transition: background 0.3s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
button:hover {
|
|
110
|
+
background: #5568d3;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
button:disabled {
|
|
114
|
+
background: #ccc;
|
|
115
|
+
cursor: not-allowed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.main-content {
|
|
119
|
+
background: white;
|
|
120
|
+
padding: 20px;
|
|
121
|
+
border-radius: 10px;
|
|
122
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.chat-container {
|
|
126
|
+
border: 1px solid #ddd;
|
|
127
|
+
border-radius: 5px;
|
|
128
|
+
min-height: 400px;
|
|
129
|
+
max-height: 600px;
|
|
130
|
+
overflow-y: auto;
|
|
131
|
+
padding: 15px;
|
|
132
|
+
background: #f9f9f9;
|
|
133
|
+
margin-bottom: 15px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.message {
|
|
137
|
+
margin-bottom: 15px;
|
|
138
|
+
padding: 10px 15px;
|
|
139
|
+
border-radius: 8px;
|
|
140
|
+
max-width: 80%;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.message.user {
|
|
144
|
+
background: #667eea;
|
|
145
|
+
color: white;
|
|
146
|
+
margin-left: auto;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.message.assistant {
|
|
150
|
+
background: white;
|
|
151
|
+
border: 1px solid #ddd;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.message.system {
|
|
155
|
+
background: #f0f0f0;
|
|
156
|
+
font-style: italic;
|
|
157
|
+
max-width: 100%;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.message.error {
|
|
161
|
+
background: #fee;
|
|
162
|
+
border: 1px solid #fcc;
|
|
163
|
+
color: #c33;
|
|
164
|
+
max-width: 100%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Markdown content styling */
|
|
168
|
+
.message.assistant .markdown-content {
|
|
169
|
+
line-height: 1.6;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.message.assistant .markdown-content p {
|
|
173
|
+
margin: 0 0 10px 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.message.assistant .markdown-content p:last-child {
|
|
177
|
+
margin-bottom: 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.message.assistant .markdown-content pre {
|
|
181
|
+
background: #f6f8fa;
|
|
182
|
+
border: 1px solid #e1e4e8;
|
|
183
|
+
border-radius: 6px;
|
|
184
|
+
padding: 12px;
|
|
185
|
+
overflow-x: auto;
|
|
186
|
+
margin: 10px 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.message.assistant .markdown-content code {
|
|
190
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
191
|
+
font-size: 13px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.message.assistant .markdown-content :not(pre) > code {
|
|
195
|
+
background: #f0f0f0;
|
|
196
|
+
padding: 2px 6px;
|
|
197
|
+
border-radius: 4px;
|
|
198
|
+
font-size: 13px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.message.assistant .markdown-content ul,
|
|
202
|
+
.message.assistant .markdown-content ol {
|
|
203
|
+
margin: 10px 0;
|
|
204
|
+
padding-left: 24px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.message.assistant .markdown-content li {
|
|
208
|
+
margin: 4px 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.message.assistant .markdown-content h1,
|
|
212
|
+
.message.assistant .markdown-content h2,
|
|
213
|
+
.message.assistant .markdown-content h3,
|
|
214
|
+
.message.assistant .markdown-content h4 {
|
|
215
|
+
margin: 16px 0 8px 0;
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.message.assistant .markdown-content h1 { font-size: 1.4em; }
|
|
220
|
+
.message.assistant .markdown-content h2 { font-size: 1.25em; }
|
|
221
|
+
.message.assistant .markdown-content h3 { font-size: 1.1em; }
|
|
222
|
+
|
|
223
|
+
.message.assistant .markdown-content blockquote {
|
|
224
|
+
border-left: 4px solid #ddd;
|
|
225
|
+
margin: 10px 0;
|
|
226
|
+
padding: 8px 16px;
|
|
227
|
+
color: #666;
|
|
228
|
+
background: #f9f9f9;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.message.assistant .markdown-content table {
|
|
232
|
+
border-collapse: collapse;
|
|
233
|
+
margin: 10px 0;
|
|
234
|
+
width: 100%;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.message.assistant .markdown-content th,
|
|
238
|
+
.message.assistant .markdown-content td {
|
|
239
|
+
border: 1px solid #ddd;
|
|
240
|
+
padding: 8px 12px;
|
|
241
|
+
text-align: left;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.message.assistant .markdown-content th {
|
|
245
|
+
background: #f6f8fa;
|
|
246
|
+
font-weight: 600;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.message.assistant .markdown-content hr {
|
|
250
|
+
border: none;
|
|
251
|
+
border-top: 1px solid #ddd;
|
|
252
|
+
margin: 16px 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.message.assistant .markdown-content a {
|
|
256
|
+
color: #667eea;
|
|
257
|
+
text-decoration: none;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.message.assistant .markdown-content a:hover {
|
|
261
|
+
text-decoration: underline;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.error-details {
|
|
265
|
+
font-size: 12px;
|
|
266
|
+
margin-top: 8px;
|
|
267
|
+
padding: 8px;
|
|
268
|
+
background: #fff;
|
|
269
|
+
border-radius: 4px;
|
|
270
|
+
font-family: monospace;
|
|
271
|
+
white-space: pre-wrap;
|
|
272
|
+
word-break: break-word;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.cost-tracker {
|
|
276
|
+
background: #f9f9f9;
|
|
277
|
+
padding: 15px;
|
|
278
|
+
border-radius: 5px;
|
|
279
|
+
margin-top: 15px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.cost-tracker h4 {
|
|
283
|
+
color: #333;
|
|
284
|
+
margin-bottom: 10px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.validation-status {
|
|
288
|
+
font-size: 13px;
|
|
289
|
+
margin-top: 8px;
|
|
290
|
+
padding: 6px 10px;
|
|
291
|
+
border-radius: 4px;
|
|
292
|
+
background: #f0f9ff;
|
|
293
|
+
border-left: 3px solid #3b82f6;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.validation-status.success {
|
|
297
|
+
background: #f0fdf4;
|
|
298
|
+
border-left-color: #22c55e;
|
|
299
|
+
color: #166534;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.validation-status.warning {
|
|
303
|
+
background: #fffbeb;
|
|
304
|
+
border-left-color: #f59e0b;
|
|
305
|
+
color: #92400e;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.validation-status.loading {
|
|
309
|
+
background: #f3f4f6;
|
|
310
|
+
border-left-color: #6b7280;
|
|
311
|
+
color: #374151;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.cost-row {
|
|
315
|
+
display: flex;
|
|
316
|
+
justify-content: space-between;
|
|
317
|
+
padding: 5px 0;
|
|
318
|
+
border-bottom: 1px solid #ddd;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.cost-row:last-child {
|
|
322
|
+
border-bottom: none;
|
|
323
|
+
font-weight: bold;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.loading {
|
|
327
|
+
display: inline-block;
|
|
328
|
+
width: 20px;
|
|
329
|
+
height: 20px;
|
|
330
|
+
border: 3px solid #f3f3f3;
|
|
331
|
+
border-top: 3px solid #667eea;
|
|
332
|
+
border-radius: 50%;
|
|
333
|
+
animation: spin 1s linear infinite;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@keyframes spin {
|
|
337
|
+
0% { transform: rotate(0deg); }
|
|
338
|
+
100% { transform: rotate(360deg); }
|
|
339
|
+
}
|
|
340
|
+
</style>
|
|
341
|
+
</head>
|
|
342
|
+
<body>
|
|
343
|
+
<div class="container">
|
|
344
|
+
<div class="header">
|
|
345
|
+
<img src="/static/stratifyai_wide_logo.png" alt="StratifyAI Logo">
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div class="main-grid">
|
|
349
|
+
<div class="sidebar">
|
|
350
|
+
<h3>Model Configuration</h3>
|
|
351
|
+
|
|
352
|
+
<div class="form-group">
|
|
353
|
+
<label for="provider">Provider</label>
|
|
354
|
+
<select id="provider">
|
|
355
|
+
<option value="">Select Provider...</option>
|
|
356
|
+
</select>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div class="form-group">
|
|
360
|
+
<label for="model">Model</label>
|
|
361
|
+
<select id="model">
|
|
362
|
+
<option value="">Select Model...</option>
|
|
363
|
+
</select>
|
|
364
|
+
<div id="validation-status" class="validation-status" style="display: none;"></div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<div class="form-group">
|
|
368
|
+
<label for="temperature">Temperature: <span id="temp-value">0.7</span></label>
|
|
369
|
+
<input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7">
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div class="form-group">
|
|
373
|
+
<label for="max-tokens">Max Tokens (optional)</label>
|
|
374
|
+
<input type="number" id="max-tokens" placeholder="Leave empty for default">
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div class="form-group">
|
|
378
|
+
<button id="reset-chat">Reset Chat</button>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div class="cost-tracker">
|
|
382
|
+
<h4>Model Cost Tracking</h4>
|
|
383
|
+
<div class="cost-row">
|
|
384
|
+
<span>Context:</span>
|
|
385
|
+
<span id="context-window">-</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="cost-row">
|
|
388
|
+
<span>Calls:</span>
|
|
389
|
+
<span id="cost-calls">0</span>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="cost-row">
|
|
392
|
+
<span>Tokens:</span>
|
|
393
|
+
<span id="cost-tokens">0 (In: 0, Out: 0)</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="cost-row">
|
|
396
|
+
<span>Total Cost:</span>
|
|
397
|
+
<span id="cost-total">$0.0000</span>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="form-group" style="margin-top: 15px; margin-bottom: 0;">
|
|
400
|
+
<a href="/models" class="view-models-btn" style="display: block; text-align: center; background: #667eea; color: white; padding: 10px; border-radius: 5px; text-decoration: none; font-weight: 500; transition: background 0.3s;">📋 View Model Catalog</a>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div class="main-content">
|
|
406
|
+
<h3>Chat Interface</h3>
|
|
407
|
+
<div class="chat-container" id="chat-container"></div>
|
|
408
|
+
|
|
409
|
+
<div class="form-group">
|
|
410
|
+
<textarea id="user-input" placeholder="Type your message here..."></textarea>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div class="form-group">
|
|
414
|
+
<label for="file-input" id="file-input-label">Attach File (optional) - Text files only</label>
|
|
415
|
+
<input type="file" id="file-input" accept=".txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h">
|
|
416
|
+
<div id="file-status" style="margin-top: 5px; font-size: 13px; color: #666;"></div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div class="form-group" style="display: flex; gap: 10px; align-items: center;">
|
|
420
|
+
<input type="checkbox" id="chunked" style="width: auto;">
|
|
421
|
+
<label for="chunked" style="margin: 0; cursor: pointer;">Enable Smart Chunking (reduces token usage for large files)</label>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<div class="form-group" id="chunk-size-group" style="display: none;">
|
|
425
|
+
<label for="chunk-size">Chunk Size (characters): <span id="chunk-size-value">50000</span></label>
|
|
426
|
+
<input type="range" id="chunk-size" min="10000" max="100000" step="10000" value="50000">
|
|
427
|
+
<div style="font-size: 12px; color: #666; margin-top: 5px;">💡 Smaller chunks = more summaries, larger chunks = fewer summaries</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div class="form-group">
|
|
431
|
+
<button id="send-btn">Send Message</button>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div class="message-history" id="message-history" style="margin-top: 20px; display: none;">
|
|
435
|
+
<h4 style="color: #666; margin-bottom: 10px; font-size: 14px;">📜 Recent Messages (Last 10)</h4>
|
|
436
|
+
<div id="history-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #eee; border-radius: 5px; padding: 10px; background: #fafafa;"></div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<script>
|
|
443
|
+
const API_BASE = 'http://localhost:8080';
|
|
444
|
+
let messages = [];
|
|
445
|
+
let totalCost = 0;
|
|
446
|
+
let totalCalls = 0;
|
|
447
|
+
let totalTokens = 0;
|
|
448
|
+
let totalPromptTokens = 0;
|
|
449
|
+
let totalCompletionTokens = 0;
|
|
450
|
+
|
|
451
|
+
// Load providers on startup
|
|
452
|
+
async function loadProviders() {
|
|
453
|
+
const response = await fetch(`${API_BASE}/api/providers`);
|
|
454
|
+
const providers = await response.json();
|
|
455
|
+
|
|
456
|
+
const select = document.getElementById('provider');
|
|
457
|
+
providers.forEach(provider => {
|
|
458
|
+
const option = document.createElement('option');
|
|
459
|
+
option.value = provider;
|
|
460
|
+
option.textContent = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
461
|
+
select.appendChild(option);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// List of reasoning models that don't support temperature
|
|
466
|
+
const REASONING_MODELS = [
|
|
467
|
+
'o1', 'o1-mini', 'o1-preview', 'o3-mini',
|
|
468
|
+
'o1-2024-12-17', 'o1-mini-2024-09-12',
|
|
469
|
+
'deepseek-reasoner', 'gpt-5'
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
// Check if model is a reasoning model
|
|
473
|
+
function isReasoningModel(model) {
|
|
474
|
+
if (!model) return false;
|
|
475
|
+
const modelLower = model.toLowerCase();
|
|
476
|
+
// Check explicit list or patterns
|
|
477
|
+
return REASONING_MODELS.some(rm => modelLower.includes(rm.toLowerCase())) ||
|
|
478
|
+
modelLower.startsWith('o1') ||
|
|
479
|
+
modelLower.startsWith('o3') ||
|
|
480
|
+
modelLower.startsWith('gpt-5') ||
|
|
481
|
+
modelLower.includes('reasoner') ||
|
|
482
|
+
modelLower.includes('reasoning');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Update temperature slider state based on model
|
|
486
|
+
async function updateTemperatureState() {
|
|
487
|
+
const provider = document.getElementById('provider').value;
|
|
488
|
+
const model = document.getElementById('model').value;
|
|
489
|
+
const tempSlider = document.getElementById('temperature');
|
|
490
|
+
const tempLabel = tempSlider.previousElementSibling;
|
|
491
|
+
|
|
492
|
+
if (!provider || !model) return;
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
// Fetch model metadata from API
|
|
496
|
+
const response = await fetch(`${API_BASE}/api/model-info/${provider}/${model}`);
|
|
497
|
+
if (!response.ok) {
|
|
498
|
+
// Fallback to pattern matching if API fails
|
|
499
|
+
if (isReasoningModel(model)) {
|
|
500
|
+
tempSlider.disabled = true;
|
|
501
|
+
tempSlider.value = 1.0;
|
|
502
|
+
document.getElementById('temp-value').textContent = '1.0 (fixed)';
|
|
503
|
+
tempLabel.style.opacity = '0.5';
|
|
504
|
+
} else {
|
|
505
|
+
tempSlider.disabled = false;
|
|
506
|
+
tempLabel.style.opacity = '1';
|
|
507
|
+
document.getElementById('temp-value').textContent = tempSlider.value;
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const modelInfo = await response.json();
|
|
513
|
+
|
|
514
|
+
// Check if model has fixed temperature
|
|
515
|
+
if (modelInfo.fixed_temperature !== null && modelInfo.fixed_temperature !== undefined) {
|
|
516
|
+
tempSlider.disabled = true;
|
|
517
|
+
tempSlider.value = modelInfo.fixed_temperature;
|
|
518
|
+
document.getElementById('temp-value').textContent = `${modelInfo.fixed_temperature} (fixed)`;
|
|
519
|
+
tempLabel.style.opacity = '0.5';
|
|
520
|
+
} else {
|
|
521
|
+
tempSlider.disabled = false;
|
|
522
|
+
tempLabel.style.opacity = '1';
|
|
523
|
+
document.getElementById('temp-value').textContent = tempSlider.value;
|
|
524
|
+
}
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error('Error fetching model info:', error);
|
|
527
|
+
// Fallback to pattern matching
|
|
528
|
+
if (isReasoningModel(model)) {
|
|
529
|
+
tempSlider.disabled = true;
|
|
530
|
+
tempSlider.value = 1.0;
|
|
531
|
+
document.getElementById('temp-value').textContent = '1.0 (fixed)';
|
|
532
|
+
tempLabel.style.opacity = '0.5';
|
|
533
|
+
} else {
|
|
534
|
+
tempSlider.disabled = false;
|
|
535
|
+
tempLabel.style.opacity = '1';
|
|
536
|
+
document.getElementById('temp-value').textContent = tempSlider.value;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Load models for selected provider
|
|
542
|
+
document.getElementById('provider').addEventListener('change', async (e) => {
|
|
543
|
+
const provider = e.target.value;
|
|
544
|
+
const validationStatus = document.getElementById('validation-status');
|
|
545
|
+
|
|
546
|
+
if (!provider) {
|
|
547
|
+
validationStatus.style.display = 'none';
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Show loading status
|
|
552
|
+
validationStatus.style.display = 'block';
|
|
553
|
+
validationStatus.className = 'validation-status loading';
|
|
554
|
+
validationStatus.textContent = `🔄 Validating ${provider} models...`;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const response = await fetch(`${API_BASE}/api/models/${provider}`);
|
|
558
|
+
const data = await response.json();
|
|
559
|
+
|
|
560
|
+
const models = data.models || data; // Support both new and old format
|
|
561
|
+
const validation = data.validation;
|
|
562
|
+
|
|
563
|
+
const select = document.getElementById('model');
|
|
564
|
+
select.innerHTML = '<option value="">Select Model...</option>';
|
|
565
|
+
|
|
566
|
+
// Check if we have models
|
|
567
|
+
if (models.length === 0) {
|
|
568
|
+
// No models available
|
|
569
|
+
const option = document.createElement('option');
|
|
570
|
+
option.value = '';
|
|
571
|
+
option.textContent = 'No validated models available';
|
|
572
|
+
option.disabled = true;
|
|
573
|
+
select.appendChild(option);
|
|
574
|
+
|
|
575
|
+
validationStatus.className = 'validation-status warning';
|
|
576
|
+
if (validation && validation.error) {
|
|
577
|
+
validationStatus.textContent = `⚠️ ${validation.error}`;
|
|
578
|
+
} else {
|
|
579
|
+
validationStatus.textContent = `⚠️ No models could be validated. Check API key configuration.`;
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
// Group models by category
|
|
583
|
+
const groupedModels = {};
|
|
584
|
+
models.forEach(model => {
|
|
585
|
+
// Handle both old format (string) and new format (object)
|
|
586
|
+
const modelId = typeof model === 'string' ? model : model.id;
|
|
587
|
+
const displayName = typeof model === 'string' ? model : model.display_name;
|
|
588
|
+
const description = typeof model === 'string' ? '' : model.description;
|
|
589
|
+
const category = typeof model === 'string' ? '' : model.category;
|
|
590
|
+
const supportsVision = typeof model === 'string' ? false : model.supports_vision;
|
|
591
|
+
const reasoningModel = typeof model === 'string' ? false : model.reasoning_model;
|
|
592
|
+
|
|
593
|
+
if (!groupedModels[category]) {
|
|
594
|
+
groupedModels[category] = [];
|
|
595
|
+
}
|
|
596
|
+
groupedModels[category].push({ modelId, displayName, description, supportsVision, reasoningModel });
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Add models to dropdown with categories
|
|
600
|
+
Object.keys(groupedModels).forEach(category => {
|
|
601
|
+
// Add category header if not empty
|
|
602
|
+
if (category) {
|
|
603
|
+
const categoryOption = document.createElement('option');
|
|
604
|
+
categoryOption.disabled = true;
|
|
605
|
+
categoryOption.textContent = `── ${category} ──`;
|
|
606
|
+
categoryOption.style.fontWeight = 'bold';
|
|
607
|
+
categoryOption.style.color = '#666';
|
|
608
|
+
select.appendChild(categoryOption);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Add models in this category
|
|
612
|
+
groupedModels[category].forEach(({ modelId, displayName, description, supportsVision, reasoningModel }) => {
|
|
613
|
+
const option = document.createElement('option');
|
|
614
|
+
option.value = modelId;
|
|
615
|
+
|
|
616
|
+
// Build display text with labels
|
|
617
|
+
let text = displayName;
|
|
618
|
+
if (description) {
|
|
619
|
+
text += ` - ${description}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
option.textContent = text;
|
|
623
|
+
option.title = description; // Tooltip
|
|
624
|
+
select.appendChild(option);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Display validation feedback
|
|
629
|
+
if (validation) {
|
|
630
|
+
if (validation.error) {
|
|
631
|
+
validationStatus.className = 'validation-status warning';
|
|
632
|
+
validationStatus.textContent = `⚠️ Default models displayed. Could not validate models.`;
|
|
633
|
+
} else {
|
|
634
|
+
validationStatus.className = 'validation-status success';
|
|
635
|
+
validationStatus.textContent = `✓ Validated ${models.length} models (${validation.validation_time_ms}ms)`;
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
// Fallback if no validation data
|
|
639
|
+
validationStatus.style.display = 'none';
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
console.error('Error loading models:', error);
|
|
644
|
+
validationStatus.className = 'validation-status warning';
|
|
645
|
+
validationStatus.textContent = `⚠️ Error loading models: ${error.message}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Reset temperature state when provider changes
|
|
649
|
+
updateTemperatureState();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Update temperature state and context window when model changes
|
|
653
|
+
document.getElementById('model').addEventListener('change', async () => {
|
|
654
|
+
await updateTemperatureState();
|
|
655
|
+
await updateContextWindow();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Update context window display and file input capabilities
|
|
659
|
+
async function updateContextWindow() {
|
|
660
|
+
const provider = document.getElementById('provider').value;
|
|
661
|
+
const model = document.getElementById('model').value;
|
|
662
|
+
const contextDisplay = document.getElementById('context-window');
|
|
663
|
+
const fileInput = document.getElementById('file-input');
|
|
664
|
+
const fileLabel = document.getElementById('file-input-label');
|
|
665
|
+
|
|
666
|
+
if (!provider || !model) {
|
|
667
|
+
contextDisplay.textContent = '-';
|
|
668
|
+
// Reset to text files only
|
|
669
|
+
fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
|
|
670
|
+
fileLabel.textContent = 'Attach File (optional) - Text files only';
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const response = await fetch(`${API_BASE}/api/model-info/${provider}/${model}`);
|
|
676
|
+
if (response.ok) {
|
|
677
|
+
const modelInfo = await response.json();
|
|
678
|
+
|
|
679
|
+
// Update context window
|
|
680
|
+
const context = modelInfo.context;
|
|
681
|
+
if (context && typeof context === 'number' && context > 0) {
|
|
682
|
+
contextDisplay.textContent = `${context.toLocaleString()} tokens`;
|
|
683
|
+
} else {
|
|
684
|
+
contextDisplay.textContent = 'N/A';
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Update file input based on vision support
|
|
688
|
+
if (modelInfo.supports_vision) {
|
|
689
|
+
fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h,.jpg,.jpeg,.png,.gif,.webp';
|
|
690
|
+
fileLabel.textContent = 'Attach File (optional) - Text files and images (vision enabled)';
|
|
691
|
+
} else {
|
|
692
|
+
fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
|
|
693
|
+
fileLabel.textContent = 'Attach File (optional) - Text files only';
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
contextDisplay.textContent = '-';
|
|
697
|
+
// Reset to text files only on error
|
|
698
|
+
fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
|
|
699
|
+
fileLabel.textContent = 'Attach File (optional) - Text files only';
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
console.error('Error fetching model info:', error);
|
|
703
|
+
contextDisplay.textContent = '-';
|
|
704
|
+
// Reset to text files only on error
|
|
705
|
+
fileInput.accept = '.txt,.py,.js,.json,.csv,.log,.md,.yaml,.yml,.xml,.html,.css,.java,.go,.rs,.cpp,.c,.h';
|
|
706
|
+
fileLabel.textContent = 'Attach File (optional) - Text files only';
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Update temperature display
|
|
711
|
+
document.getElementById('temperature').addEventListener('input', (e) => {
|
|
712
|
+
document.getElementById('temp-value').textContent = e.target.value;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Handle chunking checkbox toggle
|
|
716
|
+
document.getElementById('chunked').addEventListener('change', (e) => {
|
|
717
|
+
const chunkSizeGroup = document.getElementById('chunk-size-group');
|
|
718
|
+
chunkSizeGroup.style.display = e.target.checked ? 'block' : 'none';
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Update chunk size display
|
|
722
|
+
document.getElementById('chunk-size').addEventListener('input', (e) => {
|
|
723
|
+
document.getElementById('chunk-size-value').textContent = parseInt(e.target.value).toLocaleString();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Configure marked for code highlighting
|
|
727
|
+
marked.setOptions({
|
|
728
|
+
highlight: function(code, lang) {
|
|
729
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
730
|
+
return hljs.highlight(code, { language: lang }).value;
|
|
731
|
+
}
|
|
732
|
+
return hljs.highlightAuto(code).value;
|
|
733
|
+
},
|
|
734
|
+
breaks: true,
|
|
735
|
+
gfm: true
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Update message history display
|
|
739
|
+
function updateMessageHistory() {
|
|
740
|
+
const historyContainer = document.getElementById('message-history');
|
|
741
|
+
const historyList = document.getElementById('history-list');
|
|
742
|
+
|
|
743
|
+
// Filter out system messages and get last 10
|
|
744
|
+
const recentMessages = messages.slice(-10);
|
|
745
|
+
|
|
746
|
+
if (recentMessages.length === 0) {
|
|
747
|
+
historyContainer.style.display = 'none';
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
historyContainer.style.display = 'block';
|
|
752
|
+
|
|
753
|
+
let html = '';
|
|
754
|
+
for (const msg of recentMessages) {
|
|
755
|
+
const roleIcon = msg.role === 'user' ? '👤' : '🤖';
|
|
756
|
+
const roleColor = msg.role === 'user' ? '#667eea' : '#22c55e';
|
|
757
|
+
// Truncate long messages
|
|
758
|
+
const truncated = msg.content.length > 100 ? msg.content.substring(0, 100) + '...' : msg.content;
|
|
759
|
+
// Escape HTML
|
|
760
|
+
const escaped = truncated.replace(/</g, '<').replace(/>/g, '>');
|
|
761
|
+
html += `<div style="padding: 6px 0; border-bottom: 1px solid #eee; font-size: 13px;">
|
|
762
|
+
<span style="color: ${roleColor}; font-weight: 500;">${roleIcon} ${msg.role}:</span>
|
|
763
|
+
<span style="color: #444;">${escaped}</span>
|
|
764
|
+
</div>`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
historyList.innerHTML = html;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Add message to chat
|
|
771
|
+
function addMessage(role, content, details = null) {
|
|
772
|
+
const container = document.getElementById('chat-container');
|
|
773
|
+
const messageDiv = document.createElement('div');
|
|
774
|
+
messageDiv.className = `message ${role}`;
|
|
775
|
+
|
|
776
|
+
// Add error details if provided
|
|
777
|
+
if (details && role === 'error') {
|
|
778
|
+
// Create main message text node
|
|
779
|
+
const mainText = document.createTextNode(content);
|
|
780
|
+
messageDiv.appendChild(mainText);
|
|
781
|
+
|
|
782
|
+
// Create details section
|
|
783
|
+
const detailsDiv = document.createElement('div');
|
|
784
|
+
detailsDiv.className = 'error-details';
|
|
785
|
+
detailsDiv.textContent = details;
|
|
786
|
+
messageDiv.appendChild(detailsDiv);
|
|
787
|
+
} else if (role === 'assistant') {
|
|
788
|
+
// Render markdown for assistant messages
|
|
789
|
+
const markdownDiv = document.createElement('div');
|
|
790
|
+
markdownDiv.className = 'markdown-content';
|
|
791
|
+
markdownDiv.innerHTML = marked.parse(content);
|
|
792
|
+
messageDiv.appendChild(markdownDiv);
|
|
793
|
+
} else {
|
|
794
|
+
// For user/system messages, just set text content
|
|
795
|
+
messageDiv.textContent = content;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
container.appendChild(messageDiv);
|
|
799
|
+
container.scrollTop = container.scrollHeight;
|
|
800
|
+
|
|
801
|
+
// Don't add error messages to conversation history
|
|
802
|
+
if (role !== 'error' && role !== 'system') {
|
|
803
|
+
messages.push({role, content});
|
|
804
|
+
updateMessageHistory();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Handle file selection
|
|
809
|
+
let selectedFile = null;
|
|
810
|
+
let fileContent = null;
|
|
811
|
+
|
|
812
|
+
document.getElementById('file-input').addEventListener('change', async (e) => {
|
|
813
|
+
const file = e.target.files[0];
|
|
814
|
+
const fileStatus = document.getElementById('file-status');
|
|
815
|
+
|
|
816
|
+
if (!file) {
|
|
817
|
+
selectedFile = null;
|
|
818
|
+
fileContent = null;
|
|
819
|
+
fileStatus.textContent = '';
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Check file size (5MB limit)
|
|
824
|
+
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
825
|
+
if (file.size > maxSize) {
|
|
826
|
+
fileStatus.textContent = `⚠️ File too large (${(file.size / 1024 / 1024).toFixed(2)} MB). Max: 5 MB`;
|
|
827
|
+
fileStatus.style.color = '#c33';
|
|
828
|
+
e.target.value = ''; // Clear selection
|
|
829
|
+
selectedFile = null;
|
|
830
|
+
fileContent = null;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Read file content - handle text and image files differently
|
|
835
|
+
try {
|
|
836
|
+
const fileType = file.type;
|
|
837
|
+
const isImage = fileType.startsWith('image/');
|
|
838
|
+
|
|
839
|
+
if (isImage) {
|
|
840
|
+
// Read image as base64 data URL
|
|
841
|
+
const reader = new FileReader();
|
|
842
|
+
await new Promise((resolve, reject) => {
|
|
843
|
+
reader.onload = (e) => {
|
|
844
|
+
fileContent = e.target.result; // base64 data URL
|
|
845
|
+
resolve();
|
|
846
|
+
};
|
|
847
|
+
reader.onerror = reject;
|
|
848
|
+
reader.readAsDataURL(file);
|
|
849
|
+
});
|
|
850
|
+
selectedFile = file;
|
|
851
|
+
const sizeStr = file.size < 1024 ?
|
|
852
|
+
`${file.size} bytes` :
|
|
853
|
+
file.size < 1024 * 1024 ?
|
|
854
|
+
`${(file.size / 1024).toFixed(1)} KB` :
|
|
855
|
+
`${(file.size / 1024 / 1024).toFixed(2)} MB`;
|
|
856
|
+
fileStatus.textContent = `✓ ${file.name} (${sizeStr}, image)`;
|
|
857
|
+
fileStatus.style.color = '#22c55e';
|
|
858
|
+
} else {
|
|
859
|
+
// Read text file
|
|
860
|
+
fileContent = await file.text();
|
|
861
|
+
selectedFile = file;
|
|
862
|
+
const sizeStr = file.size < 1024 ?
|
|
863
|
+
`${file.size} bytes` :
|
|
864
|
+
file.size < 1024 * 1024 ?
|
|
865
|
+
`${(file.size / 1024).toFixed(1)} KB` :
|
|
866
|
+
`${(file.size / 1024 / 1024).toFixed(2)} MB`;
|
|
867
|
+
fileStatus.textContent = `✓ ${file.name} (${sizeStr}, ${fileContent.length.toLocaleString()} chars)`;
|
|
868
|
+
fileStatus.style.color = '#22c55e';
|
|
869
|
+
}
|
|
870
|
+
} catch (error) {
|
|
871
|
+
fileStatus.textContent = `⚠️ Error reading file: ${error.message}`;
|
|
872
|
+
fileStatus.style.color = '#c33';
|
|
873
|
+
e.target.value = '';
|
|
874
|
+
selectedFile = null;
|
|
875
|
+
fileContent = null;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// Send message
|
|
880
|
+
document.getElementById('send-btn').addEventListener('click', async () => {
|
|
881
|
+
const provider = document.getElementById('provider').value;
|
|
882
|
+
const model = document.getElementById('model').value;
|
|
883
|
+
const input = document.getElementById('user-input');
|
|
884
|
+
const userMessage = input.value.trim();
|
|
885
|
+
const fileInput = document.getElementById('file-input');
|
|
886
|
+
const fileStatus = document.getElementById('file-status');
|
|
887
|
+
|
|
888
|
+
if (!provider || !model) {
|
|
889
|
+
alert('Please select provider and model');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (!userMessage && !fileContent) {
|
|
894
|
+
alert('Please enter a message or attach a file');
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Validate that vision models are used for image files
|
|
899
|
+
if (fileContent && selectedFile) {
|
|
900
|
+
const isImage = selectedFile.type.startsWith('image/');
|
|
901
|
+
if (isImage) {
|
|
902
|
+
// Check if model supports vision
|
|
903
|
+
try {
|
|
904
|
+
const response = await fetch(`${API_BASE}/api/model-info/${provider}/${model}`);
|
|
905
|
+
if (response.ok) {
|
|
906
|
+
const modelInfo = await response.json();
|
|
907
|
+
if (!modelInfo.supports_vision) {
|
|
908
|
+
alert(`❌ Vision Not Supported\n\nThe model "${model}" cannot process image files.\n\nPlease either:\n• Select a vision-capable model (e.g., GPT-4 Vision, Claude 3), or\n• Remove the image attachment`);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
} catch (error) {
|
|
913
|
+
console.error('Error checking vision support:', error);
|
|
914
|
+
// Continue anyway - let the API handle it
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Build display message and actual content
|
|
920
|
+
let displayMessage = userMessage;
|
|
921
|
+
let actualContent = userMessage;
|
|
922
|
+
let apiFileContent = null;
|
|
923
|
+
let apiFileName = null;
|
|
924
|
+
|
|
925
|
+
if (fileContent) {
|
|
926
|
+
const isImage = selectedFile.type.startsWith('image/');
|
|
927
|
+
|
|
928
|
+
if (isImage) {
|
|
929
|
+
// For images, show indicator in display
|
|
930
|
+
displayMessage = displayMessage ?
|
|
931
|
+
`${displayMessage}\n\n🖼️ Image attached: ${selectedFile.name}` :
|
|
932
|
+
`🖼️ Image attached: ${selectedFile.name}`;
|
|
933
|
+
|
|
934
|
+
// Extract base64 data from data URL (format: data:image/jpeg;base64,<data>)
|
|
935
|
+
const base64Data = fileContent.split(',')[1]; // Get part after comma
|
|
936
|
+
const mimeType = fileContent.split(';')[0].split(':')[1]; // Extract mime type
|
|
937
|
+
|
|
938
|
+
// Format for vision models: [IMAGE:mime_type]\nbase64_data
|
|
939
|
+
const imageContent = `[IMAGE:${mimeType}]\n${base64Data}`;
|
|
940
|
+
|
|
941
|
+
actualContent = actualContent ?
|
|
942
|
+
`${actualContent}\n\n${imageContent}` :
|
|
943
|
+
imageContent;
|
|
944
|
+
} else {
|
|
945
|
+
// For text files, set API file parameters for chunking support
|
|
946
|
+
displayMessage = displayMessage ?
|
|
947
|
+
`${displayMessage}\n\n📎 Attached: ${selectedFile.name}` :
|
|
948
|
+
`📎 Attached: ${selectedFile.name}`;
|
|
949
|
+
|
|
950
|
+
// Check if chunking is enabled
|
|
951
|
+
const chunked = document.getElementById('chunked').checked;
|
|
952
|
+
if (chunked) {
|
|
953
|
+
// Pass file to API for chunking
|
|
954
|
+
apiFileContent = fileContent;
|
|
955
|
+
apiFileName = selectedFile.name;
|
|
956
|
+
displayMessage += ` (chunking enabled)`;
|
|
957
|
+
} else {
|
|
958
|
+
// Combine message with file content directly
|
|
959
|
+
actualContent = actualContent ?
|
|
960
|
+
`${actualContent}\n\n[File: ${selectedFile.name}]\n\n${fileContent}` :
|
|
961
|
+
`[File: ${selectedFile.name}]\n\n${fileContent}`;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Add user message to chat display
|
|
967
|
+
const container = document.getElementById('chat-container');
|
|
968
|
+
const messageDiv = document.createElement('div');
|
|
969
|
+
messageDiv.className = 'message user';
|
|
970
|
+
messageDiv.textContent = displayMessage;
|
|
971
|
+
container.appendChild(messageDiv);
|
|
972
|
+
container.scrollTop = container.scrollHeight;
|
|
973
|
+
|
|
974
|
+
// Add actual content (with file) to conversation history
|
|
975
|
+
messages.push({role: 'user', content: actualContent});
|
|
976
|
+
|
|
977
|
+
input.value = '';
|
|
978
|
+
|
|
979
|
+
// Clear file attachment
|
|
980
|
+
if (fileContent) {
|
|
981
|
+
fileInput.value = '';
|
|
982
|
+
fileStatus.textContent = '';
|
|
983
|
+
selectedFile = null;
|
|
984
|
+
fileContent = null;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Disable send button
|
|
988
|
+
const sendBtn = document.getElementById('send-btn');
|
|
989
|
+
sendBtn.disabled = true;
|
|
990
|
+
sendBtn.innerHTML = '<span class="loading"></span> Sending...';
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
// Prepare request
|
|
994
|
+
let temperature = parseFloat(document.getElementById('temperature').value);
|
|
995
|
+
const maxTokensInput = document.getElementById('max-tokens').value;
|
|
996
|
+
const maxTokens = maxTokensInput ? parseInt(maxTokensInput) : null;
|
|
997
|
+
|
|
998
|
+
// Force temperature to 1.0 for reasoning models
|
|
999
|
+
if (isReasoningModel(model)) {
|
|
1000
|
+
temperature = 1.0;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Build request body with optional chunking parameters
|
|
1004
|
+
const requestBody = {
|
|
1005
|
+
provider,
|
|
1006
|
+
model,
|
|
1007
|
+
messages: messages,
|
|
1008
|
+
temperature,
|
|
1009
|
+
max_tokens: maxTokens,
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// Add file and chunking parameters if file is being chunked
|
|
1013
|
+
if (apiFileContent && apiFileName) {
|
|
1014
|
+
requestBody.file_content = apiFileContent;
|
|
1015
|
+
requestBody.file_name = apiFileName;
|
|
1016
|
+
requestBody.chunked = true;
|
|
1017
|
+
requestBody.chunk_size = parseInt(document.getElementById('chunk-size').value);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const response = await fetch(`${API_BASE}/api/chat`, {
|
|
1021
|
+
method: 'POST',
|
|
1022
|
+
headers: {'Content-Type': 'application/json'},
|
|
1023
|
+
body: JSON.stringify(requestBody)
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
const data = await response.json();
|
|
1027
|
+
|
|
1028
|
+
// Check for API errors
|
|
1029
|
+
if (!response.ok) {
|
|
1030
|
+
// Handle structured error response (object) or simple error (string)
|
|
1031
|
+
let errorMsg;
|
|
1032
|
+
let errorData = null;
|
|
1033
|
+
|
|
1034
|
+
if (typeof data.detail === 'object' && data.detail !== null) {
|
|
1035
|
+
// Structured error response from our API
|
|
1036
|
+
errorData = data.detail;
|
|
1037
|
+
errorMsg = errorData.detail || errorData.message || JSON.stringify(data.detail);
|
|
1038
|
+
} else {
|
|
1039
|
+
// Simple string error
|
|
1040
|
+
errorMsg = data.detail || data.error || 'Unknown error occurred';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
let userFriendlyMsg = 'API Error';
|
|
1044
|
+
let details = errorMsg;
|
|
1045
|
+
|
|
1046
|
+
// Parse common error patterns
|
|
1047
|
+
if (errorData && errorData.error === 'input_too_long') {
|
|
1048
|
+
// Enhanced token limit error with suggestions
|
|
1049
|
+
userFriendlyMsg = '📊 Input Too Large';
|
|
1050
|
+
|
|
1051
|
+
const msg = errorData.message || errorMsg;
|
|
1052
|
+
const suggestion = errorData.suggestion || '';
|
|
1053
|
+
const tokens = errorData.estimated_tokens;
|
|
1054
|
+
const limit = errorData.api_limit || errorData.model_limit;
|
|
1055
|
+
const chunkingEnabled = errorData.chunking_enabled;
|
|
1056
|
+
|
|
1057
|
+
details = `${msg}\n\n`;
|
|
1058
|
+
|
|
1059
|
+
if (suggestion) {
|
|
1060
|
+
details += `💡 Suggestions:\n${suggestion}\n\n`;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (!chunkingEnabled) {
|
|
1064
|
+
details += `⚠️ TIP: Enable the 'Smart Chunking' checkbox below the file upload to automatically reduce token usage by 40-90%.`;
|
|
1065
|
+
}
|
|
1066
|
+
} else if (errorData && errorData.error === 'content_too_large') {
|
|
1067
|
+
// File exceeds system maximum
|
|
1068
|
+
userFriendlyMsg = '🚫 File Too Large';
|
|
1069
|
+
details = `${errorData.message}\n\n${errorData.suggestion || ''}`;
|
|
1070
|
+
} else if (errorMsg.includes('temperature') && errorMsg.includes('not support')) {
|
|
1071
|
+
const selectedTemp = parseFloat(document.getElementById('temperature').value);
|
|
1072
|
+
userFriendlyMsg = `${model} does not support temperature ${selectedTemp}. The default value is 1.0.`;
|
|
1073
|
+
details = errorMsg;
|
|
1074
|
+
} else if (errorMsg.includes('authentication') || errorMsg.includes('api key')) {
|
|
1075
|
+
userFriendlyMsg = '🔑 Authentication Error';
|
|
1076
|
+
details = `API key is missing or invalid for ${provider}.\n\nError: ${errorMsg}\n\nPlease check your .env file.`;
|
|
1077
|
+
} else if (errorMsg.includes('rate limit')) {
|
|
1078
|
+
userFriendlyMsg = '⏱️ Rate Limit Exceeded';
|
|
1079
|
+
details = `Too many requests to ${provider}.\n\nError: ${errorMsg}\n\nPlease wait a moment and try again.`;
|
|
1080
|
+
} else if (errorMsg.includes('model') && errorMsg.includes('not found')) {
|
|
1081
|
+
userFriendlyMsg = '🔍 Model Not Found';
|
|
1082
|
+
details = `The model \"${model}\" is not available.\n\nError: ${errorMsg}`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
addMessage('error', userFriendlyMsg, details);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Add assistant response
|
|
1090
|
+
addMessage('assistant', data.content);
|
|
1091
|
+
|
|
1092
|
+
// Update cost tracking
|
|
1093
|
+
totalCalls++;
|
|
1094
|
+
totalTokens += data.usage?.total_tokens || 0;
|
|
1095
|
+
totalPromptTokens += data.usage?.prompt_tokens || 0;
|
|
1096
|
+
totalCompletionTokens += data.usage?.completion_tokens || 0;
|
|
1097
|
+
totalCost += data.cost_usd || 0;
|
|
1098
|
+
|
|
1099
|
+
document.getElementById('cost-calls').textContent = totalCalls;
|
|
1100
|
+
document.getElementById('cost-tokens').textContent =
|
|
1101
|
+
`${totalTokens.toLocaleString()} (In: ${totalPromptTokens.toLocaleString()}, Out: ${totalCompletionTokens.toLocaleString()})`;
|
|
1102
|
+
document.getElementById('cost-total').textContent = `$${totalCost.toFixed(4)}`;
|
|
1103
|
+
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
// Network or parsing errors
|
|
1106
|
+
addMessage('error', '🌐 Connection Error', `Failed to communicate with the API.\n\nError: ${error.message}\n\nPlease check if the server is running.`);
|
|
1107
|
+
} finally {
|
|
1108
|
+
sendBtn.disabled = false;
|
|
1109
|
+
sendBtn.textContent = 'Send Message';
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
// Reset chat
|
|
1114
|
+
document.getElementById('reset-chat').addEventListener('click', () => {
|
|
1115
|
+
messages = [];
|
|
1116
|
+
document.getElementById('chat-container').innerHTML = '';
|
|
1117
|
+
updateMessageHistory();
|
|
1118
|
+
addMessage('system', 'Chat reset. Start a new conversation.');
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// Initialize
|
|
1122
|
+
loadProviders();
|
|
1123
|
+
addMessage('system', 'Welcome to StratifyAI! Select a provider and model to start chatting.');
|
|
1124
|
+
</script>
|
|
1125
|
+
</body>
|
|
1126
|
+
</html>
|