tahamajs/IE / Projects /front /src /components /threads /ThreadHeader.tsx
tahamajs's picture
download
raw
7.27 kB
// src/components/threads/ThreadHeader.tsx
import { useState, useRef, useEffect } from "react";
import type { Thread, ThreadStatus } from "@/types/thread";
import {
MoreHorizontal, Pin, Archive, CheckCircle2, Bell, BellOff,
Trash2, Sparkles, ChevronDown, Loader2, X,
} from "lucide-react";
import { toast } from "react-toastify";
interface ThreadHeaderProps {
thread: Thread;
onStatusChange: (status: ThreadStatus) => void;
onDelete: () => void;
onToggleSubscribe: () => void;
onToggleAiPanel: () => void;
aiPanelOpen: boolean;
onSummarize: () => Promise<string>;
isAiResponding: boolean;
}
export function ThreadHeader({
thread,
onStatusChange,
onDelete,
onToggleSubscribe,
onToggleAiPanel,
aiPanelOpen,
onSummarize,
isAiResponding,
}: ThreadHeaderProps) {
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
// Close menu when clicking outside
useEffect(() => {
if (!menuOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setMenuOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [menuOpen]);
// Close menu on Escape key
useEffect(() => {
if (!menuOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setMenuOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [menuOpen]);
const statusActions: { status: ThreadStatus; icon: typeof Pin; label: string }[] = [
{ status: "pinned", icon: Pin, label: "Pin" },
{ status: "resolved", icon: CheckCircle2, label: "Resolve" },
{ status: "archived", icon: Archive, label: "Archive" },
{ status: "active", icon: CheckCircle2, label: "Reopen" },
];
const handleSummarize = async () => {
try {
const summary = await onSummarize();
toast.success(summary.slice(0, 200) + (summary.length > 200 ? "…" : ""));
} catch {
toast.error("Failed to generate summary");
}
};
const statusColor = {
active: "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400",
resolved: "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400",
pinned: "bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400",
archived: "bg-gray-100 dark:bg-gray-700 text-gray-500",
}[thread.status] || "bg-gray-100 dark:bg-gray-700 text-gray-500";
return (
<div className="border-b border-gray-100 bg-gradient-to-r from-primary/5 to-transparent px-4 py-3 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<h3 className="truncate font-semibold text-gray-900 dark:text-gray-100">
{thread.title}
</h3>
<div className="mt-0.5 flex items-center gap-2">
<span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${statusColor}`}>
{thread.status}
</span>
<span className="text-xs text-gray-400">
{thread.messageCount} message{thread.messageCount !== 1 ? "s" : ""}
</span>
</div>
</div>
<div className="flex items-center gap-1">
{/* AI Context Panel Toggle */}
<button
onClick={onToggleAiPanel}
className={`rounded-lg p-1.5 transition ${
aiPanelOpen
? "bg-primary/10 text-primary"
: "text-gray-400 hover:bg-gray-100 hover:text-primary dark:hover:bg-gray-700"
}`}
title="AI Context Assistant"
aria-label="Toggle AI assistant panel"
aria-pressed={aiPanelOpen}
>
{isAiResponding ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
</button>
{/* Subscribe/Unsubscribe */}
<button
onClick={onToggleSubscribe}
className={`rounded-lg p-1.5 transition ${
thread.isSubscribed
? "text-primary"
: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
}`}
title={thread.isSubscribed ? "Unsubscribe from updates" : "Subscribe to updates"}
aria-label={thread.isSubscribed ? "Unsubscribe" : "Subscribe"}
>
{thread.isSubscribed ? (
<Bell className="h-4 w-4" />
) : (
<BellOff className="h-4 w-4" />
)}
</button>
{/* Dropdown Menu */}
<div className="relative" ref={menuRef}>
<button
onClick={() => setMenuOpen(!menuOpen)}
className="rounded-lg p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700"
aria-label="Thread actions"
aria-haspopup="true"
aria-expanded={menuOpen}
>
<MoreHorizontal className="h-4 w-4" />
</button>
{menuOpen && (
<div
className="absolute right-0 mt-1 z-30 w-44 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800"
role="menu"
>
{statusActions
.filter((action) => action.status !== thread.status)
.map((action) => (
<button
key={action.status}
onClick={() => {
onStatusChange(action.status);
setMenuOpen(false);
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-700"
role="menuitem"
>
<action.icon className="h-4 w-4" />
{action.label}
</button>
))}
<button
onClick={handleSummarize}
disabled={isAiResponding}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50"
role="menuitem"
>
<Sparkles className="h-4 w-4" />
Summarize
</button>
<hr className="my-1 border-gray-100 dark:border-gray-700" />
<button
onClick={() => {
onDelete();
setMenuOpen(false);
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
role="menuitem"
>
<Trash2 className="h-4 w-4" /> Delete Thread
</button>
</div>
)}
</div>
</div>
</div>
</div>
);
}

Xet Storage Details

Size:
7.27 kB
·
Xet hash:
765cca4bd34fc15ee8042f24280cf649fad3565c3f946fdeeec35cce1213d3d5

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.