'use client' import { useEffect, useMemo, useRef, useState } from 'react' import { Search, X, Loader2, MessageSquare, Pencil } from 'lucide-react' import { cn } from '@/lib/utils' import { useToast } from '@/components/ui/use-toast' import { type ConversationRow, BUCKET_LABELS, relativeTime, intentLabel, groupConversations, } from './conversation-display' interface Props { // Highlight the row for the conversation currently open in the sheet. activeConversationId?: string | null // Fired when the user picks a conversation to resume. The sheet fetches its // messages and swaps back to the chat view: the list itself stays dumb. onSelect: (id: string) => void } // In-sheet conversation picker. Renders the same grouped/searchable list as the // /chat sidebar (shared helpers in conversation-display.ts), but instead of // navigating to /chat/[id] it hands the id back so the conversation opens // inline in the sheet and the user keeps chatting without leaving the page. // Rows are renameable inline (PATCH /api/agent/conversations/[id]). export default function AgentSessionList({ activeConversationId, onSelect }: Props) { const [conversations, setConversations] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [query, setQuery] = useState('') const [editingId, setEditingId] = useState(null) const [editValue, setEditValue] = useState('') // Set by Esc so the blur that fires when the input unmounts doesn't save. const cancelRef = useRef(false) const { toast } = useToast() useEffect(() => { let cancelled = false void (async () => { setLoading(true) setError(null) try { const res = await fetch('/api/agent/conversations?limit=100') if (!res.ok) throw new Error(`HTTP ${res.status}`) const json = (await res.json()) as { data?: ConversationRow[] } if (!cancelled) setConversations(json.data ?? []) } catch { if (!cancelled) setError('Kunde inte hämta konversationer.') } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, []) const filtered = useMemo(() => { const q = query.trim().toLowerCase() if (!q) return conversations return conversations.filter( (c) => (c.title ?? '').toLowerCase().includes(q) || (c.last_message_preview ?? '').toLowerCase().includes(q) || (c.context_ref ?? '').toLowerCase().includes(q) || c.intent_id.toLowerCase().includes(q), ) }, [conversations, query]) const grouped = useMemo(() => groupConversations(filtered), [filtered]) function startEdit(c: ConversationRow) { setEditingId(c.id) setEditValue(c.title ?? '') cancelRef.current = false } function cancelEdit() { cancelRef.current = true setEditingId(null) } async function commitEdit(id: string) { if (cancelRef.current) { cancelRef.current = false return } setEditingId(null) const title = editValue.trim() const current = conversations.find((c) => c.id === id) if (!title || title === current?.title) return // Capture the pre-rename title so we can roll back if the PATCH fails. const previousTitle = current?.title ?? null setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c))) try { const res = await fetch(`/api/agent/conversations/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) } catch { // Revert the optimistic rename so the list stays in sync with the server. setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title: previousTitle } : c)), ) toast({ variant: 'destructive', title: 'Kunde inte byta namn på konversationen.', }) } } return (
setQuery(e.target.value)} placeholder="Sök konversationer…" className="w-full rounded-md border border-border bg-background pl-8 pr-7 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> {query.length > 0 && ( )}
{loading ? (
Hämtar…
) : error ? (
{error}
) : grouped.length === 0 ? (
{conversations.length === 0 ? 'Inga konversationer ännu.' : 'Inga träffar.'}
) : ( grouped.map(({ bucket, rows }) => (

{BUCKET_LABELS[bucket]}

    {rows.map((c) => (
  • {editingId === c.id ? (
    setEditValue(e.target.value)} onBlur={() => void commitEdit(c.id)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() ;(e.target as HTMLInputElement).blur() } else if (e.key === 'Escape') { e.preventDefault() cancelEdit() } }} placeholder="Namnge konversationen…" maxLength={200} aria-label="Nytt namn på konversationen" className="w-full rounded-md border border-border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
    ) : (
    )}
  • ))}
)) )}
) }