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
+49 -16
View File
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; 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'; import { toast } from 'sonner';
export function SeccionSistema() { export function SeccionSistema() {
@@ -12,6 +12,9 @@ export function SeccionSistema() {
const [isExecuting, setIsExecuting] = useState(false); const [isExecuting, setIsExecuting] = useState(false);
const historyCounter = useRef(0); const historyCounter = useRef(0);
const consoleEndRef = useRef<HTMLDivElement>(null); const consoleEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const commandHistoryRef = useRef<string[]>([]);
const historyIndexRef = useRef<number>(-1);
const scrollToBottom = () => { const scrollToBottom = () => {
consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' }); consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -19,7 +22,7 @@ export function SeccionSistema() {
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
}, [consoleHistory]); }, [consoleHistory, mongoshCommand]);
const addHistory = (type: 'input' | 'output' | 'error', content: string) => { const addHistory = (type: 'input' | 'output' | 'error', content: string) => {
setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]); setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]);
@@ -28,8 +31,14 @@ export function SeccionSistema() {
const handleExecuteMongosh = async () => { const handleExecuteMongosh = async () => {
if (!mongoshCommand.trim()) return; if (!mongoshCommand.trim()) return;
addHistory('input', mongoshCommand);
const commandToRun = 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(''); setMongoshCommand('');
setIsExecuting(true); setIsExecuting(true);
@@ -61,6 +70,23 @@ export function SeccionSistema() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handleExecuteMongosh(); 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> </div>
</CardHeader> </CardHeader>
<CardContent> <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 && ( {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) => ( {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 === 'input' && <span className="text-blue-500 mr-2">{'>'}</span>}
{item.type === 'output' && <span className="text-gray-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>} {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>
))} ))}
<div ref={consoleEndRef} />
<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>
<div className="flex gap-2"> <div ref={consoleEndRef} className="h-1" />
<input <input
ref={inputRef}
type="text" 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" className="opacity-0 absolute inset-0 w-full h-full pointer-events-none"
placeholder="Escribe un comando... (ej: db.usuarios.find())"
value={mongoshCommand} value={mongoshCommand}
onChange={(e) => setMongoshCommand(e.target.value)} onChange={(e) => setMongoshCommand(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={isExecuting} disabled={isExecuting}
autoComplete="off"
spellCheck="false"
/> />
<Button onClick={handleExecuteMongosh} disabled={isExecuting}>
<Send className="h-4 w-4 mr-2" />
Ejecutar
</Button>
</div> </div>
<div className="mt-2 text-xs text-gray-500 flex items-center gap-1"> <div className="mt-2 text-xs text-gray-500 flex items-center gap-1">
<AlertCircle className="h-3 w-3" /> <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> </div>
</CardContent> </CardContent>
</Card> </Card>