God Agent v7
feat: God Agent OS v7 — Autonomous Engineering OS (Manus+Genspark+Devin)
5d62489
'use client'
import { useState, useEffect } from 'react'
import { Folder, File, FolderOpen, RefreshCw, Code2, FileText, Image, Package } from 'lucide-react'
interface FileItem {
path: string
size: number
}
interface WorkspaceData {
workspace: string
files: FileItem[]
total: number
}
const getFileIcon = (filename: string) => {
const ext = filename.split('.').pop()?.toLowerCase()
if (['ts', 'tsx', 'js', 'jsx', 'py', 'go', 'rs'].includes(ext || '')) return Code2
if (['json', 'yaml', 'yml', 'toml'].includes(ext || '')) return Package
if (['md', 'txt', 'rst'].includes(ext || '')) return FileText
if (['png', 'jpg', 'svg', 'webp'].includes(ext || '')) return Image
return File
}
const getFileColor = (filename: string) => {
const ext = filename.split('.').pop()?.toLowerCase()
if (['ts', 'tsx'].includes(ext || '')) return '#3b82f6'
if (['py'].includes(ext || '')) return '#f59e0b'
if (['js', 'jsx'].includes(ext || '')) return '#eab308'
if (['go'].includes(ext || '')) return '#06b6d4'
if (['rs'].includes(ext || '')) return '#f97316'
if (['json', 'yaml', 'yml'].includes(ext || '')) return '#a78bfa'
if (['md'].includes(ext || '')) return '#6b7280'
return '#9ca3af'
}
export default function FileExplorer() {
const [workspace, setWorkspace] = useState<WorkspaceData | null>(null)
const [loading, setLoading] = useState(false)
const [selectedFile, setSelectedFile] = useState<string | null>(null)
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set())
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
const fetchWorkspace = async () => {
setLoading(true)
try {
const resp = await fetch(`${apiUrl}/api/v1/files/workspace`)
if (resp.ok) {
const data = await resp.json()
setWorkspace(data)
}
} catch (e) {
console.error('Failed to fetch workspace', e)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchWorkspace()
}, [])
// Build tree structure from flat file list
const buildTree = (files: FileItem[]) => {
const tree: Record<string, any> = {}
files.forEach(({ path, size }) => {
const parts = path.split('/')
let current = tree
parts.forEach((part, i) => {
if (i === parts.length - 1) {
current[part] = { _file: true, path, size }
} else {
if (!current[part]) current[part] = {}
current = current[part]
}
})
})
return tree
}
const toggleDir = (path: string) => {
setExpandedDirs(prev => {
const next = new Set(prev)
if (next.has(path)) next.delete(path)
else next.add(path)
return next
})
}
const renderTree = (node: Record<string, any>, prefix: string = '', depth: number = 0) => {
return Object.entries(node).map(([name, value]) => {
const fullPath = prefix ? `${prefix}/${name}` : name
const isFile = value?._file === true
const Icon = isFile ? getFileIcon(name) : (expandedDirs.has(fullPath) ? FolderOpen : Folder)
const color = isFile ? getFileColor(name) : '#f59e0b'
return (
<div key={fullPath}>
<div
className={`flex items-center gap-1.5 py-0.5 px-2 rounded cursor-pointer transition-all text-xs group`}
style={{
paddingLeft: `${8 + depth * 12}px`,
background: selectedFile === fullPath ? 'rgba(99,102,241,0.15)' : 'transparent',
color: selectedFile === fullPath ? 'var(--text-primary)' : 'var(--text-secondary)',
}}
onClick={() => {
if (isFile) setSelectedFile(fullPath)
else toggleDir(fullPath)
}}
onMouseEnter={e => { if (selectedFile !== fullPath) (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.04)' }}
onMouseLeave={e => { if (selectedFile !== fullPath) (e.currentTarget as HTMLElement).style.background = 'transparent' }}
>
<Icon size={11} style={{ color, flexShrink: 0 }} />
<span className="truncate flex-1">{name}</span>
{isFile && value.size && (
<span className="text-[9px] opacity-40 flex-shrink-0">
{value.size > 1024 ? `${(value.size / 1024).toFixed(1)}k` : `${value.size}b`}
</span>
)}
</div>
{!isFile && expandedDirs.has(fullPath) && renderTree(value, fullPath, depth + 1)}
</div>
)
})
}
const tree = workspace ? buildTree(workspace.files) : {}
return (
<div className="flex flex-col h-full" style={{ background: 'var(--bg-1)' }}>
{/* Header */}
<div className="flex items-center justify-between px-3 py-2.5 border-b shrink-0"
style={{ borderColor: 'var(--border)', background: 'var(--bg-2)' }}>
<div className="flex items-center gap-2">
<Folder size={13} className="text-yellow-400" />
<span className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}>
File Explorer
</span>
{workspace && (
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
style={{ background: 'var(--bg-3)', color: 'var(--text-muted)', border: '1px solid var(--border)' }}>
{workspace.total} files
</span>
)}
</div>
<button
onClick={fetchWorkspace}
disabled={loading}
className="p-1.5 rounded-lg transition-all hover:opacity-80"
style={{ background: 'var(--bg-3)', border: '1px solid var(--border)' }}>
<RefreshCw size={11} className={`${loading ? 'animate-spin' : ''}`} style={{ color: 'var(--text-muted)' }} />
</button>
</div>
{/* File Tree */}
<div className="flex-1 overflow-y-auto py-2">
{loading ? (
<div className="flex items-center justify-center h-20">
<RefreshCw size={14} className="animate-spin text-indigo-400" />
</div>
) : workspace && workspace.files.length > 0 ? (
<div>{renderTree(tree)}</div>
) : (
<div className="flex flex-col items-center justify-center h-20 gap-2">
<Folder size={20} className="opacity-30" style={{ color: 'var(--text-muted)' }} />
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>Workspace empty</p>
<p className="text-[10px] text-center px-4" style={{ color: 'var(--text-muted)' }}>
Ask God Agent to create a project
</p>
</div>
)}
</div>
{/* Selected file info */}
{selectedFile && (
<div className="px-3 py-2 border-t shrink-0" style={{ borderColor: 'var(--border)', background: 'var(--bg-2)' }}>
<p className="text-[10px] truncate font-mono" style={{ color: 'var(--text-muted)' }}>
📄 {selectedFile}
</p>
</div>
)}
</div>
)
}