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:
13
src/client/lib/constants.ts
Normal file
13
src/client/lib/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { MessageCategory } from "./types";
|
||||
|
||||
export const CATEGORY_COLORS: Record<MessageCategory, { border: string; bg: string }> = {
|
||||
user_message: { border: "border-l-blue-500", bg: "bg-blue-50" },
|
||||
assistant_text: { border: "border-l-emerald-500", bg: "bg-white" },
|
||||
thinking: { border: "border-l-violet-500", bg: "bg-violet-50" },
|
||||
tool_call: { border: "border-l-amber-500", bg: "bg-amber-50" },
|
||||
tool_result: { border: "border-l-indigo-500", bg: "bg-indigo-50" },
|
||||
system_message: { border: "border-l-gray-500", bg: "bg-gray-100" },
|
||||
hook_progress: { border: "border-l-gray-400", bg: "bg-gray-50" },
|
||||
file_snapshot: { border: "border-l-pink-500", bg: "bg-pink-50" },
|
||||
summary: { border: "border-l-teal-500", bg: "bg-teal-50" },
|
||||
};
|
||||
50
src/client/lib/markdown.ts
Normal file
50
src/client/lib/markdown.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { marked } from "marked";
|
||||
import hljs from "highlight.js";
|
||||
import { markedHighlight } from "marked-highlight";
|
||||
|
||||
marked.use(
|
||||
markedHighlight({
|
||||
highlight(code: string, lang: string) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export function renderMarkdown(text: string): string {
|
||||
if (!text) return "";
|
||||
try {
|
||||
return marked.parse(text) as string;
|
||||
} catch {
|
||||
return `<p>${escapeHtml(text)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
export function highlightSearchText(html: string, query: string): string {
|
||||
if (!query) return html;
|
||||
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(`(${escaped})`, "gi");
|
||||
|
||||
// Split HTML into tags and text segments, only highlight within text segments.
|
||||
// This avoids corrupting tag attributes or self-closing tags.
|
||||
const parts = html.split(/(<[^>]*>)/);
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
// Even indices are text content, odd indices are tags
|
||||
if (i % 2 === 0 && parts[i]) {
|
||||
parts[i] = parts[i].replace(
|
||||
regex,
|
||||
'<mark class="search-highlight">$1</mark>'
|
||||
);
|
||||
}
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
14
src/client/lib/types.ts
Normal file
14
src/client/lib/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type {
|
||||
MessageCategory,
|
||||
ParsedMessage,
|
||||
SessionEntry,
|
||||
SessionListResponse,
|
||||
SessionDetailResponse,
|
||||
ExportRequest
|
||||
} from "@shared/types";
|
||||
|
||||
export {
|
||||
ALL_CATEGORIES,
|
||||
CATEGORY_LABELS,
|
||||
DEFAULT_HIDDEN_CATEGORIES
|
||||
} from "@shared/types";
|
||||
Reference in New Issue
Block a user