'use client'; import Link from 'next/link'; import { Suspense, useEffect, useMemo, useState, useTransition } from 'react'; import { useSearchParams } from 'next/navigation'; import { useQueryClient } from '@tanstack/react-query'; import { BrainCircuit, ExternalLink, Search as SearchIcon } from 'lucide-react'; import { AppShell } from '@/components/shell/app-shell'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Panel } from '@/components/ui/panel'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { ensureTickerAutomation, getSearchAnswer } from '@/lib/api'; import { searchQueryOptions } from '@/lib/query/options'; import type { SearchAnswerResponse, SearchResult, SearchSource } from '@/lib/types'; const SOURCE_OPTIONS: Array<{ value: SearchSource; label: string }> = [ { value: 'documents', label: 'Documents' }, { value: 'filings', label: 'Filing briefs' }, { value: 'research', label: 'Research notes' } ]; function parseSourceParams(value: string | null) { if (!value) { return ['documents', 'filings', 'research'] as SearchSource[]; } const normalized = value .split(',') .map((entry) => entry.trim().toLowerCase()) .filter((entry): entry is SearchSource => entry === 'documents' || entry === 'filings' || entry === 'research'); return normalized.length > 0 ? [...new Set(normalized)] : ['documents', 'filings', 'research'] as SearchSource[]; } export default function SearchPage() { return ( Loading search desk...}> ); } function SearchPageContent() { const { isPending, isAuthenticated } = useAuthGuard(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const initialQuery = searchParams.get('q')?.trim() ?? ''; const initialTicker = searchParams.get('ticker')?.trim().toUpperCase() ?? ''; const initialSources = useMemo(() => parseSourceParams(searchParams.get('sources')), [searchParams]); const [queryInput, setQueryInput] = useState(initialQuery); const [query, setQuery] = useState(initialQuery); const [tickerInput, setTickerInput] = useState(initialTicker); const [ticker, setTicker] = useState(initialTicker); const [sources, setSources] = useState(initialSources); const [results, setResults] = useState([]); const [answer, setAnswer] = useState(null); const [loading, setLoading] = useState(false); const [answerLoading, startAnswerTransition] = useTransition(); const [error, setError] = useState(null); useEffect(() => { setQueryInput(initialQuery); setQuery(initialQuery); setTickerInput(initialTicker); setTicker(initialTicker); setSources(initialSources); }, [initialQuery, initialTicker, initialSources]); useEffect(() => { if (!query.trim() || !isAuthenticated) { setResults([]); return; } let cancelled = false; setLoading(true); setError(null); queryClient.fetchQuery(searchQueryOptions({ query, ticker: ticker || undefined, sources, limit: 10 })).then((response) => { if (!cancelled) { setResults(response.results); } }).catch((err) => { if (!cancelled) { setError(err instanceof Error ? err.message : 'Unable to search indexed sources'); setResults([]); } }).finally(() => { if (!cancelled) { setLoading(false); } }); return () => { cancelled = true; }; }, [isAuthenticated, query, queryClient, sources, ticker]); if (isPending || !isAuthenticated) { return
Loading search desk...
; } const runAnswer = () => { if (!query.trim()) { return; } if (ticker.trim()) { void ensureTickerAutomation({ ticker, source: 'search' }); } startAnswerTransition(() => { setError(null); getSearchAnswer({ query, ticker: ticker || undefined, sources, limit: 10 }).then((response) => { setAnswer(response); }).catch((err) => { setError(err instanceof Error ? err.message : 'Unable to generate cited answer'); setAnswer(null); }); }); }; return (
{ event.preventDefault(); const normalizedTicker = tickerInput.trim().toUpperCase(); if (normalizedTicker) { void ensureTickerAutomation({ ticker: normalizedTicker, source: 'search' }); } setQuery(queryInput.trim()); setTicker(normalizedTicker); setAnswer(null); }} > setQueryInput(event.target.value)} placeholder="Ask about margin drivers, segment commentary, risks, or your notes..." />
setTickerInput(event.target.value.toUpperCase())} placeholder="Ticker filter (optional)" className="sm:max-w-xs" />
{SOURCE_OPTIONS.map((option) => { const selected = sources.includes(option.value); return ( ); })}
{answer ? (

{answer.answer}

{answer.citations.length > 0 ? (
{answer.citations.map((citation) => ( [{citation.index}] {citation.label} ))}
) : (

No supporting citations were strong enough to answer.

)}
) : (

Ask a question to synthesize the top retrieved passages into a cited answer.

)}
{error ?

{error}

: null} {loading ? (

Searching indexed sources...

) : !query ? (

Enter a question or topic to search the local RAG index.

) : results.length === 0 ? (

No indexed evidence matched this query.

) : (
{results.map((result) => (

{result.source} {result.ticker ? `· ${result.ticker}` : ''} {result.filingDate ? `· ${result.filingDate}` : ''}

{result.title ?? result.citationLabel}

{result.citationLabel}

Open source
{result.headingPath ? (

{result.headingPath}

) : null}

{result.snippet}

))}
)}
); }