opengauge 0.1.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/README.md +64 -0
- package/bin/opengauge.js +70 -0
- package/dist/core/optimizer/checkpoint.d.ts +37 -0
- package/dist/core/optimizer/checkpoint.d.ts.map +1 -0
- package/dist/core/optimizer/checkpoint.js +81 -0
- package/dist/core/optimizer/checkpoint.js.map +1 -0
- package/dist/core/optimizer/compressor.d.ts +41 -0
- package/dist/core/optimizer/compressor.d.ts.map +1 -0
- package/dist/core/optimizer/compressor.js +134 -0
- package/dist/core/optimizer/compressor.js.map +1 -0
- package/dist/core/optimizer/dedup.d.ts +48 -0
- package/dist/core/optimizer/dedup.d.ts.map +1 -0
- package/dist/core/optimizer/dedup.js +147 -0
- package/dist/core/optimizer/dedup.js.map +1 -0
- package/dist/core/providers/adapter.d.ts +48 -0
- package/dist/core/providers/adapter.d.ts.map +1 -0
- package/dist/core/providers/adapter.js +22 -0
- package/dist/core/providers/adapter.js.map +1 -0
- package/dist/core/providers/anthropic.d.ts +12 -0
- package/dist/core/providers/anthropic.d.ts.map +1 -0
- package/dist/core/providers/anthropic.js +155 -0
- package/dist/core/providers/anthropic.js.map +1 -0
- package/dist/core/providers/gemini.d.ts +13 -0
- package/dist/core/providers/gemini.d.ts.map +1 -0
- package/dist/core/providers/gemini.js +154 -0
- package/dist/core/providers/gemini.js.map +1 -0
- package/dist/core/providers/ollama.d.ts +11 -0
- package/dist/core/providers/ollama.d.ts.map +1 -0
- package/dist/core/providers/ollama.js +119 -0
- package/dist/core/providers/ollama.js.map +1 -0
- package/dist/core/providers/openai.d.ts +12 -0
- package/dist/core/providers/openai.d.ts.map +1 -0
- package/dist/core/providers/openai.js +169 -0
- package/dist/core/providers/openai.js.map +1 -0
- package/dist/core/rag/assembler.d.ts +47 -0
- package/dist/core/rag/assembler.d.ts.map +1 -0
- package/dist/core/rag/assembler.js +178 -0
- package/dist/core/rag/assembler.js.map +1 -0
- package/dist/core/rag/embedder.d.ts +16 -0
- package/dist/core/rag/embedder.d.ts.map +1 -0
- package/dist/core/rag/embedder.js +223 -0
- package/dist/core/rag/embedder.js.map +1 -0
- package/dist/core/rag/retriever.d.ts +20 -0
- package/dist/core/rag/retriever.d.ts.map +1 -0
- package/dist/core/rag/retriever.js +71 -0
- package/dist/core/rag/retriever.js.map +1 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +48 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/queries.d.ts +72 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +169 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +71 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/server/config.d.ts +25 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +69 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +61 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/index.d.ts +6 -0
- package/dist/server/routes/index.d.ts.map +1 -0
- package/dist/server/routes/index.js +272 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/server/sse.d.ts +21 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +40 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/ui/static/app.js +515 -0
- package/dist/ui/static/index.html +13 -0
- package/dist/ui/static/styles.css +506 -0
- package/dist/ui/static/vendor.js +26 -0
- package/package.json +49 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import {
|
|
2
|
+
html,
|
|
3
|
+
render,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
} from './vendor.js';
|
|
9
|
+
|
|
10
|
+
// ============ API Helpers ============
|
|
11
|
+
|
|
12
|
+
const api = {
|
|
13
|
+
async getConversations() {
|
|
14
|
+
const res = await fetch('/api/conversations');
|
|
15
|
+
return res.json();
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async getConversation(id) {
|
|
19
|
+
const res = await fetch(`/api/conversations/${id}`);
|
|
20
|
+
return res.json();
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async deleteConversation(id) {
|
|
24
|
+
await fetch(`/api/conversations/${id}`, { method: 'DELETE' });
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async getTokenUsage() {
|
|
28
|
+
const res = await fetch('/api/token-usage');
|
|
29
|
+
return res.json();
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async getConfig() {
|
|
33
|
+
const res = await fetch('/api/config');
|
|
34
|
+
return res.json();
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async saveConfig(config) {
|
|
38
|
+
const res = await fetch('/api/config', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(config),
|
|
42
|
+
});
|
|
43
|
+
return res.json();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async getHealth() {
|
|
47
|
+
const res = await fetch('/api/health');
|
|
48
|
+
return res.json();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
sendMessage(message, conversationId, provider, model) {
|
|
52
|
+
return fetch('/api/chat', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
message,
|
|
57
|
+
conversation_id: conversationId,
|
|
58
|
+
provider,
|
|
59
|
+
model,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ============ SSE Parser ============
|
|
66
|
+
|
|
67
|
+
async function* parseSSE(response) {
|
|
68
|
+
const reader = response.body.getReader();
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
let buffer = '';
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
while (true) {
|
|
74
|
+
const { done, value } = await reader.read();
|
|
75
|
+
if (done) break;
|
|
76
|
+
|
|
77
|
+
buffer += decoder.decode(value, { stream: true });
|
|
78
|
+
const lines = buffer.split('\n');
|
|
79
|
+
buffer = lines.pop() || '';
|
|
80
|
+
|
|
81
|
+
let currentEvent = '';
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line.startsWith('event: ')) {
|
|
84
|
+
currentEvent = line.slice(7).trim();
|
|
85
|
+
} else if (line.startsWith('data: ')) {
|
|
86
|
+
const data = line.slice(6).trim();
|
|
87
|
+
if (data === '[DONE]') return;
|
|
88
|
+
try {
|
|
89
|
+
yield { event: currentEvent || 'data', data: JSON.parse(data) };
|
|
90
|
+
} catch {
|
|
91
|
+
// skip
|
|
92
|
+
}
|
|
93
|
+
currentEvent = '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
reader.releaseLock();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============ Components ============
|
|
103
|
+
|
|
104
|
+
function SetupWizard({ onComplete, onSkip }) {
|
|
105
|
+
const [provider, setProvider] = useState('ollama');
|
|
106
|
+
const [apiKey, setApiKey] = useState('');
|
|
107
|
+
const [model, setModel] = useState('');
|
|
108
|
+
|
|
109
|
+
const defaults = {
|
|
110
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
111
|
+
openai: 'gpt-4o',
|
|
112
|
+
gemini: 'gemini-2.0-flash',
|
|
113
|
+
ollama: 'llama3',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleSave = async () => {
|
|
117
|
+
const config = {
|
|
118
|
+
providers: {
|
|
119
|
+
[provider]: {
|
|
120
|
+
...(apiKey ? { api_key: apiKey } : {}),
|
|
121
|
+
default_model: model || defaults[provider],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
defaults: { provider },
|
|
125
|
+
};
|
|
126
|
+
await api.saveConfig(config);
|
|
127
|
+
onComplete();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return html`
|
|
131
|
+
<div class="wizard-overlay">
|
|
132
|
+
<div class="wizard">
|
|
133
|
+
<h2>⚡ Welcome to OpenGauge</h2>
|
|
134
|
+
<p>Configure your LLM provider to get started. You can change this later in settings.</p>
|
|
135
|
+
|
|
136
|
+
<div class="form-group">
|
|
137
|
+
<label>Provider</label>
|
|
138
|
+
<select value=${provider} onChange=${(e) => { setProvider(e.target.value); setModel(''); }}>
|
|
139
|
+
<option value="ollama">Ollama (Local)</option>
|
|
140
|
+
<option value="openai">OpenAI</option>
|
|
141
|
+
<option value="anthropic">Anthropic Claude</option>
|
|
142
|
+
<option value="gemini">Google Gemini</option>
|
|
143
|
+
</select>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
${provider !== 'ollama' ? html`
|
|
147
|
+
<div class="form-group">
|
|
148
|
+
<label>API Key</label>
|
|
149
|
+
<input
|
|
150
|
+
type="password"
|
|
151
|
+
placeholder="Enter your API key"
|
|
152
|
+
value=${apiKey}
|
|
153
|
+
onInput=${(e) => setApiKey(e.target.value)}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
` : html`
|
|
157
|
+
<div class="form-group">
|
|
158
|
+
<label>Base URL</label>
|
|
159
|
+
<input
|
|
160
|
+
type="text"
|
|
161
|
+
placeholder="http://localhost:11434"
|
|
162
|
+
value="http://localhost:11434"
|
|
163
|
+
disabled
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
`}
|
|
167
|
+
|
|
168
|
+
<div class="form-group">
|
|
169
|
+
<label>Model</label>
|
|
170
|
+
<input
|
|
171
|
+
type="text"
|
|
172
|
+
placeholder=${defaults[provider]}
|
|
173
|
+
value=${model}
|
|
174
|
+
onInput=${(e) => setModel(e.target.value)}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="actions">
|
|
179
|
+
<button class="btn-secondary" onClick=${onSkip}>Skip</button>
|
|
180
|
+
<button class="btn-primary" onClick=${handleSave}>Save & Start</button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Sidebar({ conversations, activeId, onSelect, onNew, onDelete, onSettings }) {
|
|
188
|
+
return html`
|
|
189
|
+
<div class="sidebar">
|
|
190
|
+
<div class="sidebar-header">
|
|
191
|
+
<h1><span>⚡</span> OpenGauge</h1>
|
|
192
|
+
<button class="new-chat-btn" onClick=${onNew}>+ New</button>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div class="conversation-list">
|
|
196
|
+
${conversations.map((conv) => html`
|
|
197
|
+
<div
|
|
198
|
+
key=${conv.id}
|
|
199
|
+
class="conversation-item ${conv.id === activeId ? 'active' : ''}"
|
|
200
|
+
onClick=${() => onSelect(conv.id)}
|
|
201
|
+
>
|
|
202
|
+
<span class="title">${conv.title || 'New Conversation'}</span>
|
|
203
|
+
<button
|
|
204
|
+
class="delete-btn"
|
|
205
|
+
onClick=${(e) => { e.stopPropagation(); onDelete(conv.id); }}
|
|
206
|
+
>✕</button>
|
|
207
|
+
</div>
|
|
208
|
+
`)}
|
|
209
|
+
|
|
210
|
+
${conversations.length === 0 ? html`
|
|
211
|
+
<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 13px;">
|
|
212
|
+
No conversations yet
|
|
213
|
+
</div>
|
|
214
|
+
` : null}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="sidebar-footer">
|
|
218
|
+
<button class="settings-btn" onClick=${onSettings}>⚙ Settings</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function TokenMeter({ tokensRaw, tokensSent, saved }) {
|
|
225
|
+
const healthLevel = saved > 30 ? '' : saved > 15 ? 'warning' : tokensSent > 0 ? 'danger' : '';
|
|
226
|
+
|
|
227
|
+
return html`
|
|
228
|
+
<div class="token-meter">
|
|
229
|
+
<div class="token-stat">
|
|
230
|
+
<span class="label">Raw:</span>
|
|
231
|
+
<span class="value">${tokensRaw.toLocaleString()}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="token-stat sent">
|
|
234
|
+
<span class="label">Sent:</span>
|
|
235
|
+
<span class="value">${tokensSent.toLocaleString()}</span>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="token-stat saved">
|
|
238
|
+
<span class="label">Saved:</span>
|
|
239
|
+
<span class="value">${saved}%</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="health-indicator ${healthLevel}" title="Context health"></div>
|
|
242
|
+
</div>
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ChatMessage({ msg }) {
|
|
247
|
+
return html`
|
|
248
|
+
<div class="message ${msg.role}">
|
|
249
|
+
<div class="role">${msg.role}</div>
|
|
250
|
+
<div class="content">${msg.content}</div>
|
|
251
|
+
${msg.tokens_raw ? html`
|
|
252
|
+
<div class="meta">
|
|
253
|
+
${msg.tokens_raw} tokens raw → ${msg.tokens_sent || msg.tokens_raw} sent
|
|
254
|
+
</div>
|
|
255
|
+
` : null}
|
|
256
|
+
</div>
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function EmptyState() {
|
|
261
|
+
return html`
|
|
262
|
+
<div class="empty-state">
|
|
263
|
+
<div class="logo">⚡</div>
|
|
264
|
+
<h2>OpenGauge</h2>
|
|
265
|
+
<p>A token-efficient LLM chat interface. Every token counts: compress before sending, retrieve instead of stuffing.</p>
|
|
266
|
+
</div>
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============ Main App ============
|
|
271
|
+
|
|
272
|
+
function App() {
|
|
273
|
+
const [conversations, setConversations] = useState([]);
|
|
274
|
+
const [activeConvId, setActiveConvId] = useState(null);
|
|
275
|
+
const [messages, setMessages] = useState([]);
|
|
276
|
+
const [input, setInput] = useState('');
|
|
277
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
278
|
+
const [showWizard, setShowWizard] = useState(false);
|
|
279
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
280
|
+
const [provider, setProvider] = useState('ollama');
|
|
281
|
+
const [model, setModel] = useState('');
|
|
282
|
+
const [tokenStats, setTokenStats] = useState({ raw: 0, sent: 0, saved: 0 });
|
|
283
|
+
|
|
284
|
+
const chatEndRef = useRef(null);
|
|
285
|
+
const textareaRef = useRef(null);
|
|
286
|
+
|
|
287
|
+
// Load conversations on mount
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
loadConversations();
|
|
290
|
+
checkConfig();
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
// Scroll to bottom on new messages
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
296
|
+
}, [messages]);
|
|
297
|
+
|
|
298
|
+
const loadConversations = async () => {
|
|
299
|
+
try {
|
|
300
|
+
const convs = await api.getConversations();
|
|
301
|
+
setConversations(convs);
|
|
302
|
+
} catch {
|
|
303
|
+
// server might not be ready
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const checkConfig = async () => {
|
|
308
|
+
try {
|
|
309
|
+
const config = await api.getConfig();
|
|
310
|
+
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
311
|
+
setShowWizard(true);
|
|
312
|
+
} else {
|
|
313
|
+
const firstProvider = Object.keys(config.providers)[0];
|
|
314
|
+
setProvider(firstProvider);
|
|
315
|
+
setModel(config.providers[firstProvider]?.default_model || '');
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
setShowWizard(true);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const selectConversation = async (id) => {
|
|
323
|
+
setActiveConvId(id);
|
|
324
|
+
try {
|
|
325
|
+
const conv = await api.getConversation(id);
|
|
326
|
+
setMessages(conv.messages || []);
|
|
327
|
+
setProvider(conv.provider);
|
|
328
|
+
setModel(conv.model);
|
|
329
|
+
} catch {
|
|
330
|
+
setMessages([]);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const newConversation = () => {
|
|
335
|
+
setActiveConvId(null);
|
|
336
|
+
setMessages([]);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const deleteConversation = async (id) => {
|
|
340
|
+
await api.deleteConversation(id);
|
|
341
|
+
if (activeConvId === id) {
|
|
342
|
+
setActiveConvId(null);
|
|
343
|
+
setMessages([]);
|
|
344
|
+
}
|
|
345
|
+
loadConversations();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const sendMessage = useCallback(async () => {
|
|
349
|
+
if (!input.trim() || isStreaming) return;
|
|
350
|
+
|
|
351
|
+
const userMessage = input.trim();
|
|
352
|
+
setInput('');
|
|
353
|
+
setIsStreaming(true);
|
|
354
|
+
|
|
355
|
+
// Add user message to UI
|
|
356
|
+
setMessages((prev) => [...prev, { role: 'user', content: userMessage }]);
|
|
357
|
+
|
|
358
|
+
// Add placeholder for assistant
|
|
359
|
+
setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const response = await api.sendMessage(userMessage, activeConvId, provider, model);
|
|
363
|
+
|
|
364
|
+
let fullContent = '';
|
|
365
|
+
for await (const { event, data } of parseSSE(response)) {
|
|
366
|
+
if (event === 'meta') {
|
|
367
|
+
if (!activeConvId && data.conversation_id) {
|
|
368
|
+
setActiveConvId(data.conversation_id);
|
|
369
|
+
}
|
|
370
|
+
setTokenStats({
|
|
371
|
+
raw: data.tokens_raw || 0,
|
|
372
|
+
sent: data.tokens_sent || 0,
|
|
373
|
+
saved: data.savings_percent || 0,
|
|
374
|
+
});
|
|
375
|
+
} else if (event === 'content') {
|
|
376
|
+
fullContent += data.text;
|
|
377
|
+
setMessages((prev) => {
|
|
378
|
+
const updated = [...prev];
|
|
379
|
+
updated[updated.length - 1] = { role: 'assistant', content: fullContent };
|
|
380
|
+
return updated;
|
|
381
|
+
});
|
|
382
|
+
} else if (event === 'done') {
|
|
383
|
+
setTokenStats((prev) => ({
|
|
384
|
+
...prev,
|
|
385
|
+
tokensIn: data.tokens_in,
|
|
386
|
+
tokensOut: data.tokens_out,
|
|
387
|
+
}));
|
|
388
|
+
} else if (event === 'error') {
|
|
389
|
+
setMessages((prev) => {
|
|
390
|
+
const updated = [...prev];
|
|
391
|
+
updated[updated.length - 1] = {
|
|
392
|
+
role: 'assistant',
|
|
393
|
+
content: `Error: ${data.message}`,
|
|
394
|
+
};
|
|
395
|
+
return updated;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
loadConversations();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
setMessages((prev) => {
|
|
403
|
+
const updated = [...prev];
|
|
404
|
+
updated[updated.length - 1] = {
|
|
405
|
+
role: 'assistant',
|
|
406
|
+
content: `Error: ${err.message}`,
|
|
407
|
+
};
|
|
408
|
+
return updated;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
setIsStreaming(false);
|
|
413
|
+
}, [input, isStreaming, activeConvId, provider, model]);
|
|
414
|
+
|
|
415
|
+
const handleKeyDown = (e) => {
|
|
416
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
sendMessage();
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Auto-resize textarea
|
|
423
|
+
const handleInput = (e) => {
|
|
424
|
+
setInput(e.target.value);
|
|
425
|
+
e.target.style.height = 'auto';
|
|
426
|
+
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
return html`
|
|
430
|
+
${showWizard ? html`
|
|
431
|
+
<${SetupWizard}
|
|
432
|
+
onComplete=${() => { setShowWizard(false); checkConfig(); }}
|
|
433
|
+
onSkip=${() => setShowWizard(false)}
|
|
434
|
+
/>
|
|
435
|
+
` : null}
|
|
436
|
+
|
|
437
|
+
${showSettings ? html`
|
|
438
|
+
<${SetupWizard}
|
|
439
|
+
onComplete=${() => { setShowSettings(false); checkConfig(); }}
|
|
440
|
+
onSkip=${() => setShowSettings(false)}
|
|
441
|
+
/>
|
|
442
|
+
` : null}
|
|
443
|
+
|
|
444
|
+
<div class="layout">
|
|
445
|
+
<${Sidebar}
|
|
446
|
+
conversations=${conversations}
|
|
447
|
+
activeId=${activeConvId}
|
|
448
|
+
onSelect=${selectConversation}
|
|
449
|
+
onNew=${newConversation}
|
|
450
|
+
onDelete=${deleteConversation}
|
|
451
|
+
onSettings=${() => setShowSettings(true)}
|
|
452
|
+
/>
|
|
453
|
+
|
|
454
|
+
<div class="main">
|
|
455
|
+
<div class="header">
|
|
456
|
+
<div class="model-selector">
|
|
457
|
+
<select value=${provider} onChange=${(e) => setProvider(e.target.value)}>
|
|
458
|
+
<option value="ollama">Ollama</option>
|
|
459
|
+
<option value="openai">OpenAI</option>
|
|
460
|
+
<option value="anthropic">Anthropic</option>
|
|
461
|
+
<option value="gemini">Gemini</option>
|
|
462
|
+
</select>
|
|
463
|
+
<input
|
|
464
|
+
type="text"
|
|
465
|
+
style="background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; font-size: 13px; width: 180px; outline: none;"
|
|
466
|
+
placeholder="model name"
|
|
467
|
+
value=${model}
|
|
468
|
+
onInput=${(e) => setModel(e.target.value)}
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<${TokenMeter}
|
|
473
|
+
tokensRaw=${tokenStats.raw}
|
|
474
|
+
tokensSent=${tokenStats.sent}
|
|
475
|
+
saved=${tokenStats.saved}
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="chat-area">
|
|
480
|
+
${messages.length === 0 ? html`<${EmptyState} />` : null}
|
|
481
|
+
|
|
482
|
+
${messages.map((msg, i) => html`
|
|
483
|
+
<${ChatMessage} key=${i} msg=${msg} />
|
|
484
|
+
`)}
|
|
485
|
+
|
|
486
|
+
<div ref=${chatEndRef} />
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div class="input-area">
|
|
490
|
+
<div class="input-container">
|
|
491
|
+
<textarea
|
|
492
|
+
ref=${textareaRef}
|
|
493
|
+
placeholder="Send a message... (Shift+Enter for new line)"
|
|
494
|
+
value=${input}
|
|
495
|
+
onInput=${handleInput}
|
|
496
|
+
onKeyDown=${handleKeyDown}
|
|
497
|
+
rows="1"
|
|
498
|
+
disabled=${isStreaming}
|
|
499
|
+
/>
|
|
500
|
+
<button
|
|
501
|
+
class="send-btn"
|
|
502
|
+
onClick=${sendMessage}
|
|
503
|
+
disabled=${isStreaming || !input.trim()}
|
|
504
|
+
>
|
|
505
|
+
${isStreaming ? 'Sending...' : 'Send'}
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ============ Mount ============
|
|
515
|
+
render(html`<${App} />`, document.getElementById('app'));
|
|
@@ -0,0 +1,13 @@
|
|
|
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>OpenGauge</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/app.js"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|