171 lines
8.4 KiB
React
171 lines
8.4 KiB
React
|
|
// HighlightsView: all saved highlights across all recordings
|
||
|
|
const { useState: useStateHV, useMemo: useMemoHV } = React;
|
||
|
|
|
||
|
|
function HighlightsView({ data, highlights, setSelectedRecording, setView }) {
|
||
|
|
const [filterTag, setFilterTag] = useStateHV('');
|
||
|
|
const [sortBy, setSortBy] = useStateHV('recent');
|
||
|
|
|
||
|
|
const allTags = [...new Set(highlights.map(h => h.tag))].sort();
|
||
|
|
|
||
|
|
const sorted = useMemoHV(() => {
|
||
|
|
let hs = [...highlights];
|
||
|
|
if (filterTag) hs = hs.filter(h => h.tag === filterTag);
|
||
|
|
if (sortBy === 'recent') hs.sort((a, b) => b.createdAt - a.createdAt);
|
||
|
|
else if (sortBy === 'recording') hs.sort((a, b) => {
|
||
|
|
const ra = data.recordings.find(r => r.id === a.recordingId);
|
||
|
|
const rb = data.recordings.find(r => r.id === b.recordingId);
|
||
|
|
return (rb?.startTime || 0) - (ra?.startTime || 0);
|
||
|
|
});
|
||
|
|
else if (sortBy === 'tag') hs.sort((a, b) => a.tag.localeCompare(b.tag));
|
||
|
|
return hs;
|
||
|
|
}, [highlights, filterTag, sortBy, data]);
|
||
|
|
|
||
|
|
const TAG_COLORS = {
|
||
|
|
important: 'var(--red)', todo: 'var(--amber)', idea: 'var(--teal)',
|
||
|
|
'follow-up': 'var(--amber-dim)', meeting: 'var(--green)', personal: 'var(--text2)', work: 'var(--teal)',
|
||
|
|
};
|
||
|
|
|
||
|
|
function formatTime(secs) {
|
||
|
|
const h = Math.floor(secs / 3600);
|
||
|
|
const m = Math.floor((secs % 3600) / 60);
|
||
|
|
const s = Math.floor(secs % 60);
|
||
|
|
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
||
|
|
return `${m}:${String(s).padStart(2, '0')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function openRecording(highlight) {
|
||
|
|
const rec = data.recordings.find(r => r.id === highlight.recordingId);
|
||
|
|
if (rec) {
|
||
|
|
// Pass the highlight's startSeconds to seek to that position
|
||
|
|
setSelectedRecording(rec, highlight.startSeconds);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Group by tag for summary
|
||
|
|
const tagCounts = {};
|
||
|
|
for (const h of highlights) tagCounts[h.tag] = (tagCounts[h.tag] || 0) + 1;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||
|
|
{/* Left panel: tag breakdown */}
|
||
|
|
<div style={{ width: 200, flexShrink: 0, borderRight: '1px solid var(--border)', background: 'var(--bg2)', padding: '18px 14px', overflow: 'auto' }}>
|
||
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 12 }}>
|
||
|
|
By Tag · {highlights.length} total
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => setFilterTag('')}
|
||
|
|
style={{
|
||
|
|
width: '100%', textAlign: 'left', padding: '7px 10px', borderRadius: 4,
|
||
|
|
fontFamily: 'var(--font-mono)', fontSize: 11,
|
||
|
|
background: !filterTag ? 'var(--amber-glow)' : 'transparent',
|
||
|
|
color: !filterTag ? 'var(--amber)' : 'var(--text2)',
|
||
|
|
borderLeft: !filterTag ? '2px solid var(--amber)' : '2px solid transparent',
|
||
|
|
marginBottom: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span>All</span>
|
||
|
|
<span style={{ color: 'var(--text3)' }}>{highlights.length}</span>
|
||
|
|
</button>
|
||
|
|
{Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).map(([tag, count]) => {
|
||
|
|
const color = TAG_COLORS[tag] || 'var(--text3)';
|
||
|
|
const isActive = filterTag === tag;
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={tag}
|
||
|
|
onClick={() => setFilterTag(tag)}
|
||
|
|
style={{
|
||
|
|
width: '100%', textAlign: 'left', padding: '7px 10px', borderRadius: 4,
|
||
|
|
fontFamily: 'var(--font-mono)', fontSize: 11,
|
||
|
|
background: isActive ? `${color}18` : 'transparent',
|
||
|
|
color: isActive ? color : 'var(--text2)',
|
||
|
|
borderLeft: isActive ? `2px solid ${color}` : '2px solid transparent',
|
||
|
|
marginBottom: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
|
|
transition: 'all 0.1s',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span>{tag}</span>
|
||
|
|
<span style={{ color: 'var(--text3)' }}>{count}</span>
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Right: highlights list */}
|
||
|
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||
|
|
<div style={{ padding: '14px 20px', borderBottom: '1px solid var(--border)', background: 'var(--bg2)', display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
|
||
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text2)' }}>
|
||
|
|
{sorted.length} highlight{sorted.length !== 1 ? 's' : ''}
|
||
|
|
{filterTag ? ` · ${filterTag}` : ''}
|
||
|
|
</div>
|
||
|
|
<div style={{ flex: 1 }} />
|
||
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text3)' }}>Sort:</span>
|
||
|
|
{['recent', 'recording', 'tag'].map(s => (
|
||
|
|
<button key={s} onClick={() => setSortBy(s)} style={{
|
||
|
|
fontFamily: 'var(--font-mono)', fontSize: 10, padding: '3px 9px', borderRadius: 3,
|
||
|
|
background: sortBy === s ? 'var(--bg4)' : 'transparent',
|
||
|
|
color: sortBy === s ? 'var(--amber)' : 'var(--text3)',
|
||
|
|
border: `1px solid ${sortBy === s ? 'var(--amber)' : 'var(--border)'}`,
|
||
|
|
}}>{s}</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style={{ flex: 1, overflow: 'auto', padding: '14px 20px' }}>
|
||
|
|
{sorted.length === 0 ? (
|
||
|
|
<div style={{ color: 'var(--text3)', fontFamily: 'var(--font-mono)', fontSize: 13, padding: '40px 0', textAlign: 'center' }}>
|
||
|
|
No highlights yet.<br /><br />Open a recording and press ◈ Highlight to mark moments.
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 10 }}>
|
||
|
|
{sorted.map(h => {
|
||
|
|
const rec = data.recordings.find(r => r.id === h.recordingId);
|
||
|
|
const color = TAG_COLORS[h.tag] || 'var(--text3)';
|
||
|
|
const seg = rec?.transcript?.find(s => s.id === h.segmentId);
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={h.id}
|
||
|
|
onClick={() => openRecording(h)}
|
||
|
|
style={{
|
||
|
|
background: 'var(--bg2)', border: `1px solid var(--border)`,
|
||
|
|
borderLeft: `3px solid ${color}`, borderRadius: '0 6px 6px 0',
|
||
|
|
padding: '12px 14px', cursor: 'pointer', transition: 'border-color 0.1s, background 0.1s',
|
||
|
|
}}
|
||
|
|
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg3)'; e.currentTarget.style.borderColor = color; }}
|
||
|
|
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg2)'; e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.borderLeftColor = color; }}
|
||
|
|
>
|
||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 9, padding: '1px 6px', borderRadius: 3, background: `${color}22`, color }}>{h.tag}</span>
|
||
|
|
{rec && (
|
||
|
|
<>
|
||
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text3)' }}>
|
||
|
|
{rec.startTime.toLocaleDateString([], { month: 'short', day: 'numeric' })}
|
||
|
|
</span>
|
||
|
|
<span className={`source-pill source-${rec.source}`} style={{ fontSize: 9 }}>{rec.source}</span>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--amber)', marginLeft: 'auto' }}>
|
||
|
|
{formatTime(h.startSeconds)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
{seg && (
|
||
|
|
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--text2)', lineHeight: 1.5, marginBottom: h.note ? 6 : 0, textWrap: 'pretty' }}>
|
||
|
|
"{seg.text}"
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{h.note && (
|
||
|
|
<div style={{ fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--text3)', fontStyle: 'italic' }}>
|
||
|
|
{h.note}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Object.assign(window, { HighlightsView });
|