3VVM commited on
Commit
bfcf2b0
·
verified ·
1 Parent(s): e9256cc

Delete frontend

Browse files
frontend/.gitignore DELETED
@@ -1,35 +0,0 @@
1
- # dependencies
2
- /node_modules
3
- /.pnp
4
- .pnp.js
5
-
6
- # testing
7
- /coverage
8
-
9
- # next.js
10
- /.next/
11
- /out/
12
-
13
- # production
14
- /build
15
-
16
- # misc
17
- .DS_Store
18
- *.pem
19
-
20
- # debug
21
- npm-debug.log*
22
- yarn-debug.log*
23
- yarn-error.log*
24
-
25
- # local env files
26
- .env*.local
27
- .env
28
-
29
- # vercel
30
- .vercel
31
-
32
- # typescript
33
- *.tsbuildinfo
34
- next-env.d.ts
35
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/next.config.js DELETED
@@ -1,31 +0,0 @@
1
- /** @type {import('next').NextConfig} */
2
- const nextConfig = {
3
- output: 'standalone',
4
- reactStrictMode: true,
5
- // Only set NEXT_PUBLIC_API_URL if explicitly provided
6
- // In Docker Space, we want it undefined so frontend uses relative URLs (proxied by Next.js)
7
- ...(process.env.NEXT_PUBLIC_API_URL && {
8
- env: {
9
- NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
10
- },
11
- }),
12
- async rewrites() {
13
- // In Docker Space, proxy /api/* requests to backend on port 8000
14
- // This allows frontend (7860) to communicate with backend (8000) internally
15
- const API_HOST = process.env.BACKEND_HOST || 'http://localhost:8000';
16
- console.log(`[Next.js] Setting up API proxy: /api/* -> ${API_HOST}/api/*`);
17
- return [
18
- {
19
- source: '/api/:path*',
20
- destination: `${API_HOST}/api/:path*`,
21
- },
22
- {
23
- source: '/ws/:path*',
24
- destination: `${API_HOST}/ws/:path*`,
25
- },
26
- ];
27
- },
28
- }
29
-
30
- module.exports = nextConfig
31
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
frontend/package.json DELETED
@@ -1,33 +0,0 @@
1
- {
2
- "name": "anycoder-frontend",
3
- "version": "1.0.0",
4
- "private": true,
5
- "scripts": {
6
- "dev": "next dev",
7
- "build": "next build",
8
- "start": "next start -p 7860",
9
- "lint": "next lint"
10
- },
11
- "dependencies": {
12
- "@monaco-editor/react": "^4.6.0",
13
- "axios": "^1.7.2",
14
- "next": "^16.0.10",
15
- "prismjs": "^1.29.0",
16
- "react": "^19.2.3",
17
- "react-dom": "^19.2.3",
18
- "react-markdown": "^9.0.1",
19
- "remark-gfm": "^4.0.0"
20
- },
21
- "devDependencies": {
22
- "@types/node": "^20",
23
- "@types/prismjs": "^1.26.4",
24
- "@types/react": "^18",
25
- "@types/react-dom": "^18",
26
- "autoprefixer": "^10.4.19",
27
- "eslint": "^9.39.2",
28
- "eslint-config-next": "^16.0.10",
29
- "postcss": "^8.4.39",
30
- "tailwindcss": "^3.4.4",
31
- "typescript": "^5"
32
- }
33
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/postcss.config.js DELETED
@@ -1,7 +0,0 @@
1
- module.exports = {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
6
- }
7
-
 
 
 
 
 
 
 
 
frontend/public/.gitkeep DELETED
@@ -1,2 +0,0 @@
1
- # This file ensures the public directory is tracked by git
2
-
 
 
 
frontend/src/app/globals.css DELETED
@@ -1,192 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- * {
6
- box-sizing: border-box;
7
- padding: 0;
8
- margin: 0;
9
- }
10
-
11
- html,
12
- body {
13
- height: 100%;
14
- overflow: hidden;
15
- }
16
-
17
- body {
18
- color: #e5e5e7;
19
- background: #1d1d1f;
20
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
21
- -webkit-font-smoothing: antialiased;
22
- -moz-osx-font-smoothing: grayscale;
23
- letter-spacing: -0.01em;
24
- }
25
-
26
- /* Apple-style scrollbar */
27
- ::-webkit-scrollbar {
28
- width: 12px;
29
- height: 12px;
30
- }
31
-
32
- ::-webkit-scrollbar-track {
33
- background: transparent;
34
- }
35
-
36
- ::-webkit-scrollbar-thumb {
37
- background: rgba(255, 255, 255, 0.2);
38
- border-radius: 10px;
39
- border: 3px solid #1d1d1f;
40
- }
41
-
42
- ::-webkit-scrollbar-thumb:hover {
43
- background: rgba(255, 255, 255, 0.3);
44
- }
45
-
46
- ::-webkit-scrollbar-corner {
47
- background: #1d1d1f;
48
- }
49
-
50
- /* Markdown styling in chat - VS Code style */
51
- .prose {
52
- max-width: none;
53
- color: #cccccc;
54
- }
55
-
56
- .prose code {
57
- background-color: #2d2d30;
58
- color: #d4d4d4;
59
- padding: 0.2em 0.4em;
60
- border-radius: 3px;
61
- font-size: 0.875em;
62
- font-family: 'SF Mono', 'Monaco', 'Menlo', 'Courier New', monospace;
63
- }
64
-
65
- .prose pre {
66
- background-color: #1e1e1e;
67
- padding: 1em;
68
- border-radius: 4px;
69
- overflow-x: auto;
70
- border: 1px solid #3e3e42;
71
- }
72
-
73
- .prose pre code {
74
- background-color: transparent;
75
- padding: 0;
76
- }
77
-
78
- .prose p {
79
- margin: 0.5em 0;
80
- }
81
-
82
- .prose a {
83
- color: #3794ff;
84
- text-decoration: none;
85
- }
86
-
87
- .prose a:hover {
88
- text-decoration: underline;
89
- }
90
-
91
- /* Selection color - Apple style */
92
- ::selection {
93
- background-color: rgba(0, 122, 255, 0.3);
94
- color: #ffffff;
95
- }
96
-
97
- ::-moz-selection {
98
- background-color: rgba(0, 122, 255, 0.3);
99
- color: #ffffff;
100
- }
101
-
102
- /* Apple-style focus rings */
103
- button:focus-visible,
104
- input:focus-visible,
105
- select:focus-visible {
106
- outline: 2px solid rgba(0, 122, 255, 0.6);
107
- outline-offset: 2px;
108
- }
109
-
110
- /* Smooth transitions */
111
- * {
112
- transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
113
- }
114
-
115
- /* Animation utilities */
116
- @keyframes fade-in {
117
- from {
118
- opacity: 0;
119
- }
120
- to {
121
- opacity: 1;
122
- }
123
- }
124
-
125
- .animate-in {
126
- animation: fade-in 0.3s ease-in-out;
127
- }
128
-
129
- /* Resize handle styles */
130
- .resize-handle {
131
- position: relative;
132
- width: 4px;
133
- cursor: col-resize;
134
- user-select: none;
135
- background: transparent;
136
- transition: background-color 0.2s ease, width 0.15s ease;
137
- flex-shrink: 0;
138
- display: flex;
139
- align-items: center;
140
- justify-content: center;
141
- }
142
-
143
- /* Clickable area (extends beyond visible width) */
144
- .resize-handle::before {
145
- content: '';
146
- position: absolute;
147
- inset: 0;
148
- left: -4px;
149
- right: -4px;
150
- }
151
-
152
- /* Visual indicator (three dots) */
153
- .resize-handle::after {
154
- content: '⋮';
155
- position: relative;
156
- z-index: 1;
157
- font-size: 16px;
158
- color: rgba(134, 134, 139, 0.4);
159
- transition: color 0.2s ease;
160
- pointer-events: none;
161
- line-height: 1;
162
- }
163
-
164
- .resize-handle:hover {
165
- background: rgba(0, 123, 255, 0.2);
166
- }
167
-
168
- .resize-handle:hover::after {
169
- color: rgba(0, 123, 255, 0.8);
170
- }
171
-
172
- .resize-handle:active,
173
- .resize-handle.resizing {
174
- background: rgba(0, 123, 255, 0.4);
175
- width: 4px;
176
- }
177
-
178
- .resize-handle:active::after,
179
- .resize-handle.resizing::after {
180
- color: rgba(0, 123, 255, 1);
181
- }
182
-
183
- /* Prevent text selection during resize */
184
- body.resizing {
185
- user-select: none !important;
186
- cursor: col-resize !important;
187
- }
188
-
189
- body.resizing * {
190
- cursor: col-resize !important;
191
- }
192
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/app/layout.tsx DELETED
@@ -1,23 +0,0 @@
1
- import type { Metadata } from 'next';
2
- import { Inter } from 'next/font/google';
3
- import './globals.css';
4
-
5
- const inter = Inter({ subsets: ['latin'] });
6
-
7
- export const metadata: Metadata = {
8
- title: 'AnyCoder - AI Code Generator',
9
- description: 'Generate code with AI - supports HTML, Gradio, React, Streamlit, and more',
10
- };
11
-
12
- export default function RootLayout({
13
- children,
14
- }: {
15
- children: React.ReactNode;
16
- }) {
17
- return (
18
- <html lang="en">
19
- <body className={inter.className}>{children}</body>
20
- </html>
21
- );
22
- }
23
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/app/page.tsx DELETED
@@ -1,1106 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useRef } from 'react';
4
- import { flushSync } from 'react-dom';
5
- import Header from '@/components/Header';
6
- import LandingPage from '@/components/LandingPage';
7
- import ChatInterface from '@/components/ChatInterface';
8
- import CodeEditor from '@/components/CodeEditor';
9
- import ControlPanel from '@/components/ControlPanel';
10
- import { apiClient } from '@/lib/api';
11
- import { isAuthenticated as checkIsAuthenticated, getStoredToken } from '@/lib/auth';
12
- import type { Message, Language, CodeGenerationRequest, Model } from '@/types';
13
-
14
- export default function Home() {
15
- // Initialize messages as empty array (will load from localStorage in useEffect)
16
- const [messages, setMessages] = useState<Message[]>([]);
17
-
18
- const [generatedCode, setGeneratedCode] = useState('');
19
- const [selectedLanguage, setSelectedLanguage] = useState<Language>('html');
20
- const [selectedModel, setSelectedModel] = useState('moonshotai/Kimi-K2.6');
21
- const [models, setModels] = useState<Model[]>([]);
22
- const [isGenerating, setIsGenerating] = useState(false);
23
- const [isAuthenticated, setIsAuthenticated] = useState(false);
24
- const [currentRepoId, setCurrentRepoId] = useState<string | null>(null); // Track imported/deployed space
25
- const [username, setUsername] = useState<string | null>(null); // Track current user
26
- const [pendingPR, setPendingPR] = useState<{ repoId: string; language: Language } | null>(null); // Track pending PR after redesign
27
- const pendingPRRef = useRef<{ repoId: string; language: Language } | null>(null); // Ref for immediate access
28
-
29
- // Landing page state - show landing page if no messages exist
30
- const [showLandingPage, setShowLandingPage] = useState(true);
31
-
32
- // Mobile view state: 'chat', 'editor', or 'settings' - start on chat for mobile
33
- const [mobileView, setMobileView] = useState<'chat' | 'editor' | 'settings'>('chat');
34
-
35
- // Resizable sidebar widths (in pixels)
36
- const [chatSidebarWidth, setChatSidebarWidth] = useState(320);
37
- const [settingsSidebarWidth, setSettingsSidebarWidth] = useState(288);
38
- const [isResizingChat, setIsResizingChat] = useState(false);
39
- const [isResizingSettings, setIsResizingSettings] = useState(false);
40
- const [isDesktop, setIsDesktop] = useState(false);
41
-
42
- // Debug: Log currentRepoId changes
43
- useEffect(() => {
44
- console.log('[App] 🔵 currentRepoId changed to:', currentRepoId);
45
- }, [currentRepoId]);
46
-
47
- // Clear cache on app startup to ensure fresh data and load models
48
- useEffect(() => {
49
- if (typeof window !== 'undefined') {
50
- console.log('[Cache] Clearing models and languages cache on app startup');
51
- localStorage.removeItem('anycoder_models');
52
- localStorage.removeItem('anycoder_languages');
53
-
54
- // Load models for checking image support
55
- loadModels();
56
- }
57
- }, []); // Run once on mount
58
-
59
- const loadModels = async () => {
60
- try {
61
- const modelsList = await apiClient.getModels();
62
- console.log('[Models] Loaded models:', modelsList);
63
- setModels(modelsList);
64
- } catch (error) {
65
- console.error('Failed to load models:', error);
66
- }
67
- };
68
-
69
- // Check if current model supports images
70
- // Show immediately for GLM-4.6V even before models load
71
- const currentModelSupportsImages =
72
- selectedModel === 'zai-org/GLM-4.6V:zai-org' ||
73
- models.find(m => m.id === selectedModel)?.supports_images ||
74
- false;
75
-
76
- // Debug log for image support
77
- useEffect(() => {
78
- console.log('[Image Support] Selected model:', selectedModel);
79
- console.log('[Image Support] Models loaded:', models.length);
80
- console.log('[Image Support] Supports images:', currentModelSupportsImages);
81
- }, [selectedModel, models, currentModelSupportsImages]);
82
-
83
- // Load messages from localStorage on mount (client-side only to avoid hydration issues)
84
- useEffect(() => {
85
- if (typeof window !== 'undefined') {
86
- const saved = localStorage.getItem('anycoder_messages');
87
- if (saved) {
88
- try {
89
- const parsed = JSON.parse(saved);
90
- console.log('[localStorage] Loaded messages from localStorage:', parsed.length, 'messages');
91
- setMessages(parsed);
92
- // If there are existing messages, show the full UI
93
- if (parsed.length > 0) {
94
- setShowLandingPage(false);
95
- }
96
- } catch (e) {
97
- console.error('[localStorage] Failed to parse saved messages:', e);
98
- }
99
- }
100
-
101
- // Load sidebar widths from localStorage
102
- const savedChatWidth = localStorage.getItem('anycoder_chat_sidebar_width');
103
- const savedSettingsWidth = localStorage.getItem('anycoder_settings_sidebar_width');
104
- if (savedChatWidth) {
105
- setChatSidebarWidth(parseInt(savedChatWidth, 10));
106
- }
107
- if (savedSettingsWidth) {
108
- setSettingsSidebarWidth(parseInt(savedSettingsWidth, 10));
109
- }
110
-
111
- // Check if desktop on mount
112
- const checkDesktop = () => {
113
- setIsDesktop(window.innerWidth >= 768);
114
- };
115
- checkDesktop();
116
-
117
- // Listen for window resize to update desktop status
118
- window.addEventListener('resize', checkDesktop);
119
- return () => window.removeEventListener('resize', checkDesktop);
120
- }
121
- }, []); // Empty deps = run once on mount
122
-
123
- // Save messages to localStorage whenever they change (CRITICAL FOR PERSISTENCE!)
124
- useEffect(() => {
125
- if (typeof window !== 'undefined' && messages.length > 0) {
126
- localStorage.setItem('anycoder_messages', JSON.stringify(messages));
127
- console.log('[localStorage] Saved', messages.length, 'messages to localStorage');
128
- }
129
- }, [messages]);
130
-
131
- // Track if we've attempted to fetch username to avoid repeated failures
132
- const usernameFetchAttemptedRef = useRef(false);
133
- // Track if backend appears to be unavailable (to avoid repeated failed requests)
134
- const backendUnavailableRef = useRef(false);
135
-
136
- // Check auth on mount and handle OAuth callback
137
- useEffect(() => {
138
- checkAuth();
139
-
140
- // Check for OAuth callback in URL (handles ?session=token)
141
- // initializeOAuth already handles this, but we call checkAuth to sync state
142
- const urlParams = new URLSearchParams(window.location.search);
143
- if (urlParams.get('session')) {
144
- // OAuth callback - reset both flags and check auth after a brief delay
145
- usernameFetchAttemptedRef.current = false;
146
- backendUnavailableRef.current = false; // Reset backend status on OAuth callback
147
- setTimeout(() => checkAuth(), 200);
148
- }
149
- }, []); // Only run once on mount
150
-
151
- // Listen for storage changes (e.g., logout from another tab)
152
- // Note: storage events only fire in OTHER tabs, not the current one
153
- useEffect(() => {
154
- const handleStorageChange = (e: StorageEvent) => {
155
- if (e.key === 'hf_oauth_token' || e.key === 'hf_user_info') {
156
- // Only reset username fetch if we have a token (might be logging in)
157
- if (e.newValue) {
158
- usernameFetchAttemptedRef.current = false;
159
- backendUnavailableRef.current = false; // Reset backend status on login
160
- }
161
- checkAuth();
162
- }
163
- };
164
-
165
- window.addEventListener('storage', handleStorageChange);
166
- return () => window.removeEventListener('storage', handleStorageChange);
167
- }, []);
168
-
169
- // Listen for authentication expiration events
170
- useEffect(() => {
171
- const handleAuthExpired = (e: CustomEvent) => {
172
- console.log('[Auth] Session expired:', e.detail?.message);
173
- // Clear authentication state
174
- setIsAuthenticated(false);
175
- setUsername(null);
176
- apiClient.setToken(null);
177
-
178
- // Show alert to user
179
- if (typeof window !== 'undefined') {
180
- alert(e.detail?.message || 'Your session has expired. Please sign in again.');
181
- }
182
- };
183
-
184
- window.addEventListener('auth-expired', handleAuthExpired as EventListener);
185
- return () => window.removeEventListener('auth-expired', handleAuthExpired as EventListener);
186
- }, []);
187
-
188
- // Listen for window focus (user returns to tab after OAuth redirect)
189
- // Only check if backend was available before or if we're authenticated with token
190
- useEffect(() => {
191
- const handleFocus = () => {
192
- // Only reset and check if we're authenticated (might have logged in elsewhere)
193
- // Don't reset if backend is known to be unavailable and we're not authenticated
194
- const authenticated = checkIsAuthenticated();
195
- if (authenticated) {
196
- usernameFetchAttemptedRef.current = false;
197
- backendUnavailableRef.current = false; // Reset backend status - might be back up
198
- }
199
- checkAuth();
200
- };
201
-
202
- window.addEventListener('focus', handleFocus);
203
- return () => window.removeEventListener('focus', handleFocus);
204
- }, []);
205
-
206
- const checkAuth = async () => {
207
- const authenticated = checkIsAuthenticated();
208
- setIsAuthenticated(authenticated);
209
-
210
- // Make sure API client has the token or clears it
211
- if (authenticated) {
212
- const token = getStoredToken();
213
- if (token) {
214
- apiClient.setToken(token);
215
-
216
- // Get username from auth status (only if we don't have it yet and backend is available)
217
- // Skip if backend is known to be unavailable to avoid repeated failed requests
218
- if (!username && !usernameFetchAttemptedRef.current && !backendUnavailableRef.current) {
219
- usernameFetchAttemptedRef.current = true;
220
- try {
221
- const authStatus = await apiClient.getAuthStatus();
222
- if (authStatus.username) {
223
- setUsername(authStatus.username);
224
- backendUnavailableRef.current = false; // Backend is working
225
- }
226
- } catch (error: any) {
227
- // Check if this is a connection error
228
- const isConnectionError =
229
- error.code === 'ECONNABORTED' ||
230
- error.code === 'ECONNRESET' ||
231
- error.code === 'ECONNREFUSED' ||
232
- error.message?.includes('socket hang up') ||
233
- error.message?.includes('timeout') ||
234
- error.message?.includes('Network Error') ||
235
- error.response?.status === 503 ||
236
- error.response?.status === 502;
237
-
238
- if (isConnectionError) {
239
- // Mark backend as unavailable to avoid repeated requests
240
- backendUnavailableRef.current = true;
241
- // Don't reset attempt flag - keep it true so we don't retry until explicitly reset
242
- // This prevents repeated failed requests when backend is down
243
- } else {
244
- // Non-connection error - log it and reset attempt flag
245
- console.error('Failed to get username:', error);
246
- usernameFetchAttemptedRef.current = false;
247
- }
248
- }
249
- }
250
- } else {
251
- // Token missing but authenticated flag is true - clear state
252
- setIsAuthenticated(false);
253
- if (username) {
254
- setUsername(null);
255
- }
256
- usernameFetchAttemptedRef.current = false;
257
- backendUnavailableRef.current = false;
258
- }
259
- } else {
260
- // Not authenticated - clear username and reset flags
261
- apiClient.setToken(null);
262
- if (username) {
263
- setUsername(null);
264
- }
265
- usernameFetchAttemptedRef.current = false;
266
- // Keep backendUnavailableRef as is - it's useful information even when not authenticated
267
- }
268
- };
269
-
270
- const handleSendMessage = async (message: string, imageUrl?: string, overrideLanguage?: Language, overrideModel?: string, overrideRepoId?: string, shouldCreatePR?: boolean) => {
271
- if (!isAuthenticated) {
272
- alert('Please sign in with HuggingFace first! Click the "Sign in with Hugging Face" button in the header.');
273
- return;
274
- }
275
-
276
- // Hide landing page and show full UI when first message is sent
277
- if (showLandingPage) {
278
- setShowLandingPage(false);
279
- }
280
-
281
- // Use override values if provided, otherwise use state
282
- const language = overrideLanguage || selectedLanguage;
283
- const model = overrideModel || selectedModel;
284
-
285
- // Update state if override values provided
286
- if (overrideLanguage) {
287
- setSelectedLanguage(overrideLanguage);
288
- }
289
- if (overrideModel) {
290
- setSelectedModel(overrideModel);
291
- }
292
-
293
- // If there's existing code, include it in the message context for modifications
294
- let enhancedMessage = message;
295
- const hasRealCode = generatedCode &&
296
- generatedCode.length > 50 &&
297
- !generatedCode.includes('Your generated code will appear here');
298
-
299
- if (hasRealCode) {
300
- enhancedMessage = `I have existing code in the editor. Please modify it based on my request.\n\nCurrent code:\n\`\`\`${language}\n${generatedCode}\n\`\`\`\n\nMy request: ${message}`;
301
- }
302
-
303
- // Add user message (show original message to user, but send enhanced to API)
304
- console.log('[handleSendMessage] Received imageUrl:', imageUrl ? 'Yes' : 'No');
305
- console.log('[handleSendMessage] Image URL length:', imageUrl?.length || 0);
306
-
307
- const userMessage: Message = {
308
- role: 'user',
309
- content: message,
310
- timestamp: new Date().toISOString(),
311
- image_url: imageUrl,
312
- };
313
- setMessages((prev) => [...prev, userMessage]);
314
- setIsGenerating(true);
315
-
316
- // Clear previous code to show streaming from start
317
- setGeneratedCode('');
318
-
319
- // Prepare request with enhanced query that includes current code
320
- // Use overrideRepoId if provided (from import/duplicate), otherwise use currentRepoId from state
321
- const effectiveRepoId = overrideRepoId || currentRepoId || undefined;
322
-
323
- console.log('[SendMessage] ========== GENERATION REQUEST ==========');
324
- console.log('[SendMessage] overrideRepoId:', overrideRepoId);
325
- console.log('[SendMessage] currentRepoId:', currentRepoId);
326
- console.log('[SendMessage] effectiveRepoId (will use):', effectiveRepoId);
327
- console.log('[SendMessage] ==========================================');
328
-
329
- console.log('[Request] Building request with imageUrl:', imageUrl ? 'Yes' : 'No');
330
- console.log('[Request] Image URL:', imageUrl?.substring(0, 50) + '...');
331
-
332
- const request: CodeGenerationRequest = {
333
- query: enhancedMessage,
334
- language: language,
335
- model_id: model,
336
- provider: 'auto',
337
- history: messages.map((m) => [m.role, m.content]),
338
- agent_mode: false,
339
- existing_repo_id: effectiveRepoId, // Pass duplicated/imported space ID for auto-deploy
340
- skip_auto_deploy: !!shouldCreatePR, // Skip auto-deploy if creating PR
341
- image_url: imageUrl, // For vision models like GLM-4.6V
342
- };
343
-
344
- const assistantMessage: Message = {
345
- role: 'assistant',
346
- content: '⏳ Generating code...',
347
- timestamp: new Date().toISOString(),
348
- };
349
-
350
- // Add placeholder for assistant message
351
- setMessages((prev) => [...prev, assistantMessage]);
352
-
353
- // Stream the response
354
- try {
355
- apiClient.generateCodeStream(
356
- request,
357
- // onChunk - Update code editor in real-time with immediate flush
358
- (chunk: string) => {
359
- console.log('[Stream] Received chunk:', chunk.substring(0, 50), '... (length:', chunk.length, ')');
360
- // Use flushSync to force immediate DOM update without React batching
361
- flushSync(() => {
362
- setGeneratedCode((prevCode) => {
363
- const newCode = prevCode + chunk;
364
- console.log('[Stream] Total code length:', newCode.length);
365
- return newCode;
366
- });
367
- });
368
- },
369
- // onComplete
370
- (code: string, reasoning?: string) => {
371
- setGeneratedCode(code);
372
- setIsGenerating(false);
373
-
374
- // Update final message - include reasoning if available
375
- setMessages((prev) => {
376
- const newMessages = [...prev];
377
- const content = reasoning
378
- ? `✅ Code generated successfully!\n\n**Reasoning:**\n${reasoning}\n\nCheck the editor →`
379
- : '✅ Code generated successfully! Check the editor →';
380
-
381
- newMessages[newMessages.length - 1] = {
382
- ...assistantMessage,
383
- content: content,
384
- };
385
- return newMessages;
386
- });
387
-
388
- // Check if we need to create a PR (redesign with PR option)
389
- console.log('[PR] onComplete - Checking pendingPR ref:', pendingPRRef.current);
390
- console.log('[PR] onComplete - Checking pendingPR state:', pendingPR);
391
- const prInfo = pendingPRRef.current;
392
- if (prInfo) {
393
- console.log('[PR] Creating pull request for:', prInfo.repoId);
394
- createPullRequestAfterGeneration(prInfo.repoId, code, prInfo.language);
395
- setPendingPR(null); // Clear state
396
- pendingPRRef.current = null; // Clear ref
397
- } else {
398
- console.log('[PR] No pending PR - skipping PR creation');
399
- }
400
- },
401
- // onError
402
- (error: string) => {
403
- setIsGenerating(false);
404
- setMessages((prev) => {
405
- const newMessages = [...prev];
406
- newMessages[newMessages.length - 1] = {
407
- ...assistantMessage,
408
- content: `❌ Error: ${error}`,
409
- };
410
- return newMessages;
411
- });
412
- },
413
- // onDeploying
414
- (message: string) => {
415
- console.log('[Deploy] Deployment started:', message);
416
- // Update message to show deployment in progress
417
- setMessages((prev) => {
418
- const newMessages = [...prev];
419
- newMessages[newMessages.length - 1] = {
420
- ...assistantMessage,
421
- content: `✅ Code generated successfully!\n\n${message}`,
422
- };
423
- return newMessages;
424
- });
425
- },
426
- // onDeployed
427
- (message: string, spaceUrl: string) => {
428
- console.log('[Deploy] Deployment successful:', spaceUrl);
429
-
430
- // Extract repo_id from space URL
431
- const match = spaceUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
432
- if (match) {
433
- setCurrentRepoId(match[1]);
434
- }
435
-
436
- // Update message with deployment success - use backend message format for history tracking
437
- setMessages((prev) => {
438
- const newMessages = [...prev];
439
- newMessages[newMessages.length - 1] = {
440
- ...assistantMessage,
441
- content: `✅ Code generated successfully!\n\n${message}`,
442
- };
443
- return newMessages;
444
- });
445
-
446
- // Open the space URL in a new tab
447
- window.open(spaceUrl, '_blank');
448
- },
449
- // onDeployError
450
- (message: string) => {
451
- console.log('[Deploy] Deployment error:', message);
452
- // Update message to show deployment failed (but code generation succeeded)
453
- setMessages((prev) => {
454
- const newMessages = [...prev];
455
- newMessages[newMessages.length - 1] = {
456
- ...assistantMessage,
457
- content: `✅ Code generated successfully!\n\n${message}\n\nYou can still use the "Publish" button to deploy manually.`,
458
- };
459
- return newMessages;
460
- });
461
- }
462
- );
463
- } catch (error) {
464
- setIsGenerating(false);
465
- setMessages((prev) => {
466
- const newMessages = [...prev];
467
- newMessages[newMessages.length - 1] = {
468
- ...assistantMessage,
469
- content: `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
470
- };
471
- return newMessages;
472
- });
473
- }
474
- };
475
-
476
- const createPullRequestAfterGeneration = async (repoId: string, code: string, language: Language) => {
477
- try {
478
- console.log('[PR] Creating PR on:', repoId);
479
-
480
- // Update message to show PR creation in progress
481
- setMessages((prev) => {
482
- const newMessages = [...prev];
483
- newMessages[newMessages.length - 1] = {
484
- ...newMessages[newMessages.length - 1],
485
- content: '✅ Code generated successfully!\n\n🔄 Creating Pull Request...',
486
- };
487
- return newMessages;
488
- });
489
-
490
- const prResult = await apiClient.createPullRequest(
491
- repoId,
492
- code,
493
- language,
494
- '🎨 Redesign from AnyCoder',
495
- undefined
496
- );
497
-
498
- if (prResult.success && prResult.pr_url) {
499
- console.log('[PR] Pull Request created:', prResult.pr_url);
500
-
501
- // Update message with PR link
502
- setMessages((prev) => {
503
- const newMessages = [...prev];
504
- newMessages[newMessages.length - 1] = {
505
- ...newMessages[newMessages.length - 1],
506
- content: `✅ Code generated successfully!\n\n✅ Pull Request created! [View PR](${prResult.pr_url})`,
507
- };
508
- return newMessages;
509
- });
510
-
511
- // Open PR in new tab
512
- window.open(prResult.pr_url, '_blank');
513
- } else {
514
- throw new Error(prResult.message || 'Failed to create Pull Request');
515
- }
516
- } catch (error: any) {
517
- console.error('[PR] Failed to create Pull Request:', error);
518
-
519
- // Update message with error
520
- setMessages((prev) => {
521
- const newMessages = [...prev];
522
- newMessages[newMessages.length - 1] = {
523
- ...newMessages[newMessages.length - 1],
524
- content: `✅ Code generated successfully!\n\n❌ Failed to create Pull Request: ${error.message || 'Unknown error'}`,
525
- };
526
- return newMessages;
527
- });
528
- }
529
- };
530
-
531
- const handleDeploy = async () => {
532
- console.log('[Deploy] 🎬 handleDeploy called');
533
- console.log('[Deploy] generatedCode exists?', !!generatedCode);
534
- console.log('[Deploy] generatedCode length:', generatedCode?.length);
535
- console.log('[Deploy] generatedCode preview:', generatedCode?.substring(0, 200));
536
-
537
- if (!generatedCode) {
538
- alert('No code to publish! Generate some code first.');
539
- return;
540
- }
541
-
542
- // Get current username (fetch if not loaded)
543
- let currentUsername = username;
544
- if (!currentUsername) {
545
- console.log('[Deploy] Username not in state, fetching from auth...');
546
- try {
547
- const authStatus = await apiClient.getAuthStatus();
548
- if (authStatus.username) {
549
- currentUsername = authStatus.username;
550
- setUsername(authStatus.username);
551
- console.log('[Deploy] Fetched username:', currentUsername);
552
- }
553
- } catch (e) {
554
- console.error('[Deploy] Could not get username:', e);
555
- // Don't fail - let backend handle auth
556
- }
557
- }
558
-
559
- // SAME LOGIC AS GRADIO VERSION: Parse message history to find existing space
560
- let existingSpace: string | null = null;
561
-
562
- // Look for previous deployment or imported space in history
563
- console.log('[Deploy] ========== DEBUG START ==========');
564
- console.log('[Deploy] Total messages in history:', messages.length);
565
- console.log('[Deploy] Current username:', currentUsername);
566
- console.log('[Deploy] Auth status:', isAuthenticated ? 'authenticated' : 'not authenticated');
567
- console.log('[Deploy] Messages:', JSON.stringify(messages, null, 2));
568
-
569
- if (messages.length > 0 && currentUsername) {
570
- console.log('[Deploy] Scanning message history FORWARD (oldest first) - MATCHING GRADIO LOGIC...');
571
- console.log('[Deploy] Total messages to scan:', messages.length);
572
-
573
- // EXACT GRADIO LOGIC: Scan forward (oldest first) and stop at first match
574
- // Gradio: for user_msg, assistant_msg in history:
575
- for (let i = 0; i < messages.length; i++) {
576
- const msg = messages[i];
577
- console.log(`[Deploy] Checking message ${i}:`, {
578
- role: msg.role,
579
- contentPreview: msg.content.substring(0, 100)
580
- });
581
-
582
- // Check assistant messages for deployment confirmations
583
- if (msg.role === 'assistant') {
584
- // Check for "✅ Deployed!" message
585
- if (msg.content.includes('✅ Deployed!')) {
586
- const match = msg.content.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
587
- if (match) {
588
- existingSpace = match[1];
589
- console.log('[Deploy] ✅ Found "✅ Deployed!" - existing_space:', existingSpace);
590
- break;
591
- }
592
- }
593
- // Check for "✅ Updated!" message
594
- else if (msg.content.includes('✅ Updated!')) {
595
- const match = msg.content.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
596
- if (match) {
597
- existingSpace = match[1];
598
- console.log('[Deploy] ✅ Found "✅ Updated!" - existing_space:', existingSpace);
599
- break;
600
- }
601
- }
602
- }
603
- // Check user messages for imports
604
- else if (msg.role === 'user' && msg.content.startsWith('Imported Space from')) {
605
- console.log('[Deploy] 🎯 Found "Imported Space from" message');
606
- const match = msg.content.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
607
- if (match) {
608
- const importedSpace = match[1];
609
- console.log('[Deploy] Extracted imported space:', importedSpace);
610
- console.log('[Deploy] Checking ownership - user:', currentUsername, 'space:', importedSpace);
611
-
612
- // Only use if user owns it (EXACT GRADIO LOGIC)
613
- if (importedSpace.startsWith(`${currentUsername}/`)) {
614
- existingSpace = importedSpace;
615
- console.log('[Deploy] ✅✅✅ USER OWNS - Will update:', existingSpace);
616
- break;
617
- } else {
618
- console.log('[Deploy] ⚠️ User does NOT own - will create new space');
619
- // existing_space remains None (create new deployment)
620
- }
621
- }
622
- }
623
- }
624
-
625
- console.log('[Deploy] Final existingSpace value:', existingSpace);
626
- } else {
627
- console.log('[Deploy] Skipping scan - no messages or no username');
628
- console.log('[Deploy] Messages length:', messages.length);
629
- console.log('[Deploy] Username:', currentUsername);
630
- }
631
- console.log('[Deploy] ========== DEBUG END ==========');
632
-
633
- // TEMPORARY DEBUG: Show what will be sent
634
- console.log('[Deploy] 🚀 ABOUT TO DEPLOY:');
635
- console.log('[Deploy] - Language:', selectedLanguage);
636
- console.log('[Deploy] - existing_repo_id:', existingSpace || 'None (new deployment)');
637
- console.log('[Deploy] - Username:', currentUsername);
638
-
639
- // Auto-generate space name (never prompt user)
640
- let spaceName = undefined; // undefined = backend will auto-generate
641
-
642
- try {
643
- console.log('[Deploy] ========== DEPLOY START (Gradio-style history parsing) ==========');
644
- console.log('[Deploy] Username:', currentUsername);
645
- console.log('[Deploy] Existing space from history:', existingSpace);
646
- console.log('[Deploy] Will create new space?', !existingSpace);
647
- console.log('[Deploy] Messages count:', messages.length);
648
- console.log('[Deploy] Messages (first 3):', messages.slice(0, 3).map(m => ({ role: m.role, content: m.content.substring(0, 100) })));
649
-
650
- // CRITICAL DEBUG: Check what we're actually sending
651
- const historyToSend = messages.map(msg => ({ role: msg.role, content: msg.content }));
652
- console.log('[Deploy] History to send (length):', historyToSend.length);
653
- console.log('[Deploy] History to send (first 2):', historyToSend.slice(0, 2));
654
- console.log('[Deploy] =================================================================');
655
-
656
- // Build deploy request, omitting undefined fields
657
- const deployRequest: any = {
658
- code: generatedCode,
659
- language: selectedLanguage,
660
- history: historyToSend // Use the variable we just logged
661
- };
662
-
663
- // Only include optional fields if they have values
664
- if (spaceName) {
665
- deployRequest.space_name = spaceName;
666
- }
667
- if (existingSpace) {
668
- deployRequest.existing_repo_id = existingSpace;
669
- deployRequest.commit_message = 'Update via AnyCoder';
670
- }
671
-
672
- console.log('[Deploy] 🚀 Sending to backend:', {
673
- existing_repo_id: deployRequest.existing_repo_id,
674
- space_name: deployRequest.space_name,
675
- language: deployRequest.language,
676
- has_code: !!deployRequest.code,
677
- code_length: deployRequest.code?.length,
678
- history_length: deployRequest.history?.length
679
- });
680
- console.log('[Deploy] Full request object:', JSON.stringify(deployRequest, null, 2).substring(0, 500));
681
-
682
- const response = await apiClient.deploy(deployRequest);
683
- console.log('[Deploy] ✅ Response received:', response);
684
-
685
- if (response.success) {
686
- // Update current repo ID if we got one back
687
- if (response.repo_id) {
688
- console.log('[Deploy] Setting currentRepoId to:', response.repo_id);
689
- setCurrentRepoId(response.repo_id);
690
- } else if (response.space_url) {
691
- // Extract repo_id from space_url as fallback
692
- const match = response.space_url.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
693
- if (match) {
694
- console.log('[Deploy] Extracted repo_id from URL:', match[1]);
695
- setCurrentRepoId(match[1]);
696
- }
697
- }
698
-
699
- // Add deployment message to chat (EXACT format backend expects)
700
- const deployMessage: Message = {
701
- role: 'assistant',
702
- content: existingSpace
703
- ? `✅ Updated! View your space at: ${response.space_url}`
704
- : `✅ Deployed! View your space at: ${response.space_url}`,
705
- timestamp: new Date().toISOString(),
706
- };
707
- setMessages((prev) => [...prev, deployMessage]);
708
-
709
- // Open the space URL in a new tab
710
- window.open(response.space_url, '_blank');
711
-
712
- // Show success message
713
- const isDev = response.dev_mode;
714
- const message = isDev
715
- ? '🚀 Opening HuggingFace Spaces creation page...\nPlease complete the space setup in the new tab.'
716
- : existingSpace
717
- ? `✅ Updated successfully!\n\nOpening: ${response.space_url}`
718
- : `✅ Published successfully!\n\nOpening: ${response.space_url}`;
719
- alert(message);
720
- } else {
721
- alert(`Deployment failed: ${response.message}`);
722
- }
723
- } catch (error: any) {
724
- console.error('[Deploy] Full error object:', error);
725
- console.error('[Deploy] Error response:', error.response);
726
- console.error('[Deploy] Error data:', error.response?.data);
727
-
728
- const errorMessage = error.response?.data?.detail
729
- || error.response?.data?.message
730
- || error.message
731
- || 'Unknown error';
732
-
733
- alert(`Deployment error: ${errorMessage}\n\nCheck console for details.`);
734
- }
735
- };
736
-
737
- const handleClear = () => {
738
- if (confirm('Start a new chat? This will clear all messages and code.')) {
739
- setMessages([]);
740
- setGeneratedCode('');
741
- setShowLandingPage(true);
742
- // Clear localStorage to remove import history
743
- if (typeof window !== 'undefined') {
744
- localStorage.removeItem('anycoder_messages');
745
- console.log('[localStorage] Cleared messages from localStorage');
746
- }
747
- }
748
- };
749
-
750
- const handleImport = (code: string, language: Language, importUrl?: string) => {
751
- console.log('[Import] ========== IMPORT START ==========');
752
- console.log('[Import] Language:', language);
753
- console.log('[Import] Import URL:', importUrl);
754
- console.log('[Import] Current username:', username);
755
- console.log('[Import] Current repo before import:', currentRepoId);
756
-
757
- // Hide landing page when importing
758
- if (showLandingPage) {
759
- setShowLandingPage(false);
760
- }
761
-
762
- setGeneratedCode(code);
763
- setSelectedLanguage(language);
764
-
765
- // Extract repo_id from import URL if provided
766
- if (importUrl) {
767
- const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
768
- console.log('[Import] Regex match result:', spaceMatch);
769
-
770
- if (spaceMatch) {
771
- const importedRepoId = spaceMatch[1];
772
- const importedUsername = importedRepoId.split('/')[0];
773
-
774
- console.log('[Import] ========================================');
775
- console.log('[Import] Extracted repo_id:', importedRepoId);
776
- console.log('[Import] Imported username:', importedUsername);
777
- console.log('[Import] Logged-in username:', username);
778
- console.log('[Import] Ownership check:', importedUsername === username);
779
- console.log('[Import] ========================================');
780
-
781
- // Only set as current repo if user owns it
782
- if (username && importedRepoId.startsWith(`${username}/`)) {
783
- console.log('[Import] ✅✅✅ BEFORE setCurrentRepoId - currentRepoId was:', currentRepoId);
784
- setCurrentRepoId(importedRepoId);
785
- console.log('[Import] ✅✅✅ CALLED setCurrentRepoId with:', importedRepoId);
786
- console.log('[Import] ✅✅✅ Note: State update is async, currentRepoId will update later');
787
- } else {
788
- // User doesn't own the imported space, clear current repo
789
- setCurrentRepoId(null);
790
- if (!username) {
791
- console.log('[Import] ⚠️⚠️⚠️ USERNAME IS NULL - Cannot set repo ownership!');
792
- } else {
793
- console.log('[Import] ⚠️ User does not own imported space:', importedRepoId, '(username:', username, ')');
794
- }
795
- }
796
- } else {
797
- console.log('[Import] ⚠️ Could not extract repo_id from URL:', importUrl);
798
- }
799
- } else {
800
- console.log('[Import] No import URL provided');
801
- }
802
-
803
- console.log('[Import] ========== IMPORT END ==========');
804
-
805
- // Add messages that include the imported code so LLM can see it
806
- const userMessage: Message = {
807
- role: 'user',
808
- content: importUrl
809
- ? `Imported Space from ${importUrl}`
810
- : `I imported a ${language} project. Here's the code that was imported.`,
811
- timestamp: new Date().toISOString(),
812
- };
813
-
814
- const assistantMessage: Message = {
815
- role: 'assistant',
816
- content: `✅ I've loaded your ${language} project. The code is now in the editor. You can ask me to:\n\n• Modify existing features\n• Add new functionality\n• Fix bugs or improve code\n• Explain how it works\n• Publish it to HuggingFace Spaces\n\nWhat would you like me to help you with?`,
817
- timestamp: new Date().toISOString(),
818
- };
819
-
820
- setMessages((prev) => [...prev, userMessage, assistantMessage]);
821
-
822
- // Switch to editor view on mobile
823
- setMobileView('editor');
824
- };
825
-
826
- // Handle landing page prompt submission
827
- const handleLandingPageStart = async (prompt: string, language: Language, modelId: string, imageUrl?: string, repoId?: string, shouldCreatePR?: boolean) => {
828
- console.log('[LandingPageStart] Received imageUrl:', imageUrl ? 'Yes' : 'No');
829
- console.log('[LandingPageStart] Image URL length:', imageUrl?.length || 0);
830
-
831
- // Hide landing page immediately for smooth transition
832
- setShowLandingPage(false);
833
-
834
- // If shouldCreatePR is true, set pending PR state and ref
835
- if (shouldCreatePR && repoId) {
836
- console.log('[PR] Setting pending PR for:', repoId);
837
- const prInfo = { repoId, language };
838
- setPendingPR(prInfo);
839
- pendingPRRef.current = prInfo; // Set ref immediately for synchronous access
840
- }
841
-
842
- // Send the message with the selected language, model, and image
843
- // Don't pass repoId to handleSendMessage when creating PR (we want to generate code first, then create PR)
844
- await handleSendMessage(prompt, imageUrl, language, modelId, shouldCreatePR ? undefined : repoId, shouldCreatePR);
845
- };
846
-
847
- // Resize handlers for chat sidebar (desktop only)
848
- const startResizingChat = () => {
849
- if (isDesktop) {
850
- setIsResizingChat(true);
851
- }
852
- };
853
-
854
- const startResizingSettings = () => {
855
- if (isDesktop) {
856
- setIsResizingSettings(true);
857
- }
858
- };
859
-
860
- // Handle mouse move for resizing (desktop only)
861
- useEffect(() => {
862
- const handleMouseMove = (e: MouseEvent) => {
863
- if (!isDesktop) return; // Skip on mobile
864
-
865
- if (isResizingChat) {
866
- const newWidth = Math.min(Math.max(e.clientX, 250), 600); // Min 250px, max 600px
867
- setChatSidebarWidth(newWidth);
868
- }
869
- if (isResizingSettings) {
870
- const newWidth = Math.min(Math.max(window.innerWidth - e.clientX, 220), 500); // Min 220px, max 500px
871
- setSettingsSidebarWidth(newWidth);
872
- }
873
- };
874
-
875
- const handleMouseUp = () => {
876
- if (isResizingChat) {
877
- setIsResizingChat(false);
878
- // Save to localStorage
879
- localStorage.setItem('anycoder_chat_sidebar_width', chatSidebarWidth.toString());
880
- document.body.classList.remove('resizing');
881
- }
882
- if (isResizingSettings) {
883
- setIsResizingSettings(false);
884
- // Save to localStorage
885
- localStorage.setItem('anycoder_settings_sidebar_width', settingsSidebarWidth.toString());
886
- document.body.classList.remove('resizing');
887
- }
888
- };
889
-
890
- if (isResizingChat || isResizingSettings) {
891
- document.addEventListener('mousemove', handleMouseMove);
892
- document.addEventListener('mouseup', handleMouseUp);
893
- // Add resizing class to body for cursor and selection styles
894
- document.body.classList.add('resizing');
895
- }
896
-
897
- return () => {
898
- document.removeEventListener('mousemove', handleMouseMove);
899
- document.removeEventListener('mouseup', handleMouseUp);
900
- document.body.classList.remove('resizing');
901
- };
902
- }, [isResizingChat, isResizingSettings, chatSidebarWidth, settingsSidebarWidth, isDesktop]);
903
-
904
- // Show landing page if no messages and showLandingPage is true
905
- if (showLandingPage && messages.length === 0) {
906
- return (
907
- <div className="min-h-screen animate-in fade-in duration-300">
908
- <LandingPage
909
- onStart={handleLandingPageStart}
910
- onImport={handleImport}
911
- isAuthenticated={isAuthenticated}
912
- initialLanguage={selectedLanguage}
913
- initialModel={selectedModel}
914
- onAuthChange={checkAuth}
915
- setPendingPR={setPendingPR}
916
- pendingPRRef={pendingPRRef}
917
- />
918
- </div>
919
- );
920
- }
921
-
922
- return (
923
- <div className="h-screen flex flex-col bg-[#000000] animate-in fade-in duration-300">
924
- <Header />
925
-
926
- {/* Apple-style layout - Responsive */}
927
- <main className="flex-1 flex overflow-hidden relative">
928
- {/* Left Sidebar - Chat Panel (Hidden on mobile, shown when mobileView='chat') */}
929
- <div
930
- className={`
931
- ${mobileView === 'chat' ? 'flex' : 'hidden'} md:flex
932
- w-full
933
- bg-[#000000] border-r border-[#424245]/30
934
- flex-col
935
- absolute md:relative inset-0 md:inset-auto z-10 md:z-auto
936
- md:flex-shrink-0
937
- `}
938
- style={isDesktop ? { width: `${chatSidebarWidth}px` } : undefined}
939
- >
940
- {/* Panel Header */}
941
- <div className="flex items-center px-4 py-3 bg-[#000000] border-b border-[#424245]/30">
942
- <span className="text-sm font-medium text-[#f5f5f7]">Chat</span>
943
- </div>
944
-
945
- {/* Chat Panel */}
946
- <div className="flex-1 overflow-hidden">
947
- <ChatInterface
948
- messages={messages}
949
- onSendMessage={handleSendMessage}
950
- isGenerating={isGenerating}
951
- isAuthenticated={isAuthenticated}
952
- supportsImages={currentModelSupportsImages}
953
- />
954
- </div>
955
- </div>
956
-
957
- {/* Resize Handle for Chat Sidebar (Desktop only) */}
958
- <div
959
- className={`hidden md:block resize-handle ${isResizingChat ? 'resizing' : ''}`}
960
- onMouseDown={startResizingChat}
961
- title="Drag to resize chat panel"
962
- />
963
-
964
- {/* Center - Editor Group (Always visible on mobile when mobileView='editor', always visible on desktop) */}
965
- <div className={`
966
- ${mobileView === 'editor' ? 'flex' : 'hidden'} md:flex
967
- flex-1 flex-col bg-[#000000]
968
- absolute md:relative inset-0 md:inset-auto z-10 md:z-auto
969
- md:min-w-0 overflow-hidden
970
- w-full
971
- `}>
972
- {/* Tab Bar */}
973
- <div className="flex items-center px-4 h-10 bg-[#1d1d1f] border-b border-[#424245]/30">
974
- <div className="flex items-center space-x-2">
975
- <div className="px-3 py-1 bg-[#2d2d2f] text-sm text-[#f5f5f7] rounded-t-lg font-normal border-t border-x border-[#424245]/50">
976
- {selectedLanguage === 'html' ? 'app.html' :
977
- selectedLanguage === 'gradio' || selectedLanguage === 'streamlit' ? 'app.py' :
978
- selectedLanguage === 'transformers.js' ? 'app.js' :
979
- selectedLanguage === 'comfyui' ? 'app.json' :
980
- selectedLanguage === 'react' ? 'app.jsx' :
981
- `${selectedLanguage}.txt`}
982
- </div>
983
- </div>
984
- <div className="ml-auto flex items-center space-x-3 text-xs text-[#86868b]">
985
- {isGenerating && (
986
- <span className="flex items-center space-x-1.5">
987
- <div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
988
- <span>Generating...</span>
989
- </span>
990
- )}
991
- <span className="font-medium">{selectedLanguage.toUpperCase()}</span>
992
- </div>
993
- </div>
994
-
995
- {/* Editor */}
996
- <div className="flex-1">
997
- <CodeEditor
998
- code={generatedCode || '// Your generated code will appear here...\n// Select a model and start chatting to generate code'}
999
- language={selectedLanguage}
1000
- onChange={setGeneratedCode}
1001
- readOnly={isGenerating}
1002
- />
1003
- </div>
1004
- </div>
1005
-
1006
- {/* Resize Handle for Settings Sidebar (Desktop only) */}
1007
- <div
1008
- className={`hidden md:block resize-handle ${isResizingSettings ? 'resizing' : ''}`}
1009
- onMouseDown={startResizingSettings}
1010
- title="Drag to resize settings panel"
1011
- />
1012
-
1013
- {/* Right Sidebar - Configuration Panel (Hidden on mobile, shown when mobileView='settings') */}
1014
- <div
1015
- className={`
1016
- ${mobileView === 'settings' ? 'flex' : 'hidden'} md:flex
1017
- w-full
1018
- bg-[#000000] border-l border-[#424245]/30
1019
- overflow-y-auto
1020
- absolute md:relative inset-0 md:inset-auto z-10 md:z-auto
1021
- flex-col
1022
- md:flex-shrink-0
1023
- `}
1024
- style={isDesktop ? { width: `${settingsSidebarWidth}px` } : undefined}
1025
- >
1026
- <ControlPanel
1027
- selectedLanguage={selectedLanguage}
1028
- selectedModel={selectedModel}
1029
- onLanguageChange={setSelectedLanguage}
1030
- onModelChange={setSelectedModel}
1031
- onClear={handleClear}
1032
- isGenerating={isGenerating}
1033
- />
1034
- </div>
1035
- </main>
1036
-
1037
- {/* Mobile Bottom Navigation (visible only on mobile) */}
1038
- <nav className="md:hidden bg-[#000000]/95 backdrop-blur-xl border-t border-[#424245]/20 flex items-center justify-around h-14 px-2 safe-area-bottom">
1039
- <button
1040
- onClick={() => setMobileView('chat')}
1041
- className={`flex flex-col items-center justify-center flex-1 py-1.5 transition-all ${mobileView === 'chat'
1042
- ? 'text-white'
1043
- : 'text-[#86868b]'
1044
- }`}
1045
- >
1046
- <svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
1047
- <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
1048
- </svg>
1049
- <span className="text-[10px]">Chat</span>
1050
- </button>
1051
-
1052
- <button
1053
- onClick={() => setMobileView('editor')}
1054
- className={`flex flex-col items-center justify-center flex-1 py-1.5 transition-all ${mobileView === 'editor'
1055
- ? 'text-white'
1056
- : 'text-[#86868b]'
1057
- }`}
1058
- >
1059
- <svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
1060
- <path strokeLinecap="round" strokeLinejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
1061
- </svg>
1062
- <span className="text-[10px]">Code</span>
1063
- </button>
1064
-
1065
- <button
1066
- onClick={() => setMobileView('settings')}
1067
- className={`flex flex-col items-center justify-center flex-1 py-1.5 transition-all ${mobileView === 'settings'
1068
- ? 'text-white'
1069
- : 'text-[#86868b]'
1070
- }`}
1071
- >
1072
- <svg className="w-5 h-5 mb-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
1073
- <path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
1074
- <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
1075
- </svg>
1076
- <span className="text-[10px]">Settings</span>
1077
- </button>
1078
- </nav>
1079
-
1080
- {/* Status Bar - Apple style (hidden on mobile) */}
1081
- <footer className="hidden md:flex h-6 bg-[#000000] border-t border-[#424245]/20 text-[#86868b] text-[11px] items-center px-4 justify-between">
1082
- <div className="flex items-center space-x-4">
1083
- <span>AnyCoder</span>
1084
- <span className="flex items-center gap-1.5">
1085
- {isAuthenticated ? (
1086
- <>
1087
- <span className="w-1.5 h-1.5 bg-[#30d158] rounded-full"></span>
1088
- <span>Connected</span>
1089
- </>
1090
- ) : (
1091
- <>
1092
- <span className="w-1.5 h-1.5 bg-[#ff9f0a] rounded-full"></span>
1093
- <span>Not authenticated</span>
1094
- </>
1095
- )}
1096
- </span>
1097
- </div>
1098
- <div className="flex items-center space-x-4">
1099
- <span>{messages.length} messages</span>
1100
- </div>
1101
- </footer>
1102
- </div>
1103
- );
1104
- }
1105
-
1106
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ChatInterface.tsx DELETED
@@ -1,212 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useRef, useEffect } from 'react';
4
- import type { Message } from '@/types';
5
- import ReactMarkdown from 'react-markdown';
6
- import remarkGfm from 'remark-gfm';
7
- import Image from 'next/image';
8
-
9
- interface ChatInterfaceProps {
10
- messages: Message[];
11
- onSendMessage: (message: string, imageUrl?: string) => void;
12
- isGenerating: boolean;
13
- isAuthenticated?: boolean;
14
- supportsImages?: boolean;
15
- }
16
-
17
- export default function ChatInterface({ messages, onSendMessage, isGenerating, isAuthenticated = false, supportsImages = false }: ChatInterfaceProps) {
18
- const [input, setInput] = useState('');
19
- const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
20
- const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
21
- const fileInputRef = useRef<HTMLInputElement>(null);
22
- const messagesEndRef = useRef<HTMLDivElement>(null);
23
-
24
- const scrollToBottom = () => {
25
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
26
- };
27
-
28
- useEffect(() => {
29
- scrollToBottom();
30
- }, [messages]);
31
-
32
- const handleSubmit = (e: React.FormEvent) => {
33
- e.preventDefault();
34
- if (input.trim() && !isGenerating) {
35
- onSendMessage(input, uploadedImageUrl || undefined);
36
- setInput('');
37
- setUploadedImageUrl(null);
38
- setUploadedImageFile(null);
39
- }
40
- };
41
-
42
- const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
43
- const file = e.target.files?.[0];
44
- if (file) {
45
- // Create a data URL for the image
46
- const reader = new FileReader();
47
- reader.onload = (event) => {
48
- const imageUrl = event.target?.result as string;
49
- setUploadedImageUrl(imageUrl);
50
- setUploadedImageFile(file);
51
- };
52
- reader.readAsDataURL(file);
53
- }
54
- };
55
-
56
- const removeImage = () => {
57
- setUploadedImageUrl(null);
58
- setUploadedImageFile(null);
59
- if (fileInputRef.current) {
60
- fileInputRef.current.value = '';
61
- }
62
- };
63
-
64
- return (
65
- <div className="flex flex-col h-full bg-[#000000]">
66
- {/* Messages */}
67
- <div className="flex-1 overflow-y-auto p-4 space-y-3">
68
- {messages.length === 0 ? (
69
- <div className="text-center text-[#86868b] mt-12">
70
- {isAuthenticated ? (
71
- <>
72
- <p className="text-lg font-medium text-[#f5f5f7]">Start a conversation</p>
73
- <p className="text-sm mt-2 text-[#86868b]">Describe what you want to build</p>
74
- </>
75
- ) : (
76
- <>
77
- <p className="text-lg font-medium text-[#f5f5f7]">Sign in to get started</p>
78
- <p className="text-sm mt-2 text-[#86868b]">Use Dev Login or sign in with Hugging Face</p>
79
- </>
80
- )}
81
- </div>
82
- ) : (
83
- messages.map((message, index) => (
84
- <div
85
- key={index}
86
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
87
- >
88
- <div
89
- className={`max-w-[85%] rounded-2xl px-4 py-3 ${
90
- message.role === 'user'
91
- ? 'bg-white text-black'
92
- : 'bg-[#2d2d2f] text-[#f5f5f7]'
93
- }`}
94
- >
95
- {message.image_url && message.role === 'user' && (
96
- <div className="mb-2">
97
- <Image
98
- src={message.image_url}
99
- alt="Uploaded image"
100
- width={200}
101
- height={200}
102
- className="rounded-lg object-cover max-w-full h-auto"
103
- unoptimized
104
- />
105
- </div>
106
- )}
107
- <div className="text-sm leading-relaxed">
108
- {message.role === 'assistant' ? (
109
- <ReactMarkdown
110
- remarkPlugins={[remarkGfm]}
111
- className="prose prose-invert prose-sm max-w-none [&>p]:my-0 [&>ul]:my-1 [&>ol]:my-1"
112
- components={{
113
- a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />
114
- }}
115
- >
116
- {message.content}
117
- </ReactMarkdown>
118
- ) : (
119
- <p className="whitespace-pre-wrap break-words">{message.content}</p>
120
- )}
121
- </div>
122
- {message.timestamp && (
123
- <div className="text-[10px] opacity-40 mt-2 text-right">
124
- {new Date(message.timestamp).toLocaleTimeString()}
125
- </div>
126
- )}
127
- </div>
128
- </div>
129
- ))
130
- )}
131
- <div ref={messagesEndRef} />
132
- </div>
133
-
134
- {/* Input */}
135
- <div className="border-t border-[#424245]/30 p-3 bg-[#000000]">
136
- {/* Image Preview */}
137
- {uploadedImageUrl && (
138
- <div className="mb-2 relative inline-block">
139
- <Image
140
- src={uploadedImageUrl}
141
- alt="Upload preview"
142
- width={120}
143
- height={120}
144
- className="rounded-lg object-cover"
145
- unoptimized
146
- />
147
- <button
148
- type="button"
149
- onClick={removeImage}
150
- className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all flex items-center justify-center text-xs font-bold"
151
- >
152
- ×
153
- </button>
154
- </div>
155
- )}
156
-
157
- <form onSubmit={handleSubmit} className="flex items-center gap-2">
158
- <input
159
- type="text"
160
- value={input}
161
- onChange={(e) => setInput(e.target.value)}
162
- placeholder={isAuthenticated ? "Message AnyCoder..." : "Sign in first..."}
163
- disabled={isGenerating || !isAuthenticated}
164
- className="flex-1 px-4 py-2.5 bg-[#2d2d2f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-full focus:outline-none focus:border-[#424245] disabled:opacity-40 disabled:cursor-not-allowed placeholder-[#86868b]"
165
- />
166
-
167
- {/* Image Upload Button (only show if model supports images) */}
168
- {supportsImages && (
169
- <>
170
- <input
171
- ref={fileInputRef}
172
- type="file"
173
- accept="image/*"
174
- onChange={handleImageUpload}
175
- className="hidden"
176
- disabled={isGenerating || !isAuthenticated}
177
- />
178
- <button
179
- type="button"
180
- onClick={() => fileInputRef.current?.click()}
181
- disabled={isGenerating || !isAuthenticated}
182
- className="p-2.5 bg-[#2d2d2f] text-[#f5f5f7] rounded-full hover:bg-[#424245] disabled:opacity-40 disabled:cursor-not-allowed transition-all active:scale-95 flex-shrink-0"
183
- title="Upload image"
184
- >
185
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
186
- <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
187
- </svg>
188
- </button>
189
- </>
190
- )}
191
-
192
- <button
193
- type="submit"
194
- disabled={isGenerating || !input.trim() || !isAuthenticated}
195
- className="p-2.5 bg-white text-black rounded-full hover:bg-[#f5f5f7] disabled:bg-[#2d2d2f] disabled:text-[#86868b] disabled:cursor-not-allowed transition-all active:scale-95 flex-shrink-0"
196
- >
197
- {isGenerating ? (
198
- <svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
199
- <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
200
- </svg>
201
- ) : (
202
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
203
- <path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14M12 5l7 7-7 7" />
204
- </svg>
205
- )}
206
- </button>
207
- </form>
208
- </div>
209
- </div>
210
- );
211
- }
212
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/CodeEditor.tsx DELETED
@@ -1,101 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect, useRef } from 'react';
4
- import Editor from '@monaco-editor/react';
5
-
6
- interface CodeEditorProps {
7
- code: string;
8
- language: string;
9
- onChange?: (value: string) => void;
10
- readOnly?: boolean;
11
- }
12
-
13
- export default function CodeEditor({ code, language, onChange, readOnly = false }: CodeEditorProps) {
14
- const editorRef = useRef<any>(null);
15
- const lastFormattedCodeRef = useRef<string>('');
16
- const formatTimeoutRef = useRef<NodeJS.Timeout | null>(null);
17
-
18
- // Map our language names to Monaco language IDs
19
- const getMonacoLanguage = (lang: string): string => {
20
- const languageMap: Record<string, string> = {
21
- 'html': 'html',
22
- 'gradio': 'python',
23
- 'streamlit': 'python',
24
- 'transformers.js': 'html', // Contains HTML, CSS, and JavaScript - HTML is primary
25
- 'react': 'javascriptreact', // JSX syntax highlighting
26
- 'comfyui': 'json',
27
- };
28
- return languageMap[lang] || 'plaintext';
29
- };
30
-
31
- const handleEditorDidMount = (editor: any) => {
32
- editorRef.current = editor;
33
- };
34
-
35
- // Format code intelligently - only when generation appears complete
36
- useEffect(() => {
37
- if (editorRef.current && code && code.length > 100) {
38
- // Clear existing timeout
39
- if (formatTimeoutRef.current) {
40
- clearTimeout(formatTimeoutRef.current);
41
- }
42
-
43
- // Only format if code hasn't been formatted yet or if it's different
44
- if (code !== lastFormattedCodeRef.current) {
45
- // Wait 1 second after code stops changing before formatting
46
- formatTimeoutRef.current = setTimeout(() => {
47
- if (editorRef.current) {
48
- editorRef.current.getAction('editor.action.formatDocument')?.run();
49
- lastFormattedCodeRef.current = code;
50
- }
51
- }, 1000);
52
- }
53
- }
54
-
55
- return () => {
56
- if (formatTimeoutRef.current) {
57
- clearTimeout(formatTimeoutRef.current);
58
- }
59
- };
60
- }, [code]);
61
-
62
- return (
63
- <div className="h-full overflow-hidden bg-[#1e1e1e]">
64
- <Editor
65
- height="100%"
66
- language={getMonacoLanguage(language)}
67
- value={code}
68
- onChange={(value) => onChange && onChange(value || '')}
69
- theme="vs-dark"
70
- options={{
71
- readOnly,
72
- minimap: { enabled: true },
73
- fontSize: 14,
74
- fontFamily: "'SF Mono', 'JetBrains Mono', 'Menlo', 'Monaco', 'Courier New', monospace",
75
- wordWrap: 'off',
76
- lineNumbers: 'on',
77
- lineNumbersMinChars: 3,
78
- glyphMargin: false,
79
- folding: true,
80
- lineDecorationsWidth: 10,
81
- scrollBeyondLastLine: false,
82
- automaticLayout: true,
83
- tabSize: 2,
84
- insertSpaces: true,
85
- padding: { top: 16, bottom: 16 },
86
- lineHeight: 22,
87
- letterSpacing: 0.5,
88
- renderLineHighlight: 'line',
89
- formatOnPaste: true,
90
- formatOnType: false,
91
- scrollbar: {
92
- verticalScrollbarSize: 10,
93
- horizontalScrollbarSize: 10,
94
- },
95
- }}
96
- onMount={handleEditorDidMount}
97
- />
98
- </div>
99
- );
100
- }
101
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ControlPanel.tsx DELETED
@@ -1,244 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useRef } from 'react';
4
- import { apiClient } from '@/lib/api';
5
- import type { Model, Language } from '@/types';
6
-
7
- interface ControlPanelProps {
8
- selectedLanguage: Language;
9
- selectedModel: string;
10
- onLanguageChange: (language: Language) => void;
11
- onModelChange: (modelId: string) => void;
12
- onClear: () => void;
13
- isGenerating: boolean;
14
- }
15
-
16
- export default function ControlPanel({
17
- selectedLanguage,
18
- selectedModel,
19
- onLanguageChange,
20
- onModelChange,
21
- onClear,
22
- isGenerating,
23
- }: ControlPanelProps) {
24
- const [models, setModels] = useState<Model[]>([]);
25
- const [languages, setLanguages] = useState<Language[]>([]);
26
- const [isLoading, setIsLoading] = useState(true);
27
-
28
- // Dropdown states
29
- const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
30
- const [showModelDropdown, setShowModelDropdown] = useState(false);
31
- const languageDropdownRef = useRef<HTMLDivElement>(null);
32
- const modelDropdownRef = useRef<HTMLDivElement>(null);
33
-
34
- useEffect(() => {
35
- loadData();
36
- }, []);
37
-
38
- // Close dropdowns when clicking outside
39
- useEffect(() => {
40
- const handleClickOutside = (event: MouseEvent) => {
41
- if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) {
42
- setShowLanguageDropdown(false);
43
- }
44
- if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
45
- setShowModelDropdown(false);
46
- }
47
- };
48
-
49
- document.addEventListener('mousedown', handleClickOutside);
50
- return () => {
51
- document.removeEventListener('mousedown', handleClickOutside);
52
- };
53
- }, []);
54
-
55
- const loadData = async () => {
56
- setIsLoading(true);
57
- await Promise.all([loadModels(), loadLanguages()]);
58
- setIsLoading(false);
59
- };
60
-
61
- const loadModels = async () => {
62
- try {
63
- console.log('Loading models...');
64
- const modelsList = await apiClient.getModels();
65
- console.log('Models loaded:', modelsList);
66
- setModels(modelsList);
67
- } catch (error) {
68
- console.error('Failed to load models:', error);
69
- }
70
- };
71
-
72
- const loadLanguages = async () => {
73
- try {
74
- console.log('Loading languages...');
75
- const { languages: languagesList } = await apiClient.getLanguages();
76
- console.log('Languages loaded:', languagesList);
77
- setLanguages(languagesList);
78
- } catch (error) {
79
- console.error('Failed to load languages:', error);
80
- }
81
- };
82
-
83
- const formatLanguageName = (lang: Language) => {
84
- if (lang === 'html') return 'HTML';
85
- if (lang === 'transformers.js') return 'Transformers.js';
86
- if (lang === 'comfyui') return 'ComfyUI';
87
- if (lang === 'daggr') return 'Daggr';
88
- return lang.charAt(0).toUpperCase() + lang.slice(1);
89
- };
90
-
91
- const formatModelName = (name: string, id: string) => {
92
- if (id === 'moonshotai/Kimi-K2.6') return 'Kimi-K2.6 ✨';
93
- if (id === 'google/gemma-4-31B-it') return 'Gemma-4-31B 🤖';
94
- if (id === 'zai-org/GLM-5.1') return 'GLM-5.1 🚀';
95
- if (id === 'Qwen/Qwen3.5-397B-A17B') return 'Qwen3.5-397B-A17B 🤖';
96
- return name;
97
- };
98
-
99
- return (
100
- <div className="bg-[#000000] h-full flex flex-col">
101
- {/* Panel Header */}
102
- <div className="flex items-center px-4 py-3 border-b border-[#424245]/30">
103
- <h3 className="text-sm font-medium text-[#f5f5f7]">Settings</h3>
104
- </div>
105
-
106
- {/* Content */}
107
- <div className="flex-1 p-4 space-y-5 overflow-y-auto">
108
-
109
- {/* Language Selection */}
110
- <div className="relative" ref={languageDropdownRef}>
111
- <label className="block text-xs font-medium text-[#f5f5f7] mb-2">
112
- Language
113
- </label>
114
- <button
115
- type="button"
116
- onClick={() => {
117
- setShowLanguageDropdown(!showLanguageDropdown);
118
- setShowModelDropdown(false);
119
- }}
120
- disabled={isGenerating || isLoading}
121
- className="w-full px-3 py-2 bg-[#1d1d1f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-lg focus:outline-none focus:border-[#424245] disabled:opacity-40 flex items-center justify-between hover:bg-[#2d2d2f] transition-colors"
122
- >
123
- <span>{isLoading ? 'Loading...' : formatLanguageName(selectedLanguage)}</span>
124
- <svg
125
- className={`w-3.5 h-3.5 text-[#86868b] transition-transform ${showLanguageDropdown ? 'rotate-180' : ''}`}
126
- fill="none"
127
- stroke="currentColor"
128
- viewBox="0 0 24 24"
129
- strokeWidth={2.5}
130
- >
131
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
132
- </svg>
133
- </button>
134
-
135
- {/* Language Dropdown Tray */}
136
- {showLanguageDropdown && !isLoading && languages.length > 0 && (
137
- <div className="absolute z-50 w-full mt-1 bg-[#1d1d1f] border border-[#424245] rounded-lg shadow-xl overflow-hidden">
138
- <div className="max-h-64 overflow-y-auto py-1">
139
- {languages.map((lang) => (
140
- <button
141
- key={lang}
142
- type="button"
143
- onClick={() => {
144
- onLanguageChange(lang);
145
- setShowLanguageDropdown(false);
146
- }}
147
- className={`w-full px-3 py-2 text-left text-sm text-[#f5f5f7] hover:bg-[#2d2d2f] transition-colors ${selectedLanguage === lang ? 'bg-[#2d2d2f]' : ''
148
- }`}
149
- >
150
- {formatLanguageName(lang)}
151
- </button>
152
- ))}
153
- </div>
154
- </div>
155
- )}
156
- </div>
157
-
158
- {/* Model Selection */}
159
- <div className="relative" ref={modelDropdownRef}>
160
- <label className="block text-xs font-medium text-[#f5f5f7] mb-2">
161
- AI Model
162
- </label>
163
- <button
164
- type="button"
165
- onClick={() => {
166
- setShowModelDropdown(!showModelDropdown);
167
- setShowLanguageDropdown(false);
168
- }}
169
- disabled={isGenerating}
170
- className="w-full px-3 py-2 bg-[#1d1d1f] text-[#f5f5f7] text-sm border border-[#424245]/50 rounded-lg focus:outline-none focus:border-[#424245] disabled:opacity-40 flex items-center justify-between hover:bg-[#2d2d2f] transition-colors"
171
- >
172
- {isLoading
173
- ? 'Loading...'
174
- : formatModelName(models.find(m => m.id === selectedModel)?.name || '', selectedModel) || selectedModel || 'Select model'
175
- }
176
- <svg
177
- className={`w-3.5 h-3.5 text-[#86868b] flex-shrink-0 ml-2 transition-transform ${showModelDropdown ? 'rotate-180' : ''}`}
178
- fill="none"
179
- stroke="currentColor"
180
- viewBox="0 0 24 24"
181
- strokeWidth={2.5}
182
- >
183
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
184
- </svg>
185
- </button>
186
-
187
- {/* Model Dropdown Tray */}
188
- {showModelDropdown && models.length > 0 && (
189
- <div className="absolute z-50 w-full mt-1 bg-[#1d1d1f] border border-[#424245] rounded-lg shadow-xl overflow-hidden">
190
- <div className="max-h-96 overflow-y-auto py-1">
191
- {models.map((model) => (
192
- <button
193
- key={model.id}
194
- type="button"
195
- onClick={() => {
196
- onModelChange(model.id);
197
- setShowModelDropdown(false);
198
- }}
199
- className={`w-full px-3 py-2 text-left transition-colors ${selectedModel === model.id
200
- ? 'bg-[#2d2d2f]'
201
- : 'hover:bg-[#2d2d2f]'
202
- }`}
203
- >
204
- <div className="flex items-center justify-between gap-2">
205
- <span className="text-sm text-[#f5f5f7]">{formatModelName(model.name, model.id)}</span>
206
- {['moonshotai/Kimi-K2.6', 'google/gemma-4-31B-it', 'zai-org/GLM-5.1', 'Qwen/Qwen3.5-397B-A17B', 'MiniMaxAI/MiniMax-M2.5'].includes(model.id) && (
207
- <span className="px-1.5 py-0.5 bg-gradient-to-r from-purple-500 to-pink-500 text-white text-[9px] font-bold rounded uppercase flex-shrink-0">
208
- NEW
209
- </span>
210
- )}
211
- </div>
212
- {model.description && (
213
- <div className="text-[10px] text-[#86868b] mt-0.5 leading-relaxed">
214
- {model.description}
215
- </div>
216
- )}
217
- </button>
218
- ))}
219
- </div>
220
- </div>
221
- )}
222
-
223
- {/* Model Description */}
224
- {!isLoading && models.find(m => m.id === selectedModel) && (
225
- <p className="text-[10px] text-[#86868b] mt-2 leading-relaxed">
226
- {models.find(m => m.id === selectedModel)?.description}
227
- </p>
228
- )}
229
- </div>
230
-
231
- {/* Action Buttons */}
232
- <div className="flex flex-col space-y-2">
233
- <button
234
- onClick={onClear}
235
- disabled={isGenerating}
236
- className="w-full px-3 py-2.5 bg-[#1d1d1f] text-[#f5f5f7] text-sm rounded-full hover:bg-[#2d2d2f] disabled:opacity-30 disabled:cursor-not-allowed transition-all font-medium border border-[#424245]/50 flex items-center justify-center active:scale-95"
237
- >
238
- New Chat
239
- </button>
240
- </div>
241
- </div>
242
- </div>
243
- );
244
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/Header.tsx DELETED
@@ -1,173 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect } from 'react';
4
- import {
5
- initializeOAuth,
6
- loginWithHuggingFace,
7
- loginDevMode,
8
- logout,
9
- getStoredUserInfo,
10
- isAuthenticated,
11
- isDevelopmentMode
12
- } from '@/lib/auth';
13
- import { apiClient } from '@/lib/api';
14
- import type { OAuthUserInfo } from '@/lib/auth';
15
-
16
- export default function Header() {
17
- const [userInfo, setUserInfo] = useState<OAuthUserInfo | null>(null);
18
- const [isLoading, setIsLoading] = useState(true);
19
- const [showDevLogin, setShowDevLogin] = useState(false);
20
- const [devUsername, setDevUsername] = useState('');
21
- const isDevMode = isDevelopmentMode();
22
-
23
- useEffect(() => {
24
- handleOAuthInit();
25
- }, []);
26
-
27
- const handleOAuthInit = async () => {
28
- setIsLoading(true);
29
- try {
30
- const oauthResult = await initializeOAuth();
31
-
32
- if (oauthResult) {
33
- setUserInfo(oauthResult.userInfo);
34
- // Set token in API client
35
- apiClient.setToken(oauthResult.accessToken);
36
- } else {
37
- // Check if we have stored user info
38
- const storedUserInfo = getStoredUserInfo();
39
- if (storedUserInfo) {
40
- setUserInfo(storedUserInfo);
41
- }
42
- }
43
- } catch (error) {
44
- console.error('OAuth initialization error:', error);
45
- } finally {
46
- setIsLoading(false);
47
- }
48
- };
49
-
50
- const handleLogin = async () => {
51
- try {
52
- await loginWithHuggingFace();
53
- } catch (error) {
54
- console.error('Login failed:', error);
55
- alert('Failed to start login process. Please try again.');
56
- }
57
- };
58
-
59
- const handleLogout = () => {
60
- logout();
61
- apiClient.logout();
62
- setUserInfo(null);
63
- // Reload page to clear state
64
- window.location.reload();
65
- };
66
-
67
- const handleDevLogin = () => {
68
- if (!devUsername.trim()) {
69
- alert('Please enter a username');
70
- return;
71
- }
72
-
73
- try {
74
- const result = loginDevMode(devUsername);
75
- setUserInfo(result.userInfo);
76
- apiClient.setToken(result.accessToken);
77
- setShowDevLogin(false);
78
- setDevUsername('');
79
- } catch (error) {
80
- console.error('Dev login failed:', error);
81
- alert('Failed to login in dev mode');
82
- }
83
- };
84
-
85
- return (
86
- <header className="bg-[#000000]/80 backdrop-blur-xl text-white border-b border-[#424245]/30">
87
- <div className="flex items-center justify-between px-3 md:px-6 h-12 md:h-14">
88
- <div className="flex items-center space-x-2 md:space-x-3">
89
- <h1 className="text-sm md:text-base font-medium text-[#f5f5f7]">AnyCoder</h1>
90
- </div>
91
-
92
- <div className="flex items-center space-x-3">
93
- {isLoading ? (
94
- <span className="text-xs text-[#86868b]">Loading...</span>
95
- ) : userInfo ? (
96
- <div className="flex items-center space-x-2 md:space-x-3">
97
- {userInfo.avatarUrl && (
98
- <img
99
- src={userInfo.avatarUrl}
100
- alt={userInfo.name}
101
- className="w-6 h-6 md:w-7 md:h-7 rounded-full"
102
- />
103
- )}
104
- <span className="hidden sm:inline text-xs md:text-sm text-[#f5f5f7] font-medium truncate max-w-[100px] md:max-w-none">
105
- {userInfo.preferredUsername || userInfo.name}
106
- </span>
107
- <button
108
- onClick={handleLogout}
109
- className="px-3 md:px-3 py-1.5 md:py-1.5 text-[#f5f5f7] text-sm hover:text-white transition-colors"
110
- >
111
- Logout
112
- </button>
113
- </div>
114
- ) : (
115
- <div className="flex items-center space-x-2 md:space-x-3">
116
- {/* Dev Mode Login (only on localhost) */}
117
- {isDevMode && (
118
- <>
119
- {showDevLogin ? (
120
- <div className="flex items-center space-x-2">
121
- <input
122
- type="text"
123
- value={devUsername}
124
- onChange={(e) => setDevUsername(e.target.value)}
125
- onKeyPress={(e) => e.key === 'Enter' && handleDevLogin()}
126
- placeholder="username"
127
- className="px-3 py-1.5 rounded-lg text-sm bg-[#1d1d1f] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 w-32 font-medium"
128
- autoFocus
129
- />
130
- <button
131
- onClick={handleDevLogin}
132
- className="px-3 py-1.5 bg-white text-black rounded-lg text-sm hover:bg-[#f5f5f7] font-medium"
133
- >
134
- OK
135
- </button>
136
- <button
137
- onClick={() => {
138
- setShowDevLogin(false);
139
- setDevUsername('');
140
- }}
141
- className="text-[#86868b] hover:text-[#f5f5f7] text-sm"
142
- >
143
-
144
- </button>
145
- </div>
146
- ) : (
147
- <button
148
- onClick={() => setShowDevLogin(true)}
149
- className="px-3 py-1.5 text-sm text-[#f5f5f7] hover:text-white transition-colors"
150
- title="Dev Mode"
151
- >
152
- Dev
153
- </button>
154
- )}
155
- <span className="text-[#86868b] text-sm">or</span>
156
- </>
157
- )}
158
-
159
- {/* OAuth Login */}
160
- <button
161
- onClick={handleLogin}
162
- className="px-3 md:px-4 py-1.5 md:py-2 bg-white text-black rounded-full text-sm hover:bg-[#f5f5f7] transition-all font-medium"
163
- >
164
- Sign in
165
- </button>
166
- </div>
167
- )}
168
- </div>
169
- </div>
170
- </header>
171
- );
172
- }
173
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/LandingPage.tsx DELETED
@@ -1,1200 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useRef } from 'react';
4
- import Image from 'next/image';
5
- import { apiClient } from '@/lib/api';
6
- import {
7
- initializeOAuth,
8
- loginWithHuggingFace,
9
- loginDevMode,
10
- logout,
11
- getStoredUserInfo,
12
- isAuthenticated as checkIsAuthenticated,
13
- isDevelopmentMode
14
- } from '@/lib/auth';
15
- import type { Model, Language } from '@/types';
16
- import type { OAuthUserInfo } from '@/lib/auth';
17
-
18
- interface LandingPageProps {
19
- onStart: (prompt: string, language: Language, modelId: string, imageUrl?: string, repoId?: string, shouldCreatePR?: boolean) => void;
20
- onImport?: (code: string, language: Language, importUrl?: string) => void;
21
- isAuthenticated: boolean;
22
- initialLanguage?: Language;
23
- initialModel?: string;
24
- onAuthChange?: () => void;
25
- setPendingPR?: (pr: { repoId: string; language: Language } | null) => void;
26
- pendingPRRef?: React.MutableRefObject<{ repoId: string; language: Language } | null>;
27
- }
28
-
29
- export default function LandingPage({
30
- onStart,
31
- onImport,
32
- isAuthenticated,
33
- initialLanguage = 'html',
34
- initialModel = 'moonshotai/Kimi-K2.6',
35
- onAuthChange,
36
- setPendingPR,
37
- pendingPRRef
38
- }: LandingPageProps) {
39
- const [prompt, setPrompt] = useState('');
40
- const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLanguage);
41
- const [selectedModel, setSelectedModel] = useState<string>(initialModel);
42
- const [models, setModels] = useState<Model[]>([]);
43
- const [languages, setLanguages] = useState<Language[]>([]);
44
- const [isLoading, setIsLoading] = useState(true);
45
-
46
- // Auth states
47
- const [userInfo, setUserInfo] = useState<OAuthUserInfo | null>(null);
48
- const [isAuthLoading, setIsAuthLoading] = useState(true);
49
- const [showDevLogin, setShowDevLogin] = useState(false);
50
- const [devUsername, setDevUsername] = useState('');
51
- const isDevMode = isDevelopmentMode();
52
-
53
- // Dropdown states
54
- const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
55
- const [showModelDropdown, setShowModelDropdown] = useState(false);
56
- const [showImportDialog, setShowImportDialog] = useState(false);
57
- const [showRedesignDialog, setShowRedesignDialog] = useState(false);
58
- const languageDropdownRef = useRef<HTMLDivElement>(null);
59
- const modelDropdownRef = useRef<HTMLDivElement>(null);
60
- const importDialogRef = useRef<HTMLDivElement>(null);
61
- const redesignDialogRef = useRef<HTMLDivElement>(null);
62
-
63
- // Trending apps state
64
- const [trendingApps, setTrendingApps] = useState<any[]>([]);
65
-
66
- // Import project state
67
- const [importUrl, setImportUrl] = useState('');
68
- const [isImporting, setIsImporting] = useState(false);
69
- const [importError, setImportError] = useState('');
70
- const [importAction, setImportAction] = useState<'duplicate' | 'update' | 'pr'>('duplicate'); // Default to duplicate
71
- const [isSpaceOwner, setIsSpaceOwner] = useState(false); // Track if user owns the space
72
-
73
- // Redesign project state
74
- const [redesignUrl, setRedesignUrl] = useState('');
75
- const [isRedesigning, setIsRedesigning] = useState(false);
76
- const [redesignError, setRedesignError] = useState('');
77
- const [createPR, setCreatePR] = useState(false); // Default to normal redesign (not PR)
78
-
79
- // Image upload state
80
- const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
81
- const fileInputRef = useRef<HTMLInputElement>(null);
82
-
83
- // Debug effect for dropdown state
84
- useEffect(() => {
85
- console.log('showModelDropdown state changed to:', showModelDropdown);
86
- }, [showModelDropdown]);
87
-
88
- // Debug effect for models state
89
- useEffect(() => {
90
- console.log('models state changed, length:', models.length, 'models:', models);
91
- }, [models]);
92
-
93
- useEffect(() => {
94
- console.log('Component mounted, initial load starting...');
95
- loadData();
96
- handleOAuthInit();
97
- loadTrendingApps();
98
- // Check auth status periodically to catch OAuth redirects
99
- const interval = setInterval(() => {
100
- const authenticated = checkIsAuthenticated();
101
- if (authenticated && !userInfo) {
102
- handleOAuthInit();
103
- }
104
- }, 1000);
105
- return () => clearInterval(interval);
106
- }, []);
107
-
108
- const handleOAuthInit = async () => {
109
- setIsAuthLoading(true);
110
- try {
111
- const oauthResult = await initializeOAuth();
112
-
113
- if (oauthResult) {
114
- setUserInfo(oauthResult.userInfo);
115
- apiClient.setToken(oauthResult.accessToken);
116
- if (onAuthChange) onAuthChange();
117
- } else {
118
- const storedUserInfo = getStoredUserInfo();
119
- if (storedUserInfo) {
120
- setUserInfo(storedUserInfo);
121
- }
122
- }
123
- } catch (error) {
124
- console.error('OAuth initialization error:', error);
125
- } finally {
126
- setIsAuthLoading(false);
127
- }
128
- };
129
-
130
- const handleLogin = async () => {
131
- try {
132
- await loginWithHuggingFace();
133
- } catch (error) {
134
- console.error('Login failed:', error);
135
- alert('Failed to start login process. Please try again.');
136
- }
137
- };
138
-
139
- const handleLogout = () => {
140
- logout();
141
- apiClient.logout();
142
- setUserInfo(null);
143
- if (onAuthChange) onAuthChange();
144
- window.location.reload();
145
- };
146
-
147
- const handleDevLogin = () => {
148
- if (!devUsername.trim()) {
149
- alert('Please enter a username');
150
- return;
151
- }
152
-
153
- try {
154
- const result = loginDevMode(devUsername);
155
- setUserInfo(result.userInfo);
156
- apiClient.setToken(result.accessToken);
157
- setShowDevLogin(false);
158
- setDevUsername('');
159
- if (onAuthChange) onAuthChange();
160
- } catch (error) {
161
- console.error('Dev login failed:', error);
162
- alert('Failed to login in dev mode');
163
- }
164
- };
165
-
166
- // Close dropdowns when clicking outside
167
- useEffect(() => {
168
- const handleClickOutside = (event: MouseEvent) => {
169
- if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) {
170
- setShowLanguageDropdown(false);
171
- }
172
- if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
173
- setShowModelDropdown(false);
174
- }
175
- if (importDialogRef.current && !importDialogRef.current.contains(event.target as Node)) {
176
- setShowImportDialog(false);
177
- }
178
- if (redesignDialogRef.current && !redesignDialogRef.current.contains(event.target as Node)) {
179
- setShowRedesignDialog(false);
180
- }
181
- };
182
-
183
- document.addEventListener('mousedown', handleClickOutside);
184
- return () => {
185
- document.removeEventListener('mousedown', handleClickOutside);
186
- };
187
- }, []);
188
-
189
- const loadData = async () => {
190
- console.log('loadData called');
191
- setIsLoading(true);
192
- await Promise.all([loadModels(), loadLanguages()]);
193
- setIsLoading(false);
194
- console.log('loadData completed');
195
- };
196
-
197
- const loadModels = async () => {
198
- try {
199
- console.log('Loading models...');
200
- const modelsList = await apiClient.getModels();
201
- console.log('Models loaded successfully:', modelsList);
202
- console.log('Number of models:', modelsList.length);
203
- setModels(modelsList);
204
- console.log('Models state updated');
205
- } catch (error) {
206
- console.error('Failed to load models:', error);
207
- setModels([]); // Set empty array on error
208
- }
209
- };
210
-
211
- const loadLanguages = async () => {
212
- try {
213
- const { languages: languagesList } = await apiClient.getLanguages();
214
- setLanguages(languagesList);
215
- } catch (error) {
216
- console.error('Failed to load languages:', error);
217
- }
218
- };
219
-
220
- // Check if current model supports images
221
- // Show immediately for GLM-4.6V even before models load
222
- const currentModelSupportsImages =
223
- selectedModel === 'zai-org/GLM-4.6V:zai-org' ||
224
- models.find(m => m.id === selectedModel)?.supports_images ||
225
- false;
226
-
227
- // Debug logging
228
- useEffect(() => {
229
- console.log('[LandingPage] Selected model:', selectedModel);
230
- console.log('[LandingPage] Models loaded:', models.length);
231
- console.log('[LandingPage] Supports images:', currentModelSupportsImages);
232
- }, [selectedModel, models, currentModelSupportsImages]);
233
-
234
- const loadTrendingApps = async () => {
235
- try {
236
- const apps = await apiClient.getTrendingAnycoderApps();
237
- setTrendingApps(apps);
238
- } catch (error) {
239
- console.error('Failed to load trending apps:', error);
240
- }
241
- };
242
-
243
- const handleSubmit = (e: React.FormEvent) => {
244
- e.preventDefault();
245
- if (prompt.trim() && isAuthenticated) {
246
- console.log('[LandingPage Submit] Sending with image:', uploadedImageUrl ? 'Yes' : 'No');
247
- console.log('[LandingPage Submit] Image URL length:', uploadedImageUrl?.length || 0);
248
- onStart(prompt.trim(), selectedLanguage, selectedModel, uploadedImageUrl || undefined);
249
- // Clear prompt and image after sending
250
- setPrompt('');
251
- setUploadedImageUrl(null);
252
- } else if (!isAuthenticated) {
253
- alert('Please sign in with HuggingFace first!');
254
- }
255
- };
256
-
257
- const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
258
- const file = e.target.files?.[0];
259
- if (file) {
260
- const reader = new FileReader();
261
- reader.onload = (event) => {
262
- const imageUrl = event.target?.result as string;
263
- setUploadedImageUrl(imageUrl);
264
- };
265
- reader.readAsDataURL(file);
266
- }
267
- };
268
-
269
- const removeImage = () => {
270
- setUploadedImageUrl(null);
271
- if (fileInputRef.current) {
272
- fileInputRef.current.value = '';
273
- }
274
- };
275
-
276
- const formatLanguageName = (lang: Language) => {
277
- if (lang === 'html') return 'HTML';
278
- if (lang === 'transformers.js') return 'Transformers.js';
279
- if (lang === 'comfyui') return 'ComfyUI';
280
- if (lang === 'daggr') return 'Daggr';
281
- return lang.charAt(0).toUpperCase() + lang.slice(1);
282
- };
283
-
284
- const formatModelName = (name: string, id: string) => {
285
- if (id === 'moonshotai/Kimi-K2.6') return 'Kimi-K2.6 ✨';
286
- if (id === 'google/gemma-4-31B-it') return 'Gemma-4-31B 🤖';
287
- if (id === 'zai-org/GLM-5.1') return 'GLM-5.1 🚀';
288
- if (id === 'Qwen/Qwen3.5-397B-A17B') return 'Qwen3.5-397B-A17B 🤖';
289
- return name;
290
- };
291
-
292
- // Check if user owns the imported space
293
- const checkSpaceOwnership = (url: string) => {
294
- if (!url || !userInfo?.preferred_username) {
295
- setIsSpaceOwner(false);
296
- return;
297
- }
298
-
299
- const spaceMatch = url.match(/huggingface\.co\/spaces\/([^\/\s\)]+)\/[^\/\s\)]+/);
300
- if (spaceMatch) {
301
- const spaceOwner = spaceMatch[1];
302
- const isOwner = spaceOwner === userInfo.preferred_username;
303
- setIsSpaceOwner(isOwner);
304
- console.log('[Import] Space owner:', spaceOwner, '| Current user:', userInfo.preferred_username, '| Is owner:', isOwner);
305
-
306
- // Auto-select update mode if owner, otherwise duplicate
307
- if (isOwner) {
308
- setImportAction('update');
309
- } else {
310
- setImportAction('duplicate');
311
- }
312
- } else {
313
- setIsSpaceOwner(false);
314
- }
315
- };
316
-
317
- const handleImportProject = async () => {
318
- if (!importUrl.trim()) {
319
- setImportError('Please enter a valid URL');
320
- return;
321
- }
322
-
323
- if (!isAuthenticated) {
324
- alert('Please sign in with HuggingFace first!');
325
- return;
326
- }
327
-
328
- setIsImporting(true);
329
- setImportError('');
330
-
331
- try {
332
- console.log('[Import] ========== STARTING IMPORT ==========');
333
- console.log('[Import] Import URL:', importUrl);
334
- console.log('[Import] Action:', importAction);
335
-
336
- // Extract space ID from URL
337
- const spaceMatch = importUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
338
- console.log('[Import] Space regex match result:', spaceMatch);
339
-
340
- if (spaceMatch) {
341
- const fromSpaceId = spaceMatch[1];
342
- console.log('[Import] ✅ Detected HF Space:', fromSpaceId);
343
-
344
- // Import the code first (always needed to load in editor)
345
- const importResult = await apiClient.importProject(importUrl);
346
-
347
- if (importResult.status !== 'success') {
348
- setImportError(importResult.message || 'Failed to import project');
349
- setIsImporting(false);
350
- return;
351
- }
352
-
353
- // Handle different import actions
354
- if (importAction === 'update' && isSpaceOwner) {
355
- // Option 1: Update existing space directly (for owners)
356
- console.log('[Import] Owner update - loading code for direct update to:', fromSpaceId);
357
-
358
- if (onImport && importResult.code) {
359
- // Pass the original space URL so future deployments update it
360
- onImport(importResult.code, importResult.language || 'html', importUrl);
361
-
362
- alert(`✅ Code loaded!\n\nYou can now make changes and deploy them directly to: ${importUrl}\n\nThe code has been loaded in the editor.`);
363
- }
364
-
365
- setShowImportDialog(false);
366
- setImportUrl('');
367
-
368
- } else if (importAction === 'pr') {
369
- // Option 2: Create Pull Request
370
- console.log('[Import] PR mode - loading code to create PR to:', fromSpaceId);
371
-
372
- if (onImport && importResult.code) {
373
- // Load code in editor with the original space for PR tracking
374
- onImport(importResult.code, importResult.language || 'html', importUrl);
375
-
376
- // Set pending PR state so any future code generation creates a PR
377
- if (setPendingPR && pendingPRRef) {
378
- const prInfo = { repoId: fromSpaceId, language: (importResult.language || 'html') as Language };
379
- setPendingPR(prInfo);
380
- pendingPRRef.current = prInfo;
381
- console.log('[Import PR] Set pending PR:', prInfo);
382
- }
383
-
384
- // Show success message
385
- alert(`✅ Code loaded in PR mode!\n\nYou can now:\n• Make manual edits in the editor\n• Generate new features with AI\n\nWhen you deploy, a Pull Request will be created to: ${fromSpaceId}`);
386
- }
387
-
388
- setShowImportDialog(false);
389
- setImportUrl('');
390
-
391
- } else {
392
- // Option 3: Duplicate space (default)
393
- console.log('[Import] Duplicate mode - will duplicate:', fromSpaceId);
394
-
395
- const duplicateResult = await apiClient.duplicateSpace(fromSpaceId);
396
- console.log('[Import] Duplicate API response:', duplicateResult);
397
-
398
- if (duplicateResult.success) {
399
- console.log('[Import] ========== DUPLICATE SUCCESS ==========');
400
- console.log('[Import] Duplicated space URL:', duplicateResult.space_url);
401
- console.log('[Import] Duplicated space ID:', duplicateResult.space_id);
402
- console.log('[Import] ==========================================');
403
-
404
- if (onImport && importResult.code) {
405
- console.log('[Import] Calling onImport with duplicated space URL:', duplicateResult.space_url);
406
- // Pass the duplicated space URL so it's tracked for future deployments
407
- onImport(importResult.code, importResult.language || 'html', duplicateResult.space_url);
408
-
409
- // Show success message with link to duplicated space
410
- alert(`✅ Space duplicated successfully!\n\nYour space: ${duplicateResult.space_url}\n\nThe code has been loaded in the editor. Any changes you deploy will update this duplicated space.`);
411
- }
412
-
413
- setShowImportDialog(false);
414
- setImportUrl('');
415
- } else {
416
- setImportError(duplicateResult.message || 'Failed to duplicate space');
417
- }
418
- }
419
- } else {
420
- // Not a Space URL - fall back to regular import
421
- console.log('[Import] ❌ Not a HF Space URL - using regular import');
422
- const result = await apiClient.importProject(importUrl);
423
-
424
- if (result.status === 'success') {
425
- if (onImport && result.code) {
426
- onImport(result.code, result.language || 'html', importUrl);
427
- } else {
428
- const importMessage = `Imported from ${importUrl}`;
429
- onStart(importMessage, result.language || 'html', selectedModel, undefined);
430
- }
431
-
432
- setShowImportDialog(false);
433
- setImportUrl('');
434
- } else {
435
- setImportError(result.message || 'Failed to import project');
436
- }
437
- }
438
- } catch (error: any) {
439
- console.error('Import error:', error);
440
- setImportError(error.response?.data?.message || error.message || 'Failed to import project');
441
- } finally {
442
- setIsImporting(false);
443
- }
444
- };
445
-
446
- const handleRedesignProject = async () => {
447
- if (!redesignUrl.trim()) {
448
- setRedesignError('Please enter a valid URL');
449
- return;
450
- }
451
-
452
- if (!isAuthenticated) {
453
- alert('Please sign in with HuggingFace first!');
454
- return;
455
- }
456
-
457
- setIsRedesigning(true);
458
- setRedesignError('');
459
-
460
- try {
461
- // Extract space ID from URL
462
- const spaceMatch = redesignUrl.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
463
- const repoId = spaceMatch ? spaceMatch[1] : null;
464
-
465
- if (!repoId) {
466
- setRedesignError('Please enter a valid HuggingFace Space URL');
467
- setIsRedesigning(false);
468
- return;
469
- }
470
-
471
- // Import the code first
472
- const result = await apiClient.importProject(redesignUrl);
473
-
474
- if (result.status !== 'success') {
475
- setRedesignError(result.message || 'Failed to import project for redesign');
476
- setIsRedesigning(false);
477
- return;
478
- }
479
-
480
- if (!createPR) {
481
- // Option 1: Redesign WITHOUT PR - Duplicate space first, then generate redesign
482
- console.log('[Redesign] Duplicating space first:', repoId);
483
-
484
- try {
485
- const duplicateResult = await apiClient.duplicateSpace(repoId);
486
- console.log('[Redesign] Duplicate result:', duplicateResult);
487
-
488
- if (!duplicateResult.success) {
489
- setRedesignError(duplicateResult.message || 'Failed to duplicate space');
490
- setIsRedesigning(false);
491
- return;
492
- }
493
-
494
- // Load code and trigger redesign
495
- if (onImport && onStart) {
496
- // Pass duplicated space URL
497
- onImport(result.code, result.language || 'html', duplicateResult.space_url);
498
-
499
- // Extract duplicated space ID to pass to generation
500
- const dupSpaceMatch = duplicateResult.space_url?.match(/huggingface\.co\/spaces\/([^\/\s\)]+\/[^\/\s\)]+)/);
501
- const duplicatedRepoId = dupSpaceMatch ? dupSpaceMatch[1] : undefined;
502
-
503
- console.log('[Redesign] Duplicated space ID:', duplicatedRepoId);
504
-
505
- setTimeout(() => {
506
- const isGradio = (result.language || 'html') === 'gradio';
507
- const redesignPrompt = `I have existing code in the editor from a duplicated space. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
508
-
509
- Current code:
510
- \`\`\`${result.language || 'html'}
511
- ${result.code}
512
- \`\`\`
513
-
514
- Please redesign this with:
515
- - Minimal, clean components
516
- - Mobile-first responsive design
517
- - Modern UI/UX best practices
518
- - Better visual hierarchy and spacing
519
-
520
- ${isGradio ? '\n\nIMPORTANT: Only output app.py with the redesigned UI (themes, layout, styling). Do NOT modify or output any other .py files (utils.py, models.py, etc.). Do NOT include requirements.txt or README.md.' : ''}`;
521
-
522
- if (onStart) {
523
- // Pass duplicated space ID so auto-deploy updates it
524
- console.log('[Redesign] Calling onStart with duplicated repo ID:', duplicatedRepoId);
525
- console.log('[Redesign] Using moonshotai/Kimi-K2.6 for redesign');
526
- onStart(redesignPrompt, result.language || 'html', 'moonshotai/Kimi-K2.6', undefined, duplicatedRepoId);
527
- }
528
- }, 100);
529
-
530
- // Show success message
531
- alert(`✅ Space duplicated!\n\nYour space: ${duplicateResult.space_url}\n\nGenerating redesign now...`);
532
- }
533
-
534
- setShowRedesignDialog(false);
535
- setRedesignUrl('');
536
-
537
- } catch (dupError: any) {
538
- console.error('[Redesign] Duplication error:', dupError);
539
- setRedesignError(dupError.response?.data?.message || dupError.message || 'Failed to duplicate space');
540
- setIsRedesigning(false);
541
- return;
542
- }
543
-
544
- } else {
545
- // Option 2: Redesign WITH PR - Import code and generate, then create PR
546
- if (onImport && onStart) {
547
- onImport(result.code, result.language || 'html', redesignUrl);
548
-
549
- setTimeout(() => {
550
- const isGradio = (result.language || 'html') === 'gradio';
551
- const redesignPrompt = `I have existing code in the editor that I imported from ${redesignUrl}. Please redesign it to make it look better with minimal components needed, mobile friendly, and modern design.
552
-
553
- Current code:
554
- \`\`\`${result.language || 'html'}
555
- ${result.code}
556
- \`\`\`
557
-
558
- Please redesign this with:
559
- - Minimal, clean components
560
- - Mobile-first responsive design
561
- - Modern UI/UX best practices
562
- - Better visual hierarchy and spacing
563
-
564
- ${isGradio ? '\n\nIMPORTANT: Only output app.py with the redesigned UI (themes, layout, styling). Do NOT modify or output any other .py files (utils.py, models.py, etc.). Do NOT include requirements.txt or README.md.' : ''}
565
-
566
- Note: After generating the redesign, I will create a Pull Request on the original space.`;
567
-
568
- if (onStart) {
569
- console.log('[Redesign] Will create PR - not passing repo ID');
570
- console.log('[Redesign] Using moonshotai/Kimi-K2.6 for redesign');
571
- onStart(redesignPrompt, result.language || 'html', 'moonshotai/Kimi-K2.6', undefined, repoId, true); // Pass true for shouldCreatePR
572
- }
573
-
574
- console.log('[Redesign] Will create PR after code generation completes');
575
- }, 100);
576
-
577
- setShowRedesignDialog(false);
578
- setRedesignUrl('');
579
- } else {
580
- setRedesignError('Missing required callbacks. Please try again.');
581
- }
582
- }
583
- } catch (error: any) {
584
- console.error('Redesign error:', error);
585
- setRedesignError(error.response?.data?.message || error.message || 'Failed to process redesign request');
586
- } finally {
587
- setIsRedesigning(false);
588
- }
589
- };
590
-
591
- return (
592
- <div className="h-screen flex flex-col bg-[#000000] overflow-hidden">
593
- {/* Header - Apple style */}
594
- <header className="flex items-center justify-between px-6 py-3 backdrop-blur-xl bg-[#000000]/80 border-b border-[#424245]/30 flex-shrink-0">
595
- <a
596
- href="https://huggingface.co/spaces/akhaliq/anycoder"
597
- target="_blank"
598
- rel="noopener noreferrer"
599
- className="text-sm font-medium text-[#f5f5f7] hover:text-white transition-colors"
600
- >
601
- AnyCoder
602
- </a>
603
-
604
- {/* Auth Section */}
605
- <div className="flex items-center space-x-3">
606
- {isAuthLoading ? (
607
- <span className="text-xs text-[#86868b]">Loading...</span>
608
- ) : userInfo ? (
609
- <div className="flex items-center space-x-3">
610
- {userInfo.avatarUrl && (
611
- <img
612
- src={userInfo.avatarUrl}
613
- alt={userInfo.name}
614
- className="w-7 h-7 rounded-full"
615
- />
616
- )}
617
- <span className="hidden sm:inline text-sm text-[#f5f5f7] truncate max-w-[120px] font-medium">
618
- {userInfo.preferredUsername || userInfo.name}
619
- </span>
620
- <button
621
- onClick={handleLogout}
622
- className="px-3 py-1.5 text-sm text-[#f5f5f7] hover:text-white transition-colors"
623
- >
624
- Logout
625
- </button>
626
- </div>
627
- ) : (
628
- <div className="flex items-center space-x-3">
629
- {/* Dev Mode Login (only on localhost) */}
630
- {isDevMode && (
631
- <>
632
- {showDevLogin ? (
633
- <div className="flex items-center space-x-2">
634
- <input
635
- type="text"
636
- value={devUsername}
637
- onChange={(e) => setDevUsername(e.target.value)}
638
- onKeyPress={(e) => e.key === 'Enter' && handleDevLogin()}
639
- placeholder="username"
640
- className="px-3 py-1.5 rounded-lg text-sm bg-[#1d1d1f] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 w-32 font-medium"
641
- autoFocus
642
- />
643
- <button
644
- onClick={handleDevLogin}
645
- className="px-3 py-1.5 bg-white text-black rounded-lg text-sm hover:bg-[#f5f5f7] font-medium"
646
- >
647
- OK
648
- </button>
649
- <button
650
- onClick={() => {
651
- setShowDevLogin(false);
652
- setDevUsername('');
653
- }}
654
- className="text-[#86868b] hover:text-[#f5f5f7] text-sm"
655
- >
656
-
657
- </button>
658
- </div>
659
- ) : (
660
- <button
661
- onClick={() => setShowDevLogin(true)}
662
- className="px-3 py-1.5 text-sm text-[#f5f5f7] hover:text-white transition-colors"
663
- title="Dev Mode"
664
- >
665
- Dev
666
- </button>
667
- )}
668
- <span className="text-[#86868b] text-sm">or</span>
669
- </>
670
- )}
671
-
672
- {/* OAuth Login */}
673
- <button
674
- onClick={handleLogin}
675
- className="px-4 py-2 bg-white text-black rounded-full text-sm hover:bg-[#f5f5f7] transition-all font-medium"
676
- >
677
- Sign in
678
- </button>
679
- </div>
680
- )}
681
- </div>
682
- </header>
683
-
684
- {/* Main Content - Apple-style centered layout */}
685
- <main className="flex-1 overflow-y-auto px-4 py-6">
686
- <div className="w-full max-w-3xl mx-auto flex flex-col items-center justify-center min-h-full">
687
- {/* Apple-style Headline */}
688
- <div className="text-center mb-8">
689
- <h2 className="text-4xl md:text-5xl font-semibold text-white mb-2 tracking-tight leading-tight">
690
- Build with AnyCoder
691
- </h2>
692
- <p className="text-base md:text-lg text-[#86868b] font-normal">
693
- Create apps with AI
694
- </p>
695
- </div>
696
-
697
- {/* Simple prompt form */}
698
- <form onSubmit={handleSubmit} className="relative w-full mb-8">
699
- <div className="relative bg-[#2d2d30] rounded-2xl border border-[#424245] shadow-2xl">
700
- {/* Image Preview */}
701
- {uploadedImageUrl && (
702
- <div className="px-4 pt-3">
703
- <div className="relative inline-block">
704
- <Image
705
- src={uploadedImageUrl}
706
- alt="Upload preview"
707
- width={120}
708
- height={120}
709
- className="rounded-lg object-cover"
710
- unoptimized
711
- />
712
- <button
713
- type="button"
714
- onClick={removeImage}
715
- className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all flex items-center justify-center text-xs font-bold"
716
- >
717
- ×
718
- </button>
719
- </div>
720
- </div>
721
- )}
722
-
723
- {/* Textarea */}
724
- <textarea
725
- value={prompt}
726
- onChange={(e) => setPrompt(e.target.value)}
727
- placeholder="Message AnyCoder"
728
- className="w-full px-4 py-3 text-sm text-[#f5f5f7] bg-transparent placeholder:text-[#86868b] resize-none focus:outline-none min-h-[48px] font-normal"
729
- rows={1}
730
- onKeyDown={(e) => {
731
- if (e.key === 'Enter' && !e.shiftKey) {
732
- e.preventDefault();
733
- handleSubmit(e);
734
- }
735
- }}
736
- />
737
-
738
- {/* Bottom controls - Apple style */}
739
- <div className="flex items-center justify-between px-3 pb-3 gap-2">
740
- {/* Compact dropdowns on the left */}
741
- <div className="flex items-center gap-2">
742
- {/* Language Dropdown */}
743
- <div className="relative" ref={languageDropdownRef}>
744
- <button
745
- type="button"
746
- onClick={(e) => {
747
- e.stopPropagation();
748
- console.log('Language button clicked, toggling dropdown');
749
- setShowLanguageDropdown(!showLanguageDropdown);
750
- setShowModelDropdown(false);
751
- }}
752
- disabled={isLoading}
753
- className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all disabled:opacity-50 flex items-center gap-1.5 font-medium"
754
- >
755
- <span>{isLoading ? '...' : formatLanguageName(selectedLanguage)}</span>
756
- <svg
757
- className={`w-3 h-3 text-[#86868b] transition-transform ${showLanguageDropdown ? 'rotate-180' : ''}`}
758
- fill="none"
759
- stroke="currentColor"
760
- viewBox="0 0 24 24"
761
- strokeWidth={2.5}
762
- >
763
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
764
- </svg>
765
- </button>
766
-
767
- {/* Language Dropdown Menu */}
768
- {showLanguageDropdown && !isLoading && languages.length > 0 && (
769
- <div
770
- className="absolute bottom-full left-0 mb-2 w-48 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl"
771
- onClick={(e) => e.stopPropagation()}
772
- >
773
- <div className="max-h-64 overflow-y-auto py-1">
774
- {languages.map((lang) => (
775
- <button
776
- key={lang}
777
- type="button"
778
- onClick={() => {
779
- setSelectedLanguage(lang);
780
- setShowLanguageDropdown(false);
781
- }}
782
- className={`w-full px-4 py-2.5 text-left text-xs text-[#f5f5f7] hover:bg-[#2d2d2f] transition-colors font-medium ${selectedLanguage === lang ? 'bg-[#2d2d2f]' : ''
783
- }`}
784
- >
785
- {formatLanguageName(lang)}
786
- </button>
787
- ))}
788
- </div>
789
- </div>
790
- )}
791
- </div>
792
-
793
- {/* Model Dropdown */}
794
- <div className="relative" ref={modelDropdownRef}>
795
- <button
796
- type="button"
797
- onClick={(e) => {
798
- e.stopPropagation();
799
- console.log('Model button clicked! Models length:', models.length, 'Show:', showModelDropdown);
800
- setShowModelDropdown(!showModelDropdown);
801
- setShowLanguageDropdown(false);
802
- setShowImportDialog(false);
803
- }}
804
- className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 max-w-[200px] font-medium"
805
- >
806
- <span className="truncate">
807
- {isLoading
808
- ? '...'
809
- : formatModelName(models.find(m => m.id === selectedModel)?.name || '', selectedModel) || selectedModel || 'Model'
810
- }
811
- </span>
812
- <svg
813
- className={`w-3 h-3 text-[#86868b] flex-shrink-0 transition-transform ${showModelDropdown ? 'rotate-180' : ''}`}
814
- fill="none"
815
- stroke="currentColor"
816
- viewBox="0 0 24 24"
817
- strokeWidth={2.5}
818
- >
819
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
820
- </svg>
821
- </button>
822
-
823
- {/* Model Dropdown Menu */}
824
- {showModelDropdown && models.length > 0 && (
825
- <div
826
- className="absolute top-full left-0 mt-2 w-56 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl z-50"
827
- onClick={(e) => e.stopPropagation()}
828
- >
829
- <div className="max-h-96 overflow-y-auto py-1">
830
- {models.map((model) => (
831
- <button
832
- key={model.id}
833
- type="button"
834
- onClick={() => {
835
- setSelectedModel(model.id);
836
- setShowModelDropdown(false);
837
- }}
838
- className={`w-full px-4 py-2 text-left transition-colors ${selectedModel === model.id
839
- ? 'bg-[#2d2d2f]'
840
- : 'hover:bg-[#2d2d2f]'
841
- }`}
842
- >
843
- <div className="flex items-center justify-between gap-2">
844
- <span className="text-xs font-medium text-[#f5f5f7]">{formatModelName(model.name, model.id)}</span>
845
- {['moonshotai/Kimi-K2.6', 'google/gemma-4-31B-it', 'zai-org/GLM-5.1', 'Qwen/Qwen3.5-397B-A17B', 'MiniMaxAI/MiniMax-M2.5'].includes(model.id) && (
846
- <span className="px-1.5 py-0.5 bg-gradient-to-r from-purple-500 to-pink-500 text-white text-[9px] font-bold rounded uppercase">
847
- NEW
848
- </span>
849
- )}
850
- </div>
851
- </button>
852
- ))}
853
- </div>
854
- </div>
855
- )}
856
- </div>
857
-
858
- {/* Import Project Button */}
859
- <div className="relative" ref={importDialogRef}>
860
- <button
861
- type="button"
862
- onClick={(e) => {
863
- e.stopPropagation();
864
- setShowImportDialog(!showImportDialog);
865
- setShowLanguageDropdown(false);
866
- setShowModelDropdown(false);
867
- setShowRedesignDialog(false);
868
- setImportError('');
869
- }}
870
- className="px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 font-medium"
871
- >
872
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
873
- <path strokeLinecap="round" strokeLinejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
874
- </svg>
875
- <span>Import</span>
876
- </button>
877
-
878
- {/* Import Dialog */}
879
- {showImportDialog && (
880
- <div
881
- className="absolute top-full left-0 mt-2 w-80 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl z-50"
882
- onClick={(e) => e.stopPropagation()}
883
- >
884
- <div className="p-4">
885
- <h3 className="text-sm font-medium text-[#f5f5f7] mb-3">Import Project</h3>
886
- <input
887
- type="text"
888
- value={importUrl}
889
- onChange={(e) => {
890
- setImportUrl(e.target.value);
891
- checkSpaceOwnership(e.target.value);
892
- }}
893
- onKeyPress={(e) => e.key === 'Enter' && handleImportProject()}
894
- placeholder="https://huggingface.co/spaces/..."
895
- className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-3"
896
- disabled={isImporting}
897
- />
898
-
899
- {/* Import Action Options */}
900
- {importUrl.includes('huggingface.co/spaces/') && (
901
- <div className="mb-3 space-y-2">
902
- <p className="text-[10px] font-medium text-[#86868b] mb-2">Import Mode:</p>
903
-
904
- {/* Update Space (only for owners) */}
905
- {isSpaceOwner && (
906
- <label className="flex items-start gap-2 cursor-pointer group">
907
- <input
908
- type="radio"
909
- checked={importAction === 'update'}
910
- onChange={() => setImportAction('update')}
911
- className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
912
- disabled={isImporting}
913
- />
914
- <div>
915
- <span className="text-[11px] text-[#f5f5f7] font-medium">Update your space directly</span>
916
- <p className="text-[10px] text-[#86868b] mt-0.5">
917
- ✅ You own this space - changes will update it
918
- </p>
919
- </div>
920
- </label>
921
- )}
922
-
923
- {/* Duplicate Space */}
924
- <label className="flex items-start gap-2 cursor-pointer group">
925
- <input
926
- type="radio"
927
- checked={importAction === 'duplicate'}
928
- onChange={() => setImportAction('duplicate')}
929
- className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
930
- disabled={isImporting}
931
- />
932
- <div>
933
- <span className="text-[11px] text-[#f5f5f7] font-medium">Duplicate to your account</span>
934
- <p className="text-[10px] text-[#86868b] mt-0.5">
935
- Create a copy you can freely modify
936
- </p>
937
- </div>
938
- </label>
939
-
940
- {/* Create PR */}
941
- <label className="flex items-start gap-2 cursor-pointer group">
942
- <input
943
- type="radio"
944
- checked={importAction === 'pr'}
945
- onChange={() => setImportAction('pr')}
946
- className="mt-0.5 w-3.5 h-3.5 rounded-full border-[#424245] bg-[#2d2d30] checked:bg-white checked:border-white"
947
- disabled={isImporting}
948
- />
949
- <div>
950
- <span className="text-[11px] text-[#f5f5f7] font-medium">Create Pull Request</span>
951
- <p className="text-[10px] text-[#86868b] mt-0.5">
952
- Propose changes to the original space
953
- </p>
954
- </div>
955
- </label>
956
-
957
- {importAction === 'pr' && (
958
- <p className="text-[10px] text-[#86868b] ml-6 mt-1">
959
- ⚠️ Requires space owner to enable PRs
960
- </p>
961
- )}
962
- </div>
963
- )}
964
-
965
- {importError && (
966
- <p className="text-xs text-red-400 mb-2">{importError}</p>
967
- )}
968
-
969
- <div className="flex gap-2">
970
- <button
971
- onClick={handleImportProject}
972
- disabled={isImporting || !importUrl.trim()}
973
- className="flex-1 px-3 py-2 bg-white text-black rounded-lg text-xs hover:bg-[#f5f5f7] disabled:opacity-50 disabled:cursor-not-allowed font-medium"
974
- >
975
- {isImporting ? 'Importing...' : 'Import'}
976
- </button>
977
- <button
978
- onClick={() => {
979
- setShowImportDialog(false);
980
- setImportUrl('');
981
- setImportError('');
982
- setIsSpaceOwner(false);
983
- setImportAction('duplicate');
984
- }}
985
- className="px-3 py-2 bg-[#2d2d30] text-[#f5f5f7] rounded-lg text-xs hover:bg-[#3d3d3f] font-medium"
986
- >
987
- Cancel
988
- </button>
989
- </div>
990
- <p className="text-[10px] text-[#86868b] mt-3">
991
- Import from HuggingFace Spaces, Models, or GitHub
992
- </p>
993
- </div>
994
- </div>
995
- )}
996
- </div>
997
-
998
- {/* Redesign Project Button */}
999
- <div className="relative" ref={redesignDialogRef}>
1000
- <button
1001
- type="button"
1002
- onClick={(e) => {
1003
- e.stopPropagation();
1004
- setShowRedesignDialog(!showRedesignDialog);
1005
- setShowLanguageDropdown(false);
1006
- setShowModelDropdown(false);
1007
- setShowImportDialog(false);
1008
- setRedesignError('');
1009
- }}
1010
- className="relative px-3 py-1.5 bg-[#1d1d1f] text-[#f5f5f7] text-xs border border-[#424245] rounded-full hover:bg-[#2d2d2f] transition-all flex items-center gap-1.5 font-medium overflow-visible"
1011
- >
1012
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
1013
- <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
1014
- </svg>
1015
- <span>Redesign</span>
1016
- </button>
1017
-
1018
- {/* Redesign Dialog */}
1019
- {showRedesignDialog && (
1020
- <div
1021
- className="absolute top-full left-0 mt-2 w-80 bg-[#1d1d1f] border border-[#424245] rounded-xl shadow-2xl overflow-hidden backdrop-blur-xl z-50"
1022
- onClick={(e) => e.stopPropagation()}
1023
- >
1024
- <div className="p-4">
1025
- <h3 className="text-sm font-medium text-[#f5f5f7] mb-3">Redesign Project</h3>
1026
- <input
1027
- type="text"
1028
- value={redesignUrl}
1029
- onChange={(e) => setRedesignUrl(e.target.value)}
1030
- onKeyPress={(e) => e.key === 'Enter' && handleRedesignProject()}
1031
- placeholder="https://huggingface.co/spaces/..."
1032
- className="w-full px-3 py-2 rounded-lg text-xs bg-[#2d2d30] text-[#f5f5f7] border border-[#424245] focus:outline-none focus:border-white/50 font-normal mb-3"
1033
- disabled={isRedesigning}
1034
- />
1035
-
1036
- {/* PR Option */}
1037
- <label className="flex items-center gap-2 mb-1 cursor-pointer">
1038
- <input
1039
- type="checkbox"
1040
- checked={createPR}
1041
- onChange={(e) => setCreatePR(e.target.checked)}
1042
- disabled={isRedesigning}
1043
- className="w-4 h-4 rounded bg-[#2d2d30] border-[#424245] text-white focus:ring-white focus:ring-offset-0"
1044
- />
1045
- <span className="text-xs text-[#f5f5f7]">
1046
- Create Pull Request on original space
1047
- </span>
1048
- </label>
1049
-
1050
- {createPR && (
1051
- <p className="text-[10px] text-[#86868b] mb-2 ml-6">
1052
- ⚠️ Note: PR creation requires space owner to enable PRs. If disabled, uncheck this to duplicate the space instead.
1053
- </p>
1054
- )}
1055
-
1056
- {redesignError && (
1057
- <p className="text-xs text-red-400 mb-2">{redesignError}</p>
1058
- )}
1059
- <div className="flex gap-2">
1060
- <button
1061
- onClick={handleRedesignProject}
1062
- disabled={isRedesigning || !redesignUrl.trim()}
1063
- className="flex-1 px-3 py-2 bg-white text-black rounded-lg text-xs hover:bg-[#f5f5f7] disabled:opacity-50 disabled:cursor-not-allowed font-medium"
1064
- >
1065
- {isRedesigning ? 'Redesigning...' : 'Redesign'}
1066
- </button>
1067
- <button
1068
- onClick={() => {
1069
- setShowRedesignDialog(false);
1070
- setRedesignUrl('');
1071
- setRedesignError('');
1072
- }}
1073
- className="px-3 py-2 bg-[#2d2d30] text-[#f5f5f7] rounded-lg text-xs hover:bg-[#3d3d3f] font-medium"
1074
- >
1075
- Cancel
1076
- </button>
1077
- </div>
1078
- <p className="text-[10px] text-[#86868b] mt-3">
1079
- {createPR
1080
- ? 'Creates a Pull Request on the original space with your redesign'
1081
- : 'Import and automatically redesign with modern, mobile-friendly design'}
1082
- </p>
1083
- </div>
1084
- </div>
1085
- )}
1086
- </div>
1087
- </div>
1088
-
1089
- {/* Right side - Image upload + Send button group */}
1090
- <div className="flex items-center gap-2">
1091
- {/* Image Upload Button (only if model supports images) */}
1092
- {currentModelSupportsImages && (
1093
- <>
1094
- <input
1095
- ref={fileInputRef}
1096
- type="file"
1097
- accept="image/*"
1098
- onChange={handleImageUpload}
1099
- className="hidden"
1100
- disabled={!isAuthenticated}
1101
- />
1102
- <button
1103
- type="button"
1104
- onClick={() => fileInputRef.current?.click()}
1105
- disabled={!isAuthenticated}
1106
- className="p-2 bg-[#1d1d1f] text-[#f5f5f7] rounded-full hover:bg-[#424245] disabled:opacity-30 disabled:cursor-not-allowed transition-all active:scale-95"
1107
- title="Upload image"
1108
- >
1109
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
1110
- <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
1111
- </svg>
1112
- </button>
1113
- </>
1114
- )}
1115
-
1116
- {/* Send button - Apple style */}
1117
- <button
1118
- type="submit"
1119
- disabled={!prompt.trim() || !isAuthenticated}
1120
- className="p-2 bg-white text-[#1d1d1f] rounded-full hover:bg-[#f5f5f7] disabled:opacity-30 disabled:cursor-not-allowed transition-all active:scale-95 shadow-lg"
1121
- title="Send"
1122
- >
1123
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
1124
- <path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14M12 5l7 7-7 7" />
1125
- </svg>
1126
- </button>
1127
- </div>
1128
- </div>
1129
- </div>
1130
-
1131
- {!isAuthenticated && (
1132
- <div className="mt-4 text-center">
1133
- <p className="text-xs text-[#86868b]">
1134
- Sign in to get started
1135
- </p>
1136
- </div>
1137
- )}
1138
- </form>
1139
-
1140
- {/* Trending Apps Section */}
1141
- {trendingApps.length > 0 && (
1142
- <div className="mt-8 w-full">
1143
- <h3 className="text-xl font-semibold text-white mb-4 text-center">
1144
- Top Trending Apps Built with AnyCoder
1145
- </h3>
1146
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
1147
- {trendingApps.map((app) => (
1148
- <a
1149
- key={app.id}
1150
- href={`https://huggingface.co/spaces/${app.id}`}
1151
- target="_blank"
1152
- rel="noopener noreferrer"
1153
- className="group bg-[#1d1d1f] border border-[#424245] rounded-xl p-4 hover:border-white/30 transition-all hover:shadow-xl hover:scale-[1.02]">
1154
- <div className="flex items-start justify-between mb-2">
1155
- <div className="flex-1 min-w-0">
1156
- <h4 className="text-xs font-medium text-[#f5f5f7] truncate group-hover:text-white transition-colors">
1157
- {app.id.split('/')[1]}
1158
- </h4>
1159
- <p className="text-[10px] text-[#86868b] mt-0.5">
1160
- by {app.id.split('/')[0]}
1161
- </p>
1162
- </div>
1163
- <div className="flex items-center gap-1.5 flex-shrink-0 ml-2">
1164
- <div className="flex items-center gap-0.5">
1165
- <svg className="w-3 h-3 text-[#86868b]" fill="currentColor" viewBox="0 0 20 20">
1166
- <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
1167
- </svg>
1168
- <span className="text-[10px] text-[#86868b] font-medium">{app.likes}</span>
1169
- </div>
1170
- <div className="flex items-center gap-0.5">
1171
- <svg className="w-3 h-3 text-[#86868b]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1172
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
1173
- </svg>
1174
- <span className="text-[10px] text-[#86868b] font-medium">{app.trendingScore}</span>
1175
- </div>
1176
- </div>
1177
- </div>
1178
- <div className="flex flex-wrap gap-1">
1179
- <span className="px-1.5 py-0.5 bg-[#2d2d30] text-[#86868b] text-[9px] rounded-full font-medium">
1180
- {app.sdk}
1181
- </span>
1182
- {app.tags?.slice(0, 2).map((tag: string) =>
1183
- tag !== 'anycoder' && tag !== app.sdk && tag !== 'region:us' && (
1184
- <span key={tag} className="px-1.5 py-0.5 bg-[#2d2d30] text-[#86868b] text-[9px] rounded-full font-medium">
1185
- {tag}
1186
- </span>
1187
- )
1188
- )}
1189
- </div>
1190
- </a>
1191
- ))}
1192
- </div>
1193
- </div>
1194
- )}
1195
- </div>
1196
- </main>
1197
- </div>
1198
- );
1199
- }
1200
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/lib/api.ts DELETED
@@ -1,541 +0,0 @@
1
- // API client for AnyCoder backend
2
-
3
- import axios, { AxiosInstance } from 'axios';
4
- import type {
5
- Model,
6
- AuthStatus,
7
- CodeGenerationRequest,
8
- DeploymentRequest,
9
- DeploymentResponse,
10
- Language,
11
- } from '@/types';
12
-
13
- // Use relative URLs in production (Next.js rewrites will proxy to backend)
14
- // In local dev, use localhost:8000 for direct backend access
15
- const getApiUrl = () => {
16
- // If explicitly set via env var, use it (for development)
17
- if (process.env.NEXT_PUBLIC_API_URL) {
18
- console.log('[API Client] Using explicit API URL:', process.env.NEXT_PUBLIC_API_URL);
19
- return process.env.NEXT_PUBLIC_API_URL;
20
- }
21
-
22
- // For server-side rendering, always use relative URLs
23
- if (typeof window === 'undefined') {
24
- console.log('[API Client] SSR mode: using relative URLs');
25
- return '';
26
- }
27
-
28
- // On localhost (dev mode), use direct backend URL
29
- const hostname = window.location.hostname;
30
- if (hostname === 'localhost' || hostname === '127.0.0.1') {
31
- console.log('[API Client] Localhost dev mode: using http://localhost:8000');
32
- return 'http://localhost:8000';
33
- }
34
-
35
- // In production (HF Space), use relative URLs (Next.js proxies to backend)
36
- console.log('[API Client] Production mode: using relative URLs (proxied by Next.js)');
37
- return '';
38
- };
39
-
40
- const API_URL = getApiUrl();
41
-
42
- class ApiClient {
43
- private client: AxiosInstance;
44
- private token: string | null = null;
45
-
46
- constructor() {
47
- this.client = axios.create({
48
- baseURL: API_URL,
49
- headers: {
50
- 'Content-Type': 'application/json',
51
- },
52
- timeout: 10000, // 10 second timeout to prevent hanging connections
53
- });
54
-
55
- // Add auth token to requests if available
56
- this.client.interceptors.request.use((config) => {
57
- // ALWAYS use OAuth token primarily, session token is for backend tracking only
58
- if (this.token) {
59
- config.headers.Authorization = `Bearer ${this.token}`;
60
- }
61
- return config;
62
- });
63
-
64
- // Add response interceptor to handle authentication errors
65
- this.client.interceptors.response.use(
66
- (response) => response,
67
- (error) => {
68
- // Handle 401 errors (expired/invalid authentication)
69
- // ONLY log out on specific auth errors, not all 401s
70
- if (error.response && error.response.status === 401) {
71
- const errorData = error.response.data;
72
- const errorMessage = errorData?.detail || errorData?.message || '';
73
-
74
- // Only log out if it's an authentication/session issue
75
- // Don't log out for permission errors on specific resources
76
- const shouldLogout =
77
- errorMessage.includes('Authentication required') ||
78
- errorMessage.includes('Invalid token') ||
79
- errorMessage.includes('Token expired') ||
80
- errorMessage.includes('Session expired') ||
81
- error.config?.url?.includes('/auth/');
82
-
83
- if (shouldLogout && typeof window !== 'undefined') {
84
- // Clear ALL authentication data including session token
85
- localStorage.removeItem('hf_oauth_token');
86
- localStorage.removeItem('hf_session_token');
87
- localStorage.removeItem('hf_user_info');
88
- this.token = null;
89
-
90
- // Dispatch custom event to notify UI components
91
- window.dispatchEvent(new CustomEvent('auth-expired', {
92
- detail: { message: 'Your session has expired. Please sign in again.' }
93
- }));
94
- }
95
- }
96
- return Promise.reject(error);
97
- }
98
- );
99
-
100
- // Load token from localStorage on client side
101
- if (typeof window !== 'undefined') {
102
- this.token = localStorage.getItem('hf_oauth_token');
103
- }
104
- }
105
-
106
- setToken(token: string | null) {
107
- this.token = token;
108
- // Note: OAuth token is stored by auth.ts, not here
109
- // We just keep it in memory for API calls
110
- }
111
-
112
- getToken(): string | null {
113
- return this.token;
114
- }
115
-
116
- // Cache helpers
117
- private getCachedData<T>(key: string, maxAgeMs: number): T | null {
118
- if (typeof window === 'undefined') return null;
119
-
120
- try {
121
- const cached = localStorage.getItem(key);
122
- if (!cached) return null;
123
-
124
- const { data, timestamp } = JSON.parse(cached);
125
- const age = Date.now() - timestamp;
126
-
127
- if (age > maxAgeMs) {
128
- localStorage.removeItem(key);
129
- return null;
130
- }
131
-
132
- return data;
133
- } catch (error) {
134
- console.error(`Failed to get cached data for ${key}:`, error);
135
- return null;
136
- }
137
- }
138
-
139
- private setCachedData<T>(key: string, data: T): void {
140
- if (typeof window === 'undefined') return;
141
-
142
- try {
143
- localStorage.setItem(key, JSON.stringify({
144
- data,
145
- timestamp: Date.now()
146
- }));
147
- } catch (error) {
148
- console.error(`Failed to cache data for ${key}:`, error);
149
- }
150
- }
151
-
152
- async getModels(): Promise<Model[]> {
153
- // Check cache first (24 hour TTL - cache once per day)
154
- const cached = this.getCachedData<Model[]>('anycoder_models', 24 * 60 * 60 * 1000);
155
- if (cached) {
156
- console.log('Using cached models:', cached.length, 'models');
157
- return cached;
158
- }
159
-
160
- try {
161
- console.log('Fetching models from API...');
162
- const response = await this.client.get<Model[]>('/api/models');
163
- const models = response.data;
164
-
165
- // Cache the successful response
166
- if (models && models.length > 0) {
167
- this.setCachedData('anycoder_models', models);
168
- console.log('Cached', models.length, 'models (valid for 24 hours)');
169
- }
170
-
171
- return models;
172
- } catch (error: any) {
173
- // Handle connection errors gracefully
174
- const isConnectionError =
175
- error.code === 'ECONNABORTED' ||
176
- error.code === 'ECONNRESET' ||
177
- error.code === 'ECONNREFUSED' ||
178
- error.message?.includes('socket hang up') ||
179
- error.message?.includes('timeout') ||
180
- error.message?.includes('Network Error') ||
181
- error.response?.status === 503 ||
182
- error.response?.status === 502;
183
-
184
- if (isConnectionError) {
185
- // Try to return stale cache if available
186
- const staleCache = this.getCachedData<Model[]>('anycoder_models', Infinity);
187
- if (staleCache && staleCache.length > 0) {
188
- console.warn('Backend not available, using stale cached models');
189
- return staleCache;
190
- }
191
-
192
- console.warn('Backend not available, cannot load models');
193
- return [];
194
- }
195
- // Re-throw other errors
196
- throw error;
197
- }
198
- }
199
-
200
- async getLanguages(): Promise<{ languages: Language[] }> {
201
- // Check cache first (24 hour TTL - cache once per day)
202
- const cached = this.getCachedData<Language[]>('anycoder_languages', 24 * 60 * 60 * 1000);
203
- if (cached) {
204
- console.log('Using cached languages:', cached.length, 'languages');
205
- return { languages: cached };
206
- }
207
-
208
- try {
209
- console.log('Fetching languages from API...');
210
- const response = await this.client.get<{ languages: Language[] }>('/api/languages');
211
- const languages = response.data.languages;
212
-
213
- // Cache the successful response
214
- if (languages && languages.length > 0) {
215
- this.setCachedData('anycoder_languages', languages);
216
- console.log('Cached', languages.length, 'languages (valid for 24 hours)');
217
- }
218
-
219
- return response.data;
220
- } catch (error: any) {
221
- // Handle connection errors gracefully
222
- const isConnectionError =
223
- error.code === 'ECONNABORTED' ||
224
- error.code === 'ECONNRESET' ||
225
- error.code === 'ECONNREFUSED' ||
226
- error.message?.includes('socket hang up') ||
227
- error.message?.includes('timeout') ||
228
- error.message?.includes('Network Error') ||
229
- error.response?.status === 503 ||
230
- error.response?.status === 502;
231
-
232
- if (isConnectionError) {
233
- // Try to return stale cache if available
234
- const staleCache = this.getCachedData<Language[]>('anycoder_languages', Infinity);
235
- if (staleCache && staleCache.length > 0) {
236
- console.warn('Backend not available, using stale cached languages');
237
- return { languages: staleCache };
238
- }
239
-
240
- // Fall back to default languages
241
- console.warn('Backend not available, using default languages');
242
- return { languages: ['html', 'gradio', 'transformers.js', 'streamlit', 'comfyui', 'react'] };
243
- }
244
- // Re-throw other errors
245
- throw error;
246
- }
247
- }
248
-
249
- async getAuthStatus(): Promise<AuthStatus> {
250
- try {
251
- const response = await this.client.get<AuthStatus>('/api/auth/status');
252
- return response.data;
253
- } catch (error: any) {
254
- // Silently handle connection errors - don't spam console
255
- if (error.code === 'ECONNABORTED' || error.code === 'ECONNRESET' || error.message?.includes('socket hang up')) {
256
- // Connection error - backend may not be ready
257
- return {
258
- authenticated: false,
259
- username: undefined,
260
- message: 'Connection error',
261
- };
262
- }
263
- // For other errors, return not authenticated
264
- return {
265
- authenticated: false,
266
- username: undefined,
267
- message: 'Not authenticated',
268
- };
269
- }
270
- }
271
-
272
- // Stream-based code generation using Fetch API with streaming (supports POST)
273
- generateCodeStream(
274
- request: CodeGenerationRequest,
275
- onChunk: (content: string) => void,
276
- onComplete: (code: string, reasoning?: string) => void,
277
- onError: (error: string) => void,
278
- onDeploying?: (message: string) => void,
279
- onDeployed?: (message: string, spaceUrl: string) => void,
280
- onDeployError?: (message: string) => void
281
- ): () => void {
282
- // Build the URL correctly whether we have a base URL or not
283
- const baseUrl = API_URL || window.location.origin;
284
- const url = new URL('/api/generate', baseUrl);
285
-
286
- let abortController = new AbortController();
287
- let accumulatedCode = '';
288
- let buffer = ''; // Buffer for incomplete SSE lines
289
-
290
- // Use fetch with POST to support large payloads
291
- fetch(url.toString(), {
292
- method: 'POST',
293
- headers: {
294
- 'Content-Type': 'application/json',
295
- ...(this.token ? { 'Authorization': `Bearer ${this.token}` } : {}),
296
- },
297
- body: JSON.stringify(request),
298
- signal: abortController.signal,
299
- })
300
- .then(async (response) => {
301
- // Handle rate limit errors before parsing response
302
- if (response.status === 429) {
303
- onError('⏱️ Rate limit exceeded. Free tier allows up to 20 requests per minute. Please wait a moment and try again.');
304
- return;
305
- }
306
-
307
- if (!response.ok) {
308
- throw new Error(`HTTP error! status: ${response.status}`);
309
- }
310
-
311
- if (!response.body) {
312
- throw new Error('Response body is null');
313
- }
314
-
315
- const reader = response.body.getReader();
316
- const decoder = new TextDecoder();
317
-
318
- while (true) {
319
- const { done, value } = await reader.read();
320
-
321
- if (done) {
322
- console.log('[Stream] Stream ended, total code length:', accumulatedCode.length);
323
- if (accumulatedCode) {
324
- onComplete(accumulatedCode);
325
- }
326
- break;
327
- }
328
-
329
- // Decode chunk and add to buffer
330
- buffer += decoder.decode(value, { stream: true });
331
-
332
- // Process complete SSE messages (ending with \n\n)
333
- const messages = buffer.split('\n\n');
334
-
335
- // Keep the last incomplete message in the buffer
336
- buffer = messages.pop() || '';
337
-
338
- // Process each complete message
339
- for (const message of messages) {
340
- if (!message.trim()) continue;
341
-
342
- // Parse SSE format: "data: {...}"
343
- const lines = message.split('\n');
344
- for (const line of lines) {
345
- if (line.startsWith('data: ')) {
346
- try {
347
- const jsonStr = line.substring(6);
348
- const data = JSON.parse(jsonStr);
349
- console.log('[Stream] Received event:', data.type, data.content?.substring(0, 50));
350
-
351
- if (data.type === 'chunk' && data.content) {
352
- accumulatedCode += data.content;
353
- onChunk(data.content);
354
- } else if (data.type === 'complete') {
355
- console.log('[Stream] Generation complete, total code length:', data.code?.length || accumulatedCode.length);
356
- // Use the complete code from the message if available, otherwise use accumulated
357
- const finalCode = data.code || accumulatedCode;
358
- onComplete(finalCode, data.reasoning);
359
- // Don't return yet - might have deployment events coming
360
- } else if (data.type === 'deploying') {
361
- console.log('[Stream] Deployment started:', data.message);
362
- if (onDeploying) {
363
- onDeploying(data.message || 'Deploying...');
364
- }
365
- } else if (data.type === 'deployed') {
366
- console.log('[Stream] Deployment successful:', data.space_url);
367
- if (onDeployed) {
368
- onDeployed(data.message || 'Deployed!', data.space_url);
369
- }
370
- } else if (data.type === 'deploy_error') {
371
- console.log('[Stream] Deployment error:', data.message);
372
- if (onDeployError) {
373
- onDeployError(data.message || 'Deployment failed');
374
- }
375
- } else if (data.type === 'error') {
376
- console.error('[Stream] Error:', data.message);
377
- onError(data.message || 'Unknown error occurred');
378
- return; // Exit the processing loop
379
- }
380
- } catch (error) {
381
- console.error('Error parsing SSE data:', error, 'Line:', line);
382
- }
383
- }
384
- }
385
- }
386
- }
387
- })
388
- .catch((error) => {
389
- if (error.name === 'AbortError') {
390
- console.log('[Stream] Request aborted');
391
- return;
392
- }
393
- console.error('[Stream] Fetch error:', error);
394
- onError(error.message || 'Connection error occurred');
395
- });
396
-
397
- // Return cleanup function
398
- return () => {
399
- abortController.abort();
400
- };
401
- }
402
-
403
- // Alternative: WebSocket-based generation
404
- generateCodeWebSocket(
405
- request: CodeGenerationRequest,
406
- onChunk: (content: string) => void,
407
- onComplete: (code: string) => void,
408
- onError: (error: string) => void
409
- ): WebSocket {
410
- // Build WebSocket URL correctly for both dev and production
411
- const baseUrl = API_URL || window.location.origin;
412
- const wsUrl = baseUrl.replace('http', 'ws') + '/ws/generate';
413
- const ws = new WebSocket(wsUrl);
414
-
415
- ws.onopen = () => {
416
- ws.send(JSON.stringify(request));
417
- };
418
-
419
- ws.onmessage = (event) => {
420
- try {
421
- const data = JSON.parse(event.data);
422
-
423
- if (data.type === 'chunk' && data.content) {
424
- onChunk(data.content);
425
- } else if (data.type === 'complete' && data.code) {
426
- onComplete(data.code);
427
- ws.close();
428
- } else if (data.type === 'error') {
429
- onError(data.message || 'Unknown error occurred');
430
- ws.close();
431
- }
432
- } catch (error) {
433
- console.error('Error parsing WebSocket data:', error);
434
- }
435
- };
436
-
437
- ws.onerror = (error) => {
438
- console.error('WebSocket error:', error);
439
- onError('Connection error occurred');
440
- };
441
-
442
- return ws;
443
- }
444
-
445
- async deploy(request: DeploymentRequest): Promise<DeploymentResponse> {
446
- console.log('[API Client] Deploy request:', {
447
- endpoint: '/api/deploy',
448
- method: 'POST',
449
- baseURL: API_URL,
450
- hasToken: !!this.token,
451
- language: request.language,
452
- code_length: request.code?.length,
453
- space_name: request.space_name,
454
- existing_repo_id: request.existing_repo_id,
455
- });
456
-
457
- try {
458
- const response = await this.client.post<DeploymentResponse>('/api/deploy', request);
459
- console.log('[API Client] Deploy response:', response.status, response.data);
460
- return response.data;
461
- } catch (error: any) {
462
- console.error('[API Client] Deploy error:', {
463
- status: error.response?.status,
464
- statusText: error.response?.statusText,
465
- data: error.response?.data,
466
- message: error.message,
467
- });
468
- throw error;
469
- }
470
- }
471
-
472
- async importProject(url: string, preferLocal: boolean = false): Promise<any> {
473
- const response = await this.client.post('/api/import', { url, prefer_local: preferLocal });
474
- return response.data;
475
- }
476
-
477
- async importSpace(username: string, spaceName: string): Promise<any> {
478
- const response = await this.client.get(`/api/import/space/${username}/${spaceName}`);
479
- return response.data;
480
- }
481
-
482
- async importModel(modelId: string, preferLocal: boolean = false): Promise<any> {
483
- const response = await this.client.get(`/api/import/model/${modelId}`, {
484
- params: { prefer_local: preferLocal }
485
- });
486
- return response.data;
487
- }
488
-
489
- async importGithub(owner: string, repo: string): Promise<any> {
490
- const response = await this.client.get(`/api/import/github/${owner}/${repo}`);
491
- return response.data;
492
- }
493
-
494
- async createPullRequest(repoId: string, code: string, language: string, prTitle?: string, prDescription?: string): Promise<any> {
495
- const response = await this.client.post('/api/create-pr', {
496
- repo_id: repoId,
497
- code,
498
- language,
499
- pr_title: prTitle,
500
- pr_description: prDescription
501
- });
502
- return response.data;
503
- }
504
-
505
- async duplicateSpace(fromSpaceId: string, toSpaceName?: string, isPrivate: boolean = false): Promise<any> {
506
- const response = await this.client.post('/api/duplicate-space', {
507
- from_space_id: fromSpaceId,
508
- to_space_name: toSpaceName,
509
- private: isPrivate
510
- });
511
- return response.data;
512
- }
513
-
514
- logout() {
515
- this.token = null;
516
- }
517
-
518
- async getTrendingAnycoderApps(): Promise<any[]> {
519
- try {
520
- // Fetch from HuggingFace API directly
521
- const response = await axios.get('https://huggingface.co/api/spaces', {
522
- timeout: 5000,
523
- });
524
-
525
- // Filter for apps with 'anycoder' tag and sort by trendingScore
526
- const anycoderApps = response.data
527
- .filter((space: any) => space.tags && space.tags.includes('anycoder'))
528
- .sort((a: any, b: any) => (b.trendingScore || 0) - (a.trendingScore || 0))
529
- .slice(0, 6);
530
-
531
- return anycoderApps;
532
- } catch (error) {
533
- console.error('Failed to fetch trending anycoder apps:', error);
534
- return [];
535
- }
536
- }
537
- }
538
-
539
- // Export singleton instance
540
- export const apiClient = new ApiClient();
541
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/lib/auth.ts DELETED
@@ -1,287 +0,0 @@
1
- // HuggingFace OAuth authentication utilities (Server-side flow for Docker Spaces)
2
-
3
- const STORAGE_KEY = 'hf_oauth_token';
4
- const SESSION_KEY = 'hf_session_token'; // NEW: Store session UUID
5
- const USER_INFO_KEY = 'hf_user_info';
6
- const DEV_MODE_KEY = 'hf_dev_mode';
7
- const API_BASE = '/api';
8
-
9
- // Check if we're in development mode (localhost)
10
- const isDevelopment = typeof window !== 'undefined' &&
11
- (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
12
-
13
- export interface OAuthUserInfo {
14
- id?: string;
15
- sub?: string;
16
- name: string;
17
- preferred_username?: string;
18
- preferredUsername?: string;
19
- picture?: string;
20
- avatarUrl?: string;
21
- }
22
-
23
- export interface OAuthResult {
24
- accessToken: string;
25
- accessTokenExpiresAt: Date;
26
- userInfo: OAuthUserInfo;
27
- }
28
-
29
- /**
30
- * Initialize OAuth and check if user is logged in
31
- * Returns OAuth result if user is already logged in
32
- */
33
- export async function initializeOAuth(): Promise<OAuthResult | null> {
34
- try {
35
- // In development mode, check for dev mode login first
36
- if (isDevelopment && isDevModeEnabled()) {
37
- const storedToken = getStoredToken();
38
- const storedUserInfo = getStoredUserInfo();
39
-
40
- if (storedToken && storedUserInfo) {
41
- return {
42
- accessToken: storedToken,
43
- accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
44
- userInfo: storedUserInfo,
45
- };
46
- }
47
- return null;
48
- }
49
-
50
- // Check if we're handling an OAuth callback (session parameter in URL)
51
- const urlParams = new URLSearchParams(window.location.search);
52
- const sessionToken = urlParams.get('session');
53
-
54
- if (sessionToken) {
55
- // Fetch session data from backend
56
- try {
57
- const response = await fetch(`${API_BASE}/auth/session?session=${sessionToken}`);
58
- if (response.ok) {
59
- const data = await response.json();
60
-
61
- // Normalize user info
62
- const userInfo: OAuthUserInfo = {
63
- id: data.user_info.sub || data.user_info.id,
64
- name: data.user_info.name,
65
- preferredUsername: data.user_info.preferred_username || data.user_info.preferredUsername,
66
- avatarUrl: data.user_info.picture || data.user_info.avatarUrl,
67
- };
68
-
69
- const oauthResult: OAuthResult = {
70
- accessToken: data.access_token,
71
- accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
72
- userInfo,
73
- };
74
-
75
- // Store the OAuth result AND session token
76
- storeOAuthData(oauthResult);
77
- storeSessionToken(sessionToken); // NEW: Store session UUID
78
-
79
- // Clean up URL
80
- window.history.replaceState({}, document.title, window.location.pathname);
81
-
82
- return oauthResult;
83
- }
84
- } catch (error) {
85
- console.error('Failed to fetch session:', error);
86
- }
87
- }
88
-
89
- // Check if we have stored credentials
90
- const storedToken = getStoredToken();
91
- const storedUserInfo = getStoredUserInfo();
92
-
93
- if (storedToken && storedUserInfo) {
94
- return {
95
- accessToken: storedToken,
96
- accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
97
- userInfo: storedUserInfo,
98
- };
99
- }
100
-
101
- return null;
102
- } catch (error) {
103
- console.error('OAuth initialization error:', error);
104
- return null;
105
- }
106
- }
107
-
108
- /**
109
- * Redirect to HuggingFace OAuth login page (via backend)
110
- */
111
- export async function loginWithHuggingFace(): Promise<void> {
112
- try {
113
- // Call backend to get OAuth URL
114
- const response = await fetch(`${API_BASE}/auth/login`);
115
- if (!response.ok) {
116
- throw new Error('Failed to get login URL');
117
- }
118
-
119
- const data = await response.json();
120
- // Redirect to the OAuth authorization URL
121
- window.location.href = data.login_url;
122
- } catch (error) {
123
- console.error('Failed to initiate OAuth login:', error);
124
- throw new Error('Failed to start login process');
125
- }
126
- }
127
-
128
- /**
129
- * Logout and clear stored credentials
130
- */
131
- export function logout(): void {
132
- if (typeof window !== 'undefined') {
133
- localStorage.removeItem(STORAGE_KEY);
134
- localStorage.removeItem(SESSION_KEY); // NEW: Clear session token
135
- localStorage.removeItem(USER_INFO_KEY);
136
- localStorage.removeItem(DEV_MODE_KEY);
137
- }
138
- }
139
-
140
- /**
141
- * Store OAuth data in localStorage
142
- */
143
- function storeOAuthData(result: OAuthResult): void {
144
- if (typeof window !== 'undefined') {
145
- localStorage.setItem(STORAGE_KEY, result.accessToken);
146
- localStorage.setItem(USER_INFO_KEY, JSON.stringify(result.userInfo));
147
- }
148
- }
149
-
150
- /**
151
- * Store session token in localStorage
152
- */
153
- function storeSessionToken(sessionToken: string): void {
154
- if (typeof window !== 'undefined') {
155
- localStorage.setItem(SESSION_KEY, sessionToken);
156
- }
157
- }
158
-
159
- /**
160
- * Get stored session token
161
- */
162
- export function getStoredSessionToken(): string | null {
163
- if (typeof window !== 'undefined') {
164
- return localStorage.getItem(SESSION_KEY);
165
- }
166
- return null;
167
- }
168
-
169
- /**
170
- * Get stored access token
171
- */
172
- export function getStoredToken(): string | null {
173
- if (typeof window !== 'undefined') {
174
- return localStorage.getItem(STORAGE_KEY);
175
- }
176
- return null;
177
- }
178
-
179
- /**
180
- * Get stored user info
181
- */
182
- export function getStoredUserInfo(): OAuthUserInfo | null {
183
- if (typeof window !== 'undefined') {
184
- const userInfoStr = localStorage.getItem(USER_INFO_KEY);
185
- if (userInfoStr) {
186
- try {
187
- return JSON.parse(userInfoStr);
188
- } catch {
189
- return null;
190
- }
191
- }
192
- }
193
- return null;
194
- }
195
-
196
- /**
197
- * Check if user is authenticated
198
- */
199
- export function isAuthenticated(): boolean {
200
- return getStoredToken() !== null;
201
- }
202
-
203
- /**
204
- * Validate authentication with backend
205
- * Returns true if authenticated, false if session expired
206
- */
207
- export async function validateAuthentication(): Promise<boolean> {
208
- const token = getStoredToken();
209
- if (!token) {
210
- return false;
211
- }
212
-
213
- // Skip validation for dev mode tokens
214
- if (isDevelopment && token.startsWith('dev_token_')) {
215
- return true;
216
- }
217
-
218
- try {
219
- const response = await fetch(`${API_BASE}/auth/status`, {
220
- headers: {
221
- 'Authorization': `Bearer ${token}`,
222
- },
223
- });
224
-
225
- if (response.status === 401) {
226
- // Session expired, clean up
227
- logout();
228
- return false;
229
- }
230
-
231
- if (!response.ok) {
232
- return false;
233
- }
234
-
235
- const data = await response.json();
236
- return data.authenticated === true;
237
- } catch (error) {
238
- console.error('Failed to validate authentication:', error);
239
- return false;
240
- }
241
- }
242
-
243
- /**
244
- * Development mode login (mock authentication)
245
- */
246
- export function loginDevMode(username: string): OAuthResult {
247
- const mockToken = `dev_token_${username}_${Date.now()}`;
248
- const mockUserInfo: OAuthUserInfo = {
249
- id: `dev_${Date.now()}`,
250
- name: username,
251
- preferredUsername: username.toLowerCase().replace(/\s+/g, '_'),
252
- avatarUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(username)}&background=random&size=128`,
253
- };
254
-
255
- const result: OAuthResult = {
256
- accessToken: mockToken,
257
- accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
258
- userInfo: mockUserInfo,
259
- };
260
-
261
- // Store the mock data
262
- storeOAuthData(result);
263
- // Mark as dev mode
264
- if (typeof window !== 'undefined') {
265
- localStorage.setItem(DEV_MODE_KEY, 'true');
266
- }
267
-
268
- return result;
269
- }
270
-
271
- /**
272
- * Check if dev mode is enabled
273
- */
274
- export function isDevModeEnabled(): boolean {
275
- if (typeof window !== 'undefined') {
276
- return localStorage.getItem(DEV_MODE_KEY) === 'true';
277
- }
278
- return false;
279
- }
280
-
281
- /**
282
- * Check if we're in development environment
283
- */
284
- export function isDevelopmentMode(): boolean {
285
- return isDevelopment;
286
- }
287
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/types/index.ts DELETED
@@ -1,69 +0,0 @@
1
- // Type definitions for AnyCoder frontend
2
-
3
- export interface Model {
4
- name: string;
5
- id: string;
6
- description: string;
7
- supports_images?: boolean;
8
- }
9
-
10
- export interface Message {
11
- role: 'user' | 'assistant' | 'system';
12
- content: string;
13
- timestamp?: string;
14
- image_url?: string; // For vision models
15
- }
16
-
17
- export interface CodeGenerationRequest {
18
- query: string;
19
- language: string;
20
- model_id: string;
21
- provider: string;
22
- history: string[][];
23
- agent_mode: boolean;
24
- existing_repo_id?: string; // For auto-deploy to update existing space
25
- skip_auto_deploy?: boolean; // Skip auto-deploy (for PR creation)
26
- image_url?: string; // For vision models like GLM-4.6V
27
- }
28
-
29
- export interface CodeGenerationResponse {
30
- code: string;
31
- history: string[][];
32
- status: string;
33
- }
34
-
35
- export interface StreamChunk {
36
- type: 'chunk' | 'complete' | 'error' | 'status';
37
- content?: string;
38
- code?: string;
39
- message?: string;
40
- progress?: number;
41
- timestamp?: string;
42
- }
43
-
44
- export interface AuthStatus {
45
- authenticated: boolean;
46
- username?: string;
47
- message: string;
48
- }
49
-
50
- export interface DeploymentRequest {
51
- code: string;
52
- space_name?: string;
53
- language: string;
54
- requirements?: string;
55
- existing_repo_id?: string; // For updating existing spaces
56
- commit_message?: string;
57
- history?: Array<{ role: string; content: string }>; // Chat history for tracking
58
- }
59
-
60
- export interface DeploymentResponse {
61
- success: boolean;
62
- space_url?: string;
63
- message: string;
64
- dev_mode?: boolean;
65
- repo_id?: string;
66
- }
67
-
68
- export type Language = 'html' | 'gradio' | 'transformers.js' | 'streamlit' | 'comfyui' | 'react' | 'daggr';
69
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/tailwind.config.js DELETED
@@ -1,29 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- module.exports = {
3
- content: [
4
- './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5
- './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6
- './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7
- ],
8
- theme: {
9
- extend: {
10
- colors: {
11
- primary: {
12
- 50: '#eff6ff',
13
- 100: '#dbeafe',
14
- 200: '#bfdbfe',
15
- 300: '#93c5fd',
16
- 400: '#60a5fa',
17
- 500: '#3b82f6',
18
- 600: '#2563eb',
19
- 700: '#1d4ed8',
20
- 800: '#1e40af',
21
- 900: '#1e3a8a',
22
- },
23
- },
24
- },
25
- },
26
- plugins: [],
27
- darkMode: 'class',
28
- }
29
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/tsconfig.json DELETED
@@ -1,41 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "lib": [
4
- "dom",
5
- "dom.iterable",
6
- "esnext"
7
- ],
8
- "allowJs": true,
9
- "skipLibCheck": true,
10
- "strict": true,
11
- "noEmit": true,
12
- "esModuleInterop": true,
13
- "module": "esnext",
14
- "moduleResolution": "bundler",
15
- "resolveJsonModule": true,
16
- "isolatedModules": true,
17
- "jsx": "react-jsx",
18
- "incremental": true,
19
- "plugins": [
20
- {
21
- "name": "next"
22
- }
23
- ],
24
- "paths": {
25
- "@/*": [
26
- "./src/*"
27
- ]
28
- },
29
- "target": "ES2017"
30
- },
31
- "include": [
32
- "next-env.d.ts",
33
- "**/*.ts",
34
- "**/*.tsx",
35
- ".next/types/**/*.ts",
36
- ".next/dev/types/**/*.ts"
37
- ],
38
- "exclude": [
39
- "node_modules"
40
- ]
41
- }