feat: Add mobile chat interface and command input to footer component
Some checks failed
GBCI / build (push) Failing after 9m21s
Some checks failed
GBCI / build (push) Failing after 9m21s
This commit is contained in:
parent
fbe6db37a6
commit
0908822924
4 changed files with 390 additions and 143 deletions
|
@ -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');
|
||||
|
@ -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');
|
||||
}}
|
||||
|
|
430
app/footer.tsx
430
app/footer.tsx
|
@ -1,120 +1,354 @@
|
|||
"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">
|
||||
<Cpu className="w-3 h-3 text-primary" />
|
||||
<span className="text-xs font-mono text-foreground">SYSTEM v3.0</span>
|
||||
</div>
|
||||
<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" />
|
||||
<span className="text-xs font-medium">General Bots Assistant</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsChatMinimized(!isChatMinimized)}
|
||||
className="h-6 w-6"
|
||||
</div>
|
||||
<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}
|
||||
>
|
||||
{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"
|
||||
<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}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
<span className="font-bold text-primary mr-1">{formatShortcutKey(shortcut.key)}</span>
|
||||
<span className="text-muted-foreground">{shortcut.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</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>
|
||||
</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"
|
||||
</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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button size="icon" className="h-7 w-7">
|
||||
</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" />
|
||||
</Button>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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
1
prompt.md
Normal file
|
@ -0,0 +1 @@
|
|||
- FULL CODE NO EXCUSES EVEN YOU HAVE TO SPLIT THE ANSWER IN SEVERAL PARTS.
|
Loading…
Add table
Reference in a new issue