refactor(dashboard): extract modular Preact component structure
Replace the monolithic single-file dashboards (dashboard.html,
dashboard-preact.html) with a proper modular directory structure:
dashboard/
index.html - Entry point, loads main.js
main.js - App bootstrap, mounts <App> to #root
styles.css - Global styles (dark theme, typography)
components/
App.js - Root component, state management, polling
Header.js - Top bar with refresh/timing info
Sidebar.js - Project tree navigation
SessionCard.js - Individual session card with status/actions
SessionGroup.js - Group sessions by project path
Modal.js - Full conversation viewer overlay
ChatMessages.js - Message list with role styling
MessageBubble.js - Individual message with markdown
QuestionBlock.js - User question input with quick options
EmptyState.js - "No sessions" placeholder
OptionButton.js - Quick response button component
SimpleInput.js - Text input with send button
lib/
preact.js - Preact + htm ESM bundle (CDN shim)
markdown.js - Lightweight markdown-to-HTML renderer
utils/
api.js - fetch wrappers for /api/* endpoints
formatting.js - Time formatting, truncation helpers
status.js - Session status logic, action availability
This structure enables:
- Browser-native ES modules (no build step required)
- Component reuse and isolation
- Easier styling and theming
- IDE support for component navigation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
103
dashboard/components/SessionCard.js
Normal file
103
dashboard/components/SessionCard.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { html, useEffect } from '../lib/preact.js';
|
||||
import { getStatusMeta } from '../utils/status.js';
|
||||
import { formatDuration, getContextUsageSummary } from '../utils/formatting.js';
|
||||
import { ChatMessages } from './ChatMessages.js';
|
||||
import { QuestionBlock } from './QuestionBlock.js';
|
||||
import { SimpleInput } from './SimpleInput.js';
|
||||
|
||||
export function SessionCard({ session, onClick, conversation, onFetchConversation, onRespond, onDismiss }) {
|
||||
const hasQuestions = session.pending_questions && session.pending_questions.length > 0;
|
||||
const statusMeta = getStatusMeta(session.status);
|
||||
const agent = session.agent === 'codex' ? 'codex' : 'claude';
|
||||
const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude';
|
||||
const contextUsage = getContextUsageSummary(session.context_usage);
|
||||
|
||||
// Fetch conversation when card mounts
|
||||
useEffect(() => {
|
||||
if (!conversation && onFetchConversation) {
|
||||
onFetchConversation(session.session_id, session.project_dir, agent);
|
||||
}
|
||||
}, [session.session_id, session.project_dir, agent, conversation, onFetchConversation]);
|
||||
|
||||
const handleDismissClick = (e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss(session.session_id);
|
||||
};
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="glass-panel flex h-[850px] max-h-[850px] w-[600px] cursor-pointer flex-col overflow-hidden rounded-xl border border-selection/70 transition-all duration-200 hover:border-starting/35 hover:shadow-panel"
|
||||
style=${{ borderLeftWidth: '3px', borderLeftColor: statusMeta.borderColor }}
|
||||
onClick=${() => onClick(session)}
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="shrink-0 border-b px-4 py-3 ${agentHeaderClass}">
|
||||
<div class="flex items-start justify-between gap-2.5">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="h-2 w-2 shrink-0 rounded-full ${statusMeta.dot}"></span>
|
||||
<span class="truncate font-display text-base font-medium text-bright">${session.project || session.name || 'Session'}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${statusMeta.badge}">
|
||||
${statusMeta.label}
|
||||
</span>
|
||||
<span class="rounded-full border px-2.5 py-1 font-mono text-micro uppercase tracking-[0.14em] ${agent === 'codex' ? 'border-emerald-400/45 bg-emerald-500/14 text-emerald-300' : 'border-violet-400/45 bg-violet-500/14 text-violet-300'}">
|
||||
${agent}
|
||||
</span>
|
||||
${session.cwd && html`
|
||||
<span class="truncate rounded-full border border-selection bg-bg/40 px-2.5 py-1 font-mono text-micro text-dim">
|
||||
${session.cwd.split('/').slice(-2).join('/')}
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
${contextUsage && html`
|
||||
<div class="mt-2 inline-flex max-w-full items-center gap-2 rounded-lg border border-selection/80 bg-bg/45 px-2.5 py-1.5 font-mono text-label text-dim" title=${contextUsage.title}>
|
||||
<span class="text-bright">${contextUsage.headline}</span>
|
||||
<span class="truncate">${contextUsage.detail}</span>
|
||||
${contextUsage.trail && html`<span class="text-dim/80">${contextUsage.trail}</span>`}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0 pt-0.5">
|
||||
<span class="font-mono text-xs tabular-nums text-dim">${formatDuration(session.started_at)}</span>
|
||||
${session.status === 'done' && html`
|
||||
<button
|
||||
onClick=${handleDismissClick}
|
||||
class="flex h-7 w-7 items-center justify-center rounded-lg border border-selection/80 text-dim transition-colors hover:border-done/40 hover:bg-done/10 hover:text-bright"
|
||||
title="Dismiss"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Content Area (Chat) -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto bg-surface px-4 py-4">
|
||||
<${ChatMessages} messages=${conversation || []} status=${session.status} />
|
||||
</div>
|
||||
|
||||
<!-- Card Footer (Input or Questions) -->
|
||||
<div class="shrink-0 border-t border-selection/70 bg-bg/55 p-4 ${hasQuestions ? 'max-h-[300px] overflow-y-auto' : ''}">
|
||||
${hasQuestions ? html`
|
||||
<${QuestionBlock}
|
||||
questions=${session.pending_questions}
|
||||
sessionId=${session.session_id}
|
||||
status=${session.status}
|
||||
onRespond=${onRespond}
|
||||
/>
|
||||
` : html`
|
||||
<${SimpleInput}
|
||||
sessionId=${session.session_id}
|
||||
status=${session.status}
|
||||
onRespond=${onRespond}
|
||||
/>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user