UNPKG

codeplot

Version:

Interactive CLI tool for feature planning and ADR generation using Gemini 2.5 Pro

377 lines (317 loc) 12.5 kB
import React, { useState, useEffect, useRef } from 'react'; import { Box, Text, useInput, useApp } from 'ink'; import TextInput from 'ink-text-input'; import AIResponse from './AIResponse.jsx'; import MarkdownText from './MarkdownText.jsx'; import { ThinkingSpinner } from './components/Spinner.jsx'; const ChatView = ({ chatSession, onComplete, onError, repomixSummary, aiAnalysis }) => { const { exit } = useApp(); const [messages, setMessages] = useState([]); const [currentInput, setCurrentInput] = useState(''); const [isWaitingForAI, setIsWaitingForAI] = useState(false); const [isStreamingAI, setIsStreamingAI] = useState(false); const [streamingContent, setStreamingContent] = useState(''); const [mode, setMode] = useState('planning'); // 'planning', 'adr_generation', 'completed' const [featureDescription, setFeatureDescription] = useState(''); const [isInitializing, setIsInitializing] = useState(true); const [inputMode, setInputMode] = useState('text'); // 'text' or 'selector' const [lastAIResponse, setLastAIResponse] = useState(''); const scrollRef = useRef(); // Helper function to start processing const startProcessing = () => { setIsWaitingForAI(true); }; // Helper function to end processing const endProcessing = (response) => { setIsWaitingForAI(false); // Add the response as a message addMessage('assistant', response); // Check if this response has numbered options const hasNumberedOptions = response.match(/^\s*\d+\./m); if (hasNumberedOptions) { setLastAIResponse(response); setInputMode('selector'); } }; // Watch for AI analysis updates useEffect(() => { if (aiAnalysis && messages.length > 0) { // Check if AI analysis message already exists const hasAIAnalysis = messages.some(msg => msg.type === 'assistant' && msg.content.includes('Codebase Analysis') ); if (!hasAIAnalysis) { // Add AI analysis as a new message addMessage('assistant', aiAnalysis); } } }, [aiAnalysis]); // Initialize the chat session useEffect(() => { const initialize = async () => { try { setIsInitializing(true); // New session - include repomix summary and AI analysis const initialMessages = []; // Add repomix summary if available if (repomixSummary) { initialMessages.push({ type: 'system', content: `📦 Repository Analysis Complete Files processed: ${repomixSummary.fileCount.toLocaleString()} Total lines: ${repomixSummary.totalLines.toLocaleString()} Content size: ${repomixSummary.sizeKB} KB Estimated tokens: ${repomixSummary.estimatedTokens.toLocaleString()}${repomixSummary.sampleFiles.length > 0 ? ` Sample files: ${repomixSummary.sampleFiles.map(file => `• ${file}`).join('\n')}${repomixSummary.hasMoreFiles ? `\n... and ${repomixSummary.remainingCount} more files` : ''}` : ''}`, timestamp: new Date() }); } // Add ready message (no initial AI analysis) initialMessages.push({ type: 'system', content: 'Ready to start feature planning! What feature would you like to build? (Type "done" when finished)', timestamp: new Date() }); setMessages(initialMessages); setIsInitializing(false); } catch (error) { addMessage('system', `Error initializing chat: ${error.message}`); setIsInitializing(false); if (onError) { onError(error); } } }; initialize(); }, []); const addMessage = (type, content) => { setMessages(prev => [...prev, { type, content, timestamp: new Date() }]); }; const handleSubmit = async (input) => { if (!input.trim()) return; const userInput = input.trim(); addMessage('user', userInput); setCurrentInput(''); setInputMode('text'); // Reset to text mode after user input // Handle different modes if (mode === 'completed') { await handleCompletedMode(userInput); } else if (mode === 'planning') { await handlePlanningMode(userInput); } }; const handleOptionSelect = async (optionText, optionValue) => { // User selected an option from the AI response addMessage('user', optionText); setInputMode('text'); // Switch back to text mode setLastAIResponse(''); // Clear last AI response // Handle the selection based on current mode if (mode === 'planning') { await handlePlanningMode(optionText); } else if (mode === 'completed') { await handleCompletedMode(optionText); } }; const handleContinueWithText = () => { // Switch from selector back to text input setInputMode('text'); setLastAIResponse(''); }; const handleCompletedMode = async (input) => { const lowerInput = input.toLowerCase(); if (lowerInput === 'review') { addMessage('system', 'Current ADR Content:'); addMessage('system', '='.repeat(80)); addMessage('assistant', chatSession.featureData.adr_content); addMessage('system', '='.repeat(80)); addMessage('system', 'You can type "modify" to make changes or "exit" to finish.'); } else if (lowerInput === 'modify') { setMode('modification'); addMessage('system', 'ADR Modification Mode'); addMessage('system', 'You can ask questions about the current ADR, request changes, or explore alternatives.'); startProcessing(); const aiResponse = await chatSession.sendMessage( `I would like to review and potentially modify the current ADR. Here is the current ADR content:\n\n${chatSession.featureData.adr_content}\n\nI may want to make changes, explore alternatives, or ask questions about this ADR. Please help me refine it based on my feedback.` ); endProcessing(aiResponse); } else if (lowerInput === 'exit' || lowerInput === 'done') { onComplete(chatSession.featureData); exit(); } else { addMessage('system', 'Available commands: "review", "modify", "exit"'); } }; const handlePlanningMode = async (input) => { if (input.toLowerCase() === 'done') { await generateADR(); return; } try { // Show processing indicator setIsWaitingForAI(true); // If this is the initial feature description if (!featureDescription) { setFeatureDescription(input); // Update chat session feature data if (chatSession) { chatSession.featureData.name = chatSession.extractFeatureName(input); chatSession.featureData.description = input; } addMessage('system', 'Starting interactive planning session...'); } // Send message to ChatSession and get response const response = await chatSession.sendMessage(input); // Clear waiting state setIsWaitingForAI(false); // End processing with the response endProcessing(response); } catch (error) { setIsWaitingForAI(false); addMessage('system', `Error in planning: ${error.message}`); console.error('Planning error:', error); } }; // Helper function to build display text from parsed data const buildDisplayText = (questionData) => { let displayText = ''; if (questionData.header) { displayText += `# ${questionData.header}\n\n`; } if (questionData.bodyText) { displayText += questionData.bodyText; } if (questionData.optionText) { displayText += `\n\n---\n\n**${questionData.optionText}**`; } return displayText; }; const generateADR = async () => { try { setMode('adr_generation'); addMessage('system', '📝 Generating Architecture Decision Record...'); // Generate ADR prompt based on conversation history const adrPrompt = `Based on our conversation about "${featureDescription}", please generate a comprehensive Architecture Decision Record (ADR) that includes: 1. Context and problem statement 2. Decision made 3. Consequences (positive and negative) 4. Implementation plan with specific steps Format it as a proper ADR document with clear sections.`; // Send the ADR generation request const adrResponse = await chatSession.sendMessage(adrPrompt); // Update chat session feature data if (chatSession) { chatSession.featureData.adr_content = adrResponse; chatSession.featureData.adr_title = chatSession.extractADRTitle(adrResponse) || featureDescription; chatSession.featureData.adrFilename = chatSession.generateADRFilename(chatSession.featureData.name || featureDescription); chatSession.featureData.implementation_plan = chatSession.extractImplementationPlan(adrResponse); } // Display the generated ADR addMessage('assistant', adrResponse); addMessage('system', '✅ ADR generation completed!'); setMode('completed'); // Call the completion handler to save ADR to file if (onComplete && chatSession) { try { await onComplete(chatSession.featureData); } catch (error) { addMessage('system', `⚠️ Warning: Failed to save ADR file: ${error.message}`); } } } catch (error) { addMessage('system', `Error generating ADR: ${error.message}`); console.error('ADR generation error:', error); } }; // Handle keyboard shortcuts useInput((input, key) => { if (key.escape) { exit(); } }); if (isInitializing) { return ( <Box flexDirection="column" padding={1}> <Text color="blue">🤖 Initializing chat session...</Text> </Box> ); } return ( <Box flexDirection="column" height="100%" padding={1}> {/* Header */} <Box borderStyle="single" paddingX={1} marginBottom={1}> <Text color="blue" bold> 📊 Codeplot - Feature Planning Chat </Text> <Text color="gray"> (ESC to exit)</Text> </Box> {/* Chat messages */} <Box flexDirection="column" flexGrow={1} paddingX={1} ref={scrollRef}> {messages.map((message, index) => ( <Box key={index} marginBottom={1}> <Box width={12}> <Text color={ message.type === 'user' ? 'green' : message.type === 'assistant' ? 'blue' : 'yellow' }> {message.type === 'user' ? '👤 You:' : message.type === 'assistant' ? '🤖 AI:' : '📋 System:'} </Text> </Box> <Box flexGrow={1}> {message.type === 'assistant' ? ( <MarkdownText wrap="wrap">{message.content}</MarkdownText> ) : ( <Text wrap="wrap">{message.content}</Text> )} </Box> </Box> ))} {isWaitingForAI && inputMode !== 'selector' && ( <Box marginBottom={1}> <Box width={12}> <Text color="blue">🤖 AI:</Text> </Box> <Box flexGrow={1}> <ThinkingSpinner /> </Box> </Box> )} </Box> {/* Selector for AI responses with options */} {inputMode === 'selector' && lastAIResponse && ( <Box borderStyle="single" borderColor="yellow" marginBottom={1}> <AIResponse content={lastAIResponse} parsedData={null} onOptionSelect={handleOptionSelect} onContinue={handleContinueWithText} /> </Box> )} {/* Input area */} {inputMode === 'text' && ( <Box borderStyle="single" paddingX={1}> <Text color="gray">{'> '}</Text> <TextInput value={currentInput} onChange={setCurrentInput} onSubmit={handleSubmit} placeholder={ mode === 'completed' ? 'Type "review", "modify", or "exit"' : mode === 'planning' ? 'Your response (or "done" to finish)' : 'Type your message...' } /> </Box> )} </Box> ); }; export default ChatView;