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.
Files changed (79) hide show
  1. package/README.md +64 -0
  2. package/bin/opengauge.js +70 -0
  3. package/dist/core/optimizer/checkpoint.d.ts +37 -0
  4. package/dist/core/optimizer/checkpoint.d.ts.map +1 -0
  5. package/dist/core/optimizer/checkpoint.js +81 -0
  6. package/dist/core/optimizer/checkpoint.js.map +1 -0
  7. package/dist/core/optimizer/compressor.d.ts +41 -0
  8. package/dist/core/optimizer/compressor.d.ts.map +1 -0
  9. package/dist/core/optimizer/compressor.js +134 -0
  10. package/dist/core/optimizer/compressor.js.map +1 -0
  11. package/dist/core/optimizer/dedup.d.ts +48 -0
  12. package/dist/core/optimizer/dedup.d.ts.map +1 -0
  13. package/dist/core/optimizer/dedup.js +147 -0
  14. package/dist/core/optimizer/dedup.js.map +1 -0
  15. package/dist/core/providers/adapter.d.ts +48 -0
  16. package/dist/core/providers/adapter.d.ts.map +1 -0
  17. package/dist/core/providers/adapter.js +22 -0
  18. package/dist/core/providers/adapter.js.map +1 -0
  19. package/dist/core/providers/anthropic.d.ts +12 -0
  20. package/dist/core/providers/anthropic.d.ts.map +1 -0
  21. package/dist/core/providers/anthropic.js +155 -0
  22. package/dist/core/providers/anthropic.js.map +1 -0
  23. package/dist/core/providers/gemini.d.ts +13 -0
  24. package/dist/core/providers/gemini.d.ts.map +1 -0
  25. package/dist/core/providers/gemini.js +154 -0
  26. package/dist/core/providers/gemini.js.map +1 -0
  27. package/dist/core/providers/ollama.d.ts +11 -0
  28. package/dist/core/providers/ollama.d.ts.map +1 -0
  29. package/dist/core/providers/ollama.js +119 -0
  30. package/dist/core/providers/ollama.js.map +1 -0
  31. package/dist/core/providers/openai.d.ts +12 -0
  32. package/dist/core/providers/openai.d.ts.map +1 -0
  33. package/dist/core/providers/openai.js +169 -0
  34. package/dist/core/providers/openai.js.map +1 -0
  35. package/dist/core/rag/assembler.d.ts +47 -0
  36. package/dist/core/rag/assembler.d.ts.map +1 -0
  37. package/dist/core/rag/assembler.js +178 -0
  38. package/dist/core/rag/assembler.js.map +1 -0
  39. package/dist/core/rag/embedder.d.ts +16 -0
  40. package/dist/core/rag/embedder.d.ts.map +1 -0
  41. package/dist/core/rag/embedder.js +223 -0
  42. package/dist/core/rag/embedder.js.map +1 -0
  43. package/dist/core/rag/retriever.d.ts +20 -0
  44. package/dist/core/rag/retriever.d.ts.map +1 -0
  45. package/dist/core/rag/retriever.js +71 -0
  46. package/dist/core/rag/retriever.js.map +1 -0
  47. package/dist/db/index.d.ts +5 -0
  48. package/dist/db/index.d.ts.map +1 -0
  49. package/dist/db/index.js +48 -0
  50. package/dist/db/index.js.map +1 -0
  51. package/dist/db/queries.d.ts +72 -0
  52. package/dist/db/queries.d.ts.map +1 -0
  53. package/dist/db/queries.js +169 -0
  54. package/dist/db/queries.js.map +1 -0
  55. package/dist/db/schema.d.ts +3 -0
  56. package/dist/db/schema.d.ts.map +1 -0
  57. package/dist/db/schema.js +71 -0
  58. package/dist/db/schema.js.map +1 -0
  59. package/dist/server/config.d.ts +25 -0
  60. package/dist/server/config.d.ts.map +1 -0
  61. package/dist/server/config.js +69 -0
  62. package/dist/server/config.js.map +1 -0
  63. package/dist/server/index.d.ts +5 -0
  64. package/dist/server/index.d.ts.map +1 -0
  65. package/dist/server/index.js +61 -0
  66. package/dist/server/index.js.map +1 -0
  67. package/dist/server/routes/index.d.ts +6 -0
  68. package/dist/server/routes/index.d.ts.map +1 -0
  69. package/dist/server/routes/index.js +272 -0
  70. package/dist/server/routes/index.js.map +1 -0
  71. package/dist/server/sse.d.ts +21 -0
  72. package/dist/server/sse.d.ts.map +1 -0
  73. package/dist/server/sse.js +40 -0
  74. package/dist/server/sse.js.map +1 -0
  75. package/dist/ui/static/app.js +515 -0
  76. package/dist/ui/static/index.html +13 -0
  77. package/dist/ui/static/styles.css +506 -0
  78. package/dist/ui/static/vendor.js +26 -0
  79. 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>