feat: Add mobile chat interface and command input to footer component
Some checks failed
GBCI / build (push) Failing after 9m21s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-06-29 20:33:05 -03:00
parent fbe6db37a6
commit 0908822924
4 changed files with 390 additions and 143 deletions

View file

@ -27,7 +27,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
// Simple date formatting functions
const formatDate = (dateString) => {
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
@ -36,7 +36,7 @@ const formatDate = (dateString) => {
});
};
const formatDateTime = (dateString) => {
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
@ -48,7 +48,7 @@ const formatDateTime = (dateString) => {
});
};
const formatDistanceToNow = (date) => {
const formatDistanceToNow = (date: string) => {
const now = new Date();
const diffMs = now.getTime() - new Date(date).getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
@ -167,7 +167,7 @@ const fileSystemData = {
}
};
const getFileIcon = (item) => {
const getFileIcon = (item: any) => {
if (item.is_dir) {
return <Folder className="w-4 h-4 text-yellow-500" />;
}
@ -188,14 +188,14 @@ const getFileIcon = (item) => {
return iconMap[item.type] || <File className="w-4 h-4 text-muted-foreground" />;
};
const formatFileSize = (bytes) => {
const formatFileSize = (bytes: number) => {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
};
const FileContextMenu = ({ file, children, onAction }) => {
const FileContextMenu = ({ file, children, onAction }: any) => {
const contextMenuItems = [
{ icon: Eye, label: "Open", action: "open" },
{ icon: Download, label: "Download", action: "download" },
@ -228,7 +228,7 @@ const FileContextMenu = ({ file, children, onAction }) => {
{ icon: Trash2, label: "Move to trash", action: "trash", destructive: true },
];
const renderContextMenuItem = (item, index) => {
const renderContextMenuItem = (item: any, index: number) => {
if (item.separator) {
return <ContextMenuSeparator key={index} />;
}
@ -241,7 +241,7 @@ const FileContextMenu = ({ file, children, onAction }) => {
{item.label}
</ContextMenuSubTrigger>
<ContextMenuSubContent className="bg-popover">
{item.submenu.map((subItem, subIndex) => (
{item.submenu.map((subItem: any, subIndex: number) => (
<ContextMenuItem
key={subIndex}
onClick={() => onAction(subItem.action, file)}
@ -283,10 +283,20 @@ const FileContextMenu = ({ file, children, onAction }) => {
);
};
const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefined, isMobile = undefined }) => {
const TreeLine = ({ depth }: { depth: number }) => {
return (
<svg width={depth * 16} height="24" viewBox={`0 0 ${depth * 16} 24`} className="inline-block">
{Array.from({ length: depth }).map((_, i) => (
<line key={i} x1={i * 16 + 8} y1="0" x2={i * 16 + 8} y2="24" stroke="#d1d5db" strokeWidth="1" />
))}
</svg>
);
};
const FolderTree = ({ onSelect, selectedPath = "", isCollapsed = false, isMobile = false }: any) => {
const [expanded, setExpanded] = useState({ "": true, "projects": true });
const toggleExpand = (path) => {
const toggleExpand = (path: string) => {
setExpanded(prev => ({ ...prev, [path]: !prev[path] }));
onSelect(path);
};
@ -299,7 +309,7 @@ const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefine
{ title: "Trash", path: "trash", icon: Trash2 },
];
const renderTreeItem = (path, level = 0) => {
const renderTreeItem = (path: string, level = 0) => {
const item = fileSystemData[path];
if (!item || !item.is_dir) return null;
@ -318,6 +328,7 @@ const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefine
>
{!isCollapsed && (
<>
<TreeLine depth={level} />
<ChevronRight className={cn("w-4 h-4 mr-1 transition-transform", isExpanded && "rotate-90")} />
{getFileIcon(item)}
<span className="ml-2 truncate">{item.name}</span>
@ -328,7 +339,7 @@ const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefine
</button>
{!isCollapsed && isExpanded && item.children && (
<div className="ml-4">
{item.children.map(childPath => {
{item.children.map((childPath: string) => {
const fullPath = path ? `${path}/${childPath}` : childPath;
return renderTreeItem(fullPath, level + 1);
})}
@ -342,14 +353,17 @@ const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefine
<div className={cn("flex flex-col h-full bg-background border-r border-border", isMobile ? "w-full" : "")}>
<div className={cn("p-2", isCollapsed ? "px-1" : "")}>
<nav className="space-y-1">
{navLinks.map((link) =>
{navLinks.map((link) => (
isCollapsed ? (
<Tooltip key={link.path} delayDuration={0}>
<TooltipTrigger asChild>
<Button
variant={selectedPath === link.path ? "default" : "ghost"}
size="icon"
className="w-9 h-9 bg-background hover:bg-accent"
className={cn(
"w-9 h-9 bg-background hover:bg-accent",
selectedPath === link.path ? "bg-primary text-primary-foreground hover:bg-primary/90" : "hover:bg-secondary hover:text-secondary-foreground"
)}
onClick={() => onSelect(link.path)}
>
<link.icon className="h-4 w-4" />
@ -361,14 +375,17 @@ const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefine
<Button
key={link.path}
variant={selectedPath === link.path ? "default" : "ghost"}
className="w-full justify-start bg-background hover:bg-accent"
className={cn(
"w-full justify-start bg-background hover:bg-accent",
selectedPath === link.path ? "bg-primary text-primary-foreground hover:bg-primary/90" : "hover:bg-secondary hover:text-secondary-foreground"
)}
onClick={() => onSelect(link.path)}
>
<link.icon className="mr-2 h-4 w-4" />
{link.title}
</Button>
)
)}
))}
</nav>
</div>
{!isCollapsed && (
@ -383,24 +400,24 @@ const FolderTree = ({ onSelect, selectedPath = undefined, isCollapsed = undefine
);
};
const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile, onContextAction, isMobile }) => {
const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile, onContextAction, isMobile }: any) => {
const files = useMemo(() => {
const currentItem = fileSystemData[path];
if (!currentItem || !currentItem.is_dir || !currentItem.children) return [];
let items = currentItem.children.map(childName => {
let items = currentItem.children.map((childName: string) => {
const childPath = path ? `${path}/${childName}` : childName;
return fileSystemData[childPath];
}).filter(Boolean);
if (searchTerm) {
items = items.filter(item =>
items = items.filter((item: any) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
if (filterType && filterType !== 'all') {
items = items.filter(item => {
items = items.filter((item: any) => {
if (filterType === 'folders') return item.is_dir;
if (filterType === 'files') return !item.is_dir;
if (filterType === 'starred') return item.starred;
@ -408,7 +425,7 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
});
}
return items.sort((a, b) => {
return items.sort((a: any, b: any) => {
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
return a.name.localeCompare(b.name);
@ -418,7 +435,7 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
return (
<ScrollArea className={cn("h-full bg-background", isMobile ? "w-full" : "")}>
<div className="flex flex-col">
{files.map((item) => (
{files.map((item: any) => (
<FileContextMenu
key={item.id}
file={item}
@ -445,7 +462,7 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
</div>
</div>
<div className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(item.modified))}
{formatDistanceToNow(item.modified)}
</div>
</button>
</FileContextMenu>
@ -455,7 +472,7 @@ const FileList = ({ path, searchTerm, filterType, selectedFile, setSelectedFile,
);
};
const FileDisplay = ({ file, isMobile }) => {
const FileDisplay = ({ file, isMobile }: any) => {
if (!file) {
return (
<div className={cn("flex flex-col items-center justify-center h-full text-center text-muted-foreground bg-background", isMobile ? "w-full" : "")}>
@ -563,10 +580,10 @@ export default function FileManager() {
const [currentPath, setCurrentPath] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState('all');
const [selectedFile, setSelectedFile] = useState(null);
const [selectedFile, setSelectedFile] = useState<any>(null);
const [isMobile, setIsMobile] = useState(false);
const [showMobileMenu, setShowMobileMenu] = useState(false);
const [activePanel, setActivePanel] = useState('files'); // 'tree', 'files', 'preview'
const [activePanel, setActivePanel] = useState('files');
const currentItem = fileSystemData[currentPath];
@ -597,7 +614,7 @@ export default function FileManager() {
]
];
const handleContextAction = (action, file) => {
const handleContextAction = (action: string, file: any) => {
console.log(`Context action: ${action}`, file);
switch (action) {
case 'star':
@ -645,7 +662,7 @@ export default function FileManager() {
}, []);
useEffect(() => {
const handleKeyDown = (e) => {
const handleKeyDown = (e: KeyboardEvent) => {
const isCtrl = e.ctrlKey || e.metaKey;
const isShift = e.shiftKey;
@ -660,7 +677,7 @@ export default function FileManager() {
console.log('Upload shortcut');
} else if (isCtrl && e.key === 'f') {
e.preventDefault();
document.querySelector('input[placeholder="Search files"]').focus();
document.querySelector('input[placeholder="Search files"]')?.focus();
} else if (e.key === 'Delete' && selectedFile) {
e.preventDefault();
console.log('Delete shortcut');
@ -680,7 +697,7 @@ export default function FileManager() {
<div className="flex flex-col h-screen bg-background">
<div className="flex items-center justify-between p-2 bg-secondary border-b border-border">
<div className="flex items-center gap-2">
<Sheet open={showMobileMenu} onOpenChange={setShowMobileMenu} >
<Sheet open={showMobileMenu} onOpenChange={setShowMobileMenu}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<Menu className="h-5 w-5" />
@ -688,7 +705,7 @@ export default function FileManager() {
</SheetTrigger>
<SheetContent side="left" className="w-[280px] p-0 bg-background">
<FolderTree
onSelect={(path) => {
onSelect={(path: string) => {
setCurrentPath(path);
setShowMobileMenu(false);
setActivePanel('files');
@ -733,7 +750,7 @@ export default function FileManager() {
searchTerm={searchTerm}
filterType={filterType}
selectedFile={selectedFile}
setSelectedFile={(file) => {
setSelectedFile={(file: any) => {
setSelectedFile(file);
setActivePanel('preview');
}}

View file

@ -1,119 +1,353 @@
"use client";
import React, { useState } from 'react';
import {
MessageCircle, Send, X, Minimize2, Maximize2
} from 'lucide-react';
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
const Footer = ({ className=undefined, shortcuts }) => {
const [isChatOpen, setIsChatOpen] = useState(false);
const [isChatMinimized, setIsChatMinimized] = useState(false);
// Expecting shortcuts as an array of two arrays: [ [row1], [row2] ]
const shortcutGroups = shortcuts || [[], []];
import React, { useState, useEffect } from 'react';
import { MessageCircle, Send, X, Minimize2, Maximize2, Monitor, Terminal, Cpu, HardDrive } from 'lucide-react';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
const Footer = ({ className = "", shortcuts }) => {
const [messages, setMessages] = useState([]);
const [inputValue, setInputValue] = useState('');
const [isMobile, setIsMobile] = useState(false);
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => {
window.removeEventListener('resize', checkMobile);
clearInterval(timer);
};
}, []);
const defaultShortcuts = [
[
{ key: "F1", label: "Help", action: () => addSystemMessage("F1: Help system activated") },
{ key: "F2", label: "Save", action: () => addSystemMessage("F2: Save operation") },
{ key: "F3", label: "Search", action: () => addSystemMessage("F3: Search mode") },
{ key: "F4", label: "Edit", action: () => addSystemMessage("F4: Edit mode") },
{ key: "F5", label: "Refresh", action: () => addSystemMessage("F5: System refresh") },
{ key: "F6", label: "Copy", action: () => addSystemMessage("F6: Copy operation") },
{ key: "F7", label: "use client", action: () => addSystemMessage("F7: use client operation") },
{ key: "F8", label: "Delete", action: () => addSystemMessage("F8: Delete operation") },
{ key: "F9", label: "Menu", action: () => addSystemMessage("F9: Menu opened") },
{ key: "F10", label: "Exit", action: () => addSystemMessage("F10: Exit requested") },
],
[
{ key: "Ctrl+N", label: "New", action: () => addSystemMessage("New file created") },
{ key: "Ctrl+O", label: "Open", action: () => addSystemMessage("Open file dialog") },
{ key: "Ctrl+S", label: "Save", action: () => addSystemMessage("File saved") },
{ key: "Ctrl+P", label: "Print", action: () => addSystemMessage("Print dialog") },
{ key: "Ctrl+X", label: "Cut", action: () => addSystemMessage("Cut to clipboard") },
{ key: "Ctrl+C", label: "Copy", action: () => addSystemMessage("Copied to clipboard") },
{ key: "Ctrl+V", label: "Paste", action: () => addSystemMessage("Pasted from clipboard") },
{ key: "Ctrl+Z", label: "Undo", action: () => addSystemMessage("Undo last action") },
{ key: "Ctrl+Y", label: "Redo", action: () => addSystemMessage("Redo last action") },
{ key: "Ctrl+A", label: "Select", action: () => addSystemMessage("Select all") },
]
];
const shortcutGroups = shortcuts || defaultShortcuts;
const addSystemMessage = (text) => {
setMessages(prev => [...prev, { text, sender: 'system', timestamp: new Date() }]);
};
const handleSendMessage = () => {
if (inputValue.trim() !== '') {
const userMessage = { text: inputValue, sender: 'user', timestamp: new Date() };
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setTimeout(() => {
const responses = [
"SYSTEM: Command processed successfully",
"ASSISTANT: Processing your request...",
"BOT: I understand your query. How can I assist further?",
"SYSTEM: Operation completed. Status: OK",
"ASSISTANT: Ready for next command",
];
const randomResponse = responses[Math.floor(Math.random() * responses.length)];
setMessages(prev => [...prev, { text: randomResponse, sender: 'bot', timestamp: new Date() }]);
}, 800);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
};
const formatTime = (date) => {
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
const formatShortcutKey = (key) => {
const parts = key.split(/(\+)/);
return parts.map((part, index) => {
if (part === '+') {
return part;
}
return <span key={index} className="text-primary">{part}</span>;
});
};
if (isMobile) {
return (
<div className={`h-80 bg-background border-t border-border ${className}`}>
<ResizablePanelGroup direction="vertical" className="h-full">
<ResizablePanel defaultSize={40} minSize={30}>
<div className="h-full flex flex-col bg-card border-b border-border">
<div className="flex items-center justify-between p-2 bg-muted border-b border-border">
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-primary" />
<span className="text-sm font-mono text-foreground">MOBILE TERMINAL</span>
</div>
<div className="text-xs text-muted-foreground font-mono">
{formatTime(currentTime)}
</div>
</div>
<div className="flex-1 p-3">
<div className="flex items-center gap-2 bg-background border border-input rounded-md">
<span className="text-primary text-sm font-mono pl-3">></span>
<input
placeholder="Enter command..."
className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground border-0 outline-none py-2 pr-3 text-sm font-mono"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
/>
<button
onClick={handleSendMessage}
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-r-md px-3 py-2 text-sm font-mono transition-colors flex items-center gap-1"
>
<Send className="w-3 h-3" />
</button>
</div>
</div>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={60} minSize={40}>
<div className="h-full flex flex-col bg-card">
<div className="flex items-center justify-between p-2 bg-muted border-b border-border">
<div className="flex items-center gap-2">
<Monitor className="w-4 h-4 text-primary" />
<span className="text-sm font-mono text-foreground">OUTPUT</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<span className="text-xs text-muted-foreground font-mono">READY</span>
</div>
</div>
<div className="flex-1 p-3 overflow-y-auto bg-background font-mono text-sm">
{messages.length === 0 ? (
<div className="text-muted-foreground text-center py-8">
<div className="animate-pulse text-primary"> SYSTEM READY </div>
<div className="mt-2 text-xs">Awaiting input...</div>
</div>
) : (
messages.map((message, index) => (
<div key={index} className="mb-3">
<div className="text-xs text-muted-foreground mb-1">
[{formatTime(message.timestamp)}] {message.sender.toUpperCase()}:
</div>
<div className={`p-2 rounded border ${
message.sender === 'user'
? 'bg-primary/10 border-primary text-foreground'
: message.sender === 'system'
? 'bg-accent/50 border-accent text-accent-foreground'
: 'bg-secondary/50 border-secondary text-secondary-foreground'
}`}>
{message.text}
</div>
</div>
))
)}
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
}
return (
<div className={cn("border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", className)}>
{/* XTreeGold-style two-row footer */}
<div className="flex flex-col">
{/* First row - F1-F10 keys */}
<div className="flex h-8 items-center px-2 border-b">
<div className="flex items-center gap-4 overflow-x-auto">
{shortcutGroups[0].map((shortcut, index) => (
<div
key={index}
className="flex items-center text-xs cursor-pointer hover:bg-accent/50 px-1 py-0.5 rounded transition-colors min-w-fit"
onClick={shortcut.action}
>
<span className="font-bold text-blue-600 mr-1">{shortcut.key}</span>
<span className="text-muted-foreground">{shortcut.label}</span>
</div>
))}
</div>
</div>
{/* Second row - Other keys */}
<div className="flex h-8 items-center px-2">
<div className="flex items-center gap-4 overflow-x-auto">
{shortcutGroups[1].map((shortcut, index) => (
<div
key={index}
className="flex items-center text-xs cursor-pointer hover:bg-accent/50 px-1 py-0.5 rounded transition-colors min-w-fit"
onClick={shortcut.action}
>
<span className="font-bold text-blue-600 mr-1">{shortcut.key}</span>
<span className="text-muted-foreground">{shortcut.label}</span>
</div>
))}
</div>
{/* Right side - Status/AI Assistant */}
<div className="ml-auto flex items-center gap-2">
<Badge variant="outline" className="text-xs h-6">
<span className="text-green-500"></span> Ready
</Badge>
<Button
variant="ghost"
size="sm"
onClick={() => setIsChatOpen(!isChatOpen)}
className="flex items-center gap-2 text-xs h-6"
>
<MessageCircle className="w-3 h-3" />
<span>Assistant</span>
</Button>
</div>
</div>
</div>
{/* Chat Window (optional) */}
{isChatOpen && !isChatMinimized && (
<div className="border-t bg-card">
<div className="h-48 flex flex-col">
<div className="flex items-center justify-between p-2 border-b bg-muted/30">
<div className={`h-48 bg-background border-t border-border ${className}`}>
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel defaultSize={30} minSize={20} maxSize={50}>
<div className="h-full flex flex-col bg-card">
<div className="flex items-center justify-between px-3 py-1 bg-muted border-b border-border">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<span className="text-xs font-medium">General Bots Assistant</span>
<Cpu className="w-3 h-3 text-primary" />
<span className="text-xs font-mono text-foreground">SYSTEM v3.0</span>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => setIsChatMinimized(!isChatMinimized)}
className="h-6 w-6"
>
{isChatMinimized ? <Maximize2 className="w-3 h-3" /> : <Minimize2 className="w-3 h-3" />}
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setIsChatOpen(false)}
className="h-6 w-6"
>
<X className="w-3 h-3" />
</Button>
<div className="flex items-center gap-2">
<HardDrive className="w-3 h-3 text-muted-foreground" />
<span className="text-xs font-mono text-muted-foreground">{formatTime(currentTime)}</span>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
</div>
</div>
<ScrollArea className="flex-1 p-2">
<div className="text-xs text-muted-foreground p-4 text-center">
General Bots Assistant is ready to help with file operations.
<div className="flex-1 flex items-center px-2 border-b border-border bg-secondary/20">
<div className="flex items-center gap-1 overflow-x-auto w-full pb-1">
{shortcutGroups[0].map((shortcut, index) => (
<button
key={index}
className="flex items-center text-xs cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1 rounded transition-colors min-w-fit border border-border hover:border-primary bg-background"
onClick={shortcut.action}
>
<span className="font-bold text-primary mr-1">{formatShortcutKey(shortcut.key)}</span>
<span className="text-muted-foreground">{shortcut.label}</span>
</button>
))}
</div>
</div>
<div className="flex-1 flex items-center px-2 bg-secondary/20">
<div className="flex items-center gap-1 overflow-x-auto w-full pb-1">
{shortcutGroups[1].map((shortcut, index) => (
<button
key={index}
className="flex items-center text-xs cursor-pointer hover:bg-accent hover:text-accent-foreground px-2 py-1 rounded transition-colors min-w-fit border border-border hover:border-primary bg-background"
onClick={shortcut.action}
>
<span className="font-bold text-primary mr-1">{formatShortcutKey(shortcut.key)}</span>
<span className="text-muted-foreground">{shortcut.label}</span>
</button>
))}
</div>
</ScrollArea>
<div className="flex items-center gap-1 p-2 border-t">
<Input
placeholder="Ask about your files..."
className="flex-1 h-7 text-xs"
/>
<Button size="icon" className="h-7 w-7">
<Send className="w-3 h-3" />
</Button>
</div>
</div>
</div>
)}
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={30} minSize={20} maxSize={50}>
<div className="h-full flex flex-col bg-card border-r border-border">
<div className="flex items-center justify-between p-2 bg-muted border-b border-border">
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-primary" />
<span className="text-sm font-mono text-foreground">INPUT</span>
</div>
<div className="text-xs text-muted-foreground font-mono">
CMD
</div>
</div>
<div className="flex-1 p-3 bg-background">
<div className="h-full flex flex-col">
<div className="text-xs text-muted-foreground mb-2 font-mono">
COMMAND LINE INTERFACE
</div>
<div className="flex-1 bg-card border border-input rounded-md p-3 font-mono text-sm">
<div className="flex items-start gap-2">
<span className="text-primary">></span>
<textarea
placeholder="Enter your command or query here..."
className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground border-0 outline-none resize-none min-h-20"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
/>
</div>
</div>
<div className="mt-2 flex justify-between items-center">
<div className="text-xs text-muted-foreground font-mono">
Enter to send Shift+Enter for new line
</div>
<button
onClick={handleSendMessage}
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-3 py-1 text-xs font-mono transition-colors flex items-center gap-1"
>
<Send className="w-3 h-3" />
SEND
</button>
</div>
</div>
</div>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={40} minSize={25}>
<div className="h-full flex flex-col bg-card">
<div className="flex items-center justify-between p-2 bg-muted border-b border-border">
<div className="flex items-center gap-2">
<Monitor className="w-4 h-4 text-primary" />
<span className="text-sm font-mono text-foreground">OUTPUT</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground font-mono">
MSG: {messages.length}
</span>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
</div>
</div>
<div className="flex-1 p-3 overflow-y-auto bg-background font-mono text-sm">
{messages.length === 0 ? (
<div className="text-muted-foreground text-center py-8">
<div className="animate-pulse text-primary text-lg"> SYSTEM READY </div>
<div className="mt-2 text-xs">
<div>Waiting for input...</div>
<div className="mt-1 text-muted-foreground/70">
Use F-keys or Ctrl commands
</div>
</div>
</div>
) : (
<div className="space-y-3">
{messages.map((message, index) => (
<div key={index} className="group">
<div className="text-xs text-muted-foreground mb-1 flex justify-between">
<span>[{formatTime(message.timestamp)}] {message.sender.toUpperCase()}:</span>
<span>#{index + 1}</span>
</div>
<div className={`p-3 rounded border transition-all group-hover:shadow-sm ${
message.sender === 'user'
? 'bg-primary/10 border-primary text-foreground'
: message.sender === 'system'
? 'bg-accent/50 border-accent text-accent-foreground'
: 'bg-secondary/50 border-secondary text-secondary-foreground'
}`}>
<div className="whitespace-pre-wrap break-words">
{message.text}
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="flex items-center justify-between px-3 py-1 bg-muted border-t border-border">
<div className="text-xs text-muted-foreground font-mono">
Ready for input
</div>
<div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground font-mono">
SCROLL: {messages.length > 10 ? 'AUTO' : 'MANUAL'}
</div>
<div className="w-1 h-1 bg-primary rounded-full animate-pulse" />
</div>
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
};

View file

@ -13,17 +13,12 @@ const themes: Theme[] = [
{ name: '3dbevel', label: '3dbevel', cssFile: '/themes/3dbevel.css' },
{ name: 'arcadeflash', label: 'Arcadeflash', cssFile: '/themes/arcadeflash.css' },
{ name: 'cyberpunk', label: 'Cyberpunk', cssFile: '/themes/cyberpunk.css' },
{ name: 'discofever', label: 'Discofever', cssFile: '/themes/discofever.css' },
{ name: 'grungeera', label: 'Grungeera', cssFile: '/themes/grungeera.css' },
{ name: 'jazzage', label: 'Jazzage', cssFile: '/themes/jazzage.css' },
{ name: 'mellowgold', label: 'Mellowgold', cssFile: '/themes/mellowgold.css' },
{ name: 'midcenturymod', label: 'Midcenturymod', cssFile: '/themes/midcenturymod.css' },
{ name: 'orange', label: 'Orange', cssFile: '/themes/orange.css' },
{ name: 'polaroidmemories', label: 'Polaroidmemories', cssFile: '/themes/polaroidmemories.css' },
{ name: 'retrowave', label: 'Retrowave', cssFile: '/themes/retrowave.css' },
{ name: 'saturdaycartoons', label: 'Saturdaycartoons', cssFile: '/themes/saturdaycartoons.css' },
{ name: 'seasidepostcard', label: 'Seasidepostcard', cssFile: '/themes/seasidepostcard.css' },
{ name: 'typewriter', label: 'Typewriter', cssFile: '/themes/typewriter.css' },
{ name: 'vapordream', label: 'Vapordream', cssFile: '/themes/vapordream.css' },
{ name: 'xeroxui', label: 'Xeroxui', cssFile: '/themes/xeroxui.css' },

1
prompt.md Normal file
View file

@ -0,0 +1 @@
- FULL CODE NO EXCUSES EVEN YOU HAVE TO SPLIT THE ANSWER IN SEVERAL PARTS.