together-code
Version:
AI-powered coding assistant that plans, then builds
354 lines (353 loc) • 15.6 kB
JavaScript
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;