Add React client: session browser, message viewer, filters, search, redaction, export
Full React 18 client application for interactive session browsing: app.tsx: - Root component orchestrating session list, viewer, filters, search, redaction controls, and export — wires together useSession and useFilters hooks - Keyboard navigation: j/k or arrow keys for message focus, Escape to clear search and redaction selection, "/" to focus search input - Derives filtered messages, match count, visible UUIDs, and category counts via useMemo to avoid render-time side effects hooks/useSession.ts: - Manages session list and detail fetching state (loading, error, data) with useCallback-wrapped fetch functions - Auto-loads session list on mount hooks/useFilters.ts: - Category filter state with toggle, set-all, and preset support - Text search with debounced query propagation - Manual redaction workflow: select messages, confirm to move to redacted set, undo individual or all redactions, select-all-visible - Auto-redact toggle for the sensitive-redactor module - Returns memoized object to prevent unnecessary re-renders components/SessionList.tsx: - Two-phase navigation: project list → session list within a project - Groups sessions by project, shows session count and latest modified date per project, auto-drills into the correct project when a session is selected externally - Formats project directory names back to paths (leading dash → /) components/SessionViewer.tsx: - Renders filtered messages with redacted dividers inserted where manually redacted messages were removed from the visible sequence - Loading spinner, empty state for no session / no filter matches - Scrolls focused message into view via ref components/MessageBubble.tsx: - Renders individual messages with category-specific Tailwind border and background colors - Markdown rendering via marked + highlight.js, with search term highlighting that splits HTML tags to avoid corrupting attributes - Click-to-select for manual redaction, visual selection indicator - Auto-redact mode applies sensitive-redactor to content before render - dangerouslySetInnerHTML is safe here: content is from local user-owned JSONL files, not untrusted external input components/FilterPanel.tsx: - Checkbox list for all 9 message categories with auto-redact toggle components/SearchBar.tsx: - Debounced text input (200ms) with match count display - "/" keyboard shortcut to focus, × button to clear components/ExportButton.tsx: - POSTs current session + visible/redacted UUIDs + auto-redact flag to /api/export, downloads the returned HTML blob as a file components/RedactedDivider.tsx: - Dashed-line visual separator indicating redacted content gap lib/types.ts: - Re-exports shared types via @shared path alias for client imports lib/constants.ts: - Tailwind CSS class mappings per message category (border + bg colors) lib/markdown.ts: - Configured marked + highlight.js instance with search highlighting that operates on text segments only (preserves HTML tags intact) styles/main.css: - Tailwind base/components/utilities, custom scrollbar, highlight.js overrides, search highlight mark, redaction selection outline, message dimming for non-matching search results index.html + main.tsx: - Vite entry point mounting React app into #root with StrictMode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
72
src/client/hooks/useSession.ts
Normal file
72
src/client/hooks/useSession.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { SessionEntry, SessionDetailResponse } from "../lib/types";
|
||||
|
||||
interface SessionState {
|
||||
sessions: SessionEntry[];
|
||||
sessionsLoading: boolean;
|
||||
sessionsError: string | null;
|
||||
currentSession: SessionDetailResponse | null;
|
||||
sessionLoading: boolean;
|
||||
sessionError: string | null;
|
||||
loadSessions: () => Promise<void>;
|
||||
loadSession: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useSession(): SessionState {
|
||||
const [sessions, setSessions] = useState<SessionEntry[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(true);
|
||||
const [sessionsError, setSessionsError] = useState<string | null>(null);
|
||||
const [currentSession, setCurrentSession] =
|
||||
useState<SessionDetailResponse | null>(null);
|
||||
const [sessionLoading, setSessionLoading] = useState(false);
|
||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
setSessionsLoading(true);
|
||||
setSessionsError(null);
|
||||
try {
|
||||
const res = await fetch("/api/sessions");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
setSessions(data.sessions);
|
||||
} catch (err) {
|
||||
setSessionsError(
|
||||
err instanceof Error ? err.message : "Failed to load sessions"
|
||||
);
|
||||
} finally {
|
||||
setSessionsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSession = useCallback(async (id: string) => {
|
||||
setSessionLoading(true);
|
||||
setSessionError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${id}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
setCurrentSession(data);
|
||||
} catch (err) {
|
||||
setSessionError(
|
||||
err instanceof Error ? err.message : "Failed to load session"
|
||||
);
|
||||
} finally {
|
||||
setSessionLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, [loadSessions]);
|
||||
|
||||
return {
|
||||
sessions,
|
||||
sessionsLoading,
|
||||
sessionsError,
|
||||
currentSession,
|
||||
sessionLoading,
|
||||
sessionError,
|
||||
loadSessions,
|
||||
loadSession,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user