File size: 6,991 Bytes
5d62489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
'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>
  )
}