Spaces:
Paused
Paused
Delete frontend/src
Browse files- frontend/src/App.tsx +0 -94
- frontend/src/components/AgentPanel.tsx +0 -141
- frontend/src/components/BackgroundTasks.tsx +0 -33
- frontend/src/components/ChatPanel.tsx +0 -251
- frontend/src/components/CodeEditor.tsx +0 -86
- frontend/src/components/CodebaseExplorer.tsx +0 -3
- frontend/src/components/FileTree.tsx +0 -83
- frontend/src/components/GitPanel.tsx +0 -148
- frontend/src/components/ModelSelector.tsx +0 -32
- frontend/src/components/StatusBar.tsx +0 -16
- frontend/src/components/Terminal.tsx +0 -242
- frontend/src/components/TerminalTabs.tsx +0 -171
- frontend/src/components/TopBar.tsx +0 -20
- frontend/src/index.css +0 -25
- frontend/src/index.tsx +0 -13
- frontend/src/services/api.ts +0 -2
frontend/src/App.tsx
DELETED
|
@@ -1,94 +0,0 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
| 3 |
-
import ChatPanel from './components/ChatPanel';
|
| 4 |
-
import AgentPanel from './components/AgentPanel';
|
| 5 |
-
import CodeEditor from './components/CodeEditor';
|
| 6 |
-
import TerminalTabs from './components/TerminalTabs';
|
| 7 |
-
import FileTree from './components/FileTree';
|
| 8 |
-
import ModelSelector from './components/ModelSelector';
|
| 9 |
-
import TopBar from './components/TopBar';
|
| 10 |
-
import StatusBar from './components/StatusBar';
|
| 11 |
-
import GitPanel from './components/GitPanel';
|
| 12 |
-
import BackgroundTasks from './components/BackgroundTasks';
|
| 13 |
-
import { FaComments, FaRobot, FaTerminal, FaCodeBranch, FaTasks } from 'react-icons/fa';
|
| 14 |
-
|
| 15 |
-
const App: React.FC = () => {
|
| 16 |
-
const [activeBottom, setActiveBottom] = useState('chat');
|
| 17 |
-
const [projectPath] = useState('/app/sandbox');
|
| 18 |
-
const [activeFile, setActiveFile] = useState<string | null>(null);
|
| 19 |
-
const [selectedModel, setSelectedModel] = useState('groq-llama-3.3-70b-versatile');
|
| 20 |
-
const [sidebarVisible, setSidebarVisible] = useState(true);
|
| 21 |
-
|
| 22 |
-
useEffect(() => {
|
| 23 |
-
const handleKeyDown = (e: KeyboardEvent) => {
|
| 24 |
-
const mod = e.ctrlKey || e.metaKey;
|
| 25 |
-
if (mod && e.key === 'b') { e.preventDefault(); setSidebarVisible(prev => !prev); }
|
| 26 |
-
else if (mod && e.key === '1') { e.preventDefault(); setActiveBottom('chat'); }
|
| 27 |
-
else if (mod && e.key === '2') { e.preventDefault(); setActiveBottom('agent'); }
|
| 28 |
-
else if (mod && e.key === '3') { e.preventDefault(); setActiveBottom('terminal'); }
|
| 29 |
-
else if (mod && e.key === '4') { e.preventDefault(); setActiveBottom('git'); }
|
| 30 |
-
else if (mod && e.key === '5') { e.preventDefault(); setActiveBottom('tasks'); }
|
| 31 |
-
};
|
| 32 |
-
window.addEventListener('keydown', handleKeyDown);
|
| 33 |
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 34 |
-
}, []);
|
| 35 |
-
|
| 36 |
-
return (
|
| 37 |
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: 'var(--bg-primary)' }}>
|
| 38 |
-
<TopBar />
|
| 39 |
-
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
| 40 |
-
{sidebarVisible && (
|
| 41 |
-
<div style={{ width: 280, background: 'var(--bg-secondary)', borderRight: '1px solid var(--border)', display: 'flex', flexDirection: 'column' }}>
|
| 42 |
-
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
| 43 |
-
<span style={{ fontWeight: 600, fontSize: 13 }}>EXPLORER</span>
|
| 44 |
-
<span onClick={() => setSidebarVisible(false)} style={{ cursor: 'pointer', color: 'var(--text-secondary)' }}>×</span>
|
| 45 |
-
</div>
|
| 46 |
-
<ModelSelector selectedModel={selectedModel} onModelChange={setSelectedModel} />
|
| 47 |
-
<FileTree projectPath={projectPath} onFileSelect={setActiveFile} />
|
| 48 |
-
</div>
|
| 49 |
-
)}
|
| 50 |
-
{!sidebarVisible && (
|
| 51 |
-
<div style={{ width: 40, background: 'var(--bg-secondary)', borderRight: '1px solid var(--border)', display: 'flex', alignItems: 'flex-start', paddingTop: 10, justifyContent: 'center' }}>
|
| 52 |
-
<span onClick={() => setSidebarVisible(true)} style={{ cursor: 'pointer', color: 'var(--text-secondary)' }}>☰</span>
|
| 53 |
-
</div>
|
| 54 |
-
)}
|
| 55 |
-
<PanelGroup direction="vertical" style={{ flex: 1 }}>
|
| 56 |
-
<Panel defaultSize={65} minSize={20}>
|
| 57 |
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
| 58 |
-
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border)', padding: '0 8px', overflowX: 'auto' }}>
|
| 59 |
-
{activeFile ? (
|
| 60 |
-
<div style={{ padding: '8px 12px', borderBottom: '2px solid var(--accent)', color: 'var(--accent)', fontSize: 13 }}>📄 {activeFile}</div>
|
| 61 |
-
) : (
|
| 62 |
-
<div style={{ padding: '8px 12px', color: 'var(--text-secondary)', fontSize: 13 }}>No file selected</div>
|
| 63 |
-
)}
|
| 64 |
-
</div>
|
| 65 |
-
<CodeEditor filePath={activeFile} projectPath={projectPath} />
|
| 66 |
-
</div>
|
| 67 |
-
</Panel>
|
| 68 |
-
<PanelResizeHandle style={{ height: 4, background: 'var(--border)' }} />
|
| 69 |
-
<Panel defaultSize={35} minSize={15}>
|
| 70 |
-
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--bg-primary)' }}>
|
| 71 |
-
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border)', padding: '0 8px' }}>
|
| 72 |
-
<button onClick={() => setActiveBottom('chat')} style={{ padding: '8px 16px', background: activeBottom === 'chat' ? 'var(--bg-primary)' : 'transparent', borderBottom: activeBottom === 'chat' ? '2px solid var(--accent)' : 'none', color: activeBottom === 'chat' ? 'var(--accent)' : 'var(--text-secondary)' }}><FaComments /> Chat (Ctrl+1)</button>
|
| 73 |
-
<button onClick={() => setActiveBottom('agent')} style={{ padding: '8px 16px', background: activeBottom === 'agent' ? 'var(--bg-primary)' : 'transparent', borderBottom: activeBottom === 'agent' ? '2px solid var(--accent)' : 'none', color: activeBottom === 'agent' ? 'var(--accent)' : 'var(--text-secondary)' }}><FaRobot /> Agent (Ctrl+2)</button>
|
| 74 |
-
<button onClick={() => setActiveBottom('terminal')} style={{ padding: '8px 16px', background: activeBottom === 'terminal' ? 'var(--bg-primary)' : 'transparent', borderBottom: activeBottom === 'terminal' ? '2px solid var(--accent)' : 'none', color: activeBottom === 'terminal' ? 'var(--accent)' : 'var(--text-secondary)' }}><FaTerminal /> Terminal (Ctrl+3)</button>
|
| 75 |
-
<button onClick={() => setActiveBottom('git')} style={{ padding: '8px 16px', background: activeBottom === 'git' ? 'var(--bg-primary)' : 'transparent', borderBottom: activeBottom === 'git' ? '2px solid var(--accent)' : 'none', color: activeBottom === 'git' ? 'var(--accent)' : 'var(--text-secondary)' }}><FaCodeBranch /> Git (Ctrl+4)</button>
|
| 76 |
-
<button onClick={() => setActiveBottom('tasks')} style={{ padding: '8px 16px', background: activeBottom === 'tasks' ? 'var(--bg-primary)' : 'transparent', borderBottom: activeBottom === 'tasks' ? '2px solid var(--accent)' : 'none', color: activeBottom === 'tasks' ? 'var(--accent)' : 'var(--text-secondary)' }}><FaTasks /> Tasks (Ctrl+5)</button>
|
| 77 |
-
</div>
|
| 78 |
-
<div style={{ flex: 1, overflow: 'hidden' }}>
|
| 79 |
-
{activeBottom === 'chat' && <ChatPanel projectPath={projectPath} model={selectedModel} />}
|
| 80 |
-
{activeBottom === 'agent' && <AgentPanel projectPath={projectPath} />}
|
| 81 |
-
{activeBottom === 'terminal' && <TerminalTabs />}
|
| 82 |
-
{activeBottom === 'git' && <GitPanel projectPath={projectPath} />}
|
| 83 |
-
{activeBottom === 'tasks' && <BackgroundTasks />}
|
| 84 |
-
</div>
|
| 85 |
-
</div>
|
| 86 |
-
</Panel>
|
| 87 |
-
</PanelGroup>
|
| 88 |
-
</div>
|
| 89 |
-
<StatusBar model={selectedModel} />
|
| 90 |
-
</div>
|
| 91 |
-
);
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
export default App;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/AgentPanel.tsx
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
import React, { useState } from 'react';
|
| 2 |
-
import { FaRobot, FaSpinner, FaCheck, FaTimes, FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
| 3 |
-
import { DiffEditor } from '@monaco-editor/react';
|
| 4 |
-
|
| 5 |
-
interface Change {
|
| 6 |
-
filepath: string;
|
| 7 |
-
action: string;
|
| 8 |
-
search?: string;
|
| 9 |
-
replace?: string;
|
| 10 |
-
content?: string;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
const AgentPanel: React.FC<{ projectPath: string }> = ({ projectPath }) => {
|
| 14 |
-
const [prompt, setPrompt] = useState('');
|
| 15 |
-
const [changes, setChanges] = useState<Change[]>([]);
|
| 16 |
-
const [approved, setApproved] = useState<Set<number>>(new Set());
|
| 17 |
-
const [currentDiff, setCurrentDiff] = useState(0);
|
| 18 |
-
const [planning, setPlanning] = useState(false);
|
| 19 |
-
const [applying, setApplying] = useState(false);
|
| 20 |
-
const [output, setOutput] = useState('');
|
| 21 |
-
|
| 22 |
-
const runPlan = async () => {
|
| 23 |
-
setPlanning(true);
|
| 24 |
-
setOutput('Planning...');
|
| 25 |
-
setChanges([]);
|
| 26 |
-
setApproved(new Set());
|
| 27 |
-
try {
|
| 28 |
-
const resp = await fetch('/api/agent/plan', {
|
| 29 |
-
method: 'POST',
|
| 30 |
-
headers: { 'Content-Type': 'application/json' },
|
| 31 |
-
body: JSON.stringify({ prompt, project_path: projectPath, auto_approve: false }),
|
| 32 |
-
});
|
| 33 |
-
const data = await resp.json();
|
| 34 |
-
setChanges(data.changes || []);
|
| 35 |
-
setOutput(`Agent proposed ${data.changes?.length || 0} changes.`);
|
| 36 |
-
} catch (e) {
|
| 37 |
-
setOutput('Error: ' + e);
|
| 38 |
-
}
|
| 39 |
-
setPlanning(false);
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
const applyApproved = async () => {
|
| 43 |
-
setApplying(true);
|
| 44 |
-
const indices = Array.from(approved);
|
| 45 |
-
try {
|
| 46 |
-
const resp = await fetch('/api/agent/apply', {
|
| 47 |
-
method: 'POST',
|
| 48 |
-
headers: { 'Content-Type': 'application/json' },
|
| 49 |
-
body: JSON.stringify({ task_id: 'default', approved_indices: indices }),
|
| 50 |
-
});
|
| 51 |
-
const data = await resp.json();
|
| 52 |
-
setOutput(prev => prev + '\n' + (data.logs || []).join('\n'));
|
| 53 |
-
} catch (e) {
|
| 54 |
-
setOutput('Error applying: ' + e);
|
| 55 |
-
}
|
| 56 |
-
setApplying(false);
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
const toggleApproval = (idx: number) => {
|
| 60 |
-
const newSet = new Set(approved);
|
| 61 |
-
if (newSet.has(idx)) newSet.delete(idx); else newSet.add(idx);
|
| 62 |
-
setApproved(newSet);
|
| 63 |
-
};
|
| 64 |
-
|
| 65 |
-
const diffOriginal = (change: Change) => {
|
| 66 |
-
if (change.action === 'create') return '';
|
| 67 |
-
return change.search || '';
|
| 68 |
-
};
|
| 69 |
-
|
| 70 |
-
const diffModified = (change: Change) => {
|
| 71 |
-
if (change.action === 'create') return change.content || '';
|
| 72 |
-
return change.replace || '';
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
return (
|
| 76 |
-
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--bg-primary)' }}>
|
| 77 |
-
<div style={{ padding: 12, borderBottom: '1px solid var(--border)' }}>
|
| 78 |
-
<div style={{ fontWeight: 600, marginBottom: 8 }}><FaRobot /> AI Agent</div>
|
| 79 |
-
<textarea
|
| 80 |
-
style={{ width: '100%', height: 60, background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 6, padding: 8, resize: 'vertical' }}
|
| 81 |
-
value={prompt}
|
| 82 |
-
onChange={e => setPrompt(e.target.value)}
|
| 83 |
-
placeholder="E.g., Convert all React components to TypeScript..."
|
| 84 |
-
/>
|
| 85 |
-
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
| 86 |
-
<button onClick={runPlan} disabled={planning} style={{ background: 'var(--accent)', color: 'white', padding: '6px 16px', borderRadius: 6, fontWeight: 600 }}>
|
| 87 |
-
{planning ? <><FaSpinner className="spin" /> Planning</> : 'Plan Changes'}
|
| 88 |
-
</button>
|
| 89 |
-
{changes.length > 0 && (
|
| 90 |
-
<button onClick={applyApproved} disabled={applying || approved.size === 0} style={{ background: 'var(--success)', color: 'white', padding: '6px 16px', borderRadius: 6, fontWeight: 600 }}>
|
| 91 |
-
{applying ? 'Applying...' : `Apply ${approved.size} Approved`}
|
| 92 |
-
</button>
|
| 93 |
-
)}
|
| 94 |
-
</div>
|
| 95 |
-
</div>
|
| 96 |
-
|
| 97 |
-
{changes.length > 0 && (
|
| 98 |
-
<div style={{ borderBottom: '1px solid var(--border)', flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
| 99 |
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 12px', background: 'var(--bg-secondary)' }}>
|
| 100 |
-
<div style={{ display: 'flex', gap: 12 }}>
|
| 101 |
-
<button onClick={() => setCurrentDiff(Math.max(0, currentDiff - 1))}><FaChevronLeft /></button>
|
| 102 |
-
<span style={{ fontSize: 13 }}>File {currentDiff + 1} of {changes.length}: <strong>{changes[currentDiff]?.filepath}</strong></span>
|
| 103 |
-
<button onClick={() => setCurrentDiff(Math.min(changes.length - 1, currentDiff + 1))}><FaChevronRight /></button>
|
| 104 |
-
</div>
|
| 105 |
-
<button
|
| 106 |
-
onClick={() => toggleApproval(currentDiff)}
|
| 107 |
-
style={{
|
| 108 |
-
background: approved.has(currentDiff) ? 'var(--success)' : 'var(--danger)',
|
| 109 |
-
color: 'white',
|
| 110 |
-
padding: '4px 12px',
|
| 111 |
-
borderRadius: 4,
|
| 112 |
-
display: 'flex',
|
| 113 |
-
alignItems: 'center',
|
| 114 |
-
gap: 4,
|
| 115 |
-
fontSize: 12,
|
| 116 |
-
}}
|
| 117 |
-
>
|
| 118 |
-
{approved.has(currentDiff) ? <><FaCheck /> Approved</> : <><FaTimes /> Rejected</>}
|
| 119 |
-
</button>
|
| 120 |
-
</div>
|
| 121 |
-
<div style={{ flex: 1, minHeight: 0 }}>
|
| 122 |
-
<DiffEditor
|
| 123 |
-
height="100%"
|
| 124 |
-
original={diffOriginal(changes[currentDiff])}
|
| 125 |
-
modified={diffModified(changes[currentDiff])}
|
| 126 |
-
language="typescript"
|
| 127 |
-
theme="vs-dark"
|
| 128 |
-
options={{ readOnly: true, renderSideBySide: true }}
|
| 129 |
-
/>
|
| 130 |
-
</div>
|
| 131 |
-
</div>
|
| 132 |
-
)}
|
| 133 |
-
|
| 134 |
-
<div style={{ padding: 12, background: 'var(--bg-secondary)', maxHeight: 150, overflowY: 'auto', fontFamily: 'monospace', fontSize: 12, whiteSpace: 'pre-wrap' }}>
|
| 135 |
-
{output}
|
| 136 |
-
</div>
|
| 137 |
-
</div>
|
| 138 |
-
);
|
| 139 |
-
};
|
| 140 |
-
|
| 141 |
-
export default AgentPanel;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/BackgroundTasks.tsx
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import axios from 'axios';
|
| 3 |
-
|
| 4 |
-
const BackgroundTasks: React.FC = () => {
|
| 5 |
-
const [tasks, setTasks] = useState<string[]>([]);
|
| 6 |
-
|
| 7 |
-
const pollTasks = async () => {
|
| 8 |
-
try {
|
| 9 |
-
const res = await axios.get('/api/agent/tasks');
|
| 10 |
-
setTasks(res.data.tasks || []);
|
| 11 |
-
} catch (e) {}
|
| 12 |
-
};
|
| 13 |
-
|
| 14 |
-
useEffect(() => {
|
| 15 |
-
pollTasks();
|
| 16 |
-
const interval = setInterval(pollTasks, 3000);
|
| 17 |
-
return () => clearInterval(interval);
|
| 18 |
-
}, []);
|
| 19 |
-
|
| 20 |
-
return (
|
| 21 |
-
<div style={{ padding: 12 }}>
|
| 22 |
-
<div style={{ fontWeight: 600, marginBottom: 8 }}>Background Agents</div>
|
| 23 |
-
{tasks.length === 0 && <div style={{ color: 'var(--text-secondary)', fontSize: 12 }}>No running tasks</div>}
|
| 24 |
-
{tasks.map(task => (
|
| 25 |
-
<div key={task} style={{ background: 'var(--bg-tertiary)', padding: '4px 8px', borderRadius: 4, marginBottom: 4, fontSize: 12 }}>
|
| 26 |
-
{task} – running
|
| 27 |
-
</div>
|
| 28 |
-
))}
|
| 29 |
-
</div>
|
| 30 |
-
);
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
export default BackgroundTasks;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/ChatPanel.tsx
DELETED
|
@@ -1,251 +0,0 @@
|
|
| 1 |
-
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
-
import ReactMarkdown from 'react-markdown';
|
| 3 |
-
import { FaUser, FaRobot, FaSpinner } from 'react-icons/fa';
|
| 4 |
-
import axios from 'axios';
|
| 5 |
-
|
| 6 |
-
interface Message {
|
| 7 |
-
role: 'user' | 'assistant';
|
| 8 |
-
content: string;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
const ChatPanel: React.FC<{ projectPath: string; model: string }> = ({ projectPath, model }) => {
|
| 12 |
-
const [messages, setMessages] = useState<Message[]>([]);
|
| 13 |
-
const [input, setInput] = useState('');
|
| 14 |
-
const [loading, setLoading] = useState(false);
|
| 15 |
-
const [mentionQuery, setMentionQuery] = useState('');
|
| 16 |
-
const [mentionItems, setMentionItems] = useState<any[]>([]);
|
| 17 |
-
const [showMentions, setShowMentions] = useState(false);
|
| 18 |
-
const [contextFiles, setContextFiles] = useState<string[]>([]);
|
| 19 |
-
const chatEnd = useRef<HTMLDivElement>(null);
|
| 20 |
-
const inputRef = useRef<HTMLInputElement>(null);
|
| 21 |
-
|
| 22 |
-
const send = async () => {
|
| 23 |
-
if (!input.trim() || loading) return;
|
| 24 |
-
const userMsg = input;
|
| 25 |
-
setInput('');
|
| 26 |
-
setMessages(prev => [...prev, { role: 'user', content: userMsg }]);
|
| 27 |
-
setLoading(true);
|
| 28 |
-
|
| 29 |
-
const assistantMsgRef = { current: '' };
|
| 30 |
-
setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
|
| 31 |
-
|
| 32 |
-
try {
|
| 33 |
-
const response = await fetch('/api/chat', {
|
| 34 |
-
method: 'POST',
|
| 35 |
-
headers: { 'Content-Type': 'application/json' },
|
| 36 |
-
body: JSON.stringify({
|
| 37 |
-
messages: [...messages, { role: 'user', content: userMsg }],
|
| 38 |
-
project_path: projectPath,
|
| 39 |
-
model: model,
|
| 40 |
-
context_files: contextFiles,
|
| 41 |
-
}),
|
| 42 |
-
});
|
| 43 |
-
|
| 44 |
-
if (!response.ok) throw new Error(`Server responded with ${response.status}`);
|
| 45 |
-
|
| 46 |
-
const reader = response.body?.getReader();
|
| 47 |
-
if (!reader) throw new Error('No response body');
|
| 48 |
-
|
| 49 |
-
const decoder = new TextDecoder();
|
| 50 |
-
let buffer = '';
|
| 51 |
-
|
| 52 |
-
while (true) {
|
| 53 |
-
const { done, value } = await reader.read();
|
| 54 |
-
if (done) break;
|
| 55 |
-
buffer += decoder.decode(value, { stream: true });
|
| 56 |
-
const lines = buffer.split('\n');
|
| 57 |
-
buffer = lines.pop() || '';
|
| 58 |
-
for (const line of lines) {
|
| 59 |
-
if (line.startsWith('data: ')) {
|
| 60 |
-
const data = line.slice(6);
|
| 61 |
-
if (data === '[DONE]') break;
|
| 62 |
-
try {
|
| 63 |
-
const { token } = JSON.parse(data);
|
| 64 |
-
if (token.startsWith('ERROR:')) {
|
| 65 |
-
assistantMsgRef.current = `❌ ${token.substring(7)}`;
|
| 66 |
-
setMessages(prev => {
|
| 67 |
-
const copy = [...prev];
|
| 68 |
-
copy[copy.length - 1] = { role: 'assistant', content: assistantMsgRef.current };
|
| 69 |
-
return copy;
|
| 70 |
-
});
|
| 71 |
-
return;
|
| 72 |
-
}
|
| 73 |
-
assistantMsgRef.current += token;
|
| 74 |
-
setMessages(prev => {
|
| 75 |
-
const copy = [...prev];
|
| 76 |
-
copy[copy.length - 1] = { role: 'assistant', content: assistantMsgRef.current };
|
| 77 |
-
return copy;
|
| 78 |
-
});
|
| 79 |
-
} catch (e) {
|
| 80 |
-
assistantMsgRef.current = `Parse error: ${data}`;
|
| 81 |
-
setMessages(prev => {
|
| 82 |
-
const copy = [...prev];
|
| 83 |
-
copy[copy.length - 1] = { role: 'assistant', content: assistantMsgRef.current };
|
| 84 |
-
return copy;
|
| 85 |
-
});
|
| 86 |
-
return;
|
| 87 |
-
}
|
| 88 |
-
}
|
| 89 |
-
}
|
| 90 |
-
}
|
| 91 |
-
} catch (err: any) {
|
| 92 |
-
setMessages(prev => {
|
| 93 |
-
const copy = [...prev];
|
| 94 |
-
copy[copy.length - 1] = { role: 'assistant', content: `❌ ${err.message}` };
|
| 95 |
-
return copy;
|
| 96 |
-
});
|
| 97 |
-
} finally {
|
| 98 |
-
setLoading(false);
|
| 99 |
-
}
|
| 100 |
-
};
|
| 101 |
-
|
| 102 |
-
useEffect(() => { chatEnd.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
|
| 103 |
-
|
| 104 |
-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 105 |
-
const value = e.target.value;
|
| 106 |
-
setInput(value);
|
| 107 |
-
|
| 108 |
-
const atIndex = value.lastIndexOf('@');
|
| 109 |
-
if (atIndex !== -1 && (atIndex === 0 || value[atIndex - 1] === ' ')) {
|
| 110 |
-
const query = value.slice(atIndex + 1).toLowerCase();
|
| 111 |
-
setMentionQuery(query);
|
| 112 |
-
|
| 113 |
-
if ('git'.startsWith(query)) {
|
| 114 |
-
setMentionItems([{ name: 'git (current diff)', type: 'special', path: '__git__' }]);
|
| 115 |
-
setShowMentions(true);
|
| 116 |
-
return;
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
axios.get('/api/files', { params: { project_path: projectPath } })
|
| 120 |
-
.then(res => {
|
| 121 |
-
const flatten = (nodes: any[]): any[] => {
|
| 122 |
-
let result: any[] = [];
|
| 123 |
-
for (const node of nodes) {
|
| 124 |
-
if (node.name.toLowerCase().includes(query))
|
| 125 |
-
result.push(node);
|
| 126 |
-
if (node.children) result = result.concat(flatten(node.children));
|
| 127 |
-
}
|
| 128 |
-
return result;
|
| 129 |
-
};
|
| 130 |
-
const matches = flatten(res.data).slice(0, 5);
|
| 131 |
-
setMentionItems(matches);
|
| 132 |
-
setShowMentions(matches.length > 0);
|
| 133 |
-
})
|
| 134 |
-
.catch(() => setShowMentions(false));
|
| 135 |
-
} else {
|
| 136 |
-
setShowMentions(false);
|
| 137 |
-
}
|
| 138 |
-
};
|
| 139 |
-
|
| 140 |
-
const insertMention = (item: any) => {
|
| 141 |
-
const atIndex = input.lastIndexOf('@');
|
| 142 |
-
const newInput = input.slice(0, atIndex) + item.name + ' ';
|
| 143 |
-
setInput(newInput);
|
| 144 |
-
setShowMentions(false);
|
| 145 |
-
if (item.path === '__git__') {
|
| 146 |
-
if (!contextFiles.includes('__git__')) {
|
| 147 |
-
setContextFiles(prev => [...prev, '__git__']);
|
| 148 |
-
}
|
| 149 |
-
} else if (!contextFiles.includes(item.path)) {
|
| 150 |
-
setContextFiles(prev => [...prev, item.path]);
|
| 151 |
-
}
|
| 152 |
-
inputRef.current?.focus();
|
| 153 |
-
};
|
| 154 |
-
|
| 155 |
-
return (
|
| 156 |
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
| 157 |
-
<div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
|
| 158 |
-
{messages.map((m, i) => (
|
| 159 |
-
<div key={i} style={{ marginBottom: 16, display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start' }}>
|
| 160 |
-
<div style={{
|
| 161 |
-
maxWidth: '80%',
|
| 162 |
-
background: m.role === 'user' ? 'var(--accent)' : 'var(--bg-tertiary)',
|
| 163 |
-
color: m.role === 'user' ? 'white' : 'var(--text-primary)',
|
| 164 |
-
borderRadius: 12,
|
| 165 |
-
borderBottomRightRadius: m.role === 'user' ? 4 : 12,
|
| 166 |
-
borderBottomLeftRadius: m.role === 'user' ? 12 : 4,
|
| 167 |
-
padding: 12,
|
| 168 |
-
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
|
| 169 |
-
}}>
|
| 170 |
-
<div style={{ fontSize: 11, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 4, color: m.role === 'user' ? 'rgba(255,255,255,0.8)' : 'var(--text-secondary)' }}>
|
| 171 |
-
{m.role === 'user' ? <FaUser size={10} /> : <FaRobot size={10} />}
|
| 172 |
-
{m.role === 'user' ? 'You' : 'Codeki'}
|
| 173 |
-
</div>
|
| 174 |
-
<ReactMarkdown>{m.content}</ReactMarkdown>
|
| 175 |
-
</div>
|
| 176 |
-
</div>
|
| 177 |
-
))}
|
| 178 |
-
{loading && (
|
| 179 |
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-secondary)', padding: 8 }}>
|
| 180 |
-
<FaSpinner className="spin" /> Codeki is thinking...
|
| 181 |
-
</div>
|
| 182 |
-
)}
|
| 183 |
-
<div ref={chatEnd} />
|
| 184 |
-
</div>
|
| 185 |
-
<div style={{ borderTop: '1px solid var(--border)', padding: 12, background: 'var(--bg-secondary)', position: 'relative' }}>
|
| 186 |
-
{showMentions && (
|
| 187 |
-
<div style={{
|
| 188 |
-
position: 'absolute',
|
| 189 |
-
bottom: '100%',
|
| 190 |
-
left: 0,
|
| 191 |
-
right: 0,
|
| 192 |
-
background: 'var(--bg-tertiary)',
|
| 193 |
-
border: '1px solid var(--border)',
|
| 194 |
-
borderRadius: 4,
|
| 195 |
-
maxHeight: 150,
|
| 196 |
-
overflowY: 'auto',
|
| 197 |
-
marginBottom: 4,
|
| 198 |
-
zIndex: 10,
|
| 199 |
-
}}>
|
| 200 |
-
{mentionItems.map(item => (
|
| 201 |
-
<div
|
| 202 |
-
key={item.path}
|
| 203 |
-
onClick={() => insertMention(item)}
|
| 204 |
-
style={{ padding: '4px 8px', cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', gap: 6 }}
|
| 205 |
-
>
|
| 206 |
-
{item.type === 'folder' ? '📁' : item.type === 'special' ? '🔗' : '📄'} {item.name}
|
| 207 |
-
</div>
|
| 208 |
-
))}
|
| 209 |
-
</div>
|
| 210 |
-
)}
|
| 211 |
-
<div style={{ display: 'flex', gap: 8 }}>
|
| 212 |
-
<input
|
| 213 |
-
ref={inputRef}
|
| 214 |
-
style={{ flex: 1 }}
|
| 215 |
-
value={input}
|
| 216 |
-
onChange={handleInputChange}
|
| 217 |
-
onKeyDown={e => e.key === 'Enter' && send()}
|
| 218 |
-
placeholder="Ask anything... Type @ to reference a file or @git"
|
| 219 |
-
disabled={loading}
|
| 220 |
-
/>
|
| 221 |
-
<button
|
| 222 |
-
onClick={send}
|
| 223 |
-
disabled={loading}
|
| 224 |
-
style={{
|
| 225 |
-
background: loading ? 'var(--border)' : 'var(--accent)',
|
| 226 |
-
color: 'white',
|
| 227 |
-
padding: '0 20px',
|
| 228 |
-
borderRadius: 6,
|
| 229 |
-
fontWeight: 600,
|
| 230 |
-
transition: 'background 0.2s'
|
| 231 |
-
}}
|
| 232 |
-
>
|
| 233 |
-
{loading ? '...' : 'Send'}
|
| 234 |
-
</button>
|
| 235 |
-
</div>
|
| 236 |
-
{contextFiles.length > 0 && (
|
| 237 |
-
<div style={{ marginTop: 6, display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
| 238 |
-
{contextFiles.map(f => (
|
| 239 |
-
<span key={f} style={{ background: 'var(--accent)', color: 'white', padding: '2px 8px', borderRadius: 10, fontSize: 11 }}>
|
| 240 |
-
📎 {f === '__git__' ? 'Git Diff' : f.split('/').pop()}
|
| 241 |
-
<span style={{ marginLeft: 4, cursor: 'pointer' }} onClick={() => setContextFiles(prev => prev.filter(x => x !== f))}>×</span>
|
| 242 |
-
</span>
|
| 243 |
-
))}
|
| 244 |
-
</div>
|
| 245 |
-
)}
|
| 246 |
-
</div>
|
| 247 |
-
</div>
|
| 248 |
-
);
|
| 249 |
-
};
|
| 250 |
-
|
| 251 |
-
export default ChatPanel;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/CodeEditor.tsx
DELETED
|
@@ -1,86 +0,0 @@
|
|
| 1 |
-
import React, { useRef } from 'react';
|
| 2 |
-
import Editor, { OnMount } from '@monaco-editor/react';
|
| 3 |
-
import * as monaco from 'monaco-editor';
|
| 4 |
-
|
| 5 |
-
const CodeEditor: React.FC<{ filePath: string | null; projectPath: string }> = ({ filePath, projectPath }) => {
|
| 6 |
-
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
| 7 |
-
|
| 8 |
-
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
| 9 |
-
editorRef.current = editor;
|
| 10 |
-
|
| 11 |
-
monaco.languages.registerInlineCompletionsProvider('*', {
|
| 12 |
-
provideInlineCompletions: async (model, position, context, token) => {
|
| 13 |
-
const prefix = model.getValueInRange({
|
| 14 |
-
startLineNumber: 1,
|
| 15 |
-
startColumn: 1,
|
| 16 |
-
endLineNumber: position.lineNumber,
|
| 17 |
-
endColumn: position.column,
|
| 18 |
-
});
|
| 19 |
-
const suffix = model.getValueInRange({
|
| 20 |
-
startLineNumber: position.lineNumber,
|
| 21 |
-
startColumn: position.column,
|
| 22 |
-
endLineNumber: model.getLineCount(),
|
| 23 |
-
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
| 24 |
-
});
|
| 25 |
-
|
| 26 |
-
try {
|
| 27 |
-
const resp = await fetch('/api/autocomplete', {
|
| 28 |
-
method: 'POST',
|
| 29 |
-
headers: { 'Content-Type': 'application/json' },
|
| 30 |
-
body: JSON.stringify({
|
| 31 |
-
prefix,
|
| 32 |
-
suffix,
|
| 33 |
-
filepath: filePath || '',
|
| 34 |
-
language: model.getLanguageId(),
|
| 35 |
-
max_tokens: 64,
|
| 36 |
-
model: 'groq-llama-3.1-8b-instant',
|
| 37 |
-
}),
|
| 38 |
-
});
|
| 39 |
-
if (!resp.ok) return { items: [] };
|
| 40 |
-
const data = await resp.json();
|
| 41 |
-
const completions = data.completions || [];
|
| 42 |
-
return {
|
| 43 |
-
items: completions.map((c: any) => ({
|
| 44 |
-
insertText: c.text,
|
| 45 |
-
range: {
|
| 46 |
-
startLineNumber: position.lineNumber,
|
| 47 |
-
startColumn: position.column,
|
| 48 |
-
endLineNumber: position.lineNumber,
|
| 49 |
-
endColumn: position.column,
|
| 50 |
-
},
|
| 51 |
-
})),
|
| 52 |
-
};
|
| 53 |
-
} catch (e) {
|
| 54 |
-
return { items: [] };
|
| 55 |
-
}
|
| 56 |
-
},
|
| 57 |
-
freeInlineCompletions: () => {},
|
| 58 |
-
});
|
| 59 |
-
};
|
| 60 |
-
|
| 61 |
-
if (!filePath) return (
|
| 62 |
-
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-secondary)', fontSize: 14 }}>
|
| 63 |
-
← Select a file from the explorer to start editing
|
| 64 |
-
</div>
|
| 65 |
-
);
|
| 66 |
-
|
| 67 |
-
return (
|
| 68 |
-
<Editor
|
| 69 |
-
height="100%"
|
| 70 |
-
defaultLanguage="typescript"
|
| 71 |
-
theme="vs-dark"
|
| 72 |
-
path={filePath}
|
| 73 |
-
onMount={handleEditorDidMount}
|
| 74 |
-
loading={<div style={{ color: 'var(--text-secondary)', padding: 20 }}>Loading editor...</div>}
|
| 75 |
-
options={{
|
| 76 |
-
fontSize: 13,
|
| 77 |
-
minimap: { enabled: true },
|
| 78 |
-
smoothScrolling: true,
|
| 79 |
-
cursorBlinking: 'smooth',
|
| 80 |
-
inlineSuggest: { enabled: true },
|
| 81 |
-
}}
|
| 82 |
-
/>
|
| 83 |
-
);
|
| 84 |
-
};
|
| 85 |
-
|
| 86 |
-
export default CodeEditor;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/CodebaseExplorer.tsx
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
const CodebaseExplorer: React.FC<{ projectPath: string }> = () => <div style={{ color: 'white' }}>Files</div>;
|
| 3 |
-
export default CodebaseExplorer;
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/FileTree.tsx
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import { FaFolder, FaFolderOpen, FaFile, FaChevronRight, FaChevronDown, FaSpinner } from 'react-icons/fa';
|
| 3 |
-
import axios from 'axios';
|
| 4 |
-
|
| 5 |
-
interface TreeNode {
|
| 6 |
-
name: string;
|
| 7 |
-
path: string;
|
| 8 |
-
type: 'file' | 'folder';
|
| 9 |
-
children?: TreeNode[];
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const FileTree: React.FC<{ projectPath: string; onFileSelect: (path: string) => void }> = ({ projectPath, onFileSelect }) => {
|
| 13 |
-
const [tree, setTree] = useState<TreeNode[]>([]);
|
| 14 |
-
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
| 15 |
-
const [selected, setSelected] = useState<string | null>(null);
|
| 16 |
-
const [loading, setLoading] = useState(true);
|
| 17 |
-
|
| 18 |
-
useEffect(() => {
|
| 19 |
-
axios.get('/api/files', { params: { project_path: projectPath } })
|
| 20 |
-
.then(res => setTree(res.data))
|
| 21 |
-
.catch(() => setTree([]))
|
| 22 |
-
.finally(() => setLoading(false));
|
| 23 |
-
}, [projectPath]);
|
| 24 |
-
|
| 25 |
-
const toggle = (path: string) => {
|
| 26 |
-
setExpanded(prev => {
|
| 27 |
-
const next = new Set(prev);
|
| 28 |
-
if (next.has(path)) next.delete(path); else next.add(path);
|
| 29 |
-
return next;
|
| 30 |
-
});
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
const handleClick = (node: TreeNode) => {
|
| 34 |
-
if (node.type === 'folder') {
|
| 35 |
-
toggle(node.path);
|
| 36 |
-
} else {
|
| 37 |
-
setSelected(node.path);
|
| 38 |
-
onFileSelect(node.path);
|
| 39 |
-
}
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
const renderNode = (node: TreeNode, depth: number = 0) => {
|
| 43 |
-
const isOpen = expanded.has(node.path);
|
| 44 |
-
return (
|
| 45 |
-
<div key={node.path}>
|
| 46 |
-
<div
|
| 47 |
-
onClick={() => handleClick(node)}
|
| 48 |
-
style={{
|
| 49 |
-
paddingLeft: depth * 16 + 8,
|
| 50 |
-
padding: '4px 8px',
|
| 51 |
-
cursor: 'pointer',
|
| 52 |
-
display: 'flex',
|
| 53 |
-
alignItems: 'center',
|
| 54 |
-
gap: 6,
|
| 55 |
-
background: selected === node.path ? 'var(--accent)' : 'transparent',
|
| 56 |
-
color: selected === node.path ? 'white' : 'var(--text-primary)',
|
| 57 |
-
fontSize: 13,
|
| 58 |
-
}}
|
| 59 |
-
>
|
| 60 |
-
{node.type === 'folder' ? (
|
| 61 |
-
<>
|
| 62 |
-
{isOpen ? <FaChevronDown size={10} /> : <FaChevronRight size={10} />}
|
| 63 |
-
{isOpen ? <FaFolderOpen size={14} /> : <FaFolder size={14} />}
|
| 64 |
-
</>
|
| 65 |
-
) : (
|
| 66 |
-
<>
|
| 67 |
-
<span style={{ width: 10 }} />
|
| 68 |
-
<FaFile size={14} />
|
| 69 |
-
</>
|
| 70 |
-
)}
|
| 71 |
-
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{node.name}</span>
|
| 72 |
-
</div>
|
| 73 |
-
{node.type === 'folder' && isOpen && node.children?.map(child => renderNode(child, depth + 1))}
|
| 74 |
-
</div>
|
| 75 |
-
);
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
if (loading) return <div style={{ padding: 12, color: 'var(--text-secondary)' }}><FaSpinner className="spin" /> Loading files...</div>;
|
| 79 |
-
|
| 80 |
-
return <div style={{ overflowY: 'auto', flex: 1 }}>{tree.map(node => renderNode(node))}</div>;
|
| 81 |
-
};
|
| 82 |
-
|
| 83 |
-
export default FileTree;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/GitPanel.tsx
DELETED
|
@@ -1,148 +0,0 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import axios from 'axios';
|
| 3 |
-
import { FaCodeBranch, FaHistory, FaPen, FaFileAlt } from 'react-icons/fa';
|
| 4 |
-
import ReactMarkdown from 'react-markdown';
|
| 5 |
-
|
| 6 |
-
const GitPanel: React.FC<{ projectPath: string }> = ({ projectPath }) => {
|
| 7 |
-
const [branches, setBranches] = useState<string[]>([]);
|
| 8 |
-
const [logs, setLogs] = useState<any[]>([]);
|
| 9 |
-
const [commitMsg, setCommitMsg] = useState('');
|
| 10 |
-
const [generating, setGenerating] = useState(false);
|
| 11 |
-
const [prSummary, setPrSummary] = useState('');
|
| 12 |
-
|
| 13 |
-
const fetchData = async () => {
|
| 14 |
-
try {
|
| 15 |
-
const [branchesRes, logsRes] = await Promise.all([
|
| 16 |
-
axios.post('/api/git', { action: 'branch', project_path: projectPath }),
|
| 17 |
-
axios.post('/api/git', { action: 'log', project_path: projectPath })
|
| 18 |
-
]);
|
| 19 |
-
setBranches(branchesRes.data.branches || []);
|
| 20 |
-
setLogs(logsRes.data.log || []);
|
| 21 |
-
} catch (e) {}
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
useEffect(() => { fetchData(); }, []);
|
| 25 |
-
|
| 26 |
-
const generateCommitMessage = async () => {
|
| 27 |
-
setGenerating(true);
|
| 28 |
-
try {
|
| 29 |
-
const diffRes = await axios.post('/api/git', { action: 'diff', project_path: projectPath });
|
| 30 |
-
const diff = diffRes.data.diff || '';
|
| 31 |
-
const resp = await fetch('/api/chat', {
|
| 32 |
-
method: 'POST',
|
| 33 |
-
headers: { 'Content-Type': 'application/json' },
|
| 34 |
-
body: JSON.stringify({
|
| 35 |
-
model: 'groq-llama-3.1-8b-instant',
|
| 36 |
-
messages: [{ role: 'user', content: `Generate a concise git commit message for the following diff:\n${diff}` }],
|
| 37 |
-
project_path: projectPath,
|
| 38 |
-
}),
|
| 39 |
-
});
|
| 40 |
-
const reader = resp.body?.getReader();
|
| 41 |
-
let msg = '';
|
| 42 |
-
while (reader) {
|
| 43 |
-
const { done, value } = await reader.read();
|
| 44 |
-
if (done) break;
|
| 45 |
-
const text = new TextDecoder().decode(value);
|
| 46 |
-
const lines = text.split('\n').filter(l => l.startsWith('data: ')).map(l => l.slice(6));
|
| 47 |
-
for (const line of lines) {
|
| 48 |
-
if (line === '[DONE]') break;
|
| 49 |
-
try {
|
| 50 |
-
const { token } = JSON.parse(line);
|
| 51 |
-
msg += token;
|
| 52 |
-
} catch (e) {}
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
setCommitMsg(msg.replace(/ERROR:.*/, '').trim());
|
| 56 |
-
} catch (e) {}
|
| 57 |
-
setGenerating(false);
|
| 58 |
-
};
|
| 59 |
-
|
| 60 |
-
const generatePRSummary = async () => {
|
| 61 |
-
try {
|
| 62 |
-
const diffRes = await axios.post('/api/git', { action: 'diff', project_path: projectPath });
|
| 63 |
-
const diff = diffRes.data.diff || '';
|
| 64 |
-
const resp = await fetch('/api/chat', {
|
| 65 |
-
method: 'POST',
|
| 66 |
-
headers: { 'Content-Type': 'application/json' },
|
| 67 |
-
body: JSON.stringify({
|
| 68 |
-
model: 'groq-llama-3.3-70b-versatile',
|
| 69 |
-
messages: [{ role: 'user', content: `Review the following diff and provide a PR summary with suggestions:\n${diff}` }],
|
| 70 |
-
project_path: projectPath,
|
| 71 |
-
}),
|
| 72 |
-
});
|
| 73 |
-
const reader = resp.body?.getReader();
|
| 74 |
-
let summary = '';
|
| 75 |
-
while (reader) {
|
| 76 |
-
const { done, value } = await reader.read();
|
| 77 |
-
if (done) break;
|
| 78 |
-
const text = new TextDecoder().decode(value);
|
| 79 |
-
const lines = text.split('\n').filter(l => l.startsWith('data: ')).map(l => l.slice(6));
|
| 80 |
-
for (const line of lines) {
|
| 81 |
-
if (line === '[DONE]') break;
|
| 82 |
-
try {
|
| 83 |
-
const { token } = JSON.parse(line);
|
| 84 |
-
summary += token;
|
| 85 |
-
} catch (e) {}
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
-
setPrSummary(summary);
|
| 89 |
-
} catch (e) {}
|
| 90 |
-
};
|
| 91 |
-
|
| 92 |
-
const commit = async () => {
|
| 93 |
-
await axios.post('/api/git', { action: 'commit', project_path: projectPath, args: { message: commitMsg } });
|
| 94 |
-
fetchData();
|
| 95 |
-
setCommitMsg('');
|
| 96 |
-
};
|
| 97 |
-
|
| 98 |
-
return (
|
| 99 |
-
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--bg-primary)', padding: 12, overflowY: 'auto' }}>
|
| 100 |
-
<div style={{ fontWeight: 600, marginBottom: 8 }}><FaCodeBranch /> Git</div>
|
| 101 |
-
|
| 102 |
-
<div style={{ marginBottom: 12 }}>
|
| 103 |
-
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 4 }}>Branches</div>
|
| 104 |
-
{branches.map(b => (
|
| 105 |
-
<div key={b} style={{ padding: '2px 8px', background: 'var(--bg-tertiary)', borderRadius: 4, marginBottom: 2, fontSize: 13 }}>{b}</div>
|
| 106 |
-
))}
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
<div style={{ marginBottom: 12 }}>
|
| 110 |
-
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 4 }}>Recent Commits</div>
|
| 111 |
-
{logs.map((log, i) => (
|
| 112 |
-
<div key={i} style={{ borderBottom: '1px solid var(--border)', padding: '4px 0', fontSize: 12 }}>
|
| 113 |
-
<span style={{ color: 'var(--accent)' }}>{log.hash?.substring(0,7)} </span>
|
| 114 |
-
{log.message}
|
| 115 |
-
</div>
|
| 116 |
-
))}
|
| 117 |
-
</div>
|
| 118 |
-
|
| 119 |
-
<div style={{ marginBottom: 12 }}>
|
| 120 |
-
<textarea
|
| 121 |
-
style={{ width: '100%', height: 40, background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 4, padding: 4, fontSize: 12 }}
|
| 122 |
-
placeholder="Commit message..."
|
| 123 |
-
value={commitMsg}
|
| 124 |
-
onChange={e => setCommitMsg(e.target.value)}
|
| 125 |
-
/>
|
| 126 |
-
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
|
| 127 |
-
<button onClick={generateCommitMessage} disabled={generating} style={{ background: 'var(--accent)', color: 'white', padding: '4px 8px', borderRadius: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
|
| 128 |
-
<FaPen /> {generating ? '...' : 'AI Message'}
|
| 129 |
-
</button>
|
| 130 |
-
<button onClick={commit} style={{ background: 'var(--success)', color: 'white', padding: '4px 8px', borderRadius: 4, fontSize: 12 }}>Commit</button>
|
| 131 |
-
</div>
|
| 132 |
-
</div>
|
| 133 |
-
|
| 134 |
-
<div style={{ marginBottom: 12 }}>
|
| 135 |
-
<button onClick={generatePRSummary} style={{ background: 'var(--accent)', color: 'white', padding: '4px 12px', borderRadius: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 4 }}>
|
| 136 |
-
<FaFileAlt /> Generate PR Summary
|
| 137 |
-
</button>
|
| 138 |
-
{prSummary && (
|
| 139 |
-
<div style={{ marginTop: 8, padding: 8, background: 'var(--bg-secondary)', borderRadius: 4 }}>
|
| 140 |
-
<ReactMarkdown>{prSummary}</ReactMarkdown>
|
| 141 |
-
</div>
|
| 142 |
-
)}
|
| 143 |
-
</div>
|
| 144 |
-
</div>
|
| 145 |
-
);
|
| 146 |
-
};
|
| 147 |
-
|
| 148 |
-
export default GitPanel;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/ModelSelector.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import axios from 'axios';
|
| 3 |
-
import { FaRobot } from 'react-icons/fa';
|
| 4 |
-
|
| 5 |
-
const ModelSelector: React.FC<{ selectedModel: string; onModelChange: (model: string) => void }> = ({ selectedModel, onModelChange }) => {
|
| 6 |
-
const [models, setModels] = useState<any[]>([]);
|
| 7 |
-
|
| 8 |
-
useEffect(() => {
|
| 9 |
-
axios.get('/api/models')
|
| 10 |
-
.then(res => setModels(res.data))
|
| 11 |
-
.catch(() => setModels([]));
|
| 12 |
-
}, []);
|
| 13 |
-
|
| 14 |
-
return (
|
| 15 |
-
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)' }}>
|
| 16 |
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6, color: 'var(--text-secondary)', fontSize: 12 }}>
|
| 17 |
-
<FaRobot /> ACTIVE MODEL
|
| 18 |
-
</div>
|
| 19 |
-
<select
|
| 20 |
-
style={{ width: '100%', background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border)', padding: '6px 8px', borderRadius: 6, outline: 'none', fontSize: 13 }}
|
| 21 |
-
value={selectedModel}
|
| 22 |
-
onChange={e => onModelChange(e.target.value)}
|
| 23 |
-
>
|
| 24 |
-
{models.map((m: any) => (
|
| 25 |
-
<option key={m.id} value={m.id}>{m.name}</option>
|
| 26 |
-
))}
|
| 27 |
-
</select>
|
| 28 |
-
</div>
|
| 29 |
-
);
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
-
export default ModelSelector;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/StatusBar.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { FaMicrochip, FaPlug } from 'react-icons/fa';
|
| 3 |
-
|
| 4 |
-
const StatusBar: React.FC<{ model: string }> = ({ model }) => {
|
| 5 |
-
return (
|
| 6 |
-
<div style={{ height: 24, background: 'var(--bg-tertiary)', borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 12px', fontSize: 12, color: 'var(--text-secondary)' }}>
|
| 7 |
-
<div style={{ display: 'flex', gap: 15 }}>
|
| 8 |
-
<span><FaPlug style={{ color: 'var(--success)' }} /> Connected</span>
|
| 9 |
-
<span><FaMicrochip /> {model}</span>
|
| 10 |
-
</div>
|
| 11 |
-
<span>Ln 1, Col 1</span>
|
| 12 |
-
</div>
|
| 13 |
-
);
|
| 14 |
-
};
|
| 15 |
-
|
| 16 |
-
export default StatusBar;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/Terminal.tsx
DELETED
|
@@ -1,242 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# Fixed Terminal.tsx - proper TypeScript with correct escaping
|
| 3 |
-
terminal_tsx = r'''import React, { useState, useEffect, useRef, useCallback } from 'react';
|
| 4 |
-
import { Terminal } from 'xterm';
|
| 5 |
-
import { FitAddon } from 'xterm-addon-fit';
|
| 6 |
-
import 'xterm/css/xterm.css';
|
| 7 |
-
|
| 8 |
-
interface Tab {
|
| 9 |
-
id: number;
|
| 10 |
-
terminal: Terminal;
|
| 11 |
-
socket: WebSocket;
|
| 12 |
-
fitAddon: FitAddon;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
const TerminalTabs: React.FC = () => {
|
| 16 |
-
const [tabs, setTabs] = useState<Tab[]>([]);
|
| 17 |
-
const [activeTab, setActiveTab] = useState<number>(0);
|
| 18 |
-
const termRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
| 19 |
-
const nextId = useRef(1);
|
| 20 |
-
|
| 21 |
-
const createTerminal = useCallback(() => {
|
| 22 |
-
const id = nextId.current++;
|
| 23 |
-
const term = new Terminal({
|
| 24 |
-
cursorBlink: true,
|
| 25 |
-
theme: {
|
| 26 |
-
background: '#0d1117',
|
| 27 |
-
foreground: '#c9d1d9',
|
| 28 |
-
cursor: '#58a6ff',
|
| 29 |
-
black: '#484f58',
|
| 30 |
-
red: '#f85149',
|
| 31 |
-
green: '#3fb950',
|
| 32 |
-
yellow: '#d29922',
|
| 33 |
-
blue: '#58a6ff',
|
| 34 |
-
magenta: '#bc8cff',
|
| 35 |
-
cyan: '#39c5cf',
|
| 36 |
-
white: '#c9d1d9',
|
| 37 |
-
},
|
| 38 |
-
fontSize: 13,
|
| 39 |
-
fontFamily: 'monospace',
|
| 40 |
-
rows: 24,
|
| 41 |
-
cols: 80,
|
| 42 |
-
});
|
| 43 |
-
|
| 44 |
-
const fitAddon = new FitAddon();
|
| 45 |
-
term.loadAddon(fitAddon);
|
| 46 |
-
|
| 47 |
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 48 |
-
const socket = new WebSocket(`${protocol}//${window.location.host}/ws/terminal`);
|
| 49 |
-
|
| 50 |
-
socket.onopen = () => {
|
| 51 |
-
term.writeln('\r\n\x1b[32mConnected to terminal\x1b[0m\r\n');
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
socket.onmessage = (event) => {
|
| 55 |
-
term.write(event.data);
|
| 56 |
-
};
|
| 57 |
-
|
| 58 |
-
socket.onerror = () => {
|
| 59 |
-
term.writeln('\r\n\x1b[31mConnection error\x1b[0m\r\n');
|
| 60 |
-
};
|
| 61 |
-
|
| 62 |
-
socket.onclose = () => {
|
| 63 |
-
term.writeln('\r\n\x1b[33mDisconnected\x1b[0m\r\n');
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
term.onData((data) => {
|
| 67 |
-
if (socket.readyState === WebSocket.OPEN) {
|
| 68 |
-
socket.send(`exec:${data}`);
|
| 69 |
-
}
|
| 70 |
-
});
|
| 71 |
-
|
| 72 |
-
const newTab: Tab = { id, terminal: term, socket, fitAddon };
|
| 73 |
-
setTabs((prev) => [...prev, newTab]);
|
| 74 |
-
setActiveTab(id);
|
| 75 |
-
return newTab;
|
| 76 |
-
}, []);
|
| 77 |
-
|
| 78 |
-
// Create initial terminal
|
| 79 |
-
useEffect(() => {
|
| 80 |
-
if (tabs.length === 0) {
|
| 81 |
-
createTerminal();
|
| 82 |
-
}
|
| 83 |
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 84 |
-
}, []);
|
| 85 |
-
|
| 86 |
-
// Mount terminal to DOM when active tab changes
|
| 87 |
-
useEffect(() => {
|
| 88 |
-
const tab = tabs.find((t) => t.id === activeTab);
|
| 89 |
-
if (!tab) return;
|
| 90 |
-
|
| 91 |
-
const container = termRefs.current.get(tab.id);
|
| 92 |
-
if (!container) return;
|
| 93 |
-
|
| 94 |
-
// Only open if not already open
|
| 95 |
-
if (!tab.terminal.element) {
|
| 96 |
-
tab.terminal.open(container);
|
| 97 |
-
tab.fitAddon.fit();
|
| 98 |
-
|
| 99 |
-
// Handle resize
|
| 100 |
-
const handleResize = () => {
|
| 101 |
-
tab.fitAddon.fit();
|
| 102 |
-
};
|
| 103 |
-
window.addEventListener('resize', handleResize);
|
| 104 |
-
|
| 105 |
-
return () => {
|
| 106 |
-
window.removeEventListener('resize', handleResize);
|
| 107 |
-
};
|
| 108 |
-
}
|
| 109 |
-
}, [activeTab, tabs]);
|
| 110 |
-
|
| 111 |
-
const removeTab = (id: number) => {
|
| 112 |
-
const tab = tabs.find((t) => t.id === id);
|
| 113 |
-
if (!tab) return;
|
| 114 |
-
|
| 115 |
-
// Clean up
|
| 116 |
-
if (tab.socket.readyState === WebSocket.OPEN || tab.socket.readyState === WebSocket.CONNECTING) {
|
| 117 |
-
tab.socket.close();
|
| 118 |
-
}
|
| 119 |
-
tab.terminal.dispose();
|
| 120 |
-
|
| 121 |
-
setTabs((prev) => prev.filter((t) => t.id !== id));
|
| 122 |
-
|
| 123 |
-
if (activeTab === id) {
|
| 124 |
-
const remaining = tabs.filter((t) => t.id !== id);
|
| 125 |
-
if (remaining.length > 0) {
|
| 126 |
-
setActiveTab(remaining[0].id);
|
| 127 |
-
}
|
| 128 |
-
}
|
| 129 |
-
};
|
| 130 |
-
|
| 131 |
-
const activeTabIndex = tabs.findIndex((t) => t.id === activeTab);
|
| 132 |
-
|
| 133 |
-
return (
|
| 134 |
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
| 135 |
-
{/* Tab Bar */}
|
| 136 |
-
<div
|
| 137 |
-
style={{
|
| 138 |
-
display: 'flex',
|
| 139 |
-
alignItems: 'center',
|
| 140 |
-
background: '#161b22',
|
| 141 |
-
borderBottom: '1px solid #30363d',
|
| 142 |
-
overflowX: 'auto',
|
| 143 |
-
}}
|
| 144 |
-
>
|
| 145 |
-
{tabs.map((tab, idx) => (
|
| 146 |
-
<div
|
| 147 |
-
key={tab.id}
|
| 148 |
-
onClick={() => setActiveTab(tab.id)}
|
| 149 |
-
style={{
|
| 150 |
-
padding: '6px 14px',
|
| 151 |
-
cursor: 'pointer',
|
| 152 |
-
background: activeTab === tab.id ? '#0d1117' : 'transparent',
|
| 153 |
-
borderRight: '1px solid #30363d',
|
| 154 |
-
display: 'flex',
|
| 155 |
-
alignItems: 'center',
|
| 156 |
-
gap: 8,
|
| 157 |
-
fontSize: 12,
|
| 158 |
-
color: activeTab === tab.id ? '#c9d1d9' : '#8b949e',
|
| 159 |
-
whiteSpace: 'nowrap',
|
| 160 |
-
userSelect: 'none',
|
| 161 |
-
}}
|
| 162 |
-
>
|
| 163 |
-
<span>Terminal {idx + 1}</span>
|
| 164 |
-
<span
|
| 165 |
-
onClick={(e) => {
|
| 166 |
-
e.stopPropagation();
|
| 167 |
-
removeTab(tab.id);
|
| 168 |
-
}}
|
| 169 |
-
style={{
|
| 170 |
-
opacity: 0.5,
|
| 171 |
-
cursor: 'pointer',
|
| 172 |
-
fontSize: 14,
|
| 173 |
-
lineHeight: 1,
|
| 174 |
-
}}
|
| 175 |
-
onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
|
| 176 |
-
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.5')}
|
| 177 |
-
>
|
| 178 |
-
×
|
| 179 |
-
</span>
|
| 180 |
-
</div>
|
| 181 |
-
))}
|
| 182 |
-
<button
|
| 183 |
-
onClick={() => createTerminal()}
|
| 184 |
-
style={{
|
| 185 |
-
padding: '6px 12px',
|
| 186 |
-
background: 'transparent',
|
| 187 |
-
border: 'none',
|
| 188 |
-
color: '#8b949e',
|
| 189 |
-
cursor: 'pointer',
|
| 190 |
-
fontSize: 16,
|
| 191 |
-
lineHeight: 1,
|
| 192 |
-
}}
|
| 193 |
-
title="New Terminal"
|
| 194 |
-
>
|
| 195 |
-
+
|
| 196 |
-
</button>
|
| 197 |
-
</div>
|
| 198 |
-
|
| 199 |
-
{/* Terminal Containers */}
|
| 200 |
-
<div style={{ flex: 1, position: 'relative', background: '#0d1117' }}>
|
| 201 |
-
{tabs.map((tab) => (
|
| 202 |
-
<div
|
| 203 |
-
key={tab.id}
|
| 204 |
-
ref={(el) => {
|
| 205 |
-
if (el) termRefs.current.set(tab.id, el);
|
| 206 |
-
}}
|
| 207 |
-
style={{
|
| 208 |
-
position: 'absolute',
|
| 209 |
-
top: 0,
|
| 210 |
-
left: 0,
|
| 211 |
-
right: 0,
|
| 212 |
-
bottom: 0,
|
| 213 |
-
display: activeTab === tab.id ? 'block' : 'none',
|
| 214 |
-
padding: 4,
|
| 215 |
-
}}
|
| 216 |
-
/>
|
| 217 |
-
))}
|
| 218 |
-
{tabs.length === 0 && (
|
| 219 |
-
<div
|
| 220 |
-
style={{
|
| 221 |
-
display: 'flex',
|
| 222 |
-
alignItems: 'center',
|
| 223 |
-
justifyContent: 'center',
|
| 224 |
-
height: '100%',
|
| 225 |
-
color: '#8b949e',
|
| 226 |
-
fontSize: 14,
|
| 227 |
-
}}
|
| 228 |
-
>
|
| 229 |
-
No terminals open. Click + to create one.
|
| 230 |
-
</div>
|
| 231 |
-
)}
|
| 232 |
-
</div>
|
| 233 |
-
</div>
|
| 234 |
-
);
|
| 235 |
-
};
|
| 236 |
-
|
| 237 |
-
export default TerminalTabs;
|
| 238 |
-
'''
|
| 239 |
-
|
| 240 |
-
with open('/mnt/agents/output/Terminal.tsx', 'w') as f:
|
| 241 |
-
f.write(terminal_tsx)
|
| 242 |
-
print("Terminal.tsx fixed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/TerminalTabs.tsx
DELETED
|
@@ -1,171 +0,0 @@
|
|
| 1 |
-
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
-
import { Terminal as XTerm } from 'xterm';
|
| 3 |
-
import { FitAddon } from 'xterm-addon-fit';
|
| 4 |
-
import 'xterm/css/xterm.css';
|
| 5 |
-
import { FaPlus, FaTimes, FaRobot } from 'react-icons/fa';
|
| 6 |
-
import axios from 'axios';
|
| 7 |
-
|
| 8 |
-
interface Tab {
|
| 9 |
-
id: number;
|
| 10 |
-
terminal: XTerm;
|
| 11 |
-
socket: WebSocket;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
const TerminalTabs: React.FC = () => {
|
| 15 |
-
const [tabs, setTabs] = useState<Tab[]>([]);
|
| 16 |
-
const [activeTab, setActiveTab] = useState(0);
|
| 17 |
-
const termRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
| 18 |
-
const nextId = useRef(1);
|
| 19 |
-
const [suggestionInput, setSuggestionInput] = useState('');
|
| 20 |
-
const [suggesting, setSuggesting] = useState(false);
|
| 21 |
-
|
| 22 |
-
const createTerminal = () => {
|
| 23 |
-
const id = nextId.current++;
|
| 24 |
-
const term = new XTerm({
|
| 25 |
-
cursorBlink: true,
|
| 26 |
-
theme: {
|
| 27 |
-
background: '#0d1117',
|
| 28 |
-
foreground: '#c9d1d9',
|
| 29 |
-
cursor: '#58a6ff',
|
| 30 |
-
black: '#484f58',
|
| 31 |
-
red: '#f85149',
|
| 32 |
-
green: '#3fb950',
|
| 33 |
-
yellow: '#d29922',
|
| 34 |
-
blue: '#58a6ff',
|
| 35 |
-
magenta: '#bc8cff',
|
| 36 |
-
cyan: '#39c5cf',
|
| 37 |
-
white: '#c9d1d9',
|
| 38 |
-
},
|
| 39 |
-
fontSize: 13,
|
| 40 |
-
fontFamily: 'monospace',
|
| 41 |
-
});
|
| 42 |
-
const fit = new FitAddon();
|
| 43 |
-
term.loadAddon(fit);
|
| 44 |
-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 45 |
-
const socket = new WebSocket(`${protocol}//${window.location.host}/ws/terminal`);
|
| 46 |
-
term.onData(data => socket.send(`exec:${data}`));
|
| 47 |
-
socket.onmessage = (event) => term.write(event.data);
|
| 48 |
-
const newTab: Tab = { id, terminal: term, socket };
|
| 49 |
-
setTabs(prev => [...prev, newTab]);
|
| 50 |
-
setActiveTab(id);
|
| 51 |
-
return { term, socket, fit };
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
useEffect(() => { createTerminal(); }, []);
|
| 55 |
-
|
| 56 |
-
useEffect(() => {
|
| 57 |
-
const tab = tabs.find(t => t.id === activeTab);
|
| 58 |
-
if (tab) {
|
| 59 |
-
const container = termRefs.current.get(tab.id);
|
| 60 |
-
if (container && !tab.terminal.element) {
|
| 61 |
-
tab.terminal.open(container);
|
| 62 |
-
const fitAddon = new FitAddon();
|
| 63 |
-
tab.terminal.loadAddon(fitAddon);
|
| 64 |
-
fitAddon.fit();
|
| 65 |
-
window.addEventListener('resize', () => fitAddon.fit());
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
}, [activeTab, tabs]);
|
| 69 |
-
|
| 70 |
-
const removeTab = (id: number) => {
|
| 71 |
-
const tab = tabs.find(t => t.id === id);
|
| 72 |
-
if (tab) {
|
| 73 |
-
tab.socket.close();
|
| 74 |
-
tab.terminal.dispose();
|
| 75 |
-
setTabs(prev => prev.filter(t => t.id !== id));
|
| 76 |
-
if (activeTab === id) {
|
| 77 |
-
const remaining = tabs.filter(t => t.id !== id);
|
| 78 |
-
if (remaining.length > 0) setActiveTab(remaining[0].id);
|
| 79 |
-
}
|
| 80 |
-
}
|
| 81 |
-
};
|
| 82 |
-
|
| 83 |
-
const handleSuggestion = async () => {
|
| 84 |
-
if (!suggestionInput.trim()) return;
|
| 85 |
-
setSuggesting(true);
|
| 86 |
-
try {
|
| 87 |
-
const resp = await fetch('/api/chat', {
|
| 88 |
-
method: 'POST',
|
| 89 |
-
headers: { 'Content-Type': 'application/json' },
|
| 90 |
-
body: JSON.stringify({
|
| 91 |
-
model: 'groq-llama-3.1-8b-instant',
|
| 92 |
-
messages: [{ role: 'user', content: `Suggest a terminal command for: ${suggestionInput}. Only respond with the command, nothing else.` }],
|
| 93 |
-
project_path: '/app/sandbox',
|
| 94 |
-
}),
|
| 95 |
-
});
|
| 96 |
-
const reader = resp.body?.getReader();
|
| 97 |
-
let command = '';
|
| 98 |
-
while (reader) {
|
| 99 |
-
const { done, value } = await reader.read();
|
| 100 |
-
if (done) break;
|
| 101 |
-
const text = new TextDecoder().decode(value);
|
| 102 |
-
const lines = text.split('\n').filter(l => l.startsWith('data: ')).map(l => l.slice(6));
|
| 103 |
-
for (const line of lines) {
|
| 104 |
-
if (line === '[DONE]') break;
|
| 105 |
-
try {
|
| 106 |
-
const { token } = JSON.parse(line);
|
| 107 |
-
command += token;
|
| 108 |
-
} catch (e) {}
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
const tab = tabs.find(t => t.id === activeTab);
|
| 112 |
-
if (tab) {
|
| 113 |
-
tab.terminal.write(command + '\r\n');
|
| 114 |
-
}
|
| 115 |
-
} catch (e) {}
|
| 116 |
-
setSuggesting(false);
|
| 117 |
-
setSuggestionInput('');
|
| 118 |
-
};
|
| 119 |
-
|
| 120 |
-
return (
|
| 121 |
-
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0d1117' }}>
|
| 122 |
-
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border)', alignItems: 'center' }}>
|
| 123 |
-
{tabs.map(tab => (
|
| 124 |
-
<div key={tab.id}
|
| 125 |
-
onClick={() => setActiveTab(tab.id)}
|
| 126 |
-
style={{
|
| 127 |
-
padding: '4px 12px',
|
| 128 |
-
cursor: 'pointer',
|
| 129 |
-
background: activeTab === tab.id ? 'var(--bg-primary)' : 'transparent',
|
| 130 |
-
borderRight: '1px solid var(--border)',
|
| 131 |
-
display: 'flex',
|
| 132 |
-
alignItems: 'center',
|
| 133 |
-
gap: 6,
|
| 134 |
-
fontSize: 12,
|
| 135 |
-
color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-secondary)',
|
| 136 |
-
}}>
|
| 137 |
-
<span>Terminal {tab.id}</span>
|
| 138 |
-
<FaTimes size={10} onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }} style={{ opacity: 0.6 }} />
|
| 139 |
-
</div>
|
| 140 |
-
))}
|
| 141 |
-
<button onClick={() => createTerminal()} style={{ padding: '4px 8px', color: 'var(--text-secondary)' }}>
|
| 142 |
-
<FaPlus size={12} />
|
| 143 |
-
</button>
|
| 144 |
-
<div style={{ flex: 1 }} />
|
| 145 |
-
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 8 }}>
|
| 146 |
-
<input
|
| 147 |
-
style={{ width: 150, fontSize: 12, padding: '2px 6px' }}
|
| 148 |
-
placeholder="Describe command..."
|
| 149 |
-
value={suggestionInput}
|
| 150 |
-
onChange={e => setSuggestionInput(e.target.value)}
|
| 151 |
-
onKeyDown={e => e.key === 'Enter' && handleSuggestion()}
|
| 152 |
-
/>
|
| 153 |
-
<button onClick={handleSuggestion} disabled={suggesting} style={{ color: 'var(--accent)', fontSize: 12 }}>
|
| 154 |
-
<FaRobot /> {suggesting ? '...' : 'Suggest'}
|
| 155 |
-
</button>
|
| 156 |
-
</div>
|
| 157 |
-
</div>
|
| 158 |
-
<div style={{ flex: 1, position: 'relative' }}>
|
| 159 |
-
{tabs.map(tab => (
|
| 160 |
-
<div
|
| 161 |
-
key={tab.id}
|
| 162 |
-
ref={el => { if (el) termRefs.current.set(tab.id, el); }}
|
| 163 |
-
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: activeTab === tab.id ? 'block' : 'none' }}
|
| 164 |
-
/>
|
| 165 |
-
))}
|
| 166 |
-
</div>
|
| 167 |
-
</div>
|
| 168 |
-
);
|
| 169 |
-
};
|
| 170 |
-
|
| 171 |
-
export default TerminalTabs;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/TopBar.tsx
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import { FaCode, FaGithub, FaRocket } from 'react-icons/fa';
|
| 3 |
-
|
| 4 |
-
const TopBar: React.FC = () => {
|
| 5 |
-
return (
|
| 6 |
-
<div style={{ height: 36, background: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 12px', fontSize: 13 }}>
|
| 7 |
-
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
| 8 |
-
<FaCode style={{ color: 'var(--accent)' }} />
|
| 9 |
-
<span style={{ fontWeight: 600 }}>Codeki</span>
|
| 10 |
-
<span style={{ color: 'var(--text-secondary)' }}>– AI Coding Assistant</span>
|
| 11 |
-
</div>
|
| 12 |
-
<div style={{ display: 'flex', gap: 15, alignItems: 'center' }}>
|
| 13 |
-
<FaGithub style={{ color: 'var(--text-secondary)', cursor: 'pointer' }} />
|
| 14 |
-
<FaRocket style={{ color: 'var(--accent)', cursor: 'pointer' }} />
|
| 15 |
-
</div>
|
| 16 |
-
</div>
|
| 17 |
-
);
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
export default TopBar;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/index.css
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
:root {
|
| 2 |
-
--bg-primary: #0d1117;
|
| 3 |
-
--bg-secondary: #161b22;
|
| 4 |
-
--bg-tertiary: #21262d;
|
| 5 |
-
--border: #30363d;
|
| 6 |
-
--text-primary: #c9d1d9;
|
| 7 |
-
--text-secondary: #8b949e;
|
| 8 |
-
--accent: #58a6ff;
|
| 9 |
-
--accent-hover: #79c0ff;
|
| 10 |
-
--danger: #f85149;
|
| 11 |
-
--success: #3fb950;
|
| 12 |
-
--warning: #d29922;
|
| 13 |
-
}
|
| 14 |
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 15 |
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif; background: var(--bg-primary); color: var(--text-primary); overflow: hidden; }
|
| 16 |
-
#root { height: 100vh; }
|
| 17 |
-
::-webkit-scrollbar { width: 8px; height: 8px; }
|
| 18 |
-
::-webkit-scrollbar-track { background: var(--bg-secondary); }
|
| 19 |
-
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
| 20 |
-
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
| 21 |
-
button { cursor: pointer; border: none; background: none; color: inherit; font-family: inherit; }
|
| 22 |
-
input, select, textarea { background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-primary); padding: 6px 10px; border-radius: 6px; font-family: inherit; outline: none; transition: border 0.2s; }
|
| 23 |
-
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
|
| 24 |
-
.spin { animation: spin 1s linear infinite; }
|
| 25 |
-
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/index.tsx
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
-
import ReactDOM from 'react-dom/client';
|
| 3 |
-
import './index.css';
|
| 4 |
-
import App from './App';
|
| 5 |
-
|
| 6 |
-
const root = ReactDOM.createRoot(
|
| 7 |
-
document.getElementById('root') as HTMLElement
|
| 8 |
-
);
|
| 9 |
-
root.render(
|
| 10 |
-
<React.StrictMode>
|
| 11 |
-
<App />
|
| 12 |
-
</React.StrictMode>
|
| 13 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/services/api.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Placeholder for API calls
|
| 2 |
-
export const apiBase = '';
|
|
|
|
|
|
|
|
|