theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

SessionList.tsx (7375B)


      1 import { useState, useRef, useEffect } from "react";
      2 import { useSessionStore, type Session } from "../../store/session";
      3 
      4 const DASHBOARD_SOURCES = new Set(["api_server"]);
      5 
      6 function formatDate(ts?: number): string {
      7   if (!ts) return "";
      8   return new Date(ts * 1000).toLocaleDateString();
      9 }
     10 
     11 function formatRelative(ts?: number): string {
     12   if (!ts) return "";
     13   const diff = Date.now() / 1000 - ts;
     14   if (diff < 60) return "just now";
     15   if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
     16   if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
     17   return `${Math.floor(diff / 86400)}d ago`;
     18 }
     19 
     20 function SessionItem({
     21   session,
     22   isActive,
     23   onSelect,
     24   onRename,
     25   onDelete,
     26   onArchive,
     27 }: {
     28   session: Session;
     29   isActive: boolean;
     30   onSelect: (id: string) => void;
     31   onRename: (id: string, name: string) => void;
     32   onDelete: (id: string) => void;
     33   onArchive?: () => void;
     34 }) {
     35   const [editing, setEditing] = useState(false);
     36   const [name, setName] = useState(session.name ?? "Untitled");
     37   const [menuOpen, setMenuOpen] = useState(false);
     38   const [confirmDelete, setConfirmDelete] = useState(false);
     39   const inputRef = useRef<HTMLInputElement>(null);
     40 
     41   useEffect(() => {
     42     if (editing) inputRef.current?.focus();
     43   }, [editing]);
     44 
     45   useEffect(() => {
     46     if (!menuOpen) return;
     47     const close = () => setMenuOpen(false);
     48     document.addEventListener("click", close);
     49     return () => document.removeEventListener("click", close);
     50   }, [menuOpen]);
     51 
     52   const handleRename = () => {
     53     if (name.trim() && name.trim() !== session.name) {
     54       onRename(session.id, name.trim());
     55     }
     56     setEditing(false);
     57   };
     58 
     59   const handleDelete = () => {
     60     onDelete(session.id);
     61     setConfirmDelete(false);
     62   };
     63 
     64   return (
     65     <div
     66       className={`group relative flex cursor-pointer items-center justify-between rounded px-2 py-1.5 text-sm transition-colors ${
     67         isActive
     68           ? "bg-indigo-600/20 text-indigo-300"
     69           : "text-gray-400 hover:bg-gray-800 hover:text-gray-200"
     70       }`}
     71       onClick={() => onSelect(session.id)}
     72     >
     73       {editing ? (
     74         <input
     75           ref={inputRef}
     76           value={name}
     77           onChange={(e) => setName(e.target.value)}
     78           onBlur={handleRename}
     79           onKeyDown={(e) => {
     80             if (e.key === "Enter") handleRename();
     81             if (e.key === "Escape") setEditing(false);
     82           }}
     83           className="w-full rounded bg-gray-800 px-1 py-0.5 text-sm text-gray-100 outline-none ring-1 ring-indigo-500"
     84           onClick={(e) => e.stopPropagation()}
     85         />
     86       ) : (
     87         <div className="min-w-0 flex-1">
     88           <div className="truncate">{session.name ?? "Untitled"}</div>
     89           <div className="mt-0.5 flex gap-2 text-xs text-gray-600">
     90             {session.created_at && <span>{formatDate(session.created_at)}</span>}
     91             {session.last_activity && (
     92               <span>{formatRelative(session.last_activity)}</span>
     93             )}
     94             {session.message_count != null && (
     95               <span>{session.message_count} msgs</span>
     96             )}
     97           </div>
     98         </div>
     99       )}
    100 
    101       {!editing && (
    102         <button
    103           className="ml-2 rounded p-0.5 text-gray-600 opacity-0 transition-opacity hover:bg-gray-700 hover:text-gray-300 group-hover:opacity-100"
    104           onClick={(e) => {
    105             e.stopPropagation();
    106             setMenuOpen(!menuOpen);
    107           }}
    108         >
    109    110         </button>
    111       )}
    112 
    113       {menuOpen && (
    114         <div className="absolute right-0 top-full z-10 mt-1 w-36 rounded-md border border-gray-700 bg-gray-900 shadow-lg">
    115           <button
    116             className="w-full px-3 py-1.5 text-left text-sm text-gray-300 hover:bg-gray-800"
    117             onClick={(e) => {
    118               e.stopPropagation();
    119               setEditing(true);
    120               setMenuOpen(false);
    121             }}
    122           >
    123             Rename
    124           </button>
    125           <button
    126             className="w-full px-3 py-1.5 text-left text-sm text-gray-400 hover:bg-gray-800"
    127             onClick={(e) => {
    128               e.stopPropagation();
    129               onArchive?.();
    130               setMenuOpen(false);
    131             }}
    132           >
    133             Archive
    134           </button>
    135           <button
    136             className="w-full px-3 py-1.5 text-left text-sm text-red-400 hover:bg-gray-800"
    137             onClick={(e) => {
    138               e.stopPropagation();
    139               setConfirmDelete(true);
    140               setMenuOpen(false);
    141             }}
    142           >
    143             Delete
    144           </button>
    145         </div>
    146       )}
    147 
    148       {confirmDelete && (
    149         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
    150           <div className="rounded-lg bg-gray-900 p-4 shadow-xl">
    151             <p className="mb-3 text-sm text-gray-300">
    152               Delete &quot;{session.name ?? "Untitled"}&quot;?
    153             </p>
    154             <div className="flex justify-end gap-2">
    155               <button
    156                 className="rounded px-3 py-1 text-sm text-gray-400 hover:bg-gray-800"
    157                 onClick={() => setConfirmDelete(false)}
    158               >
    159                 Cancel
    160               </button>
    161               <button
    162                 className="rounded bg-red-600 px-3 py-1 text-sm text-white hover:bg-red-700"
    163                 onClick={handleDelete}
    164               >
    165                 Delete
    166               </button>
    167             </div>
    168           </div>
    169         </div>
    170       )}
    171     </div>
    172   );
    173 }
    174 
    175 interface SessionListProps {
    176   onSelectSession?: (sessionId: string) => void;
    177 }
    178 
    179 export function SessionList({ onSelectSession }: SessionListProps) {
    180   const {
    181     sessions,
    182     activeSessionId,
    183     setActiveSession,
    184     updateSession,
    185     deleteSession,
    186     archiveSession,
    187     listSessions,
    188   } = useSessionStore();
    189   const [showAll, setShowAll] = useState(false);
    190 
    191   useEffect(() => {
    192     listSessions();
    193   }, [listSessions]);
    194 
    195   const filtered = showAll
    196     ? sessions
    197     : sessions.filter((s) => (!s.source || DASHBOARD_SOURCES.has(s.source)) && !s.archived);
    198   const hiddenCount = sessions.length - filtered.length;
    199 
    200   return (
    201     <div className="flex flex-col gap-1">
    202       <div className="mb-2 flex items-center justify-between">
    203         <h2 className="text-xs font-semibold uppercase text-gray-500">
    204           Sessions
    205         </h2>
    206         <button
    207           className="text-xs text-gray-500 hover:text-gray-300"
    208           onClick={() => setActiveSession(null)}
    209           title="New session"
    210         >
    211           + New
    212         </button>
    213       </div>
    214       {hiddenCount > 0 && (
    215         <button
    216           className="mb-1 text-xs text-gray-600 hover:text-gray-400"
    217           onClick={() => setShowAll(!showAll)}
    218         >
    219           {showAll
    220             ? "Hide external sessions"
    221             : `Show ${hiddenCount} sessions from other sources`}
    222         </button>
    223       )}
    224       {filtered.length === 0 && (
    225         <p className="text-sm text-gray-600">No sessions yet</p>
    226       )}
    227       {filtered.map((s) => (
    228         <SessionItem
    229           key={s.id}
    230           session={s}
    231           isActive={s.id === activeSessionId}
    232           onSelect={(id) => {
    233             setActiveSession(id);
    234             onSelectSession?.(id);
    235           }}
    236           onRename={(id, name) => updateSession(id, { name })}
    237           onDelete={deleteSession}
    238           onArchive={() => archiveSession(s.id)}
    239         />
    240       ))}
    241     </div>
    242   );
    243 }