@lovenyberg/ove 0.2.2 → 0.4.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/public/index.html CHANGED
@@ -4,80 +4,468 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Ove</title>
7
+ <link rel="icon" href="/favicon.ico">
7
8
  <style>
8
9
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
- body { font-family: monospace; background: #1a1a1a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
10
- #messages { flex: 1; overflow-y: auto; padding: 1rem; }
11
- .msg { margin-bottom: 0.75rem; white-space: pre-wrap; }
12
- .msg.user { color: #8ab4f8; }
13
- .msg.ove { color: #e0e0e0; }
14
- .msg.status { color: #888; font-size: 0.85rem; }
15
- .msg.error { color: #f28b82; }
16
- #input-bar { display: flex; padding: 0.5rem; border-top: 1px solid #333; }
17
- #input-bar input { flex: 1; background: #2a2a2a; border: 1px solid #444; color: #e0e0e0; padding: 0.5rem; font-family: monospace; font-size: 1rem; }
18
- #input-bar button { background: #333; color: #e0e0e0; border: 1px solid #444; padding: 0.5rem 1rem; cursor: pointer; font-family: monospace; }
19
- #input-bar button:hover { background: #444; }
10
+
11
+ :root {
12
+ --bg: #1a1a1a;
13
+ --bg-panel: #161616;
14
+ --bg-item: #1e1e1e;
15
+ --bg-item-hover: #252525;
16
+ --bg-item-active: #2a2a2a;
17
+ --border: #2a2a2a;
18
+ --border-light: #333;
19
+ --text: #e0e0e0;
20
+ --text-dim: #777;
21
+ --text-muted: #555;
22
+ --accent: #8ab4f8;
23
+ --green: #4ade80;
24
+ --green-dim: #16361f;
25
+ --red: #f28b82;
26
+ --red-dim: #3b1a1a;
27
+ --amber: #fbbf24;
28
+ --amber-dim: #3b2e0a;
29
+ --cyan: #22d3ee;
30
+ --cyan-dim: #0a2e33;
31
+ }
32
+
33
+ body {
34
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
35
+ background: var(--bg);
36
+ color: var(--text);
37
+ height: 100vh;
38
+ display: flex;
39
+ flex-direction: column;
40
+ overflow: hidden;
41
+ }
42
+
43
+ header {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: 0.5rem 1rem;
48
+ border-bottom: 1px solid var(--border);
49
+ background: var(--bg-panel);
50
+ flex-shrink: 0;
51
+ }
52
+
53
+ .header-left {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 1rem;
57
+ }
58
+
59
+ .header-logo {
60
+ width: 24px;
61
+ height: 24px;
62
+ border-radius: 3px;
63
+ object-fit: cover;
64
+ }
65
+
66
+ .header-left h1 {
67
+ font-size: 0.85rem;
68
+ font-weight: 600;
69
+ letter-spacing: 0.05em;
70
+ }
71
+
72
+ .nav-link {
73
+ color: var(--text-dim);
74
+ text-decoration: none;
75
+ font-size: 0.7rem;
76
+ padding: 0.2rem 0.5rem;
77
+ border: 1px solid var(--border);
78
+ border-radius: 3px;
79
+ transition: all 0.15s;
80
+ }
81
+ .nav-link:hover { color: var(--text); border-color: var(--border-light); }
82
+
83
+ .header-right {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 0.5rem;
87
+ }
88
+
89
+ .layout {
90
+ display: flex;
91
+ flex: 1;
92
+ overflow: hidden;
93
+ }
94
+
95
+ /* ── Sidebar ── */
96
+ .sidebar {
97
+ width: 280px;
98
+ min-width: 200px;
99
+ border-right: 1px solid var(--border);
100
+ display: flex;
101
+ flex-direction: column;
102
+ background: var(--bg-panel);
103
+ flex-shrink: 0;
104
+ transition: width 0.2s;
105
+ }
106
+
107
+ .sidebar.collapsed { width: 0; min-width: 0; overflow: hidden; border-right: none; }
108
+
109
+ .sidebar-toggle {
110
+ background: none;
111
+ border: 1px solid var(--border);
112
+ color: var(--text-dim);
113
+ font-family: inherit;
114
+ font-size: 0.7rem;
115
+ padding: 0.2rem 0.5rem;
116
+ border-radius: 3px;
117
+ cursor: pointer;
118
+ transition: all 0.15s;
119
+ }
120
+ .sidebar-toggle:hover { color: var(--text); border-color: var(--border-light); }
121
+
122
+ .sidebar-section {
123
+ border-bottom: 1px solid var(--border);
124
+ }
125
+
126
+ .sidebar-section-header {
127
+ padding: 0.5rem 0.75rem;
128
+ font-size: 0.6rem;
129
+ color: var(--text-muted);
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.1em;
132
+ display: flex;
133
+ justify-content: space-between;
134
+ align-items: center;
135
+ }
136
+
137
+ .sidebar-section-count {
138
+ color: var(--text-dim);
139
+ font-variant-numeric: tabular-nums;
140
+ }
141
+
142
+ .sidebar-list {
143
+ overflow-y: auto;
144
+ scrollbar-width: thin;
145
+ scrollbar-color: var(--border) transparent;
146
+ }
147
+
148
+ .sidebar-history { flex: 1; }
149
+
150
+ .sidebar-item {
151
+ padding: 0.45rem 0.75rem;
152
+ border-bottom: 1px solid var(--border);
153
+ cursor: pointer;
154
+ transition: background 0.1s;
155
+ position: relative;
156
+ }
157
+
158
+ .sidebar-item:hover { background: var(--bg-item-hover); }
159
+ .sidebar-item.active { background: var(--bg-item-active); }
160
+ .sidebar-item.active::before {
161
+ content: '';
162
+ position: absolute;
163
+ left: 0; top: 0; bottom: 0;
164
+ width: 2px;
165
+ background: var(--accent);
166
+ }
167
+
168
+ .history-row {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 0.4rem;
172
+ margin-bottom: 0.15rem;
173
+ }
174
+
175
+ .history-item-content {
176
+ font-size: 0.65rem;
177
+ color: var(--text);
178
+ white-space: nowrap;
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ line-height: 1.3;
182
+ flex: 1;
183
+ }
184
+
185
+ .history-item-preview {
186
+ font-size: 0.6rem;
187
+ color: var(--text-muted);
188
+ white-space: nowrap;
189
+ overflow: hidden;
190
+ text-overflow: ellipsis;
191
+ line-height: 1.3;
192
+ }
193
+
194
+ .history-item-time {
195
+ font-size: 0.55rem;
196
+ color: var(--text-muted);
197
+ font-variant-numeric: tabular-nums;
198
+ flex-shrink: 0;
199
+ }
200
+
201
+ /* ── Chat area ── */
202
+ .chat-panel {
203
+ flex: 1;
204
+ display: flex;
205
+ flex-direction: column;
206
+ overflow: hidden;
207
+ }
208
+
209
+ #messages {
210
+ flex: 1;
211
+ overflow-y: auto;
212
+ padding: 1rem;
213
+ scrollbar-width: thin;
214
+ scrollbar-color: var(--border) transparent;
215
+ }
216
+
217
+ .msg {
218
+ margin-bottom: 0.75rem;
219
+ white-space: pre-wrap;
220
+ word-break: break-word;
221
+ font-size: 0.8rem;
222
+ line-height: 1.5;
223
+ }
224
+ .msg.user { color: var(--accent); }
225
+ .msg.ove { color: var(--text); display: flex; gap: 0.5rem; align-items: flex-start; }
226
+ .msg.ove .ove-avatar { width: 20px; height: 20px; border-radius: 3px; flex-shrink: 0; margin-top: 2px; }
227
+ .msg.ove .ove-text { flex: 1; }
228
+ .msg.status { color: var(--text-muted); font-size: 0.75rem; }
229
+ .msg.error { color: var(--red); }
230
+ .msg.system {
231
+ color: var(--text-muted);
232
+ font-size: 0.7rem;
233
+ text-align: center;
234
+ border-bottom: 1px solid var(--border);
235
+ padding-bottom: 0.5rem;
236
+ margin-bottom: 1rem;
237
+ }
238
+
239
+ #input-bar {
240
+ display: flex;
241
+ padding: 0.5rem;
242
+ border-top: 1px solid var(--border);
243
+ background: var(--bg-panel);
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ #input-bar input {
248
+ flex: 1;
249
+ background: var(--bg-item);
250
+ border: 1px solid var(--border);
251
+ color: var(--text);
252
+ padding: 0.5rem 0.75rem;
253
+ font-family: inherit;
254
+ font-size: 0.8rem;
255
+ border-radius: 3px;
256
+ }
257
+ #input-bar input:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
258
+
259
+ #input-bar button {
260
+ background: var(--bg-item);
261
+ color: var(--text-dim);
262
+ border: 1px solid var(--border);
263
+ padding: 0.5rem 1rem;
264
+ cursor: pointer;
265
+ font-family: inherit;
266
+ font-size: 0.8rem;
267
+ border-radius: 3px;
268
+ margin-left: 0.4rem;
269
+ transition: all 0.15s;
270
+ }
271
+ #input-bar button:hover { color: var(--text); border-color: var(--border-light); }
272
+
273
+ ::-webkit-scrollbar { width: 6px; }
274
+ ::-webkit-scrollbar-track { background: transparent; }
275
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
276
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
20
277
  </style>
21
278
  </head>
22
279
  <body>
23
- <div id="messages"></div>
24
- <div id="input-bar">
25
- <input id="msg" type="text" placeholder="Ask Ove something..." autofocus />
26
- <button id="send">Send</button>
280
+ <header>
281
+ <div class="header-left">
282
+ <img src="/logo.png" class="header-logo" alt="Ove">
283
+ <h1>ove</h1>
284
+ <a href="/trace" class="nav-link">traces</a>
285
+ <a href="/status" class="nav-link">status</a>
286
+ <a href="https://github.com/jacksoncage/ove" target="_blank" class="nav-link">github</a>
287
+ </div>
288
+ <div class="header-right">
289
+ <button class="sidebar-toggle" id="sidebarToggle">sidebar</button>
290
+ </div>
291
+ </header>
292
+
293
+ <div class="layout">
294
+ <div class="sidebar" id="sidebar">
295
+ <div class="sidebar-section" style="flex:1;display:flex;flex-direction:column">
296
+ <div class="sidebar-section-header">
297
+ <span>Chat history</span>
298
+ </div>
299
+ <div class="sidebar-list sidebar-history" id="historyList"></div>
300
+ </div>
301
+ </div>
302
+
303
+ <div class="chat-panel">
304
+ <div id="messages"></div>
305
+ <div id="input-bar">
306
+ <input id="msg" type="text" placeholder="Ask Ove something..." autofocus />
307
+ <button id="send">Send</button>
308
+ </div>
309
+ </div>
27
310
  </div>
311
+
28
312
  <script>
29
- const API_KEY = localStorage.getItem("ove-api-key") || prompt("API Key:");
313
+ var API_KEY = localStorage.getItem("ove-api-key") || prompt("API Key:");
30
314
  if (API_KEY) localStorage.setItem("ove-api-key", API_KEY);
31
315
 
32
- const msgs = document.getElementById("messages");
33
- const input = document.getElementById("msg");
34
- const btn = document.getElementById("send");
316
+ var msgs = document.getElementById("messages");
317
+ var input = document.getElementById("msg");
318
+ var btn = document.getElementById("send");
319
+ var sidebarEl = document.getElementById("sidebar");
320
+ var sidebarToggleEl = document.getElementById("sidebarToggle");
321
+ var historyListEl = document.getElementById("historyList");
35
322
 
323
+ function apiHeaders() { return { "X-API-Key": API_KEY }; }
324
+
325
+ function fmtTime(iso) {
326
+ if (!iso) return "";
327
+ var d = new Date(iso);
328
+ var m = Math.floor((Date.now() - d.getTime()) / 60000);
329
+ if (m < 1) return "now";
330
+ if (m < 60) return m + "m";
331
+ var h = Math.floor(m / 60);
332
+ if (h < 24) return h + "h";
333
+ return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
334
+ }
335
+
336
+ function truncate(s, n) {
337
+ if (!s) return "";
338
+ return s.length > n ? s.slice(0, n) + "..." : s;
339
+ }
340
+
341
+ function el(tag, cls, text) {
342
+ var e = document.createElement(tag);
343
+ if (cls) e.className = cls;
344
+ if (text != null) e.textContent = text;
345
+ return e;
346
+ }
347
+
348
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
349
+
350
+ // ── Sidebar toggle ──
351
+ var sidebarHidden = localStorage.getItem("ove-sidebar") === "hidden";
352
+ if (sidebarHidden) sidebarEl.classList.add("collapsed");
353
+
354
+ sidebarToggleEl.addEventListener("click", function () {
355
+ sidebarEl.classList.toggle("collapsed");
356
+ localStorage.setItem("ove-sidebar", sidebarEl.classList.contains("collapsed") ? "hidden" : "visible");
357
+ });
358
+
359
+ // ── Chat messages ──
36
360
  function addMsg(text, cls) {
37
- const div = document.createElement("div");
38
- div.className = "msg " + cls;
39
- div.textContent = text;
361
+ var div = el("div", "msg " + cls);
362
+ if (cls === "ove") {
363
+ var img = document.createElement("img");
364
+ img.src = "/logo.png";
365
+ img.className = "ove-avatar";
366
+ img.alt = "Ove";
367
+ div.appendChild(img);
368
+ var span = el("span", "ove-text", text);
369
+ div.appendChild(span);
370
+ } else {
371
+ div.textContent = text;
372
+ }
40
373
  msgs.appendChild(div);
41
374
  msgs.scrollTop = msgs.scrollHeight;
42
375
  return div;
43
376
  }
44
377
 
45
378
  async function send() {
46
- const text = input.value.trim();
379
+ var text = input.value.trim();
47
380
  if (!text) return;
48
381
  input.value = "";
49
382
  addMsg("> " + text, "user");
50
383
 
51
384
  try {
52
- const res = await fetch("/api/message", {
385
+ var res = await fetch("/api/message", {
53
386
  method: "POST",
54
387
  headers: { "Content-Type": "application/json", "X-API-Key": API_KEY },
55
- body: JSON.stringify({ text }),
388
+ body: JSON.stringify({ text: text }),
56
389
  });
57
390
  if (!res.ok) { addMsg("Error: " + res.status, "error"); return; }
58
- const { eventId } = await res.json();
391
+ var data = await res.json();
392
+ var eventId = data.eventId;
59
393
 
60
- const statusDiv = addMsg("Working...", "status");
394
+ var statusDiv = addMsg("Working...", "status");
61
395
 
62
- const sse = new EventSource("/api/message/" + eventId + "/stream?key=" + API_KEY);
63
- sse.onmessage = (e) => {
64
- const data = JSON.parse(e.data);
65
- if (data.status === "completed") {
396
+ var sse = new EventSource("/api/message/" + eventId + "/stream?key=" + encodeURIComponent(API_KEY));
397
+ sse.onmessage = function (e) {
398
+ var d = JSON.parse(e.data);
399
+ if (d.status === "completed") {
66
400
  statusDiv.remove();
67
- addMsg(data.result, "ove");
401
+ addMsg(d.result, "ove");
68
402
  sse.close();
69
- } else if (data.statusText) {
70
- statusDiv.textContent = data.statusText;
403
+ // Refresh sidebar
404
+ fetchHistory();
405
+ } else if (d.statusText) {
406
+ statusDiv.textContent = d.statusText;
71
407
  }
72
408
  };
73
- sse.onerror = () => { sse.close(); };
409
+ sse.onerror = function () { sse.close(); };
74
410
  } catch (err) {
75
411
  addMsg("Error: " + err.message, "error");
76
412
  }
77
413
  }
78
414
 
79
415
  btn.addEventListener("click", send);
80
- input.addEventListener("keydown", (e) => { if (e.key === "Enter") send(); });
416
+ input.addEventListener("keydown", function (e) { if (e.key === "Enter") send(); });
417
+
418
+ // ── Chat history sidebar ──
419
+ async function fetchHistory() {
420
+ try {
421
+ var res = await fetch("/api/history/" + encodeURIComponent("http:web") + "?limit=60&key=" + encodeURIComponent(API_KEY), { headers: apiHeaders() });
422
+ if (!res.ok) return;
423
+ var history = await res.json();
424
+ clear(historyListEl);
425
+
426
+ // Group into conversations: each user msg + following assistant msgs
427
+ var convos = [];
428
+ for (var i = 0; i < history.length; i++) {
429
+ if (history[i].role === "user") {
430
+ var convo = { user: history[i], replies: [] };
431
+ for (var j = i + 1; j < history.length && history[j].role === "assistant"; j++) {
432
+ convo.replies.push(history[j]);
433
+ }
434
+ convos.push(convo);
435
+ }
436
+ }
437
+
438
+ // Show newest first in sidebar
439
+ for (var i = convos.length - 1; i >= 0; i--) {
440
+ var c = convos[i];
441
+ var item = el("div", "sidebar-item");
442
+ var row = el("div", "history-row");
443
+ row.appendChild(el("span", "history-item-content", truncate(c.user.content, 50)));
444
+ row.appendChild(el("span", "history-item-time", fmtTime(c.user.timestamp)));
445
+ item.appendChild(row);
446
+ if (c.replies.length > 0) {
447
+ item.appendChild(el("div", "history-item-preview", truncate(c.replies[0].content, 60)));
448
+ }
449
+
450
+ (function (convo) {
451
+ item.addEventListener("click", function () {
452
+ clear(msgs);
453
+ addMsg("> " + convo.user.content, "user");
454
+ for (var r = 0; r < convo.replies.length; r++) {
455
+ addMsg(convo.replies[r].content, "ove");
456
+ }
457
+ });
458
+ })(c);
459
+
460
+ historyListEl.appendChild(item);
461
+ }
462
+ } catch (e) {
463
+ console.error("Failed to fetch history:", e);
464
+ }
465
+ }
466
+
467
+ // ── Init ──
468
+ fetchHistory();
81
469
  </script>
82
470
  </body>
83
471
  </html>
Binary file