Enhance Panel component with additional tabs and keyboard input handling for terminal
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { X } from "lucide-react";
|
import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
const Panel = ({
|
const Panel = ({
|
||||||
height,
|
height,
|
||||||
terminalOutput = [],
|
terminalOutput = [],
|
||||||
isRunning = false,
|
isRunning = false,
|
||||||
@@ -11,125 +11,135 @@ const Panel = ({
|
|||||||
onClose,
|
onClose,
|
||||||
userInput = "",
|
userInput = "",
|
||||||
onUserInputChange,
|
onUserInputChange,
|
||||||
onInputSubmit
|
onInputSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState(initialTab);
|
const [activeTab, setActiveTab] = useState(initialTab);
|
||||||
|
const terminalRef = useRef(null);
|
||||||
|
const [inputBuffer, setInputBuffer] = useState("");
|
||||||
|
|
||||||
// Set active tab when initialTab changes
|
// Update active tab when initialTab changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTab(initialTab);
|
setActiveTab(initialTab);
|
||||||
}, [initialTab]);
|
}, [initialTab]);
|
||||||
|
|
||||||
// Update the renderTerminal function to create an interactive terminal
|
// Auto-scroll terminal to the bottom when content changes
|
||||||
const renderTerminal = () => {
|
useEffect(() => {
|
||||||
const terminalRef = useRef(null);
|
if (terminalRef.current) {
|
||||||
const [inputBuffer, setInputBuffer] = useState("");
|
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
||||||
|
}
|
||||||
// Auto-scroll terminal to bottom when content changes
|
}, [terminalOutput]);
|
||||||
useEffect(() => {
|
|
||||||
if (terminalRef.current) {
|
// Handle keyboard input for the terminal
|
||||||
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
useEffect(() => {
|
||||||
}
|
const handleKeyDown = (e) => {
|
||||||
}, [terminalOutput]);
|
if (!isRunning) return;
|
||||||
|
|
||||||
// Set up keyboard event listeners when terminal is focused
|
if (e.key === "Enter") {
|
||||||
useEffect(() => {
|
if (inputBuffer.trim() && onInputSubmit) {
|
||||||
const handleKeyDown = (e) => {
|
e.preventDefault();
|
||||||
if (!isRunning) return;
|
onUserInputChange(inputBuffer);
|
||||||
|
setTimeout(() => {
|
||||||
if (e.key === 'Enter') {
|
onInputSubmit();
|
||||||
// Send current input buffer through WebSocket
|
setInputBuffer("");
|
||||||
if (inputBuffer.trim() && onInputSubmit) {
|
}, 10);
|
||||||
e.preventDefault(); // Prevent default Enter behavior
|
|
||||||
|
|
||||||
// Important: Set user input and THEN call submit in a sequence
|
|
||||||
onUserInputChange(inputBuffer);
|
|
||||||
|
|
||||||
// Add a small delay before submitting to ensure state update
|
|
||||||
setTimeout(() => {
|
|
||||||
onInputSubmit();
|
|
||||||
// Clear buffer after submission is processed
|
|
||||||
setInputBuffer("");
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Backspace') {
|
|
||||||
// Handle backspace to remove characters
|
|
||||||
setInputBuffer(prev => prev.slice(0, -1));
|
|
||||||
} else if (e.key.length === 1) {
|
|
||||||
// Add regular characters to input buffer
|
|
||||||
setInputBuffer(prev => prev + e.key);
|
|
||||||
}
|
}
|
||||||
};
|
} else if (e.key === "Backspace") {
|
||||||
|
setInputBuffer((prev) => prev.slice(0, -1));
|
||||||
// Add event listener
|
} else if (e.key.length === 1) {
|
||||||
if (terminalRef.current) {
|
setInputBuffer((prev) => prev + e.key);
|
||||||
terminalRef.current.addEventListener('keydown', handleKeyDown);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
// Clean up
|
|
||||||
return () => {
|
const terminalElement = terminalRef.current;
|
||||||
if (terminalRef.current) {
|
terminalElement?.addEventListener("keydown", handleKeyDown);
|
||||||
terminalRef.current.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}
|
return () => {
|
||||||
};
|
terminalElement?.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]);
|
};
|
||||||
|
}, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]);
|
||||||
return (
|
|
||||||
<div
|
// Render the terminal tab
|
||||||
className="panel-terminal"
|
const renderTerminal = () => (
|
||||||
ref={terminalRef}
|
<div
|
||||||
tabIndex={0} // Make div focusable
|
className="panel-terminal"
|
||||||
onClick={() => terminalRef.current?.focus()} // Focus when clicked
|
ref={terminalRef}
|
||||||
>
|
tabIndex={0} // Make div focusable
|
||||||
{terminalOutput.length > 0 ? (
|
onClick={() => terminalRef.current?.focus()} // Focus when clicked
|
||||||
// Render output from EditorArea when available
|
>
|
||||||
<>
|
{terminalOutput.length > 0 ? (
|
||||||
{terminalOutput.map((line, index) => (
|
<>
|
||||||
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 'terminal-output'}`}>
|
{terminalOutput.map((line, index) => {
|
||||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''} {line.content}
|
const typeClass =
|
||||||
|
line.type === "warning"
|
||||||
|
? "terminal-warning"
|
||||||
|
: line.type === "error"
|
||||||
|
? "terminal-error"
|
||||||
|
: "terminal-output";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className={`terminal-line ${typeClass}`}>
|
||||||
|
{line.timestamp && (
|
||||||
|
<span className="terminal-timestamp">{line.timestamp} </span>
|
||||||
|
)}
|
||||||
|
{line.type === "command" && <span className="terminal-prompt">$</span>}
|
||||||
|
{line.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{/* Show current input with blinking cursor only when connection is active */}
|
|
||||||
{isRunning && (
|
{isRunning && (
|
||||||
<div className="terminal-line terminal-input-line">
|
<div className="terminal-line terminal-input-line">
|
||||||
<span className="terminal-prompt">$</span> {inputBuffer}
|
<span className="terminal-prompt">$</span> {inputBuffer}
|
||||||
<span className="terminal-cursor"></span>
|
<span className="terminal-cursor"></span>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Default terminal content
|
|
||||||
<>
|
|
||||||
<div className="terminal-line">
|
|
||||||
<span className="terminal-prompt">$</span>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
</>
|
||||||
</div>
|
) : (
|
||||||
);
|
<div className="terminal-line">
|
||||||
};
|
<span className="terminal-prompt">$</span>
|
||||||
|
<span className="terminal-cursor"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const renderProblems = () => {
|
// Render other tabs
|
||||||
return (
|
const renderProblems = () => (
|
||||||
<div className="panel-problems">
|
<div className="panel-problems">
|
||||||
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const renderOutput = () => {
|
const renderOutput = () => (
|
||||||
return (
|
<div className="panel-output">
|
||||||
<div className="panel-output">
|
<div className="output-line">[Extension Host] Extension host started.</div>
|
||||||
<div className="output-line">[Extension Host] Extension host started.</div>
|
<div className="output-line">[Language Server] Language server started.</div>
|
||||||
<div className="output-line">[Language Server] Language server started.</div>
|
{activeRunningFile && (
|
||||||
{activeRunningFile && (
|
<div className="output-line">[Running] {activeRunningFile}</div>
|
||||||
<div className="output-line">[Running] {activeRunningFile}</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const renderDebugConsole = () => (
|
||||||
|
<div className="panel-debug-console">
|
||||||
|
<div className="debug-line">Debug session not yet started.</div>
|
||||||
|
<div className="debug-line">Press F5 to start debugging.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPorts = () => (
|
||||||
|
<div className="panel-ports">
|
||||||
|
<div className="ports-line">No forwarded ports detected.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderComments = () => (
|
||||||
|
<div className="panel-comments">
|
||||||
|
<div className="comments-line">No comments have been added to this workspace.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get content for the active tab
|
||||||
const getTabContent = () => {
|
const getTabContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case "terminal":
|
case "terminal":
|
||||||
@@ -138,6 +148,12 @@ const Panel = ({
|
|||||||
return renderProblems();
|
return renderProblems();
|
||||||
case "output":
|
case "output":
|
||||||
return renderOutput();
|
return renderOutput();
|
||||||
|
case "debug":
|
||||||
|
return renderDebugConsole();
|
||||||
|
case "ports":
|
||||||
|
return renderPorts();
|
||||||
|
case "comments":
|
||||||
|
return renderComments();
|
||||||
default:
|
default:
|
||||||
return <div>Unknown tab</div>;
|
return <div>Unknown tab</div>;
|
||||||
}
|
}
|
||||||
@@ -146,76 +162,29 @@ const Panel = ({
|
|||||||
return (
|
return (
|
||||||
<div className="panel" style={{ height: `${height}px` }}>
|
<div className="panel" style={{ height: `${height}px` }}>
|
||||||
<div className="panel-tabs">
|
<div className="panel-tabs">
|
||||||
<div
|
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
|
||||||
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
|
<div
|
||||||
onClick={() => setActiveTab("problems")}
|
key={tab}
|
||||||
>
|
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
|
||||||
<span className="tab-icon">
|
onClick={() => setActiveTab(tab)}
|
||||||
<svg
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span className="tab-name">{tab.toUpperCase()}</span>
|
||||||
width="16"
|
</div>
|
||||||
height="16"
|
))}
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="tab-name">Problems</span>
|
|
||||||
</div>
|
|
||||||
<div className={`panel-tab ${activeTab === "output" ? "active" : ""}`} onClick={() => setActiveTab("output")}>
|
|
||||||
<span className="tab-icon">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="tab-name">Output</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`panel-tab ${activeTab === "terminal" ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("terminal")}
|
|
||||||
>
|
|
||||||
<span className="tab-icon">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="4 17 10 11 4 5"></polyline>
|
|
||||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="tab-name">Terminal</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add close button */}
|
|
||||||
<div className="panel-actions">
|
<div className="panel-actions">
|
||||||
|
<button className="panel-action-btn">
|
||||||
|
<span className="current-terminal">node - frontend</span>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
<button className="panel-action-btn">
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
<button className="panel-action-btn">
|
||||||
|
<Maximize2 size={16} />
|
||||||
|
</button>
|
||||||
<button className="panel-close-btn" onClick={onClose}>
|
<button className="panel-close-btn" onClick={onClose}>
|
||||||
<X size={14} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,5 +194,4 @@ const Panel = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Panel;
|
export default Panel;
|
||||||
|
|
||||||
Reference in New Issue
Block a user