'use client';
import {
Brain,
Search,
FileText,
ChevronDown,
ChevronUp,
BookSearch,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useEffect, useState } from 'react';
import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types';
import { useChat } from '@/lib/hooks/useChat';
import { useTranslation } from '@/lib/localization/context';
const getStepIcon = (step: ResearchBlockSubStep) => {
if (step.type === 'reasoning') {
return ;
} else if (step.type === 'searching' || step.type === 'upload_searching') {
return ;
} else if (
step.type === 'search_results' ||
step.type === 'upload_search_results'
) {
return ;
} else if (step.type === 'reading') {
return ;
}
return null;
};
const getStepTitle = (
step: ResearchBlockSubStep,
isStreaming: boolean,
t: (key: string) => string,
): string => {
if (step.type === 'reasoning') {
return isStreaming && !step.reasoning ? t('chat.brainstorming') : t('chat.thinking');
} else if (step.type === 'searching') {
const n = step.searching.length;
return t('chat.searchingQueries').replace('{count}', String(n)).replace('{plural}', n === 1 ? t('chat.query') : t('chat.queries'));
} else if (step.type === 'search_results') {
const n = step.reading.length;
return t('chat.foundResults').replace('{count}', String(n)).replace('{plural}', n === 1 ? t('chat.result') : t('chat.results'));
} else if (step.type === 'reading') {
const n = step.reading.length;
return t('chat.readingSources').replace('{count}', String(n)).replace('{plural}', n === 1 ? t('chat.source') : t('chat.sources'));
} else if (step.type === 'upload_searching') {
return t('chat.scanningDocs');
} else if (step.type === 'upload_search_results') {
const n = step.results.length;
return t('chat.readingDocs').replace('{count}', String(n)).replace('{plural}', n === 1 ? t('chat.document') : t('chat.documents'));
}
return t('chat.processing');
};
const AssistantSteps = ({
block,
status,
isLast,
}: {
block: ResearchBlock;
status: 'answering' | 'completed' | 'error';
isLast: boolean;
}) => {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(
isLast && status === 'answering' ? true : false,
);
const { researchEnded, loading } = useChat();
useEffect(() => {
if (researchEnded && isLast) {
setIsExpanded(false);
} else if (status === 'answering' && isLast) {
setIsExpanded(true);
}
}, [researchEnded, status]);
if (!block || block.data.subSteps.length === 0) return null;
return (
{isExpanded && (
{block.data.subSteps.map((step, index) => {
const isLastStep = index === block.data.subSteps.length - 1;
const isStreaming = loading && isLastStep && !researchEnded;
return (
{getStepIcon(step)}
{index < block.data.subSteps.length - 1 && (
)}
{getStepTitle(step, isStreaming, t)}
{step.type === 'reasoning' && (
<>
{step.reasoning && (
{step.reasoning}
)}
{isStreaming && !step.reasoning && (
)}
>
)}
{step.type === 'searching' &&
step.searching.length > 0 && (
{step.searching.map((query, idx) => (
{query}
))}
)}
{(step.type === 'search_results' ||
step.type === 'reading') &&
step.reading.length > 0 && (
{step.reading.slice(0, 4).map((result, idx) => {
const url = typeof result.metadata?.url === 'string' ? result.metadata.url : '';
const title = String(result.metadata?.title ?? 'Untitled');
let domain = '';
try {
if (url) domain = new URL(url).hostname;
} catch {
/* invalid url */
}
const faviconUrl = domain
? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128`
: '';
return (
{faviconUrl && (
{
e.currentTarget.style.display = 'none';
}}
/>
)}
{title}
);
})}
)}
{step.type === 'upload_searching' &&
step.queries.length > 0 && (
{step.queries.map((query, idx) => (
{query}
))}
)}
{step.type === 'upload_search_results' &&
step.results.length > 0 && (
{step.results.slice(0, 4).map((result, idx) => {
const raw =
result.metadata?.title ?? result.metadata?.fileName;
const title =
typeof raw === 'string' ? raw : 'Untitled document';
return (
);
})}
)}
);
})}
)}
);
};
export default AssistantSteps;