Delete fronend
Browse files- fronend/index.html +0 -14
- fronend/package.json +0 -25
- fronend/postcss.config.js +0 -6
- fronend/src/App.jsx +0 -221
- fronend/src/components/AgentFeed.jsx +0 -92
- fronend/src/components/ChatInterface.jsx +0 -195
- fronend/src/components/CodeView.jsx +0 -128
- fronend/src/components/ControlPanel.jsx +0 -84
- fronend/src/components/FileTree.jsx +0 -141
- fronend/src/components/Header.jsx +0 -80
- fronend/src/components/LivePreview.jsx +0 -159
- fronend/src/components/StatusBar.jsx +0 -104
- fronend/src/components/SystemTabs.jsx +0 -37
- fronend/src/components/ThemeProvider.jsx +0 -31
- fronend/src/hooks/useSSE.js +0 -61
- fronend/src/hooks/useTheme.js +0 -2
- fronend/src/index.css +0 -126
- fronend/src/main.jsx +0 -13
- fronend/src/utils/api.js +0 -80
- fronend/tailwind.config.js +0 -48
- fronend/vite.config.js +0 -15
fronend/index.html
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>NEXUS Builder — AI Full-Stack App Maker</title>
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 9 |
-
</head>
|
| 10 |
-
<body>
|
| 11 |
-
<div id="root"></div>
|
| 12 |
-
<script type="module" src="/src/main.jsx"></script>
|
| 13 |
-
</body>
|
| 14 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/package.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "nexus-builder-ui",
|
| 3 |
-
"private": true,
|
| 4 |
-
"version": "1.0.0",
|
| 5 |
-
"type": "module",
|
| 6 |
-
"scripts": {
|
| 7 |
-
"dev": "vite",
|
| 8 |
-
"build": "vite build",
|
| 9 |
-
"preview": "vite preview"
|
| 10 |
-
},
|
| 11 |
-
"dependencies": {
|
| 12 |
-
"react": "^18.3.1",
|
| 13 |
-
"react-dom": "^18.3.1",
|
| 14 |
-
"lucide-react": "^0.469.0"
|
| 15 |
-
},
|
| 16 |
-
"devDependencies": {
|
| 17 |
-
"@types/react": "^18.3.18",
|
| 18 |
-
"@types/react-dom": "^18.3.5",
|
| 19 |
-
"@vitejs/plugin-react": "^4.3.4",
|
| 20 |
-
"autoprefixer": "^10.4.20",
|
| 21 |
-
"postcss": "^8.4.49",
|
| 22 |
-
"tailwindcss": "^3.4.17",
|
| 23 |
-
"vite": "^6.0.5"
|
| 24 |
-
}
|
| 25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/postcss.config.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
export default {
|
| 2 |
-
plugins: {
|
| 3 |
-
tailwindcss: {},
|
| 4 |
-
autoprefixer: {},
|
| 5 |
-
},
|
| 6 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/App.jsx
DELETED
|
@@ -1,221 +0,0 @@
|
|
| 1 |
-
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
| 2 |
-
import Header from './components/Header'
|
| 3 |
-
import ControlPanel from './components/ControlPanel'
|
| 4 |
-
import LivePreview from './components/LivePreview'
|
| 5 |
-
import StatusBar from './components/StatusBar'
|
| 6 |
-
import { useSSE } from './hooks/useSSE'
|
| 7 |
-
import { generateProject, getProjectStatus, getProjectFiles, exportProject, fixBug } from './utils/api'
|
| 8 |
-
|
| 9 |
-
const INITIAL_AGENTS = {
|
| 10 |
-
research: { name: 'GLM 4.5 Air', status: 'idle', icon: '🌐' },
|
| 11 |
-
orchestrator: { name: 'Trinity Large', status: 'idle', icon: '🧠' },
|
| 12 |
-
frontend: { name: 'Qwen3 Coder', status: 'idle', icon: '🎨' },
|
| 13 |
-
backend: { name: 'MiniMax M2.5', status: 'idle', icon: '🔐' },
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export default function App() {
|
| 17 |
-
const [sessionId, setSessionId] = useState(null)
|
| 18 |
-
const [status, setStatus] = useState('idle')
|
| 19 |
-
const [agents, setAgents] = useState(INITIAL_AGENTS)
|
| 20 |
-
const [messages, setMessages] = useState([])
|
| 21 |
-
const [agentFeed, setAgentFeed] = useState([])
|
| 22 |
-
const [files, setFiles] = useState({})
|
| 23 |
-
const [fileTree, setFileTree] = useState([])
|
| 24 |
-
const [selectedFile, setSelectedFile] = useState(null)
|
| 25 |
-
const [previewSystem, setPreviewSystem] = useState('preview')
|
| 26 |
-
const [viewMode, setViewMode] = useState('preview') // preview | code
|
| 27 |
-
const [errors, setErrors] = useState([])
|
| 28 |
-
|
| 29 |
-
// Handle SSE events from the pipeline
|
| 30 |
-
const handleSSEEvent = useCallback((event) => {
|
| 31 |
-
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
|
| 32 |
-
|
| 33 |
-
switch (event.type || data.event_type) {
|
| 34 |
-
case 'agent_start':
|
| 35 |
-
setAgents(prev => ({
|
| 36 |
-
...prev,
|
| 37 |
-
[data.agent]: { ...prev[data.agent], status: 'active' }
|
| 38 |
-
}))
|
| 39 |
-
setAgentFeed(prev => [...prev, {
|
| 40 |
-
agent: data.agent,
|
| 41 |
-
content: data.content,
|
| 42 |
-
time: new Date().toLocaleTimeString(),
|
| 43 |
-
type: 'start'
|
| 44 |
-
}])
|
| 45 |
-
setStatus(data.content || 'Working...')
|
| 46 |
-
break
|
| 47 |
-
|
| 48 |
-
case 'token':
|
| 49 |
-
setAgentFeed(prev => {
|
| 50 |
-
const last = prev[prev.length - 1]
|
| 51 |
-
if (last && last.agent === data.agent && last.type === 'stream') {
|
| 52 |
-
return [...prev.slice(0, -1), { ...last, content: last.content + data.content }]
|
| 53 |
-
}
|
| 54 |
-
return [...prev, {
|
| 55 |
-
agent: data.agent,
|
| 56 |
-
content: data.content,
|
| 57 |
-
time: new Date().toLocaleTimeString(),
|
| 58 |
-
type: 'stream'
|
| 59 |
-
}]
|
| 60 |
-
})
|
| 61 |
-
break
|
| 62 |
-
|
| 63 |
-
case 'file_created':
|
| 64 |
-
setAgentFeed(prev => [...prev, {
|
| 65 |
-
agent: data.agent,
|
| 66 |
-
content: data.content,
|
| 67 |
-
time: new Date().toLocaleTimeString(),
|
| 68 |
-
type: 'file',
|
| 69 |
-
filePath: data.file_path
|
| 70 |
-
}])
|
| 71 |
-
if (data.file_path) {
|
| 72 |
-
setFileTree(prev => {
|
| 73 |
-
const newTree = [...new Set([...prev, data.file_path])]
|
| 74 |
-
return newTree.sort()
|
| 75 |
-
})
|
| 76 |
-
}
|
| 77 |
-
break
|
| 78 |
-
|
| 79 |
-
case 'agent_done':
|
| 80 |
-
setAgents(prev => ({
|
| 81 |
-
...prev,
|
| 82 |
-
[data.agent]: { ...prev[data.agent], status: 'done' }
|
| 83 |
-
}))
|
| 84 |
-
setAgentFeed(prev => [...prev, {
|
| 85 |
-
agent: data.agent,
|
| 86 |
-
content: data.content,
|
| 87 |
-
time: new Date().toLocaleTimeString(),
|
| 88 |
-
type: 'done'
|
| 89 |
-
}])
|
| 90 |
-
break
|
| 91 |
-
|
| 92 |
-
case 'error':
|
| 93 |
-
setErrors(prev => [...prev, data.content])
|
| 94 |
-
setAgents(prev => ({
|
| 95 |
-
...prev,
|
| 96 |
-
[data.agent]: { ...prev[data.agent], status: 'error' }
|
| 97 |
-
}))
|
| 98 |
-
break
|
| 99 |
-
|
| 100 |
-
case 'done':
|
| 101 |
-
setStatus(data.status === 'completed' ? 'completed' : 'error')
|
| 102 |
-
// Fetch final files
|
| 103 |
-
if (data.session_id) {
|
| 104 |
-
getProjectFiles(data.session_id).then(res => {
|
| 105 |
-
if (res.files) {
|
| 106 |
-
setFiles(res.files)
|
| 107 |
-
setFileTree(Object.keys(res.files).sort())
|
| 108 |
-
}
|
| 109 |
-
})
|
| 110 |
-
}
|
| 111 |
-
break
|
| 112 |
-
|
| 113 |
-
default:
|
| 114 |
-
break
|
| 115 |
-
}
|
| 116 |
-
}, [])
|
| 117 |
-
|
| 118 |
-
const { connect, disconnect } = useSSE(handleSSEEvent)
|
| 119 |
-
|
| 120 |
-
// Start generation
|
| 121 |
-
const handleGenerate = useCallback(async (prompt, appType) => {
|
| 122 |
-
// Reset state
|
| 123 |
-
setAgents(INITIAL_AGENTS)
|
| 124 |
-
setAgentFeed([])
|
| 125 |
-
setFiles({})
|
| 126 |
-
setFileTree([])
|
| 127 |
-
setErrors([])
|
| 128 |
-
setSelectedFile(null)
|
| 129 |
-
setStatus('starting')
|
| 130 |
-
|
| 131 |
-
setMessages(prev => [...prev, { role: 'user', content: prompt }])
|
| 132 |
-
|
| 133 |
-
try {
|
| 134 |
-
const res = await generateProject(prompt, appType)
|
| 135 |
-
setSessionId(res.session_id)
|
| 136 |
-
setStatus('connected')
|
| 137 |
-
|
| 138 |
-
// Connect to SSE stream
|
| 139 |
-
connect(res.session_id)
|
| 140 |
-
|
| 141 |
-
setMessages(prev => [...prev, {
|
| 142 |
-
role: 'assistant',
|
| 143 |
-
content: `🚀 Project generation started! Session: ${res.session_id}\n\nI'm coordinating 4 AI agents to build your application...`
|
| 144 |
-
}])
|
| 145 |
-
} catch (err) {
|
| 146 |
-
setStatus('error')
|
| 147 |
-
setErrors(prev => [...prev, err.message])
|
| 148 |
-
setMessages(prev => [...prev, {
|
| 149 |
-
role: 'assistant',
|
| 150 |
-
content: `❌ Error starting generation: ${err.message}`
|
| 151 |
-
}])
|
| 152 |
-
}
|
| 153 |
-
}, [connect])
|
| 154 |
-
|
| 155 |
-
// Export
|
| 156 |
-
const handleExport = useCallback(async () => {
|
| 157 |
-
if (!sessionId) return
|
| 158 |
-
try {
|
| 159 |
-
await exportProject(sessionId)
|
| 160 |
-
} catch (err) {
|
| 161 |
-
setErrors(prev => [...prev, `Export failed: ${err.message}`])
|
| 162 |
-
}
|
| 163 |
-
}, [sessionId])
|
| 164 |
-
|
| 165 |
-
// Fix bug
|
| 166 |
-
const handleFix = useCallback(async (errorMessage, filePath) => {
|
| 167 |
-
if (!sessionId) return
|
| 168 |
-
try {
|
| 169 |
-
await fixBug(sessionId, errorMessage, filePath)
|
| 170 |
-
connect(sessionId) // Reconnect SSE for fix updates
|
| 171 |
-
} catch (err) {
|
| 172 |
-
setErrors(prev => [...prev, `Fix failed: ${err.message}`])
|
| 173 |
-
}
|
| 174 |
-
}, [sessionId, connect])
|
| 175 |
-
|
| 176 |
-
return (
|
| 177 |
-
<div className="h-screen w-screen flex flex-col overflow-hidden" style={{ background: 'var(--bg)' }}>
|
| 178 |
-
<Header
|
| 179 |
-
agents={agents}
|
| 180 |
-
sessionId={sessionId}
|
| 181 |
-
status={status}
|
| 182 |
-
/>
|
| 183 |
-
|
| 184 |
-
<div className="flex flex-1 overflow-hidden">
|
| 185 |
-
{/* Left Panel — Control Center */}
|
| 186 |
-
<ControlPanel
|
| 187 |
-
messages={messages}
|
| 188 |
-
agentFeed={agentFeed}
|
| 189 |
-
fileTree={fileTree}
|
| 190 |
-
files={files}
|
| 191 |
-
selectedFile={selectedFile}
|
| 192 |
-
onSelectFile={setSelectedFile}
|
| 193 |
-
onGenerate={handleGenerate}
|
| 194 |
-
onFix={handleFix}
|
| 195 |
-
status={status}
|
| 196 |
-
/>
|
| 197 |
-
|
| 198 |
-
{/* Right Panel — Preview */}
|
| 199 |
-
<LivePreview
|
| 200 |
-
sessionId={sessionId}
|
| 201 |
-
files={files}
|
| 202 |
-
selectedFile={selectedFile}
|
| 203 |
-
previewSystem={previewSystem}
|
| 204 |
-
onChangeSystem={setPreviewSystem}
|
| 205 |
-
viewMode={viewMode}
|
| 206 |
-
onChangeViewMode={setViewMode}
|
| 207 |
-
onExport={handleExport}
|
| 208 |
-
status={status}
|
| 209 |
-
/>
|
| 210 |
-
</div>
|
| 211 |
-
|
| 212 |
-
<StatusBar
|
| 213 |
-
status={status}
|
| 214 |
-
sessionId={sessionId}
|
| 215 |
-
agents={agents}
|
| 216 |
-
errorCount={errors.length}
|
| 217 |
-
fileCount={fileTree.length}
|
| 218 |
-
/>
|
| 219 |
-
</div>
|
| 220 |
-
)
|
| 221 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/AgentFeed.jsx
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
import React, { useRef, useEffect } from 'react'
|
| 2 |
-
import { Bot, FileCode2, CheckCircle2, AlertCircle, Zap } from 'lucide-react'
|
| 3 |
-
|
| 4 |
-
const AGENT_COLORS = {
|
| 5 |
-
research: '#00D9FF',
|
| 6 |
-
orchestrator: '#6C63FF',
|
| 7 |
-
frontend: '#22D3A8',
|
| 8 |
-
backend: '#FFB547',
|
| 9 |
-
system: '#8888AA',
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const AGENT_ICONS = {
|
| 13 |
-
research: '🌐',
|
| 14 |
-
orchestrator: '🧠',
|
| 15 |
-
frontend: '🎨',
|
| 16 |
-
backend: '🔐',
|
| 17 |
-
system: '⚙️',
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export default function AgentFeed({ feed }) {
|
| 21 |
-
const feedEndRef = useRef(null)
|
| 22 |
-
|
| 23 |
-
useEffect(() => {
|
| 24 |
-
feedEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 25 |
-
}, [feed])
|
| 26 |
-
|
| 27 |
-
if (feed.length === 0) {
|
| 28 |
-
return (
|
| 29 |
-
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
| 30 |
-
<Bot className="w-12 h-12 mb-3" style={{ color: 'var(--text-secondary)', opacity: 0.4 }} />
|
| 31 |
-
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 32 |
-
Agent activity will appear here once you start generating.
|
| 33 |
-
</p>
|
| 34 |
-
</div>
|
| 35 |
-
)
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
return (
|
| 39 |
-
<div className="h-full overflow-y-auto p-3 space-y-2">
|
| 40 |
-
{feed.map((item, i) => (
|
| 41 |
-
<div
|
| 42 |
-
key={i}
|
| 43 |
-
className="flex items-start gap-2.5 p-2.5 rounded-lg animate-fade-in text-xs"
|
| 44 |
-
style={{
|
| 45 |
-
background: item.type === 'done' ? `${AGENT_COLORS[item.agent]}11` : 'var(--surface)',
|
| 46 |
-
border: `1px solid ${item.type === 'done' ? AGENT_COLORS[item.agent] + '33' : 'var(--border)'}`,
|
| 47 |
-
}}
|
| 48 |
-
>
|
| 49 |
-
{/* Agent icon */}
|
| 50 |
-
<span className="text-base flex-shrink-0 mt-0.5">
|
| 51 |
-
{AGENT_ICONS[item.agent] || '⚙️'}
|
| 52 |
-
</span>
|
| 53 |
-
|
| 54 |
-
<div className="flex-1 min-w-0">
|
| 55 |
-
{/* Agent name + time */}
|
| 56 |
-
<div className="flex items-center justify-between mb-1">
|
| 57 |
-
<span className="font-semibold capitalize" style={{ color: AGENT_COLORS[item.agent] }}>
|
| 58 |
-
{item.agent}
|
| 59 |
-
</span>
|
| 60 |
-
<span className="text-[10px]" style={{ color: 'var(--text-secondary)' }}>
|
| 61 |
-
{item.time}
|
| 62 |
-
</span>
|
| 63 |
-
</div>
|
| 64 |
-
|
| 65 |
-
{/* Content */}
|
| 66 |
-
<div style={{ color: 'var(--text-secondary)' }}>
|
| 67 |
-
{item.type === 'file' ? (
|
| 68 |
-
<div className="flex items-center gap-1.5">
|
| 69 |
-
<FileCode2 className="w-3 h-3" style={{ color: 'var(--success)' }} />
|
| 70 |
-
<span className="font-mono text-[11px]">{item.content}</span>
|
| 71 |
-
</div>
|
| 72 |
-
) : item.type === 'done' ? (
|
| 73 |
-
<div className="flex items-center gap-1.5">
|
| 74 |
-
<CheckCircle2 className="w-3 h-3" style={{ color: AGENT_COLORS[item.agent] }} />
|
| 75 |
-
<span>{item.content}</span>
|
| 76 |
-
</div>
|
| 77 |
-
) : item.type === 'stream' ? (
|
| 78 |
-
<pre className="whitespace-pre-wrap font-mono text-[11px] max-h-24 overflow-y-auto leading-relaxed">
|
| 79 |
-
{item.content.slice(-300)}
|
| 80 |
-
<span className="typing-cursor" />
|
| 81 |
-
</pre>
|
| 82 |
-
) : (
|
| 83 |
-
<span>{item.content}</span>
|
| 84 |
-
)}
|
| 85 |
-
</div>
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
-
))}
|
| 89 |
-
<div ref={feedEndRef} />
|
| 90 |
-
</div>
|
| 91 |
-
)
|
| 92 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/ChatInterface.jsx
DELETED
|
@@ -1,195 +0,0 @@
|
|
| 1 |
-
import React, { useState, useRef, useEffect } from 'react'
|
| 2 |
-
import { Send, Sparkles, Loader2 } from 'lucide-react'
|
| 3 |
-
|
| 4 |
-
const APP_TYPES = [
|
| 5 |
-
{ id: 'saas', label: 'SaaS Platform', emoji: '💼' },
|
| 6 |
-
{ id: 'ecommerce', label: 'E-Commerce', emoji: '🛒' },
|
| 7 |
-
{ id: 'marketplace', label: 'Marketplace', emoji: '🏪' },
|
| 8 |
-
{ id: 'social', label: 'Social Network', emoji: '👥' },
|
| 9 |
-
{ id: 'education', label: 'EdTech / LMS', emoji: '📚' },
|
| 10 |
-
{ id: 'health', label: 'HealthTech', emoji: '🏥' },
|
| 11 |
-
{ id: 'finance', label: 'FinTech', emoji: '💰' },
|
| 12 |
-
{ id: 'custom', label: 'Custom', emoji: '⚡' },
|
| 13 |
-
]
|
| 14 |
-
|
| 15 |
-
const EXAMPLE_PROMPTS = [
|
| 16 |
-
"Build a project management SaaS like Linear with team workspaces, sprint boards, and issue tracking",
|
| 17 |
-
"Create an online course marketplace where instructors can sell video courses with progress tracking",
|
| 18 |
-
"Build a subscription-based fitness app with workout plans, progress photos, and meal tracking",
|
| 19 |
-
]
|
| 20 |
-
|
| 21 |
-
export default function ChatInterface({ messages, onGenerate, onFix, status }) {
|
| 22 |
-
const [input, setInput] = useState('')
|
| 23 |
-
const [appType, setAppType] = useState('saas')
|
| 24 |
-
const [showTypes, setShowTypes] = useState(false)
|
| 25 |
-
const chatEndRef = useRef(null)
|
| 26 |
-
const inputRef = useRef(null)
|
| 27 |
-
|
| 28 |
-
const isGenerating = !['idle', 'completed', 'error'].includes(status)
|
| 29 |
-
|
| 30 |
-
useEffect(() => {
|
| 31 |
-
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 32 |
-
}, [messages])
|
| 33 |
-
|
| 34 |
-
const handleSubmit = (e) => {
|
| 35 |
-
e.preventDefault()
|
| 36 |
-
if (!input.trim() || isGenerating) return
|
| 37 |
-
onGenerate(input.trim(), appType)
|
| 38 |
-
setInput('')
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
const handleExampleClick = (prompt) => {
|
| 42 |
-
setInput(prompt)
|
| 43 |
-
inputRef.current?.focus()
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
return (
|
| 47 |
-
<div className="flex flex-col h-full">
|
| 48 |
-
{/* Messages area */}
|
| 49 |
-
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 50 |
-
{messages.length === 0 ? (
|
| 51 |
-
/* Welcome screen */
|
| 52 |
-
<div className="flex flex-col items-center justify-center h-full text-center px-4">
|
| 53 |
-
<div className="text-5xl mb-4">🚀</div>
|
| 54 |
-
<h2 className="text-xl font-bold gradient-text mb-2">
|
| 55 |
-
Welcome to Nexus Builder
|
| 56 |
-
</h2>
|
| 57 |
-
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
|
| 58 |
-
Describe your app idea and 4 AI agents will build it for you —
|
| 59 |
-
complete with auth, payments, analytics, and admin panel.
|
| 60 |
-
</p>
|
| 61 |
-
|
| 62 |
-
{/* App type selector */}
|
| 63 |
-
<div className="w-full mb-4">
|
| 64 |
-
<button
|
| 65 |
-
onClick={() => setShowTypes(!showTypes)}
|
| 66 |
-
className="w-full text-left text-xs font-medium px-3 py-2 rounded-lg border transition-all"
|
| 67 |
-
style={{
|
| 68 |
-
borderColor: 'var(--border)',
|
| 69 |
-
background: 'var(--surface)',
|
| 70 |
-
color: 'var(--text-secondary)',
|
| 71 |
-
}}
|
| 72 |
-
>
|
| 73 |
-
App Type: {APP_TYPES.find(t => t.id === appType)?.emoji}{' '}
|
| 74 |
-
{APP_TYPES.find(t => t.id === appType)?.label}
|
| 75 |
-
</button>
|
| 76 |
-
{showTypes && (
|
| 77 |
-
<div className="mt-1 grid grid-cols-2 gap-1 p-2 rounded-lg border animate-fade-in"
|
| 78 |
-
style={{ background: 'var(--surface)', borderColor: 'var(--border)' }}>
|
| 79 |
-
{APP_TYPES.map(type => (
|
| 80 |
-
<button
|
| 81 |
-
key={type.id}
|
| 82 |
-
onClick={() => { setAppType(type.id); setShowTypes(false) }}
|
| 83 |
-
className="text-left text-xs px-2.5 py-2 rounded-md transition-all hover:scale-[1.02]"
|
| 84 |
-
style={{
|
| 85 |
-
background: appType === type.id ? 'var(--accent)22' : 'transparent',
|
| 86 |
-
color: appType === type.id ? 'var(--accent)' : 'var(--text-secondary)',
|
| 87 |
-
}}
|
| 88 |
-
>
|
| 89 |
-
{type.emoji} {type.label}
|
| 90 |
-
</button>
|
| 91 |
-
))}
|
| 92 |
-
</div>
|
| 93 |
-
)}
|
| 94 |
-
</div>
|
| 95 |
-
|
| 96 |
-
{/* Example prompts */}
|
| 97 |
-
<div className="w-full space-y-2">
|
| 98 |
-
<p className="text-xs font-medium" style={{ color: 'var(--text-secondary)' }}>
|
| 99 |
-
Try an example:
|
| 100 |
-
</p>
|
| 101 |
-
{EXAMPLE_PROMPTS.map((prompt, i) => (
|
| 102 |
-
<button
|
| 103 |
-
key={i}
|
| 104 |
-
onClick={() => handleExampleClick(prompt)}
|
| 105 |
-
className="w-full text-left text-xs p-3 rounded-lg border transition-all hover:scale-[1.01]"
|
| 106 |
-
style={{
|
| 107 |
-
borderColor: 'var(--border)',
|
| 108 |
-
background: 'var(--surface)',
|
| 109 |
-
color: 'var(--text-secondary)',
|
| 110 |
-
}}
|
| 111 |
-
>
|
| 112 |
-
<Sparkles className="w-3 h-3 inline mr-1.5" style={{ color: 'var(--accent)' }} />
|
| 113 |
-
{prompt}
|
| 114 |
-
</button>
|
| 115 |
-
))}
|
| 116 |
-
</div>
|
| 117 |
-
</div>
|
| 118 |
-
) : (
|
| 119 |
-
/* Chat messages */
|
| 120 |
-
messages.map((msg, i) => (
|
| 121 |
-
<div
|
| 122 |
-
key={i}
|
| 123 |
-
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-slide-up`}
|
| 124 |
-
>
|
| 125 |
-
<div
|
| 126 |
-
className="max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-relaxed"
|
| 127 |
-
style={{
|
| 128 |
-
background: msg.role === 'user' ? 'var(--accent)' : 'var(--surface)',
|
| 129 |
-
color: msg.role === 'user' ? 'white' : 'var(--text-primary)',
|
| 130 |
-
border: msg.role === 'user' ? 'none' : '1px solid var(--border)',
|
| 131 |
-
}}
|
| 132 |
-
>
|
| 133 |
-
<pre className="whitespace-pre-wrap font-sans">{msg.content}</pre>
|
| 134 |
-
</div>
|
| 135 |
-
</div>
|
| 136 |
-
))
|
| 137 |
-
)}
|
| 138 |
-
|
| 139 |
-
{isGenerating && (
|
| 140 |
-
<div className="flex justify-start animate-slide-up">
|
| 141 |
-
<div className="rounded-2xl px-4 py-3 text-sm flex items-center gap-2"
|
| 142 |
-
style={{ background: 'var(--surface)', border: '1px solid var(--border)' }}>
|
| 143 |
-
<Loader2 className="w-4 h-4 animate-spin" style={{ color: 'var(--accent)' }} />
|
| 144 |
-
<span style={{ color: 'var(--text-secondary)' }}>Agents are working...</span>
|
| 145 |
-
<span className="typing-cursor" />
|
| 146 |
-
</div>
|
| 147 |
-
</div>
|
| 148 |
-
)}
|
| 149 |
-
|
| 150 |
-
<div ref={chatEndRef} />
|
| 151 |
-
</div>
|
| 152 |
-
|
| 153 |
-
{/* Input area */}
|
| 154 |
-
<form onSubmit={handleSubmit} className="p-3 border-t" style={{ borderColor: 'var(--border)' }}>
|
| 155 |
-
<div className="flex items-end gap-2">
|
| 156 |
-
<div className="flex-1 relative">
|
| 157 |
-
<textarea
|
| 158 |
-
ref={inputRef}
|
| 159 |
-
value={input}
|
| 160 |
-
onChange={(e) => setInput(e.target.value)}
|
| 161 |
-
onKeyDown={(e) => {
|
| 162 |
-
if (e.key === 'Enter' && !e.shiftKey) {
|
| 163 |
-
e.preventDefault()
|
| 164 |
-
handleSubmit(e)
|
| 165 |
-
}
|
| 166 |
-
}}
|
| 167 |
-
placeholder="Describe the app you want to build..."
|
| 168 |
-
rows={2}
|
| 169 |
-
className="w-full resize-none rounded-xl px-4 py-3 text-sm outline-none transition-all"
|
| 170 |
-
style={{
|
| 171 |
-
background: 'var(--surface)',
|
| 172 |
-
color: 'var(--text-primary)',
|
| 173 |
-
border: '1px solid var(--border)',
|
| 174 |
-
}}
|
| 175 |
-
disabled={isGenerating}
|
| 176 |
-
/>
|
| 177 |
-
</div>
|
| 178 |
-
<button
|
| 179 |
-
type="submit"
|
| 180 |
-
disabled={!input.trim() || isGenerating}
|
| 181 |
-
className="p-3 rounded-xl transition-all hover:scale-105 disabled:opacity-40 disabled:hover:scale-100"
|
| 182 |
-
style={{
|
| 183 |
-
background: 'var(--accent)',
|
| 184 |
-
color: 'white',
|
| 185 |
-
}}
|
| 186 |
-
>
|
| 187 |
-
{isGenerating
|
| 188 |
-
? <Loader2 className="w-5 h-5 animate-spin" />
|
| 189 |
-
: <Send className="w-5 h-5" />}
|
| 190 |
-
</button>
|
| 191 |
-
</div>
|
| 192 |
-
</form>
|
| 193 |
-
</div>
|
| 194 |
-
)
|
| 195 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/CodeView.jsx
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 1 |
-
import React, { useMemo } from 'react'
|
| 2 |
-
import { Copy, Check } from 'lucide-react'
|
| 3 |
-
|
| 4 |
-
// Basic syntax highlighting without external deps
|
| 5 |
-
function highlightCode(code, ext) {
|
| 6 |
-
if (!code) return ''
|
| 7 |
-
|
| 8 |
-
// Simple keyword-based highlighting
|
| 9 |
-
const keywords = {
|
| 10 |
-
js: /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|default|async|await|try|catch|throw|new|this|typeof|instanceof|switch|case|break|continue)\b/g,
|
| 11 |
-
py: /\b(def|class|import|from|return|if|elif|else|for|while|try|except|raise|with|as|yield|async|await|lambda|pass|break|continue|and|or|not|in|is|True|False|None)\b/g,
|
| 12 |
-
sql: /\b(SELECT|FROM|WHERE|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|ALTER|DROP|INDEX|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|NULL|PRIMARY|KEY|FOREIGN|REFERENCES|CASCADE|UNIQUE|DEFAULT|CHECK|CONSTRAINT|GRANT|REVOKE|BEGIN|COMMIT|ROLLBACK|TRIGGER|FUNCTION|PROCEDURE|VIEW|ENABLE|ROW|LEVEL|SECURITY|POLICY|USING|WITH)\b/gi,
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
const lang = ['jsx', 'tsx', 'js', 'ts'].includes(ext) ? 'js'
|
| 16 |
-
: ['py'].includes(ext) ? 'py'
|
| 17 |
-
: ['sql'].includes(ext) ? 'sql'
|
| 18 |
-
: 'js'
|
| 19 |
-
|
| 20 |
-
let highlighted = code
|
| 21 |
-
.replace(/&/g, '&')
|
| 22 |
-
.replace(/</g, '<')
|
| 23 |
-
.replace(/>/g, '>')
|
| 24 |
-
|
| 25 |
-
// Strings
|
| 26 |
-
highlighted = highlighted.replace(
|
| 27 |
-
/(["'`])(?:(?=(\\?))\2[\s\S])*?\1/g,
|
| 28 |
-
'<span style="color:#22D3A8">$&</span>'
|
| 29 |
-
)
|
| 30 |
-
|
| 31 |
-
// Comments
|
| 32 |
-
highlighted = highlighted.replace(
|
| 33 |
-
/(\/\/.*$|#.*$)/gm,
|
| 34 |
-
'<span style="color:#555577">$&</span>'
|
| 35 |
-
)
|
| 36 |
-
highlighted = highlighted.replace(
|
| 37 |
-
/(\/\*[\s\S]*?\*\/|--.*$)/gm,
|
| 38 |
-
'<span style="color:#555577">$&</span>'
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
// Keywords
|
| 42 |
-
if (keywords[lang]) {
|
| 43 |
-
highlighted = highlighted.replace(
|
| 44 |
-
keywords[lang],
|
| 45 |
-
'<span style="color:#6C63FF;font-weight:600">$&</span>'
|
| 46 |
-
)
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
// Numbers
|
| 50 |
-
highlighted = highlighted.replace(
|
| 51 |
-
/\b(\d+\.?\d*)\b/g,
|
| 52 |
-
'<span style="color:#FFB547">$&</span>'
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
return highlighted
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
export default function CodeView({ content, fileName }) {
|
| 59 |
-
const [copied, setCopied] = React.useState(false)
|
| 60 |
-
|
| 61 |
-
const ext = fileName?.split('.').pop()?.toLowerCase() || ''
|
| 62 |
-
const highlighted = useMemo(
|
| 63 |
-
() => highlightCode(content || '', ext),
|
| 64 |
-
[content, ext]
|
| 65 |
-
)
|
| 66 |
-
const lineCount = (content || '').split('\n').length
|
| 67 |
-
|
| 68 |
-
const handleCopy = async () => {
|
| 69 |
-
if (!content) return
|
| 70 |
-
await navigator.clipboard.writeText(content)
|
| 71 |
-
setCopied(true)
|
| 72 |
-
setTimeout(() => setCopied(false), 2000)
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
if (!content) {
|
| 76 |
-
return (
|
| 77 |
-
<div className="w-full h-full flex items-center justify-center">
|
| 78 |
-
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 79 |
-
Select a file from the tree to view its code
|
| 80 |
-
</p>
|
| 81 |
-
</div>
|
| 82 |
-
)
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
return (
|
| 86 |
-
<div className="w-full h-full flex flex-col rounded-lg overflow-hidden border"
|
| 87 |
-
style={{ borderColor: 'var(--border)' }}>
|
| 88 |
-
{/* File header */}
|
| 89 |
-
<div className="flex items-center justify-between px-4 py-2 border-b"
|
| 90 |
-
style={{ background: 'var(--bg)', borderColor: 'var(--border)' }}>
|
| 91 |
-
<span className="text-xs font-mono" style={{ color: 'var(--text-secondary)' }}>
|
| 92 |
-
{fileName}
|
| 93 |
-
</span>
|
| 94 |
-
<div className="flex items-center gap-2">
|
| 95 |
-
<span className="text-[10px]" style={{ color: 'var(--text-secondary)' }}>
|
| 96 |
-
{lineCount} lines
|
| 97 |
-
</span>
|
| 98 |
-
<button
|
| 99 |
-
onClick={handleCopy}
|
| 100 |
-
className="p-1 rounded transition-all hover:scale-110"
|
| 101 |
-
style={{ color: copied ? 'var(--success)' : 'var(--text-secondary)' }}
|
| 102 |
-
title="Copy code"
|
| 103 |
-
>
|
| 104 |
-
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
| 105 |
-
</button>
|
| 106 |
-
</div>
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
{/* Code content */}
|
| 110 |
-
<div className="flex-1 overflow-auto" style={{ background: '#0D0D14' }}>
|
| 111 |
-
<div className="flex">
|
| 112 |
-
{/* Line numbers */}
|
| 113 |
-
<div className="flex-shrink-0 text-right pr-4 pl-4 py-3 select-none"
|
| 114 |
-
style={{ color: '#333355', fontSize: '12px', lineHeight: '1.6' }}>
|
| 115 |
-
{Array.from({ length: lineCount }, (_, i) => (
|
| 116 |
-
<div key={i}>{i + 1}</div>
|
| 117 |
-
))}
|
| 118 |
-
</div>
|
| 119 |
-
{/* Code */}
|
| 120 |
-
<pre className="flex-1 py-3 pr-4 overflow-x-auto"
|
| 121 |
-
style={{ fontSize: '12px', lineHeight: '1.6' }}>
|
| 122 |
-
<code dangerouslySetInnerHTML={{ __html: highlighted }} />
|
| 123 |
-
</pre>
|
| 124 |
-
</div>
|
| 125 |
-
</div>
|
| 126 |
-
</div>
|
| 127 |
-
)
|
| 128 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/ControlPanel.jsx
DELETED
|
@@ -1,84 +0,0 @@
|
|
| 1 |
-
import React, { useState } from 'react'
|
| 2 |
-
import ChatInterface from './ChatInterface'
|
| 3 |
-
import AgentFeed from './AgentFeed'
|
| 4 |
-
import FileTree from './FileTree'
|
| 5 |
-
import { MessageSquare, Activity, FolderTree } from 'lucide-react'
|
| 6 |
-
|
| 7 |
-
const TABS = [
|
| 8 |
-
{ id: 'chat', label: 'Chat', icon: MessageSquare },
|
| 9 |
-
{ id: 'agents', label: 'Agents', icon: Activity },
|
| 10 |
-
{ id: 'files', label: 'Files', icon: FolderTree },
|
| 11 |
-
]
|
| 12 |
-
|
| 13 |
-
export default function ControlPanel({
|
| 14 |
-
messages, agentFeed, fileTree, files, selectedFile,
|
| 15 |
-
onSelectFile, onGenerate, onFix, status
|
| 16 |
-
}) {
|
| 17 |
-
const [activeTab, setActiveTab] = useState('chat')
|
| 18 |
-
|
| 19 |
-
return (
|
| 20 |
-
<div className="w-full md:w-[420px] lg:w-[480px] flex flex-col border-r"
|
| 21 |
-
style={{ borderColor: 'var(--border)', background: 'var(--bg)' }}>
|
| 22 |
-
{/* Tab bar */}
|
| 23 |
-
<div className="flex border-b" style={{ borderColor: 'var(--border)' }}>
|
| 24 |
-
{TABS.map(tab => {
|
| 25 |
-
const Icon = tab.icon
|
| 26 |
-
const isActive = activeTab === tab.id
|
| 27 |
-
return (
|
| 28 |
-
<button
|
| 29 |
-
key={tab.id}
|
| 30 |
-
onClick={() => setActiveTab(tab.id)}
|
| 31 |
-
className="flex-1 flex items-center justify-center gap-2 py-2.5 text-xs font-medium transition-all relative"
|
| 32 |
-
style={{
|
| 33 |
-
color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
|
| 34 |
-
background: isActive ? 'var(--surface)' : 'transparent',
|
| 35 |
-
}}
|
| 36 |
-
>
|
| 37 |
-
<Icon className="w-3.5 h-3.5" />
|
| 38 |
-
{tab.label}
|
| 39 |
-
{tab.id === 'agents' && agentFeed.length > 0 && (
|
| 40 |
-
<span className="ml-1 w-4 h-4 text-[10px] flex items-center justify-center rounded-full"
|
| 41 |
-
style={{ background: 'var(--accent)33', color: 'var(--accent)' }}>
|
| 42 |
-
{agentFeed.length}
|
| 43 |
-
</span>
|
| 44 |
-
)}
|
| 45 |
-
{tab.id === 'files' && fileTree.length > 0 && (
|
| 46 |
-
<span className="ml-1 w-4 h-4 text-[10px] flex items-center justify-center rounded-full"
|
| 47 |
-
style={{ background: 'var(--success)33', color: 'var(--success)' }}>
|
| 48 |
-
{fileTree.length}
|
| 49 |
-
</span>
|
| 50 |
-
)}
|
| 51 |
-
{isActive && (
|
| 52 |
-
<div className="absolute bottom-0 left-0 right-0 h-0.5"
|
| 53 |
-
style={{ background: 'var(--accent)' }} />
|
| 54 |
-
)}
|
| 55 |
-
</button>
|
| 56 |
-
)
|
| 57 |
-
})}
|
| 58 |
-
</div>
|
| 59 |
-
|
| 60 |
-
{/* Tab content */}
|
| 61 |
-
<div className="flex-1 overflow-hidden">
|
| 62 |
-
{activeTab === 'chat' && (
|
| 63 |
-
<ChatInterface
|
| 64 |
-
messages={messages}
|
| 65 |
-
onGenerate={onGenerate}
|
| 66 |
-
onFix={onFix}
|
| 67 |
-
status={status}
|
| 68 |
-
/>
|
| 69 |
-
)}
|
| 70 |
-
{activeTab === 'agents' && (
|
| 71 |
-
<AgentFeed feed={agentFeed} />
|
| 72 |
-
)}
|
| 73 |
-
{activeTab === 'files' && (
|
| 74 |
-
<FileTree
|
| 75 |
-
tree={fileTree}
|
| 76 |
-
files={files}
|
| 77 |
-
selectedFile={selectedFile}
|
| 78 |
-
onSelect={onSelectFile}
|
| 79 |
-
/>
|
| 80 |
-
)}
|
| 81 |
-
</div>
|
| 82 |
-
</div>
|
| 83 |
-
)
|
| 84 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/FileTree.jsx
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
import React, { useMemo } from 'react'
|
| 2 |
-
import {
|
| 3 |
-
Folder, FolderOpen, FileCode2, FileJson, FileType2,
|
| 4 |
-
Database, FileText, Settings
|
| 5 |
-
} from 'lucide-react'
|
| 6 |
-
|
| 7 |
-
const FILE_ICONS = {
|
| 8 |
-
jsx: { icon: FileCode2, color: '#61DAFB' },
|
| 9 |
-
tsx: { icon: FileCode2, color: '#3178C6' },
|
| 10 |
-
js: { icon: FileCode2, color: '#F7DF1E' },
|
| 11 |
-
ts: { icon: FileCode2, color: '#3178C6' },
|
| 12 |
-
py: { icon: FileCode2, color: '#3776AB' },
|
| 13 |
-
css: { icon: FileType2, color: '#1572B6' },
|
| 14 |
-
html: { icon: FileCode2, color: '#E34F26' },
|
| 15 |
-
json: { icon: FileJson, color: '#A8B9CC' },
|
| 16 |
-
sql: { icon: Database, color: '#336791' },
|
| 17 |
-
md: { icon: FileText, color: '#083FA1' },
|
| 18 |
-
yml: { icon: Settings, color: '#CB171E' },
|
| 19 |
-
yaml: { icon: Settings, color: '#CB171E' },
|
| 20 |
-
env: { icon: Settings, color: '#ECD53F' },
|
| 21 |
-
txt: { icon: FileText, color: '#8888AA' },
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
function buildTree(paths) {
|
| 25 |
-
const root = {}
|
| 26 |
-
for (const path of paths) {
|
| 27 |
-
const parts = path.split('/')
|
| 28 |
-
let current = root
|
| 29 |
-
for (let i = 0; i < parts.length; i++) {
|
| 30 |
-
const part = parts[i]
|
| 31 |
-
if (i === parts.length - 1) {
|
| 32 |
-
current[part] = path // leaf = full path
|
| 33 |
-
} else {
|
| 34 |
-
if (!current[part] || typeof current[part] === 'string') {
|
| 35 |
-
current[part] = {}
|
| 36 |
-
}
|
| 37 |
-
current = current[part]
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
return root
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
function TreeNode({ name, node, selectedFile, onSelect, depth = 0 }) {
|
| 45 |
-
const [open, setOpen] = React.useState(depth < 2)
|
| 46 |
-
const isFile = typeof node === 'string'
|
| 47 |
-
const isSelected = isFile && node === selectedFile
|
| 48 |
-
|
| 49 |
-
const ext = isFile ? name.split('.').pop()?.toLowerCase() : ''
|
| 50 |
-
const fileConfig = FILE_ICONS[ext] || { icon: FileText, color: 'var(--text-secondary)' }
|
| 51 |
-
const Icon = isFile ? fileConfig.icon : (open ? FolderOpen : Folder)
|
| 52 |
-
const iconColor = isFile ? fileConfig.color : 'var(--accent)'
|
| 53 |
-
|
| 54 |
-
if (isFile) {
|
| 55 |
-
return (
|
| 56 |
-
<button
|
| 57 |
-
onClick={() => onSelect(node)}
|
| 58 |
-
className="w-full flex items-center gap-2 py-1 px-2 rounded text-xs transition-all hover:scale-[1.01] text-left"
|
| 59 |
-
style={{
|
| 60 |
-
paddingLeft: `${depth * 16 + 8}px`,
|
| 61 |
-
background: isSelected ? 'var(--accent)15' : 'transparent',
|
| 62 |
-
color: isSelected ? 'var(--accent)' : 'var(--text-secondary)',
|
| 63 |
-
}}
|
| 64 |
-
>
|
| 65 |
-
<Icon className="w-3.5 h-3.5 flex-shrink-0" style={{ color: iconColor }} />
|
| 66 |
-
<span className="truncate font-mono">{name}</span>
|
| 67 |
-
</button>
|
| 68 |
-
)
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
const entries = Object.entries(node).sort(([a, av], [b, bv]) => {
|
| 72 |
-
const aIsFile = typeof av === 'string'
|
| 73 |
-
const bIsFile = typeof bv === 'string'
|
| 74 |
-
if (aIsFile !== bIsFile) return aIsFile ? 1 : -1
|
| 75 |
-
return a.localeCompare(b)
|
| 76 |
-
})
|
| 77 |
-
|
| 78 |
-
return (
|
| 79 |
-
<div>
|
| 80 |
-
<button
|
| 81 |
-
onClick={() => setOpen(!open)}
|
| 82 |
-
className="w-full flex items-center gap-2 py-1 px-2 rounded text-xs font-medium transition-all text-left"
|
| 83 |
-
style={{
|
| 84 |
-
paddingLeft: `${depth * 16 + 8}px`,
|
| 85 |
-
color: 'var(--text-primary)',
|
| 86 |
-
}}
|
| 87 |
-
>
|
| 88 |
-
<Icon className="w-3.5 h-3.5 flex-shrink-0" style={{ color: iconColor }} />
|
| 89 |
-
<span className="truncate">{name}</span>
|
| 90 |
-
</button>
|
| 91 |
-
{open && entries.map(([childName, childNode]) => (
|
| 92 |
-
<TreeNode
|
| 93 |
-
key={childName}
|
| 94 |
-
name={childName}
|
| 95 |
-
node={childNode}
|
| 96 |
-
selectedFile={selectedFile}
|
| 97 |
-
onSelect={onSelect}
|
| 98 |
-
depth={depth + 1}
|
| 99 |
-
/>
|
| 100 |
-
))}
|
| 101 |
-
</div>
|
| 102 |
-
)
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
export default function FileTree({ tree, files, selectedFile, onSelect }) {
|
| 106 |
-
const treeStructure = useMemo(() => buildTree(tree), [tree])
|
| 107 |
-
|
| 108 |
-
if (tree.length === 0) {
|
| 109 |
-
return (
|
| 110 |
-
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
| 111 |
-
<Folder className="w-12 h-12 mb-3" style={{ color: 'var(--text-secondary)', opacity: 0.4 }} />
|
| 112 |
-
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 113 |
-
Generated files will appear here.
|
| 114 |
-
</p>
|
| 115 |
-
</div>
|
| 116 |
-
)
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
return (
|
| 120 |
-
<div className="h-full overflow-y-auto py-2">
|
| 121 |
-
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider"
|
| 122 |
-
style={{ color: 'var(--text-secondary)' }}>
|
| 123 |
-
Project Files ({tree.length})
|
| 124 |
-
</div>
|
| 125 |
-
{Object.entries(treeStructure).sort(([a, av], [b, bv]) => {
|
| 126 |
-
const aIsFile = typeof av === 'string'
|
| 127 |
-
const bIsFile = typeof bv === 'string'
|
| 128 |
-
if (aIsFile !== bIsFile) return aIsFile ? 1 : -1
|
| 129 |
-
return a.localeCompare(b)
|
| 130 |
-
}).map(([name, node]) => (
|
| 131 |
-
<TreeNode
|
| 132 |
-
key={name}
|
| 133 |
-
name={name}
|
| 134 |
-
node={node}
|
| 135 |
-
selectedFile={selectedFile}
|
| 136 |
-
onSelect={onSelect}
|
| 137 |
-
/>
|
| 138 |
-
))}
|
| 139 |
-
</div>
|
| 140 |
-
)
|
| 141 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/Header.jsx
DELETED
|
@@ -1,80 +0,0 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
-
import { useTheme } from './ThemeProvider'
|
| 3 |
-
import { Sun, Moon, Settings, Zap } from 'lucide-react'
|
| 4 |
-
|
| 5 |
-
const AGENT_LABELS = {
|
| 6 |
-
research: { icon: '🌐', label: 'Research' },
|
| 7 |
-
orchestrator: { icon: '🧠', label: 'Orchestrator' },
|
| 8 |
-
frontend: { icon: '🎨', label: 'Frontend' },
|
| 9 |
-
backend: { icon: '🔐', label: 'Backend' },
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const STATUS_COLORS = {
|
| 13 |
-
idle: 'bg-gray-500',
|
| 14 |
-
active: 'bg-cyan-400 agent-active',
|
| 15 |
-
done: 'bg-emerald-400',
|
| 16 |
-
error: 'bg-red-400',
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export default function Header({ agents, sessionId, status }) {
|
| 20 |
-
const { theme, toggleTheme } = useTheme()
|
| 21 |
-
|
| 22 |
-
return (
|
| 23 |
-
<header className="glass flex items-center justify-between px-4 py-2.5 border-b z-50"
|
| 24 |
-
style={{ borderColor: 'var(--border)' }}>
|
| 25 |
-
{/* Logo */}
|
| 26 |
-
<div className="flex items-center gap-3">
|
| 27 |
-
<div className="flex items-center gap-2">
|
| 28 |
-
<div className="relative">
|
| 29 |
-
<Zap className="w-6 h-6" style={{ color: 'var(--accent)' }} />
|
| 30 |
-
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-400 agent-active" />
|
| 31 |
-
</div>
|
| 32 |
-
<span className="text-xl font-bold tracking-tight font-mono gradient-text">
|
| 33 |
-
NEXUS
|
| 34 |
-
</span>
|
| 35 |
-
</div>
|
| 36 |
-
<span className="text-xs px-2 py-0.5 rounded-full font-medium"
|
| 37 |
-
style={{ background: 'var(--accent)22', color: 'var(--accent)' }}>
|
| 38 |
-
BUILDER
|
| 39 |
-
</span>
|
| 40 |
-
</div>
|
| 41 |
-
|
| 42 |
-
{/* Agent Status Indicators */}
|
| 43 |
-
<div className="hidden md:flex items-center gap-4">
|
| 44 |
-
{Object.entries(agents).map(([key, agent]) => (
|
| 45 |
-
<div key={key} className="flex items-center gap-2 text-xs" title={`${agent.name}: ${agent.status}`}>
|
| 46 |
-
<span>{AGENT_LABELS[key]?.icon}</span>
|
| 47 |
-
<div className={`w-2 h-2 rounded-full ${STATUS_COLORS[agent.status] || STATUS_COLORS.idle}`} />
|
| 48 |
-
<span style={{ color: 'var(--text-secondary)' }}>
|
| 49 |
-
{AGENT_LABELS[key]?.label}
|
| 50 |
-
</span>
|
| 51 |
-
</div>
|
| 52 |
-
))}
|
| 53 |
-
</div>
|
| 54 |
-
|
| 55 |
-
{/* Right controls */}
|
| 56 |
-
<div className="flex items-center gap-3">
|
| 57 |
-
{sessionId && (
|
| 58 |
-
<span className="text-xs font-mono px-2 py-1 rounded"
|
| 59 |
-
style={{ background: 'var(--surface)', color: 'var(--text-secondary)', border: '1px solid var(--border)' }}>
|
| 60 |
-
{sessionId}
|
| 61 |
-
</span>
|
| 62 |
-
)}
|
| 63 |
-
|
| 64 |
-
<button
|
| 65 |
-
onClick={toggleTheme}
|
| 66 |
-
className="p-2 rounded-lg transition-all hover:scale-110"
|
| 67 |
-
style={{ color: 'var(--text-secondary)' }}
|
| 68 |
-
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
| 69 |
-
>
|
| 70 |
-
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
| 71 |
-
</button>
|
| 72 |
-
|
| 73 |
-
<button className="p-2 rounded-lg transition-all"
|
| 74 |
-
style={{ color: 'var(--text-secondary)' }}>
|
| 75 |
-
<Settings className="w-4 h-4" />
|
| 76 |
-
</button>
|
| 77 |
-
</div>
|
| 78 |
-
</header>
|
| 79 |
-
)
|
| 80 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/LivePreview.jsx
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
import React, { useState } from 'react'
|
| 2 |
-
import SystemTabs from './SystemTabs'
|
| 3 |
-
import CodeView from './CodeView'
|
| 4 |
-
import {
|
| 5 |
-
Eye, Code2, Download, Monitor, Tablet, Smartphone,
|
| 6 |
-
ExternalLink, RefreshCw, Maximize2
|
| 7 |
-
} from 'lucide-react'
|
| 8 |
-
|
| 9 |
-
const VIEWPORT_SIZES = {
|
| 10 |
-
desktop: { width: '100%', label: 'Desktop', icon: Monitor },
|
| 11 |
-
tablet: { width: '768px', label: 'Tablet', icon: Tablet },
|
| 12 |
-
mobile: { width: '375px', label: 'Mobile', icon: Smartphone },
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
export default function LivePreview({
|
| 16 |
-
sessionId, files, selectedFile, previewSystem, onChangeSystem,
|
| 17 |
-
viewMode, onChangeViewMode, onExport, status
|
| 18 |
-
}) {
|
| 19 |
-
const [viewport, setViewport] = useState('desktop')
|
| 20 |
-
const [iframeKey, setIframeKey] = useState(0)
|
| 21 |
-
|
| 22 |
-
const previewUrl = sessionId
|
| 23 |
-
? (previewSystem === 'preview'
|
| 24 |
-
? `/api/preview/${sessionId}`
|
| 25 |
-
: `/api/preview/${sessionId}/${previewSystem}`)
|
| 26 |
-
: null
|
| 27 |
-
|
| 28 |
-
const selectedFileContent = selectedFile && files[selectedFile]
|
| 29 |
-
? files[selectedFile]
|
| 30 |
-
: null
|
| 31 |
-
|
| 32 |
-
const showCode = viewMode === 'code' || selectedFileContent
|
| 33 |
-
|
| 34 |
-
return (
|
| 35 |
-
<div className="flex-1 flex flex-col overflow-hidden" style={{ background: 'var(--bg)' }}>
|
| 36 |
-
{/* System tabs */}
|
| 37 |
-
<SystemTabs active={previewSystem} onChange={onChangeSystem} />
|
| 38 |
-
|
| 39 |
-
{/* Toolbar */}
|
| 40 |
-
<div className="flex items-center justify-between px-3 py-2 border-b"
|
| 41 |
-
style={{ borderColor: 'var(--border)' }}>
|
| 42 |
-
<div className="flex items-center gap-1">
|
| 43 |
-
{/* View mode toggle */}
|
| 44 |
-
<button
|
| 45 |
-
onClick={() => onChangeViewMode('preview')}
|
| 46 |
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all"
|
| 47 |
-
style={{
|
| 48 |
-
background: viewMode === 'preview' ? 'var(--accent)15' : 'transparent',
|
| 49 |
-
color: viewMode === 'preview' ? 'var(--accent)' : 'var(--text-secondary)',
|
| 50 |
-
}}
|
| 51 |
-
>
|
| 52 |
-
<Eye className="w-3.5 h-3.5" />
|
| 53 |
-
Preview
|
| 54 |
-
</button>
|
| 55 |
-
<button
|
| 56 |
-
onClick={() => onChangeViewMode('code')}
|
| 57 |
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all"
|
| 58 |
-
style={{
|
| 59 |
-
background: viewMode === 'code' ? 'var(--accent)15' : 'transparent',
|
| 60 |
-
color: viewMode === 'code' ? 'var(--accent)' : 'var(--text-secondary)',
|
| 61 |
-
}}
|
| 62 |
-
>
|
| 63 |
-
<Code2 className="w-3.5 h-3.5" />
|
| 64 |
-
Code
|
| 65 |
-
</button>
|
| 66 |
-
</div>
|
| 67 |
-
|
| 68 |
-
<div className="flex items-center gap-1">
|
| 69 |
-
{/* Viewport toggle */}
|
| 70 |
-
{viewMode === 'preview' && Object.entries(VIEWPORT_SIZES).map(([key, val]) => {
|
| 71 |
-
const Icon = val.icon
|
| 72 |
-
return (
|
| 73 |
-
<button
|
| 74 |
-
key={key}
|
| 75 |
-
onClick={() => setViewport(key)}
|
| 76 |
-
className="p-1.5 rounded-md transition-all"
|
| 77 |
-
style={{
|
| 78 |
-
color: viewport === key ? 'var(--accent)' : 'var(--text-secondary)',
|
| 79 |
-
background: viewport === key ? 'var(--accent)11' : 'transparent',
|
| 80 |
-
}}
|
| 81 |
-
title={val.label}
|
| 82 |
-
>
|
| 83 |
-
<Icon className="w-3.5 h-3.5" />
|
| 84 |
-
</button>
|
| 85 |
-
)
|
| 86 |
-
})}
|
| 87 |
-
|
| 88 |
-
{/* Refresh */}
|
| 89 |
-
<button
|
| 90 |
-
onClick={() => setIframeKey(k => k + 1)}
|
| 91 |
-
className="p-1.5 rounded-md transition-all"
|
| 92 |
-
style={{ color: 'var(--text-secondary)' }}
|
| 93 |
-
title="Refresh preview"
|
| 94 |
-
>
|
| 95 |
-
<RefreshCw className="w-3.5 h-3.5" />
|
| 96 |
-
</button>
|
| 97 |
-
|
| 98 |
-
{/* Export */}
|
| 99 |
-
{sessionId && (
|
| 100 |
-
<button
|
| 101 |
-
onClick={onExport}
|
| 102 |
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all hover:scale-105"
|
| 103 |
-
style={{ background: 'var(--accent)', color: 'white' }}
|
| 104 |
-
>
|
| 105 |
-
<Download className="w-3.5 h-3.5" />
|
| 106 |
-
Export ZIP
|
| 107 |
-
</button>
|
| 108 |
-
)}
|
| 109 |
-
</div>
|
| 110 |
-
</div>
|
| 111 |
-
|
| 112 |
-
{/* Preview area */}
|
| 113 |
-
<div className="flex-1 overflow-hidden flex items-center justify-center p-4"
|
| 114 |
-
style={{ background: viewMode === 'code' ? 'var(--surface)' : '#0D0D12' }}>
|
| 115 |
-
{showCode ? (
|
| 116 |
-
<CodeView
|
| 117 |
-
content={selectedFileContent || _getFirstFile(files)}
|
| 118 |
-
fileName={selectedFile || Object.keys(files)[0] || ''}
|
| 119 |
-
/>
|
| 120 |
-
) : previewUrl ? (
|
| 121 |
-
<div
|
| 122 |
-
className="h-full rounded-lg overflow-hidden border transition-all duration-300"
|
| 123 |
-
style={{
|
| 124 |
-
width: VIEWPORT_SIZES[viewport].width,
|
| 125 |
-
maxWidth: '100%',
|
| 126 |
-
borderColor: 'var(--border)',
|
| 127 |
-
}}
|
| 128 |
-
>
|
| 129 |
-
<iframe
|
| 130 |
-
key={iframeKey}
|
| 131 |
-
src={previewUrl}
|
| 132 |
-
className="w-full h-full border-0"
|
| 133 |
-
style={{ background: 'white', borderRadius: '8px' }}
|
| 134 |
-
title="App Preview"
|
| 135 |
-
sandbox="allow-scripts allow-same-origin"
|
| 136 |
-
/>
|
| 137 |
-
</div>
|
| 138 |
-
) : (
|
| 139 |
-
/* Empty state */
|
| 140 |
-
<div className="text-center">
|
| 141 |
-
<div className="text-6xl mb-4 opacity-20">🖥️</div>
|
| 142 |
-
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
| 143 |
-
Live Preview
|
| 144 |
-
</h3>
|
| 145 |
-
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
| 146 |
-
Your generated app will be previewed here in real-time.
|
| 147 |
-
<br />Start by describing your app idea in the chat.
|
| 148 |
-
</p>
|
| 149 |
-
</div>
|
| 150 |
-
)}
|
| 151 |
-
</div>
|
| 152 |
-
</div>
|
| 153 |
-
)
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
function _getFirstFile(files) {
|
| 157 |
-
const keys = Object.keys(files)
|
| 158 |
-
return keys.length > 0 ? files[keys[0]] : ''
|
| 159 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/StatusBar.jsx
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
-
import { Wifi, WifiOff, AlertTriangle, Files, Cpu } from 'lucide-react'
|
| 3 |
-
|
| 4 |
-
const STATUS_LABELS = {
|
| 5 |
-
idle: { label: 'Ready', color: 'var(--text-secondary)' },
|
| 6 |
-
starting: { label: 'Starting...', color: 'var(--warning)' },
|
| 7 |
-
connected: { label: 'Connected', color: 'var(--success)' },
|
| 8 |
-
researching: { label: '🔍 Researching...', color: 'var(--accent-2)' },
|
| 9 |
-
orchestrating: { label: '🧠 Creating Blueprint...', color: 'var(--accent)' },
|
| 10 |
-
building: { label: '🚀 Building...', color: 'var(--accent)' },
|
| 11 |
-
building_frontend: { label: '🎨 Building Frontend...', color: 'var(--success)' },
|
| 12 |
-
building_backend: { label: '🔐 Building Backend...', color: 'var(--warning)' },
|
| 13 |
-
merging: { label: '📦 Merging...', color: 'var(--accent-2)' },
|
| 14 |
-
fixing: { label: '🔧 Fixing...', color: 'var(--warning)' },
|
| 15 |
-
completed: { label: '✅ Complete', color: 'var(--success)' },
|
| 16 |
-
error: { label: '❌ Error', color: 'var(--error)' },
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export default function StatusBar({ status, sessionId, agents, errorCount, fileCount }) {
|
| 20 |
-
const statusConfig = STATUS_LABELS[status] || STATUS_LABELS.idle
|
| 21 |
-
const isActive = !['idle', 'completed', 'error'].includes(status)
|
| 22 |
-
|
| 23 |
-
return (
|
| 24 |
-
<footer
|
| 25 |
-
className="flex items-center justify-between px-4 py-1.5 text-[11px] border-t"
|
| 26 |
-
style={{
|
| 27 |
-
borderColor: 'var(--border)',
|
| 28 |
-
background: 'var(--surface)',
|
| 29 |
-
color: 'var(--text-secondary)',
|
| 30 |
-
}}
|
| 31 |
-
>
|
| 32 |
-
{/* Left: status */}
|
| 33 |
-
<div className="flex items-center gap-3">
|
| 34 |
-
<div className="flex items-center gap-1.5">
|
| 35 |
-
{isActive ? (
|
| 36 |
-
<Wifi className="w-3 h-3" style={{ color: 'var(--success)' }} />
|
| 37 |
-
) : (
|
| 38 |
-
<WifiOff className="w-3 h-3" />
|
| 39 |
-
)}
|
| 40 |
-
<span style={{ color: statusConfig.color, fontWeight: 500 }}>
|
| 41 |
-
{statusConfig.label}
|
| 42 |
-
</span>
|
| 43 |
-
</div>
|
| 44 |
-
|
| 45 |
-
{/* Build progress */}
|
| 46 |
-
{isActive && (
|
| 47 |
-
<div className="hidden sm:flex items-center gap-1">
|
| 48 |
-
{['researching', 'orchestrating', 'building', 'merging'].map((step, i) => {
|
| 49 |
-
const stepStatuses = {
|
| 50 |
-
researching: ['researching'],
|
| 51 |
-
orchestrating: ['orchestrating'],
|
| 52 |
-
building: ['building', 'building_frontend', 'building_backend'],
|
| 53 |
-
merging: ['merging', 'completed'],
|
| 54 |
-
}
|
| 55 |
-
const isCurrentOrPast = stepStatuses[step]?.includes(status) ||
|
| 56 |
-
['researching', 'orchestrating', 'building', 'merging'].indexOf(step) <
|
| 57 |
-
['researching', 'orchestrating', 'building', 'merging'].findIndex(s =>
|
| 58 |
-
stepStatuses[s]?.includes(status)
|
| 59 |
-
)
|
| 60 |
-
return (
|
| 61 |
-
<React.Fragment key={step}>
|
| 62 |
-
<div
|
| 63 |
-
className="w-1.5 h-1.5 rounded-full"
|
| 64 |
-
style={{
|
| 65 |
-
background: stepStatuses[step]?.includes(status)
|
| 66 |
-
? 'var(--accent)'
|
| 67 |
-
: isCurrentOrPast ? 'var(--success)' : 'var(--border)',
|
| 68 |
-
}}
|
| 69 |
-
/>
|
| 70 |
-
{i < 3 && (
|
| 71 |
-
<div className="w-4 h-px" style={{ background: 'var(--border)' }} />
|
| 72 |
-
)}
|
| 73 |
-
</React.Fragment>
|
| 74 |
-
)
|
| 75 |
-
})}
|
| 76 |
-
</div>
|
| 77 |
-
)}
|
| 78 |
-
</div>
|
| 79 |
-
|
| 80 |
-
{/* Right: metrics */}
|
| 81 |
-
<div className="flex items-center gap-4">
|
| 82 |
-
{fileCount > 0 && (
|
| 83 |
-
<div className="flex items-center gap-1">
|
| 84 |
-
<Files className="w-3 h-3" />
|
| 85 |
-
<span>{fileCount} files</span>
|
| 86 |
-
</div>
|
| 87 |
-
)}
|
| 88 |
-
{errorCount > 0 && (
|
| 89 |
-
<div className="flex items-center gap-1" style={{ color: 'var(--error)' }}>
|
| 90 |
-
<AlertTriangle className="w-3 h-3" />
|
| 91 |
-
<span>{errorCount} errors</span>
|
| 92 |
-
</div>
|
| 93 |
-
)}
|
| 94 |
-
<div className="flex items-center gap-1">
|
| 95 |
-
<Cpu className="w-3 h-3" />
|
| 96 |
-
<span>HuggingFace CPU</span>
|
| 97 |
-
</div>
|
| 98 |
-
{sessionId && (
|
| 99 |
-
<span className="font-mono">{sessionId}</span>
|
| 100 |
-
)}
|
| 101 |
-
</div>
|
| 102 |
-
</footer>
|
| 103 |
-
)
|
| 104 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/SystemTabs.jsx
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
-
import { Layout, Globe, Megaphone, BarChart3, Shield } from 'lucide-react'
|
| 3 |
-
|
| 4 |
-
const SYSTEMS = [
|
| 5 |
-
{ id: 'preview', label: 'Overview', icon: Layout },
|
| 6 |
-
{ id: 'client_portal', label: 'Portal', icon: Layout },
|
| 7 |
-
{ id: 'public_landing', label: 'Landing', icon: Globe },
|
| 8 |
-
{ id: 'marketing_cms', label: 'Marketing', icon: Megaphone },
|
| 9 |
-
{ id: 'analytics_dashboard', label: 'Analytics', icon: BarChart3 },
|
| 10 |
-
{ id: 'admin_panel', label: 'Admin', icon: Shield },
|
| 11 |
-
]
|
| 12 |
-
|
| 13 |
-
export default function SystemTabs({ active, onChange }) {
|
| 14 |
-
return (
|
| 15 |
-
<div className="flex items-center gap-0.5 overflow-x-auto px-2 py-1.5 border-b"
|
| 16 |
-
style={{ borderColor: 'var(--border)' }}>
|
| 17 |
-
{SYSTEMS.map(sys => {
|
| 18 |
-
const Icon = sys.icon
|
| 19 |
-
const isActive = active === sys.id
|
| 20 |
-
return (
|
| 21 |
-
<button
|
| 22 |
-
key={sys.id}
|
| 23 |
-
onClick={() => onChange(sys.id)}
|
| 24 |
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium whitespace-nowrap transition-all"
|
| 25 |
-
style={{
|
| 26 |
-
background: isActive ? 'var(--accent)15' : 'transparent',
|
| 27 |
-
color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
|
| 28 |
-
}}
|
| 29 |
-
>
|
| 30 |
-
<Icon className="w-3.5 h-3.5" />
|
| 31 |
-
{sys.label}
|
| 32 |
-
</button>
|
| 33 |
-
)
|
| 34 |
-
})}
|
| 35 |
-
</div>
|
| 36 |
-
)
|
| 37 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/components/ThemeProvider.jsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
import React, { createContext, useContext, useState, useEffect } from 'react'
|
| 2 |
-
|
| 3 |
-
const ThemeContext = createContext()
|
| 4 |
-
|
| 5 |
-
export function ThemeProvider({ children }) {
|
| 6 |
-
const [theme, setTheme] = useState(() => {
|
| 7 |
-
if (typeof window !== 'undefined') {
|
| 8 |
-
return localStorage.getItem('nexus-theme') || 'dark'
|
| 9 |
-
}
|
| 10 |
-
return 'dark'
|
| 11 |
-
})
|
| 12 |
-
|
| 13 |
-
useEffect(() => {
|
| 14 |
-
document.documentElement.setAttribute('data-theme', theme)
|
| 15 |
-
localStorage.setItem('nexus-theme', theme)
|
| 16 |
-
}, [theme])
|
| 17 |
-
|
| 18 |
-
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark')
|
| 19 |
-
|
| 20 |
-
return (
|
| 21 |
-
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
| 22 |
-
{children}
|
| 23 |
-
</ThemeContext.Provider>
|
| 24 |
-
)
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
export function useTheme() {
|
| 28 |
-
const ctx = useContext(ThemeContext)
|
| 29 |
-
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
|
| 30 |
-
return ctx
|
| 31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/hooks/useSSE.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
import { useRef, useCallback } from 'react'
|
| 2 |
-
|
| 3 |
-
export function useSSE(onEvent) {
|
| 4 |
-
const eventSourceRef = useRef(null)
|
| 5 |
-
|
| 6 |
-
const connect = useCallback((sessionId) => {
|
| 7 |
-
// Close existing connection
|
| 8 |
-
if (eventSourceRef.current) {
|
| 9 |
-
eventSourceRef.current.close()
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const url = `/api/stream/${sessionId}`
|
| 13 |
-
const eventSource = new EventSource(url)
|
| 14 |
-
eventSourceRef.current = eventSource
|
| 15 |
-
|
| 16 |
-
const eventTypes = [
|
| 17 |
-
'agent_start', 'token', 'code_block', 'agent_done',
|
| 18 |
-
'error', 'file_created', 'done'
|
| 19 |
-
]
|
| 20 |
-
|
| 21 |
-
eventTypes.forEach(type => {
|
| 22 |
-
eventSource.addEventListener(type, (event) => {
|
| 23 |
-
onEvent({ type, data: event.data })
|
| 24 |
-
})
|
| 25 |
-
})
|
| 26 |
-
|
| 27 |
-
// Also handle generic messages
|
| 28 |
-
eventSource.onmessage = (event) => {
|
| 29 |
-
try {
|
| 30 |
-
const data = JSON.parse(event.data)
|
| 31 |
-
onEvent({ type: data.event_type || 'message', data: event.data })
|
| 32 |
-
} catch {
|
| 33 |
-
// ignore parse errors
|
| 34 |
-
}
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
eventSource.onerror = (err) => {
|
| 38 |
-
console.error('SSE error:', err)
|
| 39 |
-
// Don't auto-reconnect on error to avoid infinite loops
|
| 40 |
-
if (eventSource.readyState === EventSource.CLOSED) {
|
| 41 |
-
onEvent({
|
| 42 |
-
type: 'error',
|
| 43 |
-
data: JSON.stringify({
|
| 44 |
-
event_type: 'error',
|
| 45 |
-
agent: 'system',
|
| 46 |
-
content: 'Connection lost. Check the status and try again.'
|
| 47 |
-
})
|
| 48 |
-
})
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
}, [onEvent])
|
| 52 |
-
|
| 53 |
-
const disconnect = useCallback(() => {
|
| 54 |
-
if (eventSourceRef.current) {
|
| 55 |
-
eventSourceRef.current.close()
|
| 56 |
-
eventSourceRef.current = null
|
| 57 |
-
}
|
| 58 |
-
}, [])
|
| 59 |
-
|
| 60 |
-
return { connect, disconnect }
|
| 61 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/hooks/useTheme.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Re-exported from ThemeProvider for convenience
|
| 2 |
-
export { useTheme } from '../components/ThemeProvider'
|
|
|
|
|
|
|
|
|
fronend/src/index.css
DELETED
|
@@ -1,126 +0,0 @@
|
|
| 1 |
-
@tailwind base;
|
| 2 |
-
@tailwind components;
|
| 3 |
-
@tailwind utilities;
|
| 4 |
-
|
| 5 |
-
:root {
|
| 6 |
-
--bg: #0A0A0F;
|
| 7 |
-
--surface: #111118;
|
| 8 |
-
--border: #1E1E2E;
|
| 9 |
-
--accent: #6C63FF;
|
| 10 |
-
--accent-2: #00D9FF;
|
| 11 |
-
--text-primary: #F0F0FF;
|
| 12 |
-
--text-secondary: #8888AA;
|
| 13 |
-
--success: #22D3A8;
|
| 14 |
-
--error: #FF4D6D;
|
| 15 |
-
--warning: #FFB547;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
[data-theme="light"] {
|
| 19 |
-
--bg: #F8F8FC;
|
| 20 |
-
--surface: #FFFFFF;
|
| 21 |
-
--border: #E0E0EF;
|
| 22 |
-
--accent: #5B53E8;
|
| 23 |
-
--accent-2: #0099CC;
|
| 24 |
-
--text-primary: #0A0A1A;
|
| 25 |
-
--text-secondary: #666688;
|
| 26 |
-
--success: #16A085;
|
| 27 |
-
--error: #E74C6F;
|
| 28 |
-
--warning: #E6A030;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
* {
|
| 32 |
-
margin: 0;
|
| 33 |
-
padding: 0;
|
| 34 |
-
box-sizing: border-box;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
html, body, #root {
|
| 38 |
-
height: 100%;
|
| 39 |
-
width: 100%;
|
| 40 |
-
overflow: hidden;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
body {
|
| 44 |
-
font-family: 'Inter', system-ui, sans-serif;
|
| 45 |
-
background-color: var(--bg);
|
| 46 |
-
color: var(--text-primary);
|
| 47 |
-
-webkit-font-smoothing: antialiased;
|
| 48 |
-
-moz-osx-font-smoothing: grayscale;
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
/* Custom scrollbar */
|
| 52 |
-
::-webkit-scrollbar {
|
| 53 |
-
width: 6px;
|
| 54 |
-
height: 6px;
|
| 55 |
-
}
|
| 56 |
-
::-webkit-scrollbar-track {
|
| 57 |
-
background: transparent;
|
| 58 |
-
}
|
| 59 |
-
::-webkit-scrollbar-thumb {
|
| 60 |
-
background: var(--border);
|
| 61 |
-
border-radius: 3px;
|
| 62 |
-
}
|
| 63 |
-
::-webkit-scrollbar-thumb:hover {
|
| 64 |
-
background: var(--text-secondary);
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
/* Code blocks */
|
| 68 |
-
pre, code {
|
| 69 |
-
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
/* Selection */
|
| 73 |
-
::selection {
|
| 74 |
-
background: var(--accent);
|
| 75 |
-
color: white;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
/* Focus styles */
|
| 79 |
-
*:focus-visible {
|
| 80 |
-
outline: 2px solid var(--accent);
|
| 81 |
-
outline-offset: 2px;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
/* Glassmorphism utility */
|
| 85 |
-
.glass {
|
| 86 |
-
background: rgba(17, 17, 24, 0.7);
|
| 87 |
-
backdrop-filter: blur(12px);
|
| 88 |
-
-webkit-backdrop-filter: blur(12px);
|
| 89 |
-
border: 1px solid var(--border);
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
[data-theme="light"] .glass {
|
| 93 |
-
background: rgba(255, 255, 255, 0.75);
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
/* Agent status dots */
|
| 97 |
-
@keyframes agentPulse {
|
| 98 |
-
0%, 100% { opacity: 1; transform: scale(1); }
|
| 99 |
-
50% { opacity: 0.5; transform: scale(1.3); }
|
| 100 |
-
}
|
| 101 |
-
.agent-active {
|
| 102 |
-
animation: agentPulse 1.5s ease-in-out infinite;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
/* Gradient text */
|
| 106 |
-
.gradient-text {
|
| 107 |
-
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
| 108 |
-
-webkit-background-clip: text;
|
| 109 |
-
-webkit-text-fill-color: transparent;
|
| 110 |
-
background-clip: text;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
/* Typing indicator */
|
| 114 |
-
@keyframes blink {
|
| 115 |
-
0%, 50% { opacity: 1; }
|
| 116 |
-
51%, 100% { opacity: 0; }
|
| 117 |
-
}
|
| 118 |
-
.typing-cursor {
|
| 119 |
-
display: inline-block;
|
| 120 |
-
width: 2px;
|
| 121 |
-
height: 1.1em;
|
| 122 |
-
background: var(--accent);
|
| 123 |
-
margin-left: 2px;
|
| 124 |
-
animation: blink 1s infinite;
|
| 125 |
-
vertical-align: text-bottom;
|
| 126 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/main.jsx
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
-
import ReactDOM from 'react-dom/client'
|
| 3 |
-
import App from './App'
|
| 4 |
-
import { ThemeProvider } from './components/ThemeProvider'
|
| 5 |
-
import './index.css'
|
| 6 |
-
|
| 7 |
-
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 8 |
-
<React.StrictMode>
|
| 9 |
-
<ThemeProvider>
|
| 10 |
-
<App />
|
| 11 |
-
</ThemeProvider>
|
| 12 |
-
</React.StrictMode>,
|
| 13 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/src/utils/api.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
| 1 |
-
const BASE = '' // Same origin
|
| 2 |
-
|
| 3 |
-
export async function generateProject(prompt, appType = 'saas', systems = null) {
|
| 4 |
-
const res = await fetch(`${BASE}/api/generate`, {
|
| 5 |
-
method: 'POST',
|
| 6 |
-
headers: { 'Content-Type': 'application/json' },
|
| 7 |
-
body: JSON.stringify({ prompt, app_type: appType, systems }),
|
| 8 |
-
})
|
| 9 |
-
if (!res.ok) {
|
| 10 |
-
const err = await res.text()
|
| 11 |
-
throw new Error(`Generation failed: ${err}`)
|
| 12 |
-
}
|
| 13 |
-
return res.json()
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export async function getProjectStatus(sessionId) {
|
| 17 |
-
const res = await fetch(`${BASE}/api/status/${sessionId}`)
|
| 18 |
-
if (!res.ok) throw new Error('Failed to get status')
|
| 19 |
-
return res.json()
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export async function getProjectFiles(sessionId) {
|
| 23 |
-
const res = await fetch(`${BASE}/api/files/${sessionId}`)
|
| 24 |
-
if (!res.ok) throw new Error('Failed to get files')
|
| 25 |
-
return res.json()
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
export async function getFileContent(sessionId, path) {
|
| 29 |
-
const res = await fetch(`${BASE}/api/file/${sessionId}/${path}`)
|
| 30 |
-
if (!res.ok) throw new Error('Failed to get file')
|
| 31 |
-
return res.json()
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
export async function exportProject(sessionId) {
|
| 35 |
-
const res = await fetch(`${BASE}/api/export/${sessionId}`)
|
| 36 |
-
if (!res.ok) throw new Error('Export failed')
|
| 37 |
-
const blob = await res.blob()
|
| 38 |
-
const url = URL.createObjectURL(blob)
|
| 39 |
-
const a = document.createElement('a')
|
| 40 |
-
a.href = url
|
| 41 |
-
a.download = `nexus-project-${sessionId}.zip`
|
| 42 |
-
document.body.appendChild(a)
|
| 43 |
-
a.click()
|
| 44 |
-
document.body.removeChild(a)
|
| 45 |
-
URL.revokeObjectURL(url)
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
export async function fixBug(sessionId, errorMessage, filePath = null) {
|
| 49 |
-
const res = await fetch(`${BASE}/api/fix/${sessionId}`, {
|
| 50 |
-
method: 'POST',
|
| 51 |
-
headers: { 'Content-Type': 'application/json' },
|
| 52 |
-
body: JSON.stringify({ error_message: errorMessage, file_path: filePath }),
|
| 53 |
-
})
|
| 54 |
-
if (!res.ok) throw new Error('Fix request failed')
|
| 55 |
-
return res.json()
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
// Keepalive ping to prevent HuggingFace sleep
|
| 59 |
-
let keepaliveInterval = null
|
| 60 |
-
|
| 61 |
-
export function startKeepalive() {
|
| 62 |
-
if (keepaliveInterval) return
|
| 63 |
-
keepaliveInterval = setInterval(async () => {
|
| 64 |
-
try {
|
| 65 |
-
await fetch(`${BASE}/api/health`)
|
| 66 |
-
} catch {
|
| 67 |
-
// ignore
|
| 68 |
-
}
|
| 69 |
-
}, 30000) // every 30 seconds
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
export function stopKeepalive() {
|
| 73 |
-
if (keepaliveInterval) {
|
| 74 |
-
clearInterval(keepaliveInterval)
|
| 75 |
-
keepaliveInterval = null
|
| 76 |
-
}
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
// Start keepalive on load
|
| 80 |
-
startKeepalive()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/tailwind.config.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
/** @type {import('tailwindcss').Config} */
|
| 2 |
-
export default {
|
| 3 |
-
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
| 4 |
-
darkMode: 'class',
|
| 5 |
-
theme: {
|
| 6 |
-
extend: {
|
| 7 |
-
colors: {
|
| 8 |
-
nexus: {
|
| 9 |
-
bg: 'var(--bg)',
|
| 10 |
-
surface: 'var(--surface)',
|
| 11 |
-
border: 'var(--border)',
|
| 12 |
-
accent: 'var(--accent)',
|
| 13 |
-
'accent-2': 'var(--accent-2)',
|
| 14 |
-
'text-primary': 'var(--text-primary)',
|
| 15 |
-
'text-secondary': 'var(--text-secondary)',
|
| 16 |
-
success: 'var(--success)',
|
| 17 |
-
error: 'var(--error)',
|
| 18 |
-
warning: 'var(--warning)',
|
| 19 |
-
},
|
| 20 |
-
},
|
| 21 |
-
fontFamily: {
|
| 22 |
-
sans: ['Inter', 'system-ui', 'sans-serif'],
|
| 23 |
-
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
|
| 24 |
-
},
|
| 25 |
-
animation: {
|
| 26 |
-
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
| 27 |
-
'fade-in': 'fadeIn 0.5s ease forwards',
|
| 28 |
-
'slide-up': 'slideUp 0.4s ease forwards',
|
| 29 |
-
'glow': 'glow 2s ease-in-out infinite alternate',
|
| 30 |
-
},
|
| 31 |
-
keyframes: {
|
| 32 |
-
fadeIn: {
|
| 33 |
-
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
| 34 |
-
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 35 |
-
},
|
| 36 |
-
slideUp: {
|
| 37 |
-
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
| 38 |
-
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 39 |
-
},
|
| 40 |
-
glow: {
|
| 41 |
-
'0%': { boxShadow: '0 0 5px var(--accent)33' },
|
| 42 |
-
'100%': { boxShadow: '0 0 20px var(--accent)55' },
|
| 43 |
-
},
|
| 44 |
-
},
|
| 45 |
-
},
|
| 46 |
-
},
|
| 47 |
-
plugins: [],
|
| 48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fronend/vite.config.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
import { defineConfig } from 'vite'
|
| 2 |
-
import react from '@vitejs/plugin-react'
|
| 3 |
-
|
| 4 |
-
export default defineConfig({
|
| 5 |
-
plugins: [react()],
|
| 6 |
-
build: {
|
| 7 |
-
outDir: 'dist',
|
| 8 |
-
sourcemap: false,
|
| 9 |
-
},
|
| 10 |
-
server: {
|
| 11 |
-
proxy: {
|
| 12 |
-
'/api': 'http://localhost:7860',
|
| 13 |
-
},
|
| 14 |
-
},
|
| 15 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|