Mayank2027 commited on
Commit
2e6cd8d
·
verified ·
1 Parent(s): 498ec89

Create frontend/src/components/TerminalTabs.tsx

Browse files
frontend/src/components/TerminalTabs.tsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;