Redesign all client components with dark theme, polish, and UX improvements

MessageBubble: Replace border-left colored bars with rounded cards featuring
accent strips, category dot indicators, and timestamp display. Use shared
escapeHtml. Render tool_result, hook_progress, and file_snapshot as
preformatted text instead of markdown (avoids expensive marked.parse on
large JSON/log blobs).

ExportButton: Add state machine (idle/exporting/success/error) with animated
icons, gradient backgrounds, and auto-reset timers. Replace alert() with
inline error state.

FilterPanel: Add collapsible panel with category dot colors, enable count
badge, custom checkbox styling, and smooth animations.

SessionList: Replace text loading state with skeleton placeholders. Add
empty state illustration with descriptive text. Style session items as
rounded cards with hover/selected states, glow effects, and staggered
entry animations. Add project name decode explanation comment.

RedactedDivider: Add eye-slash SVG icon, red accent color, and styled
dashed lines replacing plain text divider.

useFilters: Remove unused exports (setAllCategories, setPreset, undoRedaction,
clearAllRedactions, selectAllVisible, getMatchCount) to reduce hook surface
area. Match counting moved to App component for search navigation.

SessionList.test: Update assertions for skeleton loading state and expanded
empty state text.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 01:09:41 -05:00
parent 40b3ccf33e
commit 15a312d98c
7 changed files with 298 additions and 171 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect, useRef } from "react";
import type { SessionDetailResponse } from "../lib/types";
interface Props {
@@ -14,10 +14,17 @@ export function ExportButton({
redactedMessageUuids,
autoRedactEnabled,
}: Props) {
const [exporting, setExporting] = useState(false);
const [state, setState] = useState<"idle" | "exporting" | "success" | "error">("idle");
const timerRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
async function handleExport() {
setExporting(true);
setState("exporting");
try {
const res = await fetch("/api/export", {
method: "POST",
@@ -40,21 +47,67 @@ export function ExportButton({
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setState("success");
timerRef.current = setTimeout(() => setState("idle"), 2000);
} catch (err) {
console.error("Export failed:", err);
alert("Export failed. Check console for details.");
} finally {
setExporting(false);
setState("error");
timerRef.current = setTimeout(() => setState("idle"), 3000);
}
}
return (
<button
onClick={handleExport}
disabled={exporting}
className="px-4 py-1.5 text-sm bg-gray-900 text-white rounded-md hover:bg-gray-800 disabled:opacity-50 flex-shrink-0"
disabled={state === "exporting"}
className={`
btn btn-sm flex-shrink-0 gap-1.5 transition-all duration-200
${state === "success" ? "text-white shadow-glow-success" : ""}
${state === "error" ? "text-white" : ""}
${state === "idle" || state === "exporting" ? "btn-primary" : ""}
`}
style={
state === "success"
? { background: "linear-gradient(135deg, #22c55e, #16a34a)" }
: state === "error"
? { background: "linear-gradient(135deg, #dc2626, #b91c1c)" }
: undefined
}
>
{exporting ? "Exporting..." : "Export HTML"}
{state === "idle" && (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Export
</>
)}
{state === "exporting" && (
<>
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Exporting...
</>
)}
{state === "success" && (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
Downloaded
</>
)}
{state === "error" && (
<>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
Failed
</>
)}
</button>
);
}