Skip to content
This repository was archived by the owner on Apr 16, 2026. It is now read-only.

Commit 861e88c

Browse files
authored
Merge pull request #23 from yepzdk/feat/issue-4-observation-detail
feat: add observation detail view with full text and timeline
2 parents f9efab3 + 6d26849 commit 861e88c

1 file changed

Lines changed: 128 additions & 2 deletions

File tree

assets/viewer/index.html

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,11 @@
7676
.card {
7777
padding: 0.75rem 1rem; background: #161b22;
7878
border: 1px solid #30363d; border-radius: 6px;
79-
margin-bottom: 0.5rem;
79+
margin-bottom: 0.5rem; cursor: pointer;
80+
transition: border-color 0.15s;
8081
}
82+
.card:hover { border-color: #58a6ff; }
83+
.card.expanded { border-color: #58a6ff; }
8184
.card .type {
8285
color: #58a6ff; font-weight: 600; font-size: 0.8rem;
8386
text-transform: uppercase; letter-spacing: 0.025em;
@@ -91,6 +94,50 @@
9194
margin-top: 0.375rem; color: #8b949e; font-size: 0.75rem;
9295
display: flex; gap: 1rem; flex-wrap: wrap;
9396
}
97+
.card .obs-detail {
98+
margin-top: 0.75rem; padding-top: 0.75rem;
99+
border-top: 1px solid #30363d;
100+
}
101+
.card .obs-detail .obs-full-text {
102+
white-space: pre-wrap; word-break: break-word;
103+
font-size: 0.875rem; line-height: 1.5; margin-bottom: 0.5rem;
104+
}
105+
.card .obs-detail .obs-meta-table {
106+
font-size: 0.8rem; color: #8b949e;
107+
display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 0.75rem;
108+
}
109+
.card .obs-detail .obs-meta-label {
110+
color: #8b949e; font-weight: 600;
111+
}
112+
.card .obs-detail .obs-meta-value {
113+
word-break: break-all;
114+
}
115+
.card .obs-detail .obs-timeline {
116+
margin-top: 0.75rem; padding-top: 0.5rem;
117+
border-top: 1px solid #21262d;
118+
}
119+
.card .obs-detail .obs-timeline-header {
120+
font-size: 0.8rem; color: #8b949e; margin-bottom: 0.5rem;
121+
}
122+
.card .obs-detail .timeline-item {
123+
padding: 0.375rem 0.5rem; font-size: 0.8rem;
124+
border-left: 2px solid #30363d; margin-bottom: 0.25rem;
125+
margin-left: 0.25rem;
126+
}
127+
.card .obs-detail .timeline-item.current {
128+
border-left-color: #58a6ff; background: #0d1117;
129+
border-radius: 0 4px 4px 0;
130+
}
131+
.card .obs-detail .timeline-item .tl-type {
132+
color: #58a6ff; font-weight: 600; text-transform: uppercase;
133+
font-size: 0.7rem; letter-spacing: 0.025em;
134+
}
135+
.card .obs-detail .timeline-item .tl-title {
136+
color: #c9d1d9;
137+
}
138+
.card .obs-detail .timeline-item .tl-time {
139+
color: #8b949e; font-size: 0.7rem;
140+
}
94141

95142
/* Placeholder */
96143
.placeholder {
@@ -460,16 +507,95 @@ <h2 id="section-title">Live Feed</h2>
460507
function createEventCard(data, timestamp) {
461508
const div = document.createElement('div');
462509
div.className = 'card';
510+
const obsId = data.ID || data.id;
511+
if (obsId) div.dataset.obsId = obsId;
463512
div.innerHTML =
464513
'<div class="type">' + esc(data.Type || data.type || 'observation') + '</div>' +
465514
'<div class="title">' + esc(data.Title || data.title || '') + '</div>' +
466515
(data.Text || data.text ? '<div class="text">' + esc(data.Text || data.text) + '</div>' : '') +
467516
'<div class="meta"><span>' + esc(timestamp) + '</span>' +
468517
(data.Project || data.project ? '<span>' + esc(data.Project || data.project) + '</span>' : '') +
469-
'</div>';
518+
'</div>' +
519+
'<div class="obs-detail" style="display:none"></div>';
520+
if (obsId) {
521+
div.addEventListener('click', function() {
522+
const detail = this.querySelector('.obs-detail');
523+
const isExpanded = this.classList.contains('expanded');
524+
if (isExpanded) {
525+
this.classList.remove('expanded');
526+
detail.style.display = 'none';
527+
return;
528+
}
529+
this.classList.add('expanded');
530+
detail.style.display = 'block';
531+
if (detail.dataset.loaded === 'true') return;
532+
loadObservationDetail(obsId, detail);
533+
});
534+
}
470535
return div;
471536
}
472537

538+
async function loadObservationDetail(obsId, container) {
539+
container.innerHTML = '<div class="obs-timeline-header">Loading...</div>';
540+
try {
541+
const [obsResp, tlResp] = await Promise.all([
542+
fetch('/api/observations/' + obsId),
543+
fetch('/api/observations/timeline/' + obsId + '?before=3&after=3')
544+
]);
545+
if (!obsResp.ok) {
546+
container.innerHTML = '<div class="obs-timeline-header">Failed to load observation.</div>';
547+
container.dataset.loaded = 'true';
548+
return;
549+
}
550+
const obs = await obsResp.json();
551+
let html = '';
552+
553+
// Full text
554+
if (obs.Text) {
555+
html += '<div class="obs-full-text">' + esc(obs.Text) + '</div>';
556+
}
557+
558+
// Metadata table
559+
html += '<div class="obs-meta-table">';
560+
html += '<span class="obs-meta-label">Type</span><span class="obs-meta-value">' + esc(obs.Type || '') + '</span>';
561+
if (obs.Project) {
562+
html += '<span class="obs-meta-label">Project</span><span class="obs-meta-value">' + esc(obs.Project) + '</span>';
563+
}
564+
if (obs.SessionID) {
565+
html += '<span class="obs-meta-label">Session</span><span class="obs-meta-value" style="font-family:monospace;font-size:0.75rem">' + esc(obs.SessionID) + '</span>';
566+
}
567+
html += '<span class="obs-meta-label">Created</span><span class="obs-meta-value">' + esc(fmtTime(obs.CreatedAt)) + '</span>';
568+
if (obs.Metadata && obs.Metadata !== '{}' && obs.Metadata !== '') {
569+
html += '<span class="obs-meta-label">Metadata</span><span class="obs-meta-value" style="font-family:monospace;font-size:0.75rem">' + esc(obs.Metadata) + '</span>';
570+
}
571+
html += '</div>';
572+
573+
// Timeline
574+
if (tlResp.ok) {
575+
const timeline = await tlResp.json();
576+
if (timeline && timeline.length > 1) {
577+
html += '<div class="obs-timeline">';
578+
html += '<div class="obs-timeline-header">Timeline</div>';
579+
for (const t of timeline) {
580+
const isCurrent = t.ID === obs.ID;
581+
html += '<div class="timeline-item' + (isCurrent ? ' current' : '') + '">';
582+
html += '<span class="tl-type">' + esc(t.Type || '') + '</span> ';
583+
html += '<span class="tl-title">' + esc(t.Title || '') + '</span>';
584+
html += '<div class="tl-time">' + esc(fmtTime(t.CreatedAt)) + '</div>';
585+
html += '</div>';
586+
}
587+
html += '</div>';
588+
}
589+
}
590+
591+
container.innerHTML = html;
592+
container.dataset.loaded = 'true';
593+
} catch (err) {
594+
container.innerHTML = '<div class="obs-timeline-header">Failed to load details.</div>';
595+
console.error('Observation detail error:', err);
596+
}
597+
}
598+
473599
async function loadRecent() {
474600
try {
475601
const resp = await fetch('/api/observations/recent?limit=50');

0 commit comments

Comments
 (0)