clicodelog 0.1.0__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.
@@ -0,0 +1,1067 @@
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>AI Conversation History</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ /* Dark Theme (Default) */
12
+ :root {
13
+ --bg-primary: #0d1117;
14
+ --bg-secondary: #161b22;
15
+ --bg-tertiary: #21262d;
16
+ --border-color: #30363d;
17
+ --text-primary: #e6edf3;
18
+ --text-secondary: #8b949e;
19
+ --text-muted: #6e7681;
20
+ --accent-blue: #58a6ff;
21
+ --accent-purple: #a371f7;
22
+ --accent-green: #3fb950;
23
+ --accent-orange: #d29922;
24
+ --user-bg: #1f6feb22;
25
+ --assistant-bg: #238636;
26
+ --thinking-bg: #a371f722;
27
+ --shadow-color: rgba(0, 0, 0, 0.3);
28
+ }
29
+
30
+ /* Light Theme - Soft Blue */
31
+ [data-theme="light"] {
32
+ --bg-primary: #f0f6fc;
33
+ --bg-secondary: #ffffff;
34
+ --bg-tertiary: #e8f1fb;
35
+ --border-color: #c8d8e8;
36
+ --text-primary: #1a3a5c;
37
+ --text-secondary: #4a6a8a;
38
+ --text-muted: #6b8aaa;
39
+ --accent-blue: #2563eb;
40
+ --accent-purple: #7c3aed;
41
+ --accent-green: #16a34a;
42
+ --accent-orange: #d97706;
43
+ --user-bg: #dbeafe;
44
+ --assistant-bg: #16a34a;
45
+ --thinking-bg: #ede9fe;
46
+ --shadow-color: rgba(37, 99, 235, 0.1);
47
+ }
48
+
49
+ * {
50
+ box-sizing: border-box;
51
+ margin: 0;
52
+ padding: 0;
53
+ }
54
+
55
+ body {
56
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
57
+ background: var(--bg-primary);
58
+ color: var(--text-primary);
59
+ line-height: 1.6;
60
+ }
61
+
62
+ .app {
63
+ display: grid;
64
+ grid-template-columns: 280px 320px 1fr;
65
+ height: 100vh;
66
+ }
67
+
68
+ .panel {
69
+ border-right: 1px solid var(--border-color);
70
+ overflow-y: auto;
71
+ display: flex;
72
+ flex-direction: column;
73
+ }
74
+
75
+ .panel-header {
76
+ padding: 16px;
77
+ border-bottom: 1px solid var(--border-color);
78
+ background: var(--bg-secondary);
79
+ position: sticky;
80
+ top: 0;
81
+ z-index: 10;
82
+ }
83
+
84
+ .panel-header h2 {
85
+ font-size: 14px;
86
+ font-weight: 600;
87
+ color: var(--text-secondary);
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.5px;
90
+ }
91
+
92
+ .panel-content {
93
+ flex: 1;
94
+ overflow-y: auto;
95
+ }
96
+
97
+ .search-box {
98
+ padding: 12px 16px;
99
+ border-bottom: 1px solid var(--border-color);
100
+ }
101
+
102
+ .search-box input {
103
+ width: 100%;
104
+ padding: 8px 12px;
105
+ background: var(--bg-tertiary);
106
+ border: 1px solid var(--border-color);
107
+ border-radius: 6px;
108
+ color: var(--text-primary);
109
+ font-size: 13px;
110
+ }
111
+
112
+ .search-box input:focus {
113
+ outline: none;
114
+ border-color: var(--accent-blue);
115
+ }
116
+
117
+ .list-item {
118
+ padding: 12px 16px;
119
+ border-bottom: 1px solid var(--border-color);
120
+ cursor: pointer;
121
+ transition: background 0.15s;
122
+ }
123
+
124
+ .list-item:hover {
125
+ background: var(--bg-secondary);
126
+ }
127
+
128
+ .list-item.active {
129
+ background: var(--bg-tertiary);
130
+ border-left: 3px solid var(--accent-blue);
131
+ }
132
+
133
+ .list-item-title {
134
+ font-size: 14px;
135
+ font-weight: 500;
136
+ margin-bottom: 4px;
137
+ white-space: nowrap;
138
+ overflow: hidden;
139
+ text-overflow: ellipsis;
140
+ }
141
+
142
+ .list-item-meta {
143
+ font-size: 12px;
144
+ color: var(--text-muted);
145
+ }
146
+
147
+ .conversation-panel {
148
+ border-right: none;
149
+ background: var(--bg-primary);
150
+ }
151
+
152
+ .conversation-header {
153
+ padding: 16px 24px;
154
+ border-bottom: 1px solid var(--border-color);
155
+ background: var(--bg-secondary);
156
+ }
157
+
158
+ .conversation-header h1 {
159
+ font-size: 18px;
160
+ font-weight: 600;
161
+ margin-bottom: 8px;
162
+ }
163
+
164
+ .conversation-meta {
165
+ display: flex;
166
+ gap: 16px;
167
+ font-size: 13px;
168
+ color: var(--text-secondary);
169
+ }
170
+
171
+ .messages {
172
+ padding: 24px;
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 24px;
176
+ }
177
+
178
+ .message {
179
+ max-width: 100%;
180
+ }
181
+
182
+ .message-header {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 8px;
186
+ margin-bottom: 8px;
187
+ }
188
+
189
+ .message-role {
190
+ font-size: 12px;
191
+ font-weight: 600;
192
+ text-transform: uppercase;
193
+ letter-spacing: 0.5px;
194
+ padding: 2px 8px;
195
+ border-radius: 4px;
196
+ }
197
+
198
+ .message.user .message-role {
199
+ background: var(--accent-blue);
200
+ color: white;
201
+ }
202
+
203
+ .message.assistant .message-role {
204
+ background: var(--accent-purple);
205
+ color: white;
206
+ }
207
+
208
+ .message-time {
209
+ font-size: 11px;
210
+ color: var(--text-muted);
211
+ }
212
+
213
+ .message-content {
214
+ padding: 16px;
215
+ border-radius: 8px;
216
+ font-size: 14px;
217
+ white-space: pre-wrap;
218
+ word-wrap: break-word;
219
+ }
220
+
221
+ .message.user .message-content {
222
+ background: var(--user-bg);
223
+ border: 1px solid var(--border-color);
224
+ }
225
+
226
+ .message.assistant .message-content {
227
+ background: var(--bg-secondary);
228
+ border: 1px solid var(--border-color);
229
+ }
230
+
231
+ .thinking-block {
232
+ margin-top: 12px;
233
+ padding: 12px;
234
+ background: var(--thinking-bg);
235
+ border-radius: 6px;
236
+ border-left: 3px solid var(--accent-purple);
237
+ }
238
+
239
+ .thinking-header {
240
+ font-size: 11px;
241
+ font-weight: 600;
242
+ color: var(--accent-purple);
243
+ text-transform: uppercase;
244
+ margin-bottom: 8px;
245
+ cursor: pointer;
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 6px;
249
+ }
250
+
251
+ .thinking-header::before {
252
+ content: '▶';
253
+ font-size: 8px;
254
+ transition: transform 0.2s;
255
+ }
256
+
257
+ .thinking-block.expanded .thinking-header::before {
258
+ transform: rotate(90deg);
259
+ }
260
+
261
+ .thinking-content {
262
+ display: none;
263
+ font-size: 13px;
264
+ color: var(--text-secondary);
265
+ font-family: 'JetBrains Mono', monospace;
266
+ white-space: pre-wrap;
267
+ max-height: 400px;
268
+ overflow-y: auto;
269
+ }
270
+
271
+ .thinking-block.expanded .thinking-content {
272
+ display: block;
273
+ }
274
+
275
+ .tool-uses {
276
+ margin-top: 12px;
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 8px;
280
+ }
281
+
282
+ .tool-use {
283
+ padding: 12px;
284
+ background: var(--bg-tertiary);
285
+ border-radius: 6px;
286
+ border-left: 3px solid var(--accent-orange);
287
+ }
288
+
289
+ .tool-name {
290
+ font-size: 12px;
291
+ font-weight: 600;
292
+ color: var(--accent-orange);
293
+ margin-bottom: 6px;
294
+ font-family: 'JetBrains Mono', monospace;
295
+ }
296
+
297
+ .tool-input {
298
+ font-size: 12px;
299
+ font-family: 'JetBrains Mono', monospace;
300
+ color: var(--text-secondary);
301
+ white-space: pre-wrap;
302
+ max-height: 200px;
303
+ overflow-y: auto;
304
+ }
305
+
306
+ .empty-state {
307
+ display: flex;
308
+ flex-direction: column;
309
+ align-items: center;
310
+ justify-content: center;
311
+ height: 100%;
312
+ color: var(--text-muted);
313
+ text-align: center;
314
+ padding: 40px;
315
+ }
316
+
317
+ .empty-state-icon {
318
+ font-size: 48px;
319
+ margin-bottom: 16px;
320
+ }
321
+
322
+ .summaries {
323
+ padding: 12px 16px;
324
+ background: var(--bg-tertiary);
325
+ border-bottom: 1px solid var(--border-color);
326
+ }
327
+
328
+ .summary-tag {
329
+ display: inline-block;
330
+ padding: 4px 8px;
331
+ background: var(--accent-green);
332
+ color: white;
333
+ border-radius: 4px;
334
+ font-size: 11px;
335
+ margin: 2px;
336
+ }
337
+
338
+ .model-badge {
339
+ font-size: 11px;
340
+ padding: 2px 6px;
341
+ background: var(--bg-tertiary);
342
+ border-radius: 4px;
343
+ color: var(--text-muted);
344
+ font-family: 'JetBrains Mono', monospace;
345
+ }
346
+
347
+ .usage-info {
348
+ font-size: 11px;
349
+ color: var(--text-muted);
350
+ margin-left: auto;
351
+ }
352
+
353
+ code {
354
+ font-family: 'JetBrains Mono', monospace;
355
+ background: var(--bg-tertiary);
356
+ padding: 2px 6px;
357
+ border-radius: 4px;
358
+ font-size: 0.9em;
359
+ }
360
+
361
+ pre {
362
+ background: var(--bg-tertiary);
363
+ padding: 12px;
364
+ border-radius: 6px;
365
+ overflow-x: auto;
366
+ font-family: 'JetBrains Mono', monospace;
367
+ font-size: 13px;
368
+ }
369
+
370
+ .loading {
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ padding: 40px;
375
+ color: var(--text-muted);
376
+ }
377
+
378
+ @keyframes spin {
379
+ to { transform: rotate(360deg); }
380
+ }
381
+
382
+ .spinner {
383
+ width: 20px;
384
+ height: 20px;
385
+ border: 2px solid var(--border-color);
386
+ border-top-color: var(--accent-blue);
387
+ border-radius: 50%;
388
+ animation: spin 0.8s linear infinite;
389
+ margin-right: 12px;
390
+ }
391
+
392
+ /* Top Controls */
393
+ .top-controls {
394
+ position: fixed;
395
+ top: 16px;
396
+ right: 16px;
397
+ z-index: 100;
398
+ display: flex;
399
+ gap: 8px;
400
+ align-items: center;
401
+ }
402
+
403
+ .control-btn {
404
+ background: var(--bg-secondary);
405
+ border: 1px solid var(--border-color);
406
+ border-radius: 8px;
407
+ padding: 8px 12px;
408
+ cursor: pointer;
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 8px;
412
+ font-size: 13px;
413
+ font-weight: 500;
414
+ color: var(--text-primary);
415
+ transition: all 0.2s ease;
416
+ box-shadow: 0 2px 8px var(--shadow-color);
417
+ }
418
+
419
+ .control-btn:hover {
420
+ background: var(--bg-tertiary);
421
+ border-color: var(--accent-blue);
422
+ transform: translateY(-1px);
423
+ }
424
+
425
+ .control-btn .icon {
426
+ font-size: 16px;
427
+ }
428
+
429
+ .control-btn.syncing .icon {
430
+ animation: spin 1s linear infinite;
431
+ }
432
+
433
+ .control-btn:disabled {
434
+ opacity: 0.4;
435
+ cursor: not-allowed;
436
+ transform: none;
437
+ }
438
+
439
+ .control-btn:disabled:hover {
440
+ background: var(--bg-secondary);
441
+ border-color: var(--border-color);
442
+ transform: none;
443
+ }
444
+
445
+ .sync-status {
446
+ font-size: 11px;
447
+ color: var(--text-muted);
448
+ background: var(--bg-secondary);
449
+ border: 1px solid var(--border-color);
450
+ border-radius: 6px;
451
+ padding: 6px 10px;
452
+ }
453
+
454
+
455
+ /* Light theme specific adjustments */
456
+ [data-theme="light"] .message.user .message-content {
457
+ background: var(--user-bg);
458
+ border: 1px solid #bfdbfe;
459
+ }
460
+
461
+ [data-theme="light"] .message.assistant .message-content {
462
+ background: var(--bg-secondary);
463
+ border: 1px solid var(--border-color);
464
+ box-shadow: 0 1px 3px var(--shadow-color);
465
+ }
466
+
467
+ [data-theme="light"] .thinking-block {
468
+ background: var(--thinking-bg);
469
+ border-left: 3px solid var(--accent-purple);
470
+ }
471
+
472
+ [data-theme="light"] .tool-use {
473
+ background: #fef3c7;
474
+ border-left: 3px solid var(--accent-orange);
475
+ }
476
+
477
+ [data-theme="light"] .search-box input {
478
+ background: var(--bg-secondary);
479
+ border: 1px solid var(--border-color);
480
+ }
481
+
482
+ [data-theme="light"] .search-box input:focus {
483
+ border-color: var(--accent-blue);
484
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
485
+ }
486
+
487
+ [data-theme="light"] .list-item:hover {
488
+ background: var(--bg-tertiary);
489
+ }
490
+
491
+ [data-theme="light"] .list-item.active {
492
+ background: #dbeafe;
493
+ border-left: 3px solid var(--accent-blue);
494
+ }
495
+
496
+ [data-theme="light"] .panel-header {
497
+ background: var(--bg-secondary);
498
+ box-shadow: 0 1px 2px var(--shadow-color);
499
+ }
500
+
501
+ [data-theme="light"] .conversation-header {
502
+ background: var(--bg-secondary);
503
+ box-shadow: 0 1px 2px var(--shadow-color);
504
+ }
505
+
506
+ [data-theme="light"] code {
507
+ background: #e0e7ff;
508
+ color: #3730a3;
509
+ }
510
+
511
+ [data-theme="light"] pre {
512
+ background: #f1f5f9;
513
+ border: 1px solid var(--border-color);
514
+ }
515
+
516
+ [data-theme="light"] .model-badge {
517
+ background: #e0e7ff;
518
+ color: #3730a3;
519
+ }
520
+
521
+ [data-theme="light"] .summaries {
522
+ background: #ecfdf5;
523
+ border-bottom: 1px solid #a7f3d0;
524
+ }
525
+
526
+ [data-theme="light"] .summary-tag {
527
+ background: var(--accent-green);
528
+ }
529
+
530
+ /* Source Selector Styles */
531
+ .source-selector {
532
+ display: flex;
533
+ align-items: center;
534
+ gap: 8px;
535
+ background: var(--bg-secondary);
536
+ border: 1px solid var(--border-color);
537
+ border-radius: 8px;
538
+ padding: 6px 12px;
539
+ box-shadow: 0 2px 8px var(--shadow-color);
540
+ }
541
+
542
+ .source-selector label {
543
+ font-size: 12px;
544
+ color: var(--text-secondary);
545
+ font-weight: 500;
546
+ }
547
+
548
+ .source-selector select {
549
+ background: var(--bg-tertiary);
550
+ border: 1px solid var(--border-color);
551
+ border-radius: 4px;
552
+ padding: 4px 8px;
553
+ color: var(--text-primary);
554
+ font-size: 13px;
555
+ font-weight: 500;
556
+ cursor: pointer;
557
+ min-width: 140px;
558
+ }
559
+
560
+ .source-selector select:focus {
561
+ outline: none;
562
+ border-color: var(--accent-blue);
563
+ }
564
+
565
+ .source-indicator {
566
+ width: 8px;
567
+ height: 8px;
568
+ border-radius: 50%;
569
+ background: var(--accent-green);
570
+ }
571
+
572
+ .source-indicator.unavailable {
573
+ background: var(--text-muted);
574
+ }
575
+
576
+ [data-theme="light"] .source-selector select {
577
+ background: var(--bg-secondary);
578
+ }
579
+ </style>
580
+ </head>
581
+ <body>
582
+ <!-- Top Controls -->
583
+ <div class="top-controls">
584
+ <div class="source-selector">
585
+ <label>Source:</label>
586
+ <select id="source-select" onchange="changeSource(this.value)">
587
+ <option value="claude-code">Claude Code</option>
588
+ <option value="codex">OpenAI Codex</option>
589
+ <option value="gemini">Google Gemini</option>
590
+ </select>
591
+ </div>
592
+ <span class="sync-status" id="sync-status" title="Last sync time">Syncing...</span>
593
+ <button class="control-btn" id="export-btn" onclick="exportConversation()" title="Export conversation as TXT" disabled>
594
+ <span class="icon">📥</span>
595
+ <span>Export</span>
596
+ </button>
597
+ <button class="control-btn" id="sync-btn" onclick="manualSync()" title="Sync data now">
598
+ <span class="icon">🔄</span>
599
+ <span>Sync</span>
600
+ </button>
601
+ <button class="control-btn" onclick="toggleTheme()" title="Toggle theme">
602
+ <span class="icon" id="theme-icon">🌙</span>
603
+ <span id="theme-label">Dark</span>
604
+ </button>
605
+ </div>
606
+
607
+ <div class="app">
608
+ <!-- Projects Panel -->
609
+ <div class="panel" id="projects-panel">
610
+ <div class="panel-header">
611
+ <h2>Projects</h2>
612
+ </div>
613
+ <div class="search-box">
614
+ <input type="text" id="project-search" placeholder="Search projects...">
615
+ </div>
616
+ <div class="panel-content" id="projects-list">
617
+ <div class="loading">
618
+ <div class="spinner"></div>
619
+ Loading projects...
620
+ </div>
621
+ </div>
622
+ </div>
623
+
624
+ <!-- Sessions Panel -->
625
+ <div class="panel" id="sessions-panel">
626
+ <div class="panel-header">
627
+ <h2>Sessions</h2>
628
+ </div>
629
+ <div class="panel-content" id="sessions-list">
630
+ <div class="empty-state">
631
+ <div class="empty-state-icon">📁</div>
632
+ <p>Select a project to view sessions</p>
633
+ </div>
634
+ </div>
635
+ </div>
636
+
637
+ <!-- Conversation Panel -->
638
+ <div class="panel conversation-panel" id="conversation-panel">
639
+ <div id="conversation-content">
640
+ <div class="empty-state">
641
+ <div class="empty-state-icon">💬</div>
642
+ <h3>AI Conversation History</h3>
643
+ <p>Select a session to view the conversation</p>
644
+ </div>
645
+ </div>
646
+ </div>
647
+ </div>
648
+
649
+ <script>
650
+ let currentProjectId = null;
651
+ let currentSessionId = null;
652
+ let currentSource = 'claude-code';
653
+ let projects = [];
654
+ let availableSources = [];
655
+
656
+ // Theme Management
657
+ function getPreferredTheme() {
658
+ const saved = localStorage.getItem('theme');
659
+ if (saved) return saved;
660
+ // Default to light theme as requested
661
+ return 'light';
662
+ }
663
+
664
+ function setTheme(theme) {
665
+ document.documentElement.setAttribute('data-theme', theme);
666
+ localStorage.setItem('theme', theme);
667
+ updateThemeToggle(theme);
668
+ }
669
+
670
+ function updateThemeToggle(theme) {
671
+ const icon = document.getElementById('theme-icon');
672
+ const label = document.getElementById('theme-label');
673
+ if (theme === 'light') {
674
+ icon.textContent = '☀️';
675
+ label.textContent = 'Light';
676
+ } else {
677
+ icon.textContent = '🌙';
678
+ label.textContent = 'Dark';
679
+ }
680
+ }
681
+
682
+ function toggleTheme() {
683
+ const current = document.documentElement.getAttribute('data-theme') || 'dark';
684
+ const newTheme = current === 'light' ? 'dark' : 'light';
685
+ setTheme(newTheme);
686
+ }
687
+
688
+ // Initialize theme on page load
689
+ (function() {
690
+ const theme = getPreferredTheme();
691
+ setTheme(theme);
692
+ })();
693
+
694
+ // Source Management
695
+ async function loadSources() {
696
+ try {
697
+ const response = await fetch('/api/sources');
698
+ const data = await response.json();
699
+ availableSources = data.sources;
700
+ currentSource = data.current;
701
+
702
+ const select = document.getElementById('source-select');
703
+ select.innerHTML = availableSources.map(source => `
704
+ <option value="${source.id}" ${source.id === currentSource ? 'selected' : ''} ${!source.available ? 'disabled' : ''}>
705
+ ${source.name}${!source.available ? ' (not found)' : ''}
706
+ </option>
707
+ `).join('');
708
+ } catch (error) {
709
+ console.error('Error loading sources:', error);
710
+ }
711
+ }
712
+
713
+ async function changeSource(sourceId) {
714
+ if (sourceId === currentSource) return;
715
+
716
+ currentSource = sourceId;
717
+ currentProjectId = null;
718
+ currentSessionId = null;
719
+
720
+ // Update UI
721
+ document.getElementById('export-btn').disabled = true;
722
+
723
+ // Reset panels
724
+ document.getElementById('sessions-list').innerHTML = `
725
+ <div class="empty-state">
726
+ <div class="empty-state-icon">📁</div>
727
+ <p>Select a project to view sessions</p>
728
+ </div>
729
+ `;
730
+ document.getElementById('conversation-content').innerHTML = `
731
+ <div class="empty-state">
732
+ <div class="empty-state-icon">💬</div>
733
+ <h3>AI Conversation History</h3>
734
+ <p>Select a session to view the conversation</p>
735
+ </div>
736
+ `;
737
+
738
+ // Load projects for new source
739
+ await loadProjects();
740
+ await fetchSyncStatus();
741
+ }
742
+
743
+ // Sync Management
744
+ async function fetchSyncStatus() {
745
+ try {
746
+ const response = await fetch(`/api/status?source=${currentSource}`);
747
+ const data = await response.json();
748
+ updateSyncStatus(data.last_sync);
749
+ } catch (error) {
750
+ console.error('Error fetching sync status:', error);
751
+ document.getElementById('sync-status').textContent = 'Status unavailable';
752
+ }
753
+ }
754
+
755
+ function updateSyncStatus(lastSync) {
756
+ const statusEl = document.getElementById('sync-status');
757
+ if (lastSync) {
758
+ const date = new Date(lastSync);
759
+ const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
760
+ statusEl.textContent = `Last sync: ${timeStr}`;
761
+ statusEl.title = `Last sync: ${date.toLocaleString()}`;
762
+ } else {
763
+ statusEl.textContent = 'Not synced';
764
+ }
765
+ }
766
+
767
+ async function manualSync() {
768
+ const btn = document.getElementById('sync-btn');
769
+ const statusEl = document.getElementById('sync-status');
770
+
771
+ btn.classList.add('syncing');
772
+ btn.disabled = true;
773
+ statusEl.textContent = 'Syncing...';
774
+
775
+ try {
776
+ const response = await fetch(`/api/sync?source=${currentSource}`, { method: 'POST' });
777
+ const data = await response.json();
778
+
779
+ if (data.status === 'success') {
780
+ updateSyncStatus(data.last_sync);
781
+ // Reload projects to show any new data
782
+ await loadProjects();
783
+ } else {
784
+ statusEl.textContent = 'Sync failed';
785
+ }
786
+ } catch (error) {
787
+ console.error('Error syncing:', error);
788
+ statusEl.textContent = 'Sync error';
789
+ } finally {
790
+ btn.classList.remove('syncing');
791
+ btn.disabled = false;
792
+ }
793
+ }
794
+
795
+ // Fetch sync status on load and periodically
796
+ fetchSyncStatus();
797
+ setInterval(fetchSyncStatus, 60000); // Update status every minute
798
+
799
+ // Export conversation as text file
800
+ function exportConversation() {
801
+ if (!currentProjectId || !currentSessionId) return;
802
+
803
+ const url = `/api/projects/${currentProjectId}/sessions/${currentSessionId}/export?source=${currentSource}`;
804
+
805
+ // Create a temporary link and trigger download
806
+ const link = document.createElement('a');
807
+ link.href = url;
808
+ link.download = `${currentSessionId}.txt`;
809
+ document.body.appendChild(link);
810
+ link.click();
811
+ document.body.removeChild(link);
812
+ }
813
+
814
+ // Format timestamp
815
+ function formatTime(timestamp) {
816
+ if (!timestamp) return '';
817
+ const date = new Date(timestamp);
818
+ return date.toLocaleString();
819
+ }
820
+
821
+ // Format file size
822
+ function formatSize(bytes) {
823
+ if (bytes < 1024) return bytes + ' B';
824
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
825
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
826
+ }
827
+
828
+ // Escape HTML
829
+ function escapeHtml(text) {
830
+ if (!text) return '';
831
+ const div = document.createElement('div');
832
+ div.textContent = text;
833
+ return div.innerHTML;
834
+ }
835
+
836
+ // Format tool input
837
+ function formatToolInput(input) {
838
+ if (typeof input === 'string') return escapeHtml(input);
839
+ try {
840
+ return escapeHtml(JSON.stringify(input, null, 2));
841
+ } catch {
842
+ return escapeHtml(String(input));
843
+ }
844
+ }
845
+
846
+ // Load projects
847
+ async function loadProjects() {
848
+ try {
849
+ const response = await fetch(`/api/projects?source=${currentSource}`);
850
+ projects = await response.json();
851
+ renderProjects(projects);
852
+ } catch (error) {
853
+ console.error('Error loading projects:', error);
854
+ document.getElementById('projects-list').innerHTML = `
855
+ <div class="empty-state">
856
+ <p>Error loading projects</p>
857
+ </div>
858
+ `;
859
+ }
860
+ }
861
+
862
+ // Render projects list
863
+ function renderProjects(projectsList) {
864
+ const container = document.getElementById('projects-list');
865
+ if (projectsList.length === 0) {
866
+ container.innerHTML = `
867
+ <div class="empty-state">
868
+ <p>No projects found</p>
869
+ </div>
870
+ `;
871
+ return;
872
+ }
873
+
874
+ container.innerHTML = projectsList.map(project => `
875
+ <div class="list-item ${project.id === currentProjectId ? 'active' : ''}"
876
+ onclick="selectProject('${project.id}')">
877
+ <div class="list-item-title">${escapeHtml(project.name)}</div>
878
+ <div class="list-item-meta">${project.session_count} sessions</div>
879
+ </div>
880
+ `).join('');
881
+ }
882
+
883
+ // Select project
884
+ async function selectProject(projectId) {
885
+ currentProjectId = projectId;
886
+ currentSessionId = null;
887
+ renderProjects(projects);
888
+
889
+ // Disable export button when switching projects
890
+ document.getElementById('export-btn').disabled = true;
891
+
892
+ const sessionsContainer = document.getElementById('sessions-list');
893
+ sessionsContainer.innerHTML = `
894
+ <div class="loading">
895
+ <div class="spinner"></div>
896
+ Loading sessions...
897
+ </div>
898
+ `;
899
+
900
+ try {
901
+ const response = await fetch(`/api/projects/${projectId}/sessions?source=${currentSource}`);
902
+ const sessions = await response.json();
903
+ renderSessions(sessions);
904
+ } catch (error) {
905
+ console.error('Error loading sessions:', error);
906
+ sessionsContainer.innerHTML = `
907
+ <div class="empty-state">
908
+ <p>Error loading sessions</p>
909
+ </div>
910
+ `;
911
+ }
912
+ }
913
+
914
+ // Render sessions list
915
+ function renderSessions(sessions) {
916
+ const container = document.getElementById('sessions-list');
917
+ if (sessions.length === 0) {
918
+ container.innerHTML = `
919
+ <div class="empty-state">
920
+ <p>No sessions found</p>
921
+ </div>
922
+ `;
923
+ return;
924
+ }
925
+
926
+ container.innerHTML = sessions.map(session => `
927
+ <div class="list-item ${session.id === currentSessionId ? 'active' : ''}"
928
+ onclick="selectSession('${session.id}')">
929
+ <div class="list-item-title">${escapeHtml(session.summary)}</div>
930
+ <div class="list-item-meta">
931
+ ${session.message_count} messages &bull; ${formatSize(session.size)}
932
+ <br>${formatTime(session.last_timestamp)}
933
+ </div>
934
+ </div>
935
+ `).join('');
936
+ }
937
+
938
+ // Select session
939
+ async function selectSession(sessionId) {
940
+ currentSessionId = sessionId;
941
+ // Re-render sessions to update active state
942
+ const sessionsResponse = await fetch(`/api/projects/${currentProjectId}/sessions?source=${currentSource}`);
943
+ const sessions = await sessionsResponse.json();
944
+ renderSessions(sessions);
945
+
946
+ const conversationContainer = document.getElementById('conversation-content');
947
+ conversationContainer.innerHTML = `
948
+ <div class="loading">
949
+ <div class="spinner"></div>
950
+ Loading conversation...
951
+ </div>
952
+ `;
953
+
954
+ try {
955
+ const response = await fetch(`/api/projects/${currentProjectId}/sessions/${sessionId}?source=${currentSource}`);
956
+ const conversation = await response.json();
957
+ renderConversation(conversation);
958
+ } catch (error) {
959
+ console.error('Error loading conversation:', error);
960
+ conversationContainer.innerHTML = `
961
+ <div class="empty-state">
962
+ <p>Error loading conversation</p>
963
+ </div>
964
+ `;
965
+ }
966
+ }
967
+
968
+ // Render conversation
969
+ function renderConversation(conversation) {
970
+ const container = document.getElementById('conversation-content');
971
+
972
+ let summariesHtml = '';
973
+ if (conversation.summaries && conversation.summaries.length > 0) {
974
+ summariesHtml = `
975
+ <div class="summaries">
976
+ <strong>Summaries:</strong><br>
977
+ ${conversation.summaries.map(s => `<span class="summary-tag">${escapeHtml(s)}</span>`).join('')}
978
+ </div>
979
+ `;
980
+ }
981
+
982
+ const messagesHtml = conversation.messages.map(msg => {
983
+ let thinkingHtml = '';
984
+ if (msg.thinking) {
985
+ thinkingHtml = `
986
+ <div class="thinking-block" onclick="this.classList.toggle('expanded')">
987
+ <div class="thinking-header">Thinking</div>
988
+ <div class="thinking-content">${escapeHtml(msg.thinking)}</div>
989
+ </div>
990
+ `;
991
+ }
992
+
993
+ let toolUsesHtml = '';
994
+ if (msg.tool_uses && msg.tool_uses.length > 0) {
995
+ toolUsesHtml = `
996
+ <div class="tool-uses">
997
+ ${msg.tool_uses.map(tool => `
998
+ <div class="tool-use">
999
+ <div class="tool-name">${escapeHtml(tool.name)}</div>
1000
+ <div class="tool-input">${formatToolInput(tool.input)}</div>
1001
+ </div>
1002
+ `).join('')}
1003
+ </div>
1004
+ `;
1005
+ }
1006
+
1007
+ let usageHtml = '';
1008
+ if (msg.usage) {
1009
+ const tokens = msg.usage.input_tokens + (msg.usage.output_tokens || 0);
1010
+ usageHtml = `<span class="usage-info">${tokens.toLocaleString()} tokens</span>`;
1011
+ }
1012
+
1013
+ let modelHtml = '';
1014
+ if (msg.model) {
1015
+ modelHtml = `<span class="model-badge">${msg.model}</span>`;
1016
+ }
1017
+
1018
+ return `
1019
+ <div class="message ${msg.role}">
1020
+ <div class="message-header">
1021
+ <span class="message-role">${msg.role}</span>
1022
+ ${modelHtml}
1023
+ <span class="message-time">${formatTime(msg.timestamp)}</span>
1024
+ ${usageHtml}
1025
+ </div>
1026
+ <div class="message-content">${escapeHtml(msg.content)}</div>
1027
+ ${thinkingHtml}
1028
+ ${toolUsesHtml}
1029
+ </div>
1030
+ `;
1031
+ }).join('');
1032
+
1033
+ container.innerHTML = `
1034
+ <div class="conversation-header">
1035
+ <h1>Session: ${conversation.session_id}</h1>
1036
+ <div class="conversation-meta">
1037
+ <span>${conversation.messages.length} messages</span>
1038
+ </div>
1039
+ </div>
1040
+ ${summariesHtml}
1041
+ <div class="messages">
1042
+ ${messagesHtml}
1043
+ </div>
1044
+ `;
1045
+
1046
+ // Enable export button
1047
+ document.getElementById('export-btn').disabled = false;
1048
+ }
1049
+
1050
+ // Search projects
1051
+ document.getElementById('project-search').addEventListener('input', (e) => {
1052
+ const query = e.target.value.toLowerCase();
1053
+ const filtered = projects.filter(p =>
1054
+ p.name.toLowerCase().includes(query)
1055
+ );
1056
+ renderProjects(filtered);
1057
+ });
1058
+
1059
+ // Initialize
1060
+ async function init() {
1061
+ await loadSources();
1062
+ await loadProjects();
1063
+ }
1064
+ init();
1065
+ </script>
1066
+ </body>
1067
+ </html>