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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user