Codeki / frontend /src /components /TerminalTabs.tsx
Mayank2027's picture
Create frontend/src/components/TerminalTabs.tsx
1251f6f verified
Raw
History Blame Contribute Delete
6.06 kB
import React, { useState, useRef, useEffect } from 'react';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import { FaPlus, FaTimes, FaRobot } from 'react-icons/fa';
interface Tab {
id: number;
terminal: XTerm;
socket: WebSocket;
}
const TerminalTabs: React.FC = () => {
const [tabs, setTabs] = useState<Tab[]>([]);
const [activeTab, setActiveTab] = useState(0);
const termRefs = useRef<Map<number, HTMLDivElement>>(new Map());
const nextId = useRef(1);
const [suggestionInput, setSuggestionInput] = useState('');
const [suggesting, setSuggesting] = useState(false);
const createTerminal = () => {
const id = nextId.current++;
const term = new XTerm({
cursorBlink: true,
theme: {
background: '#0d1117',
foreground: '#c9d1d9',
cursor: '#58a6ff',
black: '#484f58',
red: '#f85149',
green: '#3fb950',
yellow: '#d29922',
blue: '#58a6ff',
magenta: '#bc8cff',
cyan: '#39c5cf',
white: '#c9d1d9',
},
fontSize: 13,
fontFamily: 'monospace',
});
const fit = new FitAddon();
term.loadAddon(fit);
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(protocol + '//' + window.location.host + '/ws/terminal');
term.onData(data => socket.send('exec:' + data));
socket.onmessage = (event) => term.write(event.data);
const newTab: Tab = { id, terminal: term, socket };
setTabs(prev => [...prev, newTab]);
setActiveTab(id);
return { term, socket, fit };
};
useEffect(() => { createTerminal(); }, []);
useEffect(() => {
const tab = tabs.find(t => t.id === activeTab);
if (tab) {
const container = termRefs.current.get(tab.id);
if (container && !tab.terminal.element) {
tab.terminal.open(container);
const fitAddon = new FitAddon();
tab.terminal.loadAddon(fitAddon);
fitAddon.fit();
window.addEventListener('resize', () => fitAddon.fit());
}
}
}, [activeTab, tabs]);
const removeTab = (id: number) => {
const tab = tabs.find(t => t.id === id);
if (tab) {
tab.socket.close();
tab.terminal.dispose();
setTabs(prev => prev.filter(t => t.id !== id));
if (activeTab === id) {
const remaining = tabs.filter(t => t.id !== id);
if (remaining.length > 0) setActiveTab(remaining[0].id);
}
}
};
const handleSuggestion = async () => {
if (!suggestionInput.trim()) return;
setSuggesting(true);
try {
const resp = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'groq-llama-3.1-8b-instant',
messages: [{ role: 'user', content: 'Suggest a terminal command for: ' + suggestionInput + '. Only respond with the command, nothing else.' }],
project_path: '/app/sandbox',
}),
});
const reader = resp.body?.getReader();
let command = '';
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const text = new TextDecoder().decode(value);
const lines = text.split('\n').filter(l => l.startsWith('data: ')).map(l => l.slice(6));
for (const line of lines) {
if (line === '[DONE]') break;
try {
const { token } = JSON.parse(line);
command += token;
} catch (e) {}
}
}
const tab = tabs.find(t => t.id === activeTab);
if (tab) {
tab.terminal.write(command + '\r\n');
}
} catch (e) {}
setSuggesting(false);
setSuggestionInput('');
};
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0d1117' }}>
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderBottom: '1px solid var(--border)', alignItems: 'center' }}>
{tabs.map(tab => (
<div key={tab.id}
onClick={() => setActiveTab(tab.id)}
style={{
padding: '4px 12px',
cursor: 'pointer',
background: activeTab === tab.id ? 'var(--bg-primary)' : 'transparent',
borderRight: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
gap: 6,
fontSize: 12,
color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-secondary)',
}}>
<span>Terminal {tab.id}</span>
<FaTimes size={10} onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }} style={{ opacity: 0.6 }} />
</div>
))}
<button onClick={() => createTerminal()} style={{ padding: '4px 8px', color: 'var(--text-secondary)' }}>
<FaPlus size={12} />
</button>
<div style={{ flex: 1 }} />
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 8 }}>
<input
style={{ width: 150, fontSize: 12, padding: '2px 6px' }}
placeholder="Describe command..."
value={suggestionInput}
onChange={e => setSuggestionInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSuggestion()}
/>
<button onClick={handleSuggestion} disabled={suggesting} style={{ color: 'var(--accent)', fontSize: 12 }}>
<FaRobot /> {suggesting ? '...' : 'Suggest'}
</button>
</div>
</div>
<div style={{ flex: 1, position: 'relative' }}>
{tabs.map(tab => (
<div
key={tab.id}
ref={el => { if (el) termRefs.current.set(tab.id, el); }}
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: activeTab === tab.id ? 'block' : 'none' }}
/>
))}
</div>
</div>
);
};
export default TerminalTabs;