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:
113
src/client/app.tsx
Normal file
113
src/client/app.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { SessionList } from "./components/SessionList";
|
||||
import { SessionViewer } from "./components/SessionViewer";
|
||||
import { FilterPanel } from "./components/FilterPanel";
|
||||
import { SearchBar } from "./components/SearchBar";
|
||||
import { ExportButton } from "./components/ExportButton";
|
||||
import { useSession } from "./hooks/useSession";
|
||||
import { useFilters } from "./hooks/useFilters";
|
||||
|
||||
export function App() {
|
||||
const {
|
||||
sessions,
|
||||
sessionsLoading,
|
||||
currentSession,
|
||||
sessionLoading,
|
||||
loadSession,
|
||||
} = useSession();
|
||||
|
||||
const filters = useFilters();
|
||||
|
||||
const filteredMessages = useMemo(() => {
|
||||
if (!currentSession) return [];
|
||||
return filters.filterMessages(currentSession.messages);
|
||||
}, [currentSession, filters.filterMessages]);
|
||||
|
||||
// Derive match count from filtered messages - no setState during render
|
||||
const matchCount = useMemo(
|
||||
() => filters.getMatchCount(filteredMessages),
|
||||
[filters.getMatchCount, filteredMessages]
|
||||
);
|
||||
|
||||
const visibleUuids = useMemo(
|
||||
() => filteredMessages.map((m) => m.uuid),
|
||||
[filteredMessages]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50">
|
||||
{/* Sidebar */}
|
||||
<div className="w-80 flex-shrink-0 border-r border-gray-200 bg-white flex flex-col">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h1 className="text-lg font-bold text-gray-900">Session Viewer</h1>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<SessionList
|
||||
sessions={sessions}
|
||||
loading={sessionsLoading}
|
||||
selectedId={currentSession?.id}
|
||||
onSelect={loadSession}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-gray-200">
|
||||
<FilterPanel
|
||||
enabledCategories={filters.enabledCategories}
|
||||
onToggle={filters.toggleCategory}
|
||||
autoRedactEnabled={filters.autoRedactEnabled}
|
||||
onAutoRedactToggle={filters.setAutoRedactEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex items-center gap-3 p-3 border-b border-gray-200 bg-white">
|
||||
<SearchBar
|
||||
query={filters.searchQuery}
|
||||
onQueryChange={filters.setSearchQuery}
|
||||
matchCount={matchCount}
|
||||
/>
|
||||
{filters.selectedForRedaction.size > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-red-600 font-medium">
|
||||
{filters.selectedForRedaction.size} selected
|
||||
</span>
|
||||
<button
|
||||
onClick={filters.confirmRedaction}
|
||||
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700"
|
||||
>
|
||||
Redact Selected
|
||||
</button>
|
||||
<button
|
||||
onClick={filters.clearRedactionSelection}
|
||||
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{currentSession && (
|
||||
<ExportButton
|
||||
session={currentSession}
|
||||
visibleMessageUuids={visibleUuids}
|
||||
redactedMessageUuids={[...filters.redactedUuids]}
|
||||
autoRedactEnabled={filters.autoRedactEnabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<SessionViewer
|
||||
messages={filteredMessages}
|
||||
allMessages={currentSession?.messages || []}
|
||||
redactedUuids={filters.redactedUuids}
|
||||
loading={sessionLoading}
|
||||
searchQuery={filters.searchQuery}
|
||||
selectedForRedaction={filters.selectedForRedaction}
|
||||
onToggleRedactionSelection={filters.toggleRedactionSelection}
|
||||
autoRedactEnabled={filters.autoRedactEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user