import { useState } from 'react' import { fetchAPI, postAPI } from '../api/client' import { useToast } from '../components/Toast' import ErrorBanner from '../components/ErrorBanner' interface SemanticResult { id: number title: string media_type: string year: number | null score: number overview: string } function scorePercent(score: number): string { return `${Math.round(score * 100)}%` } function mediaTypeBadge(type: string): string { const colors: Record = { movie: 'bg-blue-600', series: 'bg-green-600', music: 'bg-purple-600', book: 'bg-orange-600', audiobook: 'bg-yellow-600', podcast: 'bg-pink-600', } return colors[type] ?? 'bg-gray-600' } export default function SemanticSearch() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [hasSearched, setHasSearched] = useState(false) const [addingToQueue, setAddingToQueue] = useState>(new Set()) const { showToast } = useToast() async function doSearch() { const trimmed = query.trim() if (!trimmed) return setLoading(true) setHasSearched(true) setError(null) try { const res = await fetchAPI<{ results: SemanticResult[] }>( '/api/search/semantic?q=' + encodeURIComponent(trimmed) + '&k=5' ) setResults(res.results ?? []) } catch (err) { const message = err instanceof Error ? err.message : 'Semantic search failed' setError(message) } finally { setLoading(false) } } function handleSearchSubmit(e: React.FormEvent) { e.preventDefault() doSearch() } async function handleAddToQueue(item: SemanticResult) { setAddingToQueue(prev => new Set(prev).add(item.id)) try { await postAPI<{ id: number }>('/api/queue', { media_id: item.id }) showToast(`Added "${item.title}" to queue`) } catch (err) { const message = err instanceof Error ? err.message : 'Failed to add to queue' showToast(`Error: ${message}`) } finally { setAddingToQueue(prev => { const next = new Set(prev) next.delete(item.id) return next }) } } return (

Semantic Search

setQuery(e.target.value)} placeholder="Describe what you're looking for..." className="bg-gray-800 border border-gray-700 focus:border-indigo-500 text-white rounded-lg px-4 py-3 w-full outline-none transition-colors" />

AI-powered search using natural language descriptions

{!hasSearched && (

Enter a natural language query to find media

)} {hasSearched && loading && (
{[1, 2, 3].map(i => (
))}
)} {hasSearched && error && !loading && } {hasSearched && !loading && !error && results.length === 0 && (
No results found
)} {hasSearched && !loading && !error && results.length > 0 && (

{results.length} result{results.length !== 1 ? 's' : ''}

{results.map(item => { const isAdding = addingToQueue.has(item.id) return (
{item.media_type}

{item.title}

{item.year && {item.year}}
{scorePercent(item.score)} match
{item.overview && (

{item.overview}

)}
) })}
)}
) }