'use client'; import React, {useState, useRef, useEffect} from 'react'; import {FaChevronLeft, FaChevronRight } from 'react-icons/fa'; import Markdown from './Markdown'; import { useLanguage } from '@/contexts/LanguageContext'; import RepoInfo from '@/types/repoinfo'; import getRepoUrl from '@/utils/getRepoUrl'; import ModelSelectionModal from './ModelSelectionModal'; import { createChatWebSocket, closeWebSocket, ChatCompletionRequest } from '@/utils/websocketClient'; interface Model { id: string; name: string; } interface Provider { id: string; name: string; models: Model[]; supportsCustomModel?: boolean; } interface Message { role: 'user' | 'assistant' | 'system'; content: string; } interface ResearchStage { title: string; content: string; iteration: number; type: 'plan' | 'update' | 'conclusion'; } interface AskProps { repoInfo: RepoInfo; provider?: string; model?: string; isCustomModel?: boolean; customModel?: string; language?: string; onRef?: (ref: { clearConversation: () => void }) => void; } const Ask: React.FC = ({ repoInfo, provider = '', model = '', isCustomModel = false, customModel = '', language = 'en', onRef }) => { const [question, setQuestion] = useState(''); const [response, setResponse] = useState(''); const [isLoading, setIsLoading] = useState(false); const [deepResearch, setDeepResearch] = useState(false); // Model selection state const [selectedProvider, setSelectedProvider] = useState(provider); const [selectedModel, setSelectedModel] = useState(model); const [isCustomSelectedModel, setIsCustomSelectedModel] = useState(isCustomModel); const [customSelectedModel, setCustomSelectedModel] = useState(customModel); const [isModelSelectionModalOpen, setIsModelSelectionModalOpen] = useState(false); const [isComprehensiveView, setIsComprehensiveView] = useState(true); // Get language context for translations const { messages } = useLanguage(); // Research navigation state const [researchStages, setResearchStages] = useState([]); const [currentStageIndex, setCurrentStageIndex] = useState(0); const [conversationHistory, setConversationHistory] = useState([]); const [researchIteration, setResearchIteration] = useState(0); const [researchComplete, setResearchComplete] = useState(false); const inputRef = useRef(null); const responseRef = useRef(null); const providerRef = useRef(provider); const modelRef = useRef(model); // Focus input on component mount useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, []); // Expose clearConversation method to parent component useEffect(() => { if (onRef) { onRef({ clearConversation }); } }, [onRef]); // Scroll to bottom of response when it changes useEffect(() => { if (responseRef.current) { responseRef.current.scrollTop = responseRef.current.scrollHeight; } }, [response]); // Close WebSocket when component unmounts useEffect(() => { return () => { closeWebSocket(webSocketRef.current); }; }, []); useEffect(() => { providerRef.current = provider; modelRef.current = model; }, [provider, model]); useEffect(() => { const fetchModel = async () => { try { setIsLoading(true); const response = await fetch('/api/models/config'); if (!response.ok) { throw new Error(`Error fetching model configurations: ${response.status}`); } const data = await response.json(); // use latest provider/model ref to check if(providerRef.current == '' || modelRef.current== '') { setSelectedProvider(data.defaultProvider); // Find the default provider and set its default model const selectedProvider = data.providers.find((p:Provider) => p.id === data.defaultProvider); if (selectedProvider && selectedProvider.models.length > 0) { setSelectedModel(selectedProvider.models[0].id); } } else { setSelectedProvider(providerRef.current); setSelectedModel(modelRef.current); } } catch (err) { console.error('Failed to fetch model configurations:', err); } finally { setIsLoading(false); } }; if(provider == '' || model == '') { fetchModel() } }, [provider, model]); const clearConversation = () => { setQuestion(''); setResponse(''); setConversationHistory([]); setResearchIteration(0); setResearchComplete(false); setResearchStages([]); setCurrentStageIndex(0); if (inputRef.current) { inputRef.current.focus(); } }; const downloadresponse = () =>{ const blob = new Blob([response], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `response-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.md`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // Function to check if research is complete based on response content const checkIfResearchComplete = (content: string): boolean => { // Check for explicit final conclusion markers if (content.includes('## Final Conclusion')) { return true; } // Check for conclusion sections that don't indicate further research if ((content.includes('## Conclusion') || content.includes('## Summary')) && !content.includes('I will now proceed to') && !content.includes('Next Steps') && !content.includes('next iteration')) { return true; } // Check for phrases that explicitly indicate completion if (content.includes('This concludes our research') || content.includes('This completes our investigation') || content.includes('This concludes the deep research process') || content.includes('Key Findings and Implementation Details') || content.includes('In conclusion,') || (content.includes('Final') && content.includes('Conclusion'))) { return true; } // Check for topic-specific completion indicators if (content.includes('Dockerfile') && (content.includes('This Dockerfile') || content.includes('The Dockerfile')) && !content.includes('Next Steps') && !content.includes('In the next iteration')) { return true; } return false; }; // Function to extract research stages from the response const extractResearchStage = (content: string, iteration: number): ResearchStage | null => { // Check for research plan (first iteration) if (iteration === 1 && content.includes('## Research Plan')) { const planMatch = content.match(/## Research Plan([\s\S]*?)(?:## Next Steps|$)/); if (planMatch) { return { title: 'Research Plan', content: content, iteration: 1, type: 'plan' }; } } // Check for research updates (iterations 1-4) if (iteration >= 1 && iteration <= 4) { const updateMatch = content.match(new RegExp(`## Research Update ${iteration}([\\s\\S]*?)(?:## Next Steps|$)`)); if (updateMatch) { return { title: `Research Update ${iteration}`, content: content, iteration: iteration, type: 'update' }; } } // Check for final conclusion if (content.includes('## Final Conclusion')) { const conclusionMatch = content.match(/## Final Conclusion([\s\S]*?)$/); if (conclusionMatch) { return { title: 'Final Conclusion', content: content, iteration: iteration, type: 'conclusion' }; } } return null; }; // Function to navigate to a specific research stage const navigateToStage = (index: number) => { if (index >= 0 && index < researchStages.length) { setCurrentStageIndex(index); setResponse(researchStages[index].content); } }; // Function to navigate to the next research stage const navigateToNextStage = () => { if (currentStageIndex < researchStages.length - 1) { navigateToStage(currentStageIndex + 1); } }; // Function to navigate to the previous research stage const navigateToPreviousStage = () => { if (currentStageIndex > 0) { navigateToStage(currentStageIndex - 1); } }; // WebSocket reference const webSocketRef = useRef(null); // Function to continue research automatically const continueResearch = async () => { if (!deepResearch || researchComplete || !response || isLoading) return; // Add a small delay to allow the user to read the current response await new Promise(resolve => setTimeout(resolve, 2000)); setIsLoading(true); try { // Store the current response for use in the history const currentResponse = response; // Create a new message from the AI's previous response const newHistory: Message[] = [ ...conversationHistory, { role: 'assistant', content: currentResponse }, { role: 'user', content: '[DEEP RESEARCH] Continue the research' } ]; // Update conversation history setConversationHistory(newHistory); // Increment research iteration const newIteration = researchIteration + 1; setResearchIteration(newIteration); // Clear previous response setResponse(''); // Prepare the request body const requestBody: ChatCompletionRequest = { repo_url: getRepoUrl(repoInfo), type: repoInfo.type, messages: newHistory.map(msg => ({ role: msg.role as 'user' | 'assistant', content: msg.content })), provider: selectedProvider, model: isCustomSelectedModel ? customSelectedModel : selectedModel, language: language }; // Add tokens if available if (repoInfo?.token) { requestBody.token = repoInfo.token; } // Close any existing WebSocket connection closeWebSocket(webSocketRef.current); let fullResponse = ''; // Create a new WebSocket connection webSocketRef.current = createChatWebSocket( requestBody, // Message handler (message: string) => { fullResponse += message; setResponse(fullResponse); // Extract research stage if this is a deep research response if (deepResearch) { const stage = extractResearchStage(fullResponse, newIteration); if (stage) { // Add the stage to the research stages if it's not already there setResearchStages(prev => { // Check if we already have this stage const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); if (existingStageIndex >= 0) { // Update existing stage const newStages = [...prev]; newStages[existingStageIndex] = stage; return newStages; } else { // Add new stage return [...prev, stage]; } }); // Update current stage index to the latest stage setCurrentStageIndex(researchStages.length); } } }, // Error handler (error: Event) => { console.error('WebSocket error:', error); setResponse(prev => prev + '\n\nError: WebSocket connection failed. Falling back to HTTP...'); // Fallback to HTTP if WebSocket fails fallbackToHttp(requestBody); }, // Close handler () => { // Check if research is complete when the WebSocket closes const isComplete = checkIfResearchComplete(fullResponse); // Force completion after a maximum number of iterations (5) const forceComplete = newIteration >= 5; if (forceComplete && !isComplete) { // If we're forcing completion, append a comprehensive conclusion to the response const completionNote = "\n\n## Final Conclusion\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question."; fullResponse += completionNote; setResponse(fullResponse); setResearchComplete(true); } else { setResearchComplete(isComplete); } setIsLoading(false); } ); } catch (error) { console.error('Error during API call:', error); setResponse(prev => prev + '\n\nError: Failed to continue research. Please try again.'); setResearchComplete(true); setIsLoading(false); } }; // Fallback to HTTP if WebSocket fails const fallbackToHttp = async (requestBody: ChatCompletionRequest) => { try { // Make the API call using HTTP const apiResponse = await fetch(`/api/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody) }); if (!apiResponse.ok) { throw new Error(`API error: ${apiResponse.status}`); } // Process the streaming response const reader = apiResponse.body?.getReader(); const decoder = new TextDecoder(); if (!reader) { throw new Error('Failed to get response reader'); } // Read the stream let fullResponse = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); fullResponse += chunk; setResponse(fullResponse); // Extract research stage if this is a deep research response if (deepResearch) { const stage = extractResearchStage(fullResponse, researchIteration); if (stage) { // Add the stage to the research stages setResearchStages(prev => { const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); if (existingStageIndex >= 0) { const newStages = [...prev]; newStages[existingStageIndex] = stage; return newStages; } else { return [...prev, stage]; } }); } } } // Check if research is complete const isComplete = checkIfResearchComplete(fullResponse); // Force completion after a maximum number of iterations (5) const forceComplete = researchIteration >= 5; if (forceComplete && !isComplete) { // If we're forcing completion, append a comprehensive conclusion to the response const completionNote = "\n\n## Final Conclusion\nAfter multiple iterations of deep research, we've gathered significant insights about this topic. This concludes our investigation process, having reached the maximum number of research iterations. The findings presented across all iterations collectively form our comprehensive answer to the original question."; fullResponse += completionNote; setResponse(fullResponse); setResearchComplete(true); } else { setResearchComplete(isComplete); } } catch (error) { console.error('Error during HTTP fallback:', error); setResponse(prev => prev + '\n\nError: Failed to get a response. Please try again.'); setResearchComplete(true); } finally { setIsLoading(false); } }; // Effect to continue research when response is updated useEffect(() => { if (deepResearch && response && !isLoading && !researchComplete) { const isComplete = checkIfResearchComplete(response); if (isComplete) { setResearchComplete(true); } else if (researchIteration > 0 && researchIteration < 5) { // Only auto-continue if we're already in a research process and haven't reached max iterations // Use setTimeout to avoid potential infinite loops const timer = setTimeout(() => { continueResearch(); }, 1000); return () => clearTimeout(timer); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [response, isLoading, deepResearch, researchComplete, researchIteration]); // Effect to update research stages when the response changes useEffect(() => { if (deepResearch && response && !isLoading) { // Try to extract a research stage from the response const stage = extractResearchStage(response, researchIteration); if (stage) { // Add or update the stage in the research stages setResearchStages(prev => { // Check if we already have this stage const existingStageIndex = prev.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); if (existingStageIndex >= 0) { // Update existing stage const newStages = [...prev]; newStages[existingStageIndex] = stage; return newStages; } else { // Add new stage return [...prev, stage]; } }); // Update current stage index to point to this stage setCurrentStageIndex(prev => { const newIndex = researchStages.findIndex(s => s.iteration === stage.iteration && s.type === stage.type); return newIndex >= 0 ? newIndex : prev; }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [response, isLoading, deepResearch, researchIteration]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!question.trim() || isLoading) return; handleConfirmAsk(); }; // Handle confirm and send request const handleConfirmAsk = async () => { setIsLoading(true); setResponse(''); setResearchIteration(0); setResearchComplete(false); try { // Create initial message const initialMessage: Message = { role: 'user', content: deepResearch ? `[DEEP RESEARCH] ${question}` : question }; // Set initial conversation history const newHistory: Message[] = [initialMessage]; setConversationHistory(newHistory); // Prepare request body const requestBody: ChatCompletionRequest = { repo_url: getRepoUrl(repoInfo), type: repoInfo.type, messages: newHistory.map(msg => ({ role: msg.role as 'user' | 'assistant', content: msg.content })), provider: selectedProvider, model: isCustomSelectedModel ? customSelectedModel : selectedModel, language: language }; // Add tokens if available if (repoInfo?.token) { requestBody.token = repoInfo.token; } // Close any existing WebSocket connection closeWebSocket(webSocketRef.current); let fullResponse = ''; // Create a new WebSocket connection webSocketRef.current = createChatWebSocket( requestBody, // Message handler (message: string) => { fullResponse += message; setResponse(fullResponse); // Extract research stage if this is a deep research response if (deepResearch) { const stage = extractResearchStage(fullResponse, 1); // First iteration if (stage) { // Add the stage to the research stages setResearchStages([stage]); setCurrentStageIndex(0); } } }, // Error handler (error: Event) => { console.error('WebSocket error:', error); setResponse(prev => prev + '\n\nError: WebSocket connection failed. Falling back to HTTP...'); // Fallback to HTTP if WebSocket fails fallbackToHttp(requestBody); }, // Close handler () => { // If deep research is enabled, check if we should continue if (deepResearch) { const isComplete = checkIfResearchComplete(fullResponse); setResearchComplete(isComplete); // If not complete, start the research process if (!isComplete) { setResearchIteration(1); // The continueResearch function will be triggered by the useEffect } } setIsLoading(false); } ); } catch (error) { console.error('Error during API call:', error); setResponse(prev => prev + '\n\nError: Failed to get a response. Please try again.'); setResearchComplete(true); setIsLoading(false); } }; const [buttonWidth, setButtonWidth] = useState(0); const buttonRef = useRef(null); // Measure button width and update state useEffect(() => { if (buttonRef.current) { const width = buttonRef.current.offsetWidth; setButtonWidth(width); } }, [messages.ask?.askButton, isLoading]); return (
{/* Model selection button */}
{/* Question input */}
setQuestion(e.target.value)} placeholder={messages.ask?.placeholder || 'What would you like to know about this codebase?'} className="block w-full rounded-md border border-[var(--border-color)] bg-[var(--input-bg)] text-[var(--foreground)] px-5 py-3.5 text-base shadow-sm focus:border-[var(--accent-primary)] focus:ring-2 focus:ring-[var(--accent-primary)]/30 focus:outline-none transition-all" style={{ paddingRight: `${buttonWidth + 24}px` }} disabled={isLoading} />
{/* Deep Research toggle */}

Deep Research conducts a multi-turn investigation process:

  • Initial Research: Creates a research plan and initial findings
  • Iteration 1: Explores specific aspects in depth
  • Iteration 2: Investigates remaining questions
  • Iterations 3-4: Dives deeper into complex areas
  • Final Conclusion: Comprehensive answer based on all iterations

The AI automatically continues research until complete (up to 5 iterations)

{deepResearch && (
Multi-turn research process enabled {researchIteration > 0 && !researchComplete && ` (iteration ${researchIteration})`} {researchComplete && ` (complete)`}
)}
{/* Response area */} {response && (
{/* Research navigation and clear button */}
{/* Research navigation */} {deepResearch && researchStages.length > 1 && (
{currentStageIndex + 1} / {researchStages.length}
{researchStages[currentStageIndex]?.title || `Stage ${currentStageIndex + 1}`}
)}
{/* Download button */} {/* Clear button */}
)} {/* Loading indicator */} {isLoading && !response && (
{deepResearch ? (researchIteration === 0 ? "Planning research approach..." : `Research iteration ${researchIteration} in progress...`) : "Thinking..."}
{deepResearch && (
{researchIteration === 0 && ( <>
Creating research plan...
Identifying key areas to investigate...
)} {researchIteration === 1 && ( <>
Exploring first research area in depth...
Analyzing code patterns and structures...
)} {researchIteration === 2 && ( <>
Investigating remaining questions...
Connecting findings from previous iterations...
)} {researchIteration === 3 && ( <>
Exploring deeper connections...
Analyzing complex patterns...
)} {researchIteration === 4 && ( <>
Refining research conclusions...
Addressing remaining edge cases...
)} {researchIteration >= 5 && ( <>
Finalizing comprehensive answer...
Synthesizing all research findings...
)}
)}
)}
{/* Model Selection Modal */} setIsModelSelectionModalOpen(false)} provider={selectedProvider} setProvider={setSelectedProvider} model={selectedModel} setModel={setSelectedModel} isCustomModel={isCustomSelectedModel} setIsCustomModel={setIsCustomSelectedModel} customModel={customSelectedModel} setCustomModel={setCustomSelectedModel} isComprehensiveView={isComprehensiveView} setIsComprehensiveView={setIsComprehensiveView} showFileFilters={false} onApply={() => { console.log('Model selection applied:', selectedProvider, selectedModel); }} showWikiType={false} authRequired={false} isAuthLoading={false} />
); }; export default Ask;