UNPKG

together-code

Version:

AI-powered coding assistant that plans, then builds

354 lines (353 loc) 15.6 kB
import React, { useState, useEffect, useRef } from 'react'; import { Box, Text, useInput } from 'ink'; import TextInput from 'ink-text-input'; import { TogetherAIService } from '../services/togetherAI.js'; import { FileWriter } from '../services/fileWriter.js'; import Spinner from 'ink-spinner'; import PlanDisplay from './PlanDisplay.js'; import StepResult from './StepResult.js'; import FilePermissionDialog from './FilePermissionDialog.js'; const CodeGenerator = ({ initialPrompt }) => { const [prompt, setPrompt] = useState(''); const [workflowState, setWorkflowState] = useState('input'); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(''); const [chatHistory, setChatHistory] = useState([]); const [generationStartTime, setGenerationStartTime] = useState(null); const [elapsedTime, setElapsedTime] = useState(0); const [tokensGenerated, setTokensGenerated] = useState(0); // Planning state const [currentPlan, setCurrentPlan] = useState(null); const [currentStepId, setCurrentStepId] = useState(null); const [completedSteps, setCompletedSteps] = useState([]); const [stepResults, setStepResults] = useState(new Map()); // File permission state const [pendingFiles, setPendingFiles] = useState([]); const [existingFiles, setExistingFiles] = useState([]); const [pendingStepData, setPendingStepData] = useState(null); const intervalRef = useRef(null); const togetherAI = new TogetherAIService(); const fileWriter = new FileWriter(); useEffect(() => { if (initialPrompt) { setWorkflowState('planning'); generatePlan(initialPrompt); } }, [initialPrompt]); useEffect(() => { if (isGenerating && generationStartTime) { intervalRef.current = setInterval(() => { setElapsedTime(Math.floor((Date.now() - generationStartTime.getTime()) / 1000)); }, 1000); } else { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [isGenerating, generationStartTime]); const generatePlan = async (inputPrompt) => { if (!inputPrompt.trim()) return; const startTime = new Date(); setGenerationStartTime(startTime); setIsGenerating(true); setError(''); setElapsedTime(0); setTokensGenerated(0); setWorkflowState('planning'); // Add user message to chat history const userMessage = { role: 'user', content: inputPrompt, timestamp: startTime }; setChatHistory(prev => [...prev, userMessage]); try { const response = await togetherAI.generatePlan({ prompt: inputPrompt, context: chatHistory.slice(-4).map(msg => ({ role: msg.role, content: msg.content })) }); const actualTokens = response.tokens || Math.floor(JSON.stringify(response.plan).length / 4); setTokensGenerated(actualTokens); setCurrentPlan(response.plan); setWorkflowState('plan-review'); // Add plan to chat history const planMessage = { role: 'assistant', content: `Plan created with ${response.plan.steps.length} steps: ${response.plan.overview}`, timestamp: new Date() }; setChatHistory(prev => [...prev, planMessage]); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error occurred'); setWorkflowState('input'); } finally { setIsGenerating(false); setGenerationStartTime(null); } }; const completeStep = async (step, content, files, tokens, duration) => { // Store step result with metadata setStepResults(prev => new Map(prev).set(step.id, { content, files, tokens, duration })); setCompletedSteps(prev => [...prev, step.id]); // Add step completion to chat history const stepMessage = { role: 'assistant', content: `Completed: ${step.title}`, timestamp: new Date(), files: files.length > 0 ? files : undefined }; setChatHistory(prev => [...prev, stepMessage]); // Check if all steps are completed if (currentPlan && completedSteps.length + 1 >= currentPlan.steps.length) { setWorkflowState('completed'); } else { setWorkflowState('plan-review'); // Auto-continue to next step after a brief pause setTimeout(() => { executeNextStep(); }, 1000); } }; const handleFilePermissionApprove = async () => { if (!pendingStepData || pendingFiles.length === 0) return; try { // Write files with backup await fileWriter.writeFilesWithBackup(pendingFiles); // Complete the step completeStep(pendingStepData.step, pendingStepData.content, pendingFiles, pendingStepData.tokens, pendingStepData.duration); } catch (error) { setError(`Failed to write files: ${error instanceof Error ? error.message : 'Unknown error'}`); setWorkflowState('plan-review'); } finally { // Clear pending state setPendingFiles([]); setExistingFiles([]); setPendingStepData(null); } }; const handleFilePermissionReject = () => { if (!pendingStepData) return; // Complete step without writing files completeStep(pendingStepData.step, pendingStepData.content, [], // No files written pendingStepData.tokens, pendingStepData.duration); // Clear pending state setPendingFiles([]); setExistingFiles([]); setPendingStepData(null); }; const handleFilePermissionPartial = async (selectedFiles) => { if (!pendingStepData) return; try { if (selectedFiles.length > 0) { // Write only selected files with backup await fileWriter.writeFilesWithBackup(selectedFiles); } // Complete the step with selected files completeStep(pendingStepData.step, pendingStepData.content, selectedFiles, pendingStepData.tokens, pendingStepData.duration); } catch (error) { setError(`Failed to write files: ${error instanceof Error ? error.message : 'Unknown error'}`); setWorkflowState('plan-review'); } finally { // Clear pending state setPendingFiles([]); setExistingFiles([]); setPendingStepData(null); } }; const executeNextStep = async () => { if (!currentPlan) return; const remainingSteps = currentPlan.steps.filter(step => !completedSteps.includes(step.id)); if (remainingSteps.length === 0) { setWorkflowState('completed'); return; } const nextStep = remainingSteps[0]; setCurrentStepId(nextStep.id); setWorkflowState('executing'); const startTime = new Date(); setGenerationStartTime(startTime); setIsGenerating(true); setError(''); setElapsedTime(0); setTokensGenerated(0); try { const response = await togetherAI.executeStep({ step: nextStep, plan: currentPlan, context: chatHistory.slice(-4).map(msg => ({ role: msg.role, content: msg.content })) }); const actualTokens = response.tokens || Math.floor(response.content.length / 4); setTokensGenerated(actualTokens); // Parse files from the response const parsedFiles = fileWriter.parseCodeBlocks(response.content); const stepDuration = Math.floor((Date.now() - startTime.getTime()) / 1000); if (parsedFiles.length > 0) { // Check for existing files const existingFilesList = await fileWriter.checkExistingFiles(parsedFiles); // Store pending data for after permission setPendingStepData({ step: nextStep, content: response.content, tokens: actualTokens, duration: stepDuration }); // Set file permission state setPendingFiles(parsedFiles); setExistingFiles(existingFilesList); setWorkflowState('file-permission'); setCurrentStepId(null); } else { // No files to write, complete the step completeStep(nextStep, response.content, parsedFiles, actualTokens, stepDuration); } } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error occurred'); setCurrentStepId(null); setWorkflowState('plan-review'); } finally { setIsGenerating(false); setGenerationStartTime(null); } }; const handleSubmit = (value) => { if (!value.trim()) return; setPrompt(''); if (workflowState === 'completed' || workflowState === 'chat') { // Continue chat mode for modifications/new requests setWorkflowState('planning'); generatePlan(value); } else { // Initial request setWorkflowState('planning'); generatePlan(value); } }; const handlePlanApproval = () => { if (currentPlan) { executeNextStep(); } }; const handlePlanModification = () => { setWorkflowState('input'); setCurrentPlan(null); setCompletedSteps([]); setCurrentStepId(null); }; useInput((input, key) => { if (workflowState === 'plan-review' && !isGenerating) { if (key.return) { handlePlanApproval(); } else if (input.toLowerCase() === 'm') { handlePlanModification(); } else if (key.escape) { setWorkflowState('input'); setCurrentPlan(null); setCompletedSteps([]); setCurrentStepId(null); } } else if ((workflowState === 'completed' || workflowState === 'chat') && key.return && !isGenerating) { setWorkflowState('input'); } else if (key.escape) { process.exit(0); } }); const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; }; return (React.createElement(Box, { flexDirection: "column", padding: 1 }, error && (React.createElement(Box, { marginBottom: 1 }, React.createElement(Text, { color: "red" }, "\u274C ", error))), workflowState === 'planning' && isGenerating && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Box, null, React.createElement(Text, { color: "yellow" }, React.createElement(Spinner, { type: "dots" }), " Creating plan with DeepSeek v3...")), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: "gray" }, "\u23F1\uFE0F ", formatTime(elapsedTime), " elapsed"), tokensGenerated > 0 && (React.createElement(Text, { color: "gray" }, " \u2022 \uD83D\uDD24 ~", tokensGenerated, " tokens"))))), (workflowState === 'plan-review' || workflowState === 'executing' || workflowState === 'completed') && currentPlan && (React.createElement(PlanDisplay, { plan: currentPlan, currentStepId: currentStepId || undefined, completedSteps: completedSteps, onApprove: handlePlanApproval, onModify: handlePlanModification })), stepResults.size > 0 && currentPlan && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, React.createElement(Text, { color: "cyan", bold: true }, "\uD83D\uDCCB Step Results:"), currentPlan.steps .filter(step => stepResults.has(step.id)) .map(step => { const result = stepResults.get(step.id); return (React.createElement(StepResult, { key: step.id, step: step, content: result.content, files: result.files, tokens: result.tokens, duration: result.duration })); }))), workflowState === 'executing' && isGenerating && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, React.createElement(Box, null, React.createElement(Text, { color: "yellow" }, React.createElement(Spinner, { type: "dots" }), " Executing step with DeepSeek v3...")), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: "gray" }, "\u23F1\uFE0F ", formatTime(elapsedTime), " elapsed"), tokensGenerated > 0 && (React.createElement(Text, { color: "gray" }, " \u2022 \uD83D\uDD24 ~", tokensGenerated, " tokens"))))), workflowState === 'input' && (React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, { color: "cyan" }, chatHistory.length === 0 ? 'What would you like me to build?' : 'What would you like to do next?'), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: "yellow" }, "\uD83D\uDCAC "), React.createElement(TextInput, { value: prompt, onChange: setPrompt, onSubmit: handleSubmit, placeholder: chatHistory.length === 0 ? "Describe what you want to create..." : "Make changes, create new files, or ask questions..." })))), workflowState === 'file-permission' && (React.createElement(FilePermissionDialog, { files: pendingFiles, existingFiles: existingFiles, onApprove: handleFilePermissionApprove, onReject: handleFilePermissionReject, onApproveSelected: handleFilePermissionPartial })), workflowState === 'completed' && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, React.createElement(Text, { color: "green", bold: true }, "\uD83C\uDF89 Project completed successfully!"), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: "yellow" }, "Press ENTER to start a new request, ESC to exit")))))); }; export default CodeGenerator;