From ee6b8b6f1f67b0859fcf967762840d8692a3c7fb Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 4 Jun 2025 00:47:13 -0400 Subject: [PATCH 01/28] inspiration for bradie --- app/ide/page.tsx | 223 ++++++ components/dashboard-layout.tsx | 24 +- components/ide/ai-chat.tsx | 298 +++++++ components/ide/file-explorer.tsx | 225 ++++++ components/ide/terminal.tsx | 288 +++++++ components/ui/dropdown-menu.tsx | 200 +++++ package.json | 11 +- yarn.lock | 1250 +++++++++++++++++++++++++++++- 8 files changed, 2508 insertions(+), 11 deletions(-) create mode 100644 app/ide/page.tsx create mode 100644 components/ide/ai-chat.tsx create mode 100644 components/ide/file-explorer.tsx create mode 100644 components/ide/terminal.tsx create mode 100644 components/ui/dropdown-menu.tsx diff --git a/app/ide/page.tsx b/app/ide/page.tsx new file mode 100644 index 0000000..64c1d5e --- /dev/null +++ b/app/ide/page.tsx @@ -0,0 +1,223 @@ +'use client' + +import { useState, useEffect } from 'react' +import dynamic from 'next/dynamic' +import { Button } from '@/components/ui/button' +import { FileExplorer } from '@/components/ide/file-explorer' +import { Terminal } from '@/components/ide/terminal' +import { fs } from '@zenfs/core' +import { + Play, + GitBranch, + GitCommit, + Upload, + Download, + Save, + Terminal as TerminalIcon, + Code +} from 'lucide-react' + +// Dynamic imports to avoid SSR issues +const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { ssr: false }) + +export default function IDEPage() { + const [activeFile, setActiveFile] = useState(null) + const [fileContent, setFileContent] = useState('') + const [terminalVisible, setTerminalVisible] = useState(true) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const [fileLanguage, setFileLanguage] = useState('javascript') + + // Handle file selection from FileExplorer + const handleFileSelect = (path: string, content: string) => { + setActiveFile(path) + setFileContent(content) + setHasUnsavedChanges(false) + + // Detect language from file extension + const ext = path.split('.').pop()?.toLowerCase() + switch (ext) { + case 'js': + case 'jsx': + setFileLanguage('javascript') + break + case 'ts': + case 'tsx': + setFileLanguage('typescript') + break + case 'html': + setFileLanguage('html') + break + case 'css': + setFileLanguage('css') + break + case 'json': + setFileLanguage('json') + break + case 'md': + setFileLanguage('markdown') + break + case 'yaml': + case 'yml': + setFileLanguage('yaml') + break + default: + setFileLanguage('plaintext') + } + } + + // Handle file content changes + const handleContentChange = (value: string | undefined) => { + setFileContent(value || '') + setHasUnsavedChanges(true) + } + + // Save file + const saveFile = async () => { + if (activeFile && hasUnsavedChanges) { + try { + await fs.promises.writeFile(activeFile, fileContent) + setHasUnsavedChanges(false) + } catch (error) { + console.error('Failed to save file:', error) + } + } + } + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 's') { + e.preventDefault() + saveFile() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [activeFile, fileContent]) + + return ( +
+ {/* Top Bar */} +
+
+

Web IDE

+ + {activeFile ? activeFile.split('/').pop() : 'No file selected'} + {hasUnsavedChanges && ' •'} + +
+ +
+ {/* Run Button */} + + + {/* Git Controls */} + + + + + + {/* Save Button */} + +
+
+ + {/* Main Content Area */} +
+ {/* File Explorer (Left Sidebar) */} +
+ +
+ + {/* Editor and Terminal Container */} +
+ {/* Code Editor */} +
+ {activeFile ? ( + + ) : ( +
+
+ +

Select a file to start editing

+
+
+ )} +
+ + {/* Terminal */} + {terminalVisible && ( +
+
+
+ + Terminal +
+ +
+
+ +
+
+ )} +
+
+ + {/* Status Bar */} +
+
+ UTF-8 + {fileLanguage} + Ln 1, Col 1 +
+
+ + {hasUnsavedChanges ? 'Modified' : 'Ready'} +
+
+
+ ) +} \ No newline at end of file diff --git a/components/dashboard-layout.tsx b/components/dashboard-layout.tsx index 033fc28..cc1d37b 100644 --- a/components/dashboard-layout.tsx +++ b/components/dashboard-layout.tsx @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation' import { Button } from '@/components/ui/button' import { NamespaceSwitcher } from '@/components/namespace-switcher' import { ThemeToggle } from '@/components/ui/theme-toggle' +import { AIChat } from '@/components/ide/ai-chat' import { useKubernetes } from '../k8s/context' import { Package, @@ -40,7 +41,9 @@ import { Grid3x3, ChevronDown, ChevronRight, - Heart + Heart, + Code, + MessageSquare } from 'lucide-react' const navigationItems = [ @@ -48,6 +51,7 @@ const navigationItems = [ { id: 'overview', label: 'Overview', icon: Home, href: '/' }, { id: 'all', label: 'All Resources', icon: Grid3x3, href: '/all' }, { id: 'templates', label: 'Templates', icon: FileCode2, href: '/templates' }, + { id: 'ide', label: 'IDE', icon: Code, href: '/ide' }, // Workloads { id: 'workloads-header', label: 'Workloads', isHeader: true, section: 'workloads' }, @@ -99,6 +103,8 @@ interface DashboardLayoutProps { export function DashboardLayout({ children }: DashboardLayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(true) const [expandedSections, setExpandedSections] = useState>(new Set(['workloads', 'config', 'network', 'rbac', 'cluster'])) + const [chatVisible, setChatVisible] = useState(false) + const [chatWidth, setChatWidth] = useState(400) const pathname = usePathname() const { config } = useKubernetes() @@ -227,6 +233,14 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
Cluster: {config.restEndpoint} +
@@ -236,6 +250,14 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { {children} + + {/* Global AI Chat Sidebar */} + setChatVisible(!chatVisible)} + width={chatWidth} + onWidthChange={setChatWidth} + /> ) } \ No newline at end of file diff --git a/components/ide/ai-chat.tsx b/components/ide/ai-chat.tsx new file mode 100644 index 0000000..863e864 --- /dev/null +++ b/components/ide/ai-chat.tsx @@ -0,0 +1,298 @@ +'use client' + +import { useState, useRef, useEffect, KeyboardEvent } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import { + MoreVertical, + Send, + User, + Bot, + Copy, + Check, + ChevronRight, + MessageSquare, + Trash2, + Plus +} from 'lucide-react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' + +interface Message { + id: string + role: 'user' | 'assistant' + content: string + timestamp: Date +} + +interface AIChatProps { + isOpen: boolean + onToggle: () => void + width: number + onWidthChange: (width: number) => void +} + +export function AIChat({ isOpen, onToggle, width, onWidthChange }: AIChatProps) { + const [messages, setMessages] = useState([ + { + id: '1', + role: 'assistant', + content: "Hello! I'm your AI coding assistant. I can help you with:\n\n- Code explanations\n- Debugging assistance\n- Best practices\n- Code generation\n\nFeel free to ask me anything about your code!", + timestamp: new Date() + } + ]) + const [input, setInput] = useState('') + const [showTimestamps, setShowTimestamps] = useState(false) + const [copiedCode, setCopiedCode] = useState(null) + const [isResizing, setIsResizing] = useState(false) + const resizeRef = useRef(null) + const chatEndRef = useRef(null) + + // Auto scroll to bottom on new messages + useEffect(() => { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages]) + + // Handle resize + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing) return + const newWidth = window.innerWidth - e.clientX + if (newWidth >= 300 && newWidth <= 800) { + onWidthChange(newWidth) + } + } + + const handleMouseUp = () => { + setIsResizing(false) + } + + if (isResizing) { + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + }, [isResizing, onWidthChange]) + + const handleSend = () => { + if (!input.trim()) return + + const newMessage: Message = { + id: Date.now().toString(), + role: 'user', + content: input.trim(), + timestamp: new Date() + } + + setMessages([...messages, newMessage]) + setInput('') + + // Simulate AI response + setTimeout(() => { + const aiResponse: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: "I'm a UI mock-up and don't have AI capabilities yet. However, I can render **markdown** content!\n\n```javascript\n// Example code block\nfunction hello() {\n console.log('Hello, World!');\n}\n```\n\nI support:\n- **Bold** and *italic* text\n- Lists\n- Code blocks with syntax highlighting\n- And more!", + timestamp: new Date() + } + setMessages(prev => [...prev, aiResponse]) + }, 1000) + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + setCopiedCode(text) + setTimeout(() => setCopiedCode(null), 2000) + } + + const clearChat = () => { + setMessages([{ + id: '1', + role: 'assistant', + content: "Chat cleared. How can I help you?", + timestamp: new Date() + }]) + } + + const newChat = () => { + clearChat() + } + + return ( +
+ {/* Resize Handle */} +
setIsResizing(true)} + /> + + {/* Toggle Button */} + + + {/* Chat Container */} +
+ {/* Header */} +
+
+ +

AI Assistant

+
+ + + + + + setShowTimestamps(!showTimestamps)}> + {showTimestamps ? 'Hide' : 'Show'} Timestamps + + + + New Chat + + + + Clear Chat + + + +
+ + {/* Messages */} +
+ {messages.map((message) => ( +
+
+ {/* Avatar */} +
+
+ {message.role === 'user' ? : } +
+
+ + {/* Message Content */} +
+
+ {message.role === 'assistant' ? ( + + + {code} + + +
+ ) : ( + + {children} + + ) + }, + p: ({ children }) =>

{children}

, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + }} + > + {message.content} + + ) : ( +

{message.content}

+ )} +
+ {showTimestamps && ( +

+ {message.timestamp.toLocaleTimeString()} +

+ )} +
+
+
+ ))} +
+
+ + {/* Input Area */} +
+
+