theo-agent-dashboard

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

SessionList.tsx (6389B)


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