show reasoning
Browse files- backend_api.py +47 -3
- frontend/src/app/page.tsx +7 -3
- frontend/src/lib/api.ts +45 -45
backend_api.py
CHANGED
|
@@ -733,12 +733,49 @@ def cleanup_generated_code(code: str, language: str) -> str:
|
|
| 733 |
return code
|
| 734 |
|
| 735 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
@app.post("/api/generate")
|
| 737 |
async def generate_code(
|
| 738 |
request: CodeGenerationRequest,
|
| 739 |
authorization: Optional[str] = Header(None)
|
| 740 |
):
|
| 741 |
"""Generate code based on user query - returns streaming response"""
|
|
|
|
|
|
|
| 742 |
# Dev mode: No authentication required - just use server's HF_TOKEN
|
| 743 |
# In production, you would check real OAuth tokens here
|
| 744 |
|
|
@@ -872,14 +909,21 @@ async def generate_code(
|
|
| 872 |
})
|
| 873 |
yield f"data: {event_data}\n\n"
|
| 874 |
|
|
|
|
|
|
|
|
|
|
| 875 |
# Clean up generated code (remove LLM explanatory text and markdown)
|
| 876 |
generated_code = cleanup_generated_code(generated_code, language)
|
| 877 |
|
| 878 |
-
# Send completion event (
|
| 879 |
-
|
| 880 |
"type": "complete",
|
| 881 |
"code": generated_code
|
| 882 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
yield f"data: {completion_data}\n\n"
|
| 884 |
|
| 885 |
# Auto-deploy after code generation (if authenticated and not skipped)
|
|
|
|
| 733 |
return code
|
| 734 |
|
| 735 |
|
| 736 |
+
def extract_reasoning(code: str, language: str) -> str:
|
| 737 |
+
"""Extract LLM reasoning/explanatory text that's outside the main code block"""
|
| 738 |
+
try:
|
| 739 |
+
if not code:
|
| 740 |
+
return ""
|
| 741 |
+
|
| 742 |
+
# 1. Check for <think> tags (e.g. from DeepSeek-R1 or newer GLM-4)
|
| 743 |
+
think_match = re.search(r'<think>([\s\S]*?)</think>', code, re.IGNORECASE)
|
| 744 |
+
if think_match:
|
| 745 |
+
return think_match.group(1).strip()
|
| 746 |
+
|
| 747 |
+
# 2. Extract everything outside of markdown code blocks
|
| 748 |
+
blocks = list(re.finditer(r'```(?:[\w]*)\s*\n([\s\S]*?)(?:\n```|$)', code, re.IGNORECASE))
|
| 749 |
+
|
| 750 |
+
if not blocks:
|
| 751 |
+
return ""
|
| 752 |
+
|
| 753 |
+
text_parts = []
|
| 754 |
+
last_end = 0
|
| 755 |
+
for match in blocks:
|
| 756 |
+
pre_text = code[last_end:match.start()].strip()
|
| 757 |
+
if pre_text and len(pre_text) > 10:
|
| 758 |
+
text_parts.append(pre_text)
|
| 759 |
+
last_end = match.end()
|
| 760 |
+
|
| 761 |
+
post_text = code[last_end:].strip()
|
| 762 |
+
if post_text and len(post_text) > 10:
|
| 763 |
+
text_parts.append(post_text)
|
| 764 |
+
|
| 765 |
+
return "\n\n".join(text_parts).strip()
|
| 766 |
+
except Exception as e:
|
| 767 |
+
print(f"[Reasoning Extraction] Error: {e}")
|
| 768 |
+
return ""
|
| 769 |
+
|
| 770 |
+
|
| 771 |
@app.post("/api/generate")
|
| 772 |
async def generate_code(
|
| 773 |
request: CodeGenerationRequest,
|
| 774 |
authorization: Optional[str] = Header(None)
|
| 775 |
):
|
| 776 |
"""Generate code based on user query - returns streaming response"""
|
| 777 |
+
|
| 778 |
+
|
| 779 |
# Dev mode: No authentication required - just use server's HF_TOKEN
|
| 780 |
# In production, you would check real OAuth tokens here
|
| 781 |
|
|
|
|
| 909 |
})
|
| 910 |
yield f"data: {event_data}\n\n"
|
| 911 |
|
| 912 |
+
# Extract reasoning before cleaning up
|
| 913 |
+
reasoning = extract_reasoning(generated_code, language)
|
| 914 |
+
|
| 915 |
# Clean up generated code (remove LLM explanatory text and markdown)
|
| 916 |
generated_code = cleanup_generated_code(generated_code, language)
|
| 917 |
|
| 918 |
+
# Send completion event (include reasoning for GLM-4.7)
|
| 919 |
+
completion_dict = {
|
| 920 |
"type": "complete",
|
| 921 |
"code": generated_code
|
| 922 |
+
}
|
| 923 |
+
if selected_model_id == "zai-org/GLM-4.7" and reasoning:
|
| 924 |
+
completion_dict["reasoning"] = reasoning
|
| 925 |
+
|
| 926 |
+
completion_data = json.dumps(completion_dict)
|
| 927 |
yield f"data: {completion_data}\n\n"
|
| 928 |
|
| 929 |
# Auto-deploy after code generation (if authenticated and not skipped)
|
frontend/src/app/page.tsx
CHANGED
|
@@ -367,16 +367,20 @@ export default function Home() {
|
|
| 367 |
});
|
| 368 |
},
|
| 369 |
// onComplete
|
| 370 |
-
(code: string) => {
|
| 371 |
setGeneratedCode(code);
|
| 372 |
setIsGenerating(false);
|
| 373 |
|
| 374 |
-
// Update final message -
|
| 375 |
setMessages((prev) => {
|
| 376 |
const newMessages = [...prev];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
newMessages[newMessages.length - 1] = {
|
| 378 |
...assistantMessage,
|
| 379 |
-
content:
|
| 380 |
};
|
| 381 |
return newMessages;
|
| 382 |
});
|
|
|
|
| 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 |
});
|
frontend/src/lib/api.ts
CHANGED
|
@@ -18,20 +18,20 @@ const getApiUrl = () => {
|
|
| 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 '';
|
|
@@ -70,23 +70,23 @@ class ApiClient {
|
|
| 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.' }
|
|
@@ -116,19 +116,19 @@ class ApiClient {
|
|
| 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);
|
|
@@ -138,7 +138,7 @@ class ApiClient {
|
|
| 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,
|
|
@@ -161,26 +161,26 @@ class ApiClient {
|
|
| 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);
|
|
@@ -188,7 +188,7 @@ class ApiClient {
|
|
| 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 |
}
|
|
@@ -209,26 +209,26 @@ class ApiClient {
|
|
| 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);
|
|
@@ -236,7 +236,7 @@ class ApiClient {
|
|
| 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'] };
|
|
@@ -273,7 +273,7 @@ class ApiClient {
|
|
| 273 |
generateCodeStream(
|
| 274 |
request: CodeGenerationRequest,
|
| 275 |
onChunk: (content: string) => void,
|
| 276 |
-
onComplete: (code: string) => void,
|
| 277 |
onError: (error: string) => void,
|
| 278 |
onDeploying?: (message: string) => void,
|
| 279 |
onDeployed?: (message: string, spaceUrl: string) => void,
|
|
@@ -282,11 +282,11 @@ class ApiClient {
|
|
| 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',
|
|
@@ -303,21 +303,21 @@ class ApiClient {
|
|
| 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) {
|
|
@@ -325,20 +325,20 @@ class ApiClient {
|
|
| 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) {
|
|
@@ -347,7 +347,7 @@ class ApiClient {
|
|
| 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);
|
|
@@ -355,7 +355,7 @@ class ApiClient {
|
|
| 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);
|
| 359 |
// Don't return yet - might have deployment events coming
|
| 360 |
} else if (data.type === 'deploying') {
|
| 361 |
console.log('[Stream] Deployment started:', data.message);
|
|
@@ -419,7 +419,7 @@ class ApiClient {
|
|
| 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) {
|
|
@@ -453,7 +453,7 @@ class ApiClient {
|
|
| 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);
|
|
@@ -521,13 +521,13 @@ class ApiClient {
|
|
| 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);
|
|
|
|
| 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 '';
|
|
|
|
| 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.' }
|
|
|
|
| 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);
|
|
|
|
| 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,
|
|
|
|
| 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);
|
|
|
|
| 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 |
}
|
|
|
|
| 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);
|
|
|
|
| 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'] };
|
|
|
|
| 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,
|
|
|
|
| 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',
|
|
|
|
| 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) {
|
|
|
|
| 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) {
|
|
|
|
| 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);
|
|
|
|
| 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);
|
|
|
|
| 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) {
|
|
|
|
| 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);
|
|
|
|
| 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);
|