feat: Emulador de terminal con cursor para mongosh

This commit is contained in:
2026-08-12 03:22:53 +00:00
parent 205c8f1c7c
commit 827db0a356
+50 -17
View File
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Terminal, Download, Upload, FileText, Send, AlertCircle, RefreshCw } from 'lucide-react';
import { Terminal, Download, Upload, FileText, AlertCircle, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
export function SeccionSistema() {
@@ -12,6 +12,9 @@ export function SeccionSistema() {
const [isExecuting, setIsExecuting] = useState(false);
const historyCounter = useRef(0);
const consoleEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const commandHistoryRef = useRef<string[]>([]);
const historyIndexRef = useRef<number>(-1);
const scrollToBottom = () => {
consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -19,7 +22,7 @@ export function SeccionSistema() {
useEffect(() => {
scrollToBottom();
}, [consoleHistory]);
}, [consoleHistory, mongoshCommand]);
const addHistory = (type: 'input' | 'output' | 'error', content: string) => {
setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]);
@@ -28,8 +31,14 @@ export function SeccionSistema() {
const handleExecuteMongosh = async () => {
if (!mongoshCommand.trim()) return;
addHistory('input', mongoshCommand);
const commandToRun = mongoshCommand;
addHistory('input', commandToRun);
if (commandHistoryRef.current[commandHistoryRef.current.length - 1] !== commandToRun) {
commandHistoryRef.current.push(commandToRun);
}
historyIndexRef.current = commandHistoryRef.current.length;
setMongoshCommand('');
setIsExecuting(true);
@@ -61,6 +70,23 @@ export function SeccionSistema() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleExecuteMongosh();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndexRef.current > 0) {
const newIndex = historyIndexRef.current - 1;
historyIndexRef.current = newIndex;
setMongoshCommand(commandHistoryRef.current[newIndex]);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndexRef.current < commandHistoryRef.current.length - 1) {
const newIndex = historyIndexRef.current + 1;
historyIndexRef.current = newIndex;
setMongoshCommand(commandHistoryRef.current[newIndex]);
} else if (historyIndexRef.current === commandHistoryRef.current.length - 1) {
historyIndexRef.current = commandHistoryRef.current.length;
setMongoshCommand('');
}
}
};
@@ -141,39 +167,46 @@ export function SeccionSistema() {
</div>
</CardHeader>
<CardContent>
<div className="bg-gray-900 text-green-400 font-mono text-sm p-4 rounded-md h-[400px] overflow-y-auto mb-4 flex flex-col gap-2 relative">
<div
className="bg-gray-900 text-green-400 font-mono text-sm p-4 rounded-md h-[400px] overflow-y-auto flex flex-col gap-1 relative cursor-text group"
onClick={() => inputRef.current?.focus()}
>
{consoleHistory.length === 0 && (
<div className="text-gray-500 italic">Conectado. Esperando comandos... Ej: db.pacientes.find()</div>
<div className="text-gray-500 italic mb-2">Conectado. Esperando comandos... Ej: db.pacientes.find()</div>
)}
{consoleHistory.map((item) => (
<div key={item.id} className={`${item.type === 'error' ? 'text-red-400' : item.type === 'input' ? 'text-blue-300' : 'text-gray-300'}`}>
<div key={item.id} className={`break-words ${item.type === 'error' ? 'text-red-400' : item.type === 'input' ? 'text-blue-300' : 'text-gray-300'}`}>
{item.type === 'input' && <span className="text-blue-500 mr-2">{'>'}</span>}
{item.type === 'output' && <span className="text-gray-500 mr-2">{'<-'}</span>}
{item.type === 'error' && <span className="text-red-500 mr-2">{'!'}</span>}
<pre className="inline-block whitespace-pre-wrap font-inherit m-0">{item.content}</pre>
<pre className="inline whitespace-pre-wrap font-inherit m-0">{item.content}</pre>
</div>
))}
<div ref={consoleEndRef} />
</div>
<div className="flex gap-2">
<div className="flex items-center text-blue-300 mt-1">
<span className="text-blue-500 mr-2">{'>'}</span>
<span className="whitespace-pre-wrap break-all">{mongoshCommand}</span>
<span className="w-2 bg-green-400 animate-pulse ml-[1px] inline-block h-4 opacity-0 group-focus-within:opacity-100 transition-opacity"></span>
</div>
<div ref={consoleEndRef} className="h-1" />
<input
ref={inputRef}
type="text"
className="flex-1 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
placeholder="Escribe un comando... (ej: db.usuarios.find())"
className="opacity-0 absolute inset-0 w-full h-full pointer-events-none"
value={mongoshCommand}
onChange={(e) => setMongoshCommand(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isExecuting}
autoComplete="off"
spellCheck="false"
/>
<Button onClick={handleExecuteMongosh} disabled={isExecuting}>
<Send className="h-4 w-4 mr-2" />
Ejecutar
</Button>
</div>
<div className="mt-2 text-xs text-gray-500 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
Soporta asincronía. Variables globales: `db`, `ObjectId`. Las promesas se resuelven automáticamente.
Soporta asincronía. Variables globales: `db`, `ObjectId`. Flechas arriba/abajo para navegar el historial.
</div>
</CardContent>
</Card>