'use client' import { useEffect, useState } from 'react' import { Search, X, Loader2, MessageSquare, Pencil, Pin, PinOff, Archive } from 'lucide-react' import { cn } from '@/lib/utils' import { type ConversationRow, BUCKET_LABELS, relativeTime, intentLabel, } from './conversation-display' import { useConversationList } from './use-conversation-list' 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) { // Same hook the /chat sidebar uses, so pin/archive/rename behave identically // on both surfaces (optimistic, rolled back on failure, with a message). // Pin and archive are new here: the sheet could only rename before. const { conversations, setConversations, query, setQuery, grouped, togglePin, archive, editingId, editValue, setEditValue, startEdit, cancelEdit, commitEdit, } = useConversationList([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) 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 } // setConversations is the hook's stable useState setter; listing it would // not change when this runs, and this must fire exactly once on mount. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) 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" />
    ) : (
    )}
  • ))}
)) )}
) }