Shantanupathak94 commited on
Commit
deaa2dd
·
1 Parent(s): a4028c4

frontend connecting

Browse files
Files changed (1) hide show
  1. client/src/useAIService.js +40 -960
client/src/useAIService.js CHANGED
@@ -1,974 +1,54 @@
1
- /**
2
- * ╔══════════════════════════════════════════════════════════════════════════╗
3
- * ║ ETHRIX-FORGE — AI API SERVICE INTEGRATION MODULE ║
4
- * ║ useAIService.js | Custom React Hook ║
5
- * ║ ║
6
- * ║ Providers Supported: ║
7
- * ║ • Google Gemini (google-genai SDK — new 2024+ SDK) ║
8
- * ║ • Groq (OpenAI-compatible REST) ║
9
- * ║ • OpenRouter (OpenAI-compatible REST, multi-model gateway) ║
10
- * ╚══════════════════════════════════════════════════════════════════════════╝
11
- *
12
- * USAGE IN App.jsx:
13
- * import { useAIService } from './useAIService';
14
- *
15
- * const {
16
- * generate,
17
- * isLoading,
18
- * error,
19
- * activeProvider,
20
- * setActiveProvider,
21
- * apiKey,
22
- * setApiKey,
23
- * activeModel,
24
- * setActiveModel,
25
- * lastRawResponse,
26
- * retryLastRequest,
27
- * } = useAIService();
28
- *
29
- * // Then call:
30
- * const files = await generate("Build me a responsive landing page");
31
- * // Returns: [{ filename, language, code }, ...] — ready for Monaco injection
32
- */
33
 
34
- import { useState, useCallback, useRef } from "react";
35
-
36
- // ─────────────────────────────────────────────────────────────────────────────
37
- // 1. PROVIDER REGISTRY
38
- // Central configuration for all supported AI providers.
39
- // Add new providers here without touching hook logic.
40
- // ─────────────────────────────────────────────────────────────────────────────
41
 
42
  export const PROVIDERS = {
43
  GEMINI: "gemini",
44
  GROQ: "groq",
45
- OPENROUTER: "openrouter",
46
- };
47
-
48
- export const PROVIDER_CONFIG = {
49
- [PROVIDERS.GEMINI]: {
50
- label: "Google Gemini",
51
- sdkType: "gemini", // uses google-genai SDK
52
- defaultModel: "gemini-2.0-flash",
53
- availableModels: [
54
- { id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
55
- { id: "gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite" },
56
- { id: "gemini-1.5-pro", label: "Gemini 1.5 Pro" },
57
- { id: "gemini-1.5-flash", label: "Gemini 1.5 Flash" },
58
- ],
59
- docsUrl: "https://ai.google.dev/gemini-api/docs",
60
- },
61
- [PROVIDERS.GROQ]: {
62
- label: "Groq",
63
- sdkType: "openai-compat",
64
- endpoint: "https://api.groq.com/openai/v1/chat/completions",
65
- defaultModel: "llama-3.3-70b-versatile",
66
- availableModels: [
67
- { id: "llama-3.3-70b-versatile", label: "LLaMA 3.3 70B Versatile" },
68
- { id: "llama-3.1-8b-instant", label: "LLaMA 3.1 8B Instant" },
69
- { id: "mixtral-8x7b-32768", label: "Mixtral 8x7B" },
70
- { id: "gemma2-9b-it", label: "Gemma 2 9B" },
71
- ],
72
- docsUrl: "https://console.groq.com/docs/openai",
73
- },
74
- [PROVIDERS.OPENROUTER]: {
75
- label: "OpenRouter",
76
- sdkType: "openai-compat",
77
- endpoint: "https://openrouter.ai/api/v1/chat/completions",
78
- defaultModel: "anthropic/claude-3.5-sonnet",
79
- availableModels: [
80
- { id: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet" },
81
- { id: "openai/gpt-4o", label: "GPT-4o" },
82
- { id: "openai/gpt-4o-mini", label: "GPT-4o Mini" },
83
- { id: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" },
84
- { id: "meta-llama/llama-3.3-70b-instruct", label: "LLaMA 3.3 70B" },
85
- { id: "deepseek/deepseek-r1", label: "DeepSeek R1" },
86
- ],
87
- extraHeaders: {
88
- "HTTP-Referer": "https://ethrix-forge.dev", // Replace with your domain
89
- "X-Title": "Ethrix-Forge IDE",
90
- },
91
- docsUrl: "https://openrouter.ai/docs",
92
- },
93
  };
94
 
95
- // ─────────────────────────────────────────────────────────────────────────────
96
- // 2. SYSTEM PROMPT
97
- // Strictly instructs the AI to return ONLY a valid JSON array.
98
- // No markdown, no prose, no backticks — ever.
99
- // ─────────────────────────────────────────────────────────────────────────────
100
-
101
- const SYSTEM_PROMPT = `You are Ethrix, an elite autonomous software engineer embedded inside the Ethrix-Forge AI IDE. Your sole function is to generate complete, production-ready source code in response to user requests.
102
-
103
- ABSOLUTE OUTPUT RULES — NEVER VIOLATE THESE:
104
- 1. Your response MUST be a single, raw, valid JSON array. Nothing else.
105
- 2. The array contains file objects. Each file object MUST have exactly three keys:
106
- - "filename": the full file name with extension (e.g., "index.html", "styles/main.css", "src/App.jsx")
107
- - "language": the lowercase language identifier used by Monaco Editor (e.g., "html", "css", "javascript", "typescript", "python", "json")
108
- - "code": the complete, untruncated source code for that file as a single string
109
- 3. DO NOT wrap the JSON in markdown code fences (\`\`\`json ... \`\`\`).
110
- 4. DO NOT include any explanation, preamble, commentary, or text before or after the JSON array.
111
- 5. DO NOT truncate code. Every file must be fully complete and functional.
112
- 6. DO NOT use placeholder comments like "// ... rest of code here".
113
- 7. Strings inside the "code" value MUST have all double-quotes escaped as \\", and all newlines as \\n.
114
-
115
- VALID OUTPUT EXAMPLE:
116
- [{"filename":"index.html","language":"html","code":"<!DOCTYPE html>\\n<html>\\n<head>\\n <title>App</title>\\n</head>\\n<body>\\n <h1>Hello</h1>\\n</body>\\n</html>"},{"filename":"style.css","language":"css","code":"body {\\n margin: 0;\\n font-family: sans-serif;\\n}"}]
117
-
118
- CODE QUALITY STANDARDS:
119
- - Write modern, idiomatic code for the target language/framework.
120
- - Include all necessary imports, dependencies, and boilerplate.
121
- - Produce fully functional code that runs without modification.
122
- - Separate concerns properly across multiple files when appropriate.
123
- - Use best practices: error handling, accessibility, responsive design where relevant.
124
-
125
- You are a machine that outputs JSON. You do not converse. You do not explain. You only output the JSON array.`;
126
-
127
- // ─────────────────────────────────────────────────────────────────────────────
128
- // 3. ERROR CLASSES
129
- // Custom errors for granular error handling in consuming components.
130
- // ─────────────────────────────────────────────────────────────────────────────
131
-
132
- export class AIServiceError extends Error {
133
- constructor(message, code, provider, retryable = false) {
134
- super(message);
135
- this.name = "AIServiceError";
136
- this.code = code;
137
- this.provider = provider;
138
- this.retryable = retryable;
139
- this.timestamp = new Date().toISOString();
140
- }
141
- }
142
-
143
- export const ERROR_CODES = {
144
- INVALID_API_KEY: "INVALID_API_KEY",
145
- RATE_LIMITED: "RATE_LIMITED",
146
- QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
147
- INVALID_JSON: "INVALID_JSON",
148
- EMPTY_RESPONSE: "EMPTY_RESPONSE",
149
- NETWORK_ERROR: "NETWORK_ERROR",
150
- PROVIDER_ERROR: "PROVIDER_ERROR",
151
- NO_API_KEY: "NO_API_KEY",
152
- MODEL_NOT_FOUND: "MODEL_NOT_FOUND",
153
- CONTEXT_TOO_LONG: "CONTEXT_TOO_LONG",
154
- SAFETY_BLOCKED: "SAFETY_BLOCKED",
155
- UNKNOWN: "UNKNOWN",
156
- };
157
-
158
- // ─────────────────────────────────────────────────────────────────────────────
159
- // 4. JSON PARSER — Safe, multi-strategy extraction
160
- // ─────────────────────────────────────────────────────────────────────────────
161
-
162
- /**
163
- * Safely extracts and parses a JSON array from an AI text response.
164
- * Handles cases where the model accidentally wraps output in markdown fences.
165
- *
166
- * @param {string} rawText - Raw text response from AI
167
- * @param {string} provider - Provider name for error attribution
168
- * @returns {Array<{filename: string, language: string, code: string}>}
169
- * @throws {AIServiceError} if valid JSON array cannot be extracted
170
- */
171
- function parseAIResponse(rawText, provider) {
172
- if (!rawText || typeof rawText !== "string" || rawText.trim().length === 0) {
173
- throw new AIServiceError(
174
- "AI returned an empty response. The model may have been rate limited or the request was too large.",
175
- ERROR_CODES.EMPTY_RESPONSE,
176
- provider,
177
- true
178
- );
179
- }
180
-
181
- let text = rawText.trim();
182
-
183
- // Strategy 1: Direct parse (model obeyed instructions perfectly)
184
- try {
185
- const parsed = JSON.parse(text);
186
- return validateFileArray(parsed, provider);
187
- } catch (_) {
188
- // Fall through to extraction strategies
189
- }
190
-
191
- // Strategy 2: Strip markdown code fences (```json ... ``` or ``` ... ```)
192
- const fencePatterns = [
193
- /^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i,
194
- /^`([\s\S]*?)`$/,
195
- ];
196
- for (const pattern of fencePatterns) {
197
- const match = text.match(pattern);
198
- if (match) {
199
- try {
200
- const parsed = JSON.parse(match[1].trim());
201
- return validateFileArray(parsed, provider);
202
- } catch (_) {
203
- // Continue to next strategy
204
- }
205
- }
206
- }
207
-
208
- // Strategy 3: Extract the first [...] block found in the string
209
- const arrayMatch = text.match(/\[[\s\S]*\]/);
210
- if (arrayMatch) {
211
- try {
212
- const parsed = JSON.parse(arrayMatch[0]);
213
- return validateFileArray(parsed, provider);
214
- } catch (parseError) {
215
- throw new AIServiceError(
216
- `The AI returned malformed JSON that could not be repaired. Parse error: ${parseError.message}. ` +
217
- `Raw preview: ${text.substring(0, 200)}...`,
218
- ERROR_CODES.INVALID_JSON,
219
- provider,
220
- false
221
- );
222
- }
223
- }
224
-
225
- // All strategies failed
226
- throw new AIServiceError(
227
- `No valid JSON array found in the AI response. ` +
228
- `The model may have ignored formatting instructions. ` +
229
- `Raw preview: ${text.substring(0, 300)}`,
230
- ERROR_CODES.INVALID_JSON,
231
- provider,
232
- false
233
- );
234
- }
235
-
236
- /**
237
- * Validates that a parsed value is an array of valid file objects.
238
- * Auto-repairs common issues (missing language, wrong types).
239
- */
240
- function validateFileArray(data, provider) {
241
- if (!Array.isArray(data)) {
242
- throw new AIServiceError(
243
- `Expected a JSON array but received ${typeof data}. The AI returned a non-array JSON value.`,
244
- ERROR_CODES.INVALID_JSON,
245
- provider,
246
- false
247
- );
248
- }
249
-
250
- if (data.length === 0) {
251
- throw new AIServiceError(
252
- "The AI returned an empty file array. No files were generated.",
253
- ERROR_CODES.EMPTY_RESPONSE,
254
- provider,
255
- true
256
- );
257
- }
258
-
259
- return data.map((item, index) => {
260
- if (typeof item !== "object" || item === null) {
261
- throw new AIServiceError(
262
- `File at index ${index} is not a valid object.`,
263
- ERROR_CODES.INVALID_JSON,
264
- provider,
265
- false
266
- );
267
- }
268
-
269
- const filename = String(item.filename || `file_${index + 1}.txt`).trim();
270
- const code = String(item.code || "").trim();
271
-
272
- // Auto-infer language from extension if missing
273
- const language =
274
- item.language ||
275
- inferLanguageFromFilename(filename) ||
276
- "plaintext";
277
-
278
- return {
279
- id: `${filename}-${Date.now()}-${index}`, // Unique ID for React keys / Monaco models
280
- filename,
281
- language: String(language).toLowerCase().trim(),
282
- code,
283
- };
284
- });
285
- }
286
-
287
- /** Maps common file extensions to Monaco Editor language identifiers */
288
- function inferLanguageFromFilename(filename) {
289
- const ext = filename.split(".").pop()?.toLowerCase();
290
- const map = {
291
- js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
292
- html: "html", htm: "html", css: "css", scss: "scss", sass: "sass",
293
- json: "json", md: "markdown", py: "python", rb: "ruby",
294
- java: "java", go: "go", rs: "rust", cpp: "cpp", c: "c",
295
- sh: "shell", bash: "shell", yml: "yaml", yaml: "yaml",
296
- xml: "xml", svg: "xml", sql: "sql", php: "php",
297
- kt: "kotlin", swift: "swift", dart: "dart",
298
- };
299
- return map[ext] || "plaintext";
300
- }
301
-
302
- // ─────────────────────────────────────────────────────────────────────────────
303
- // 5. PROVIDER-SPECIFIC API CALLERS
304
- // ─────────────────────────────────────────────────────────────────────────────
305
-
306
- /**
307
- * HTTP status code → structured AIServiceError mapper for OpenAI-compatible APIs
308
- */
309
- function mapHttpError(status, body, provider) {
310
- const message = body?.error?.message || body?.message || `HTTP ${status}`;
311
-
312
- switch (status) {
313
- case 401:
314
- case 403:
315
- return new AIServiceError(
316
- `Invalid or unauthorized API key for ${provider}. Please check your key in Settings. Details: ${message}`,
317
- ERROR_CODES.INVALID_API_KEY,
318
- provider,
319
- false
320
- );
321
- case 429: {
322
- const isQuota =
323
- message.toLowerCase().includes("quota") ||
324
- message.toLowerCase().includes("billing");
325
- return new AIServiceError(
326
- isQuota
327
- ? `${provider} quota exceeded. Check your billing dashboard.`
328
- : `${provider} rate limit hit. Please wait a moment and try again.`,
329
- isQuota ? ERROR_CODES.QUOTA_EXCEEDED : ERROR_CODES.RATE_LIMITED,
330
- provider,
331
- true
332
- );
333
- }
334
- case 404:
335
- return new AIServiceError(
336
- `Model not found on ${provider}. Try selecting a different model. Details: ${message}`,
337
- ERROR_CODES.MODEL_NOT_FOUND,
338
- provider,
339
- false
340
- );
341
- case 413:
342
- return new AIServiceError(
343
- `Your request is too long for this model's context window. Try a simpler prompt or a model with a larger context.`,
344
- ERROR_CODES.CONTEXT_TOO_LONG,
345
- provider,
346
- false
347
- );
348
- case 500:
349
- case 502:
350
- case 503:
351
- return new AIServiceError(
352
- `${provider} server error (${status}). This is a provider-side issue. Retrying may help. Details: ${message}`,
353
- ERROR_CODES.PROVIDER_ERROR,
354
- provider,
355
- true
356
- );
357
- default:
358
- return new AIServiceError(
359
- `${provider} returned unexpected status ${status}. Details: ${message}`,
360
- ERROR_CODES.UNKNOWN,
361
- provider,
362
- false
363
- );
364
- }
365
- }
366
-
367
- // ── 5a. GEMINI (google-genai SDK) ────────────────────────────────────────────
368
- /**
369
- * Calls Gemini using the new `google-genai` SDK.
370
- *
371
- * Install: npm install @google/genai
372
- *
373
- * The new SDK uses `GoogleGenAI` (not `GoogleGenerativeAI`).
374
- * generateContent now lives on `ai.models.generateContent(...)`.
375
- */
376
- async function callGemini({ apiKey, model, userPrompt, signal }) {
377
- // Dynamic import keeps the SDK optional — won't crash if not installed
378
- let GoogleGenAI;
379
- try {
380
- ({ GoogleGenAI } = await import("@google/genai"));
381
- } catch {
382
- throw new AIServiceError(
383
- 'The @google/genai package is not installed. Run: npm install @google/genai',
384
- ERROR_CODES.PROVIDER_ERROR,
385
- PROVIDERS.GEMINI,
386
- false
387
- );
388
- }
389
-
390
- const ai = new GoogleGenAI({ apiKey });
391
-
392
- const requestConfig = {
393
- model,
394
- contents: [
395
- {
396
- role: "user",
397
- parts: [{ text: userPrompt }],
398
- },
399
- ],
400
- config: {
401
- systemInstruction: SYSTEM_PROMPT,
402
- // Gemini 2.0+ supports JSON response mime type — enforce structured output
403
- responseMimeType: "application/json",
404
- temperature: 0.2, // Low temp for deterministic code generation
405
- maxOutputTokens: 8192,
406
- },
407
- };
408
-
409
- let response;
410
- try {
411
- response = await ai.models.generateContent(requestConfig);
412
- } catch (err) {
413
- // Map Gemini SDK-specific errors
414
- const msg = err?.message || String(err);
415
-
416
- if (msg.includes("API_KEY_INVALID") || msg.includes("API key not valid")) {
417
- throw new AIServiceError(
418
- `Invalid Gemini API key. Please check your key in Settings.`,
419
- ERROR_CODES.INVALID_API_KEY,
420
- PROVIDERS.GEMINI,
421
- false
422
- );
423
- }
424
- if (msg.includes("RESOURCE_EXHAUSTED") || msg.includes("quota")) {
425
- throw new AIServiceError(
426
- `Gemini quota exceeded. Check your Google AI Studio dashboard.`,
427
- ERROR_CODES.QUOTA_EXCEEDED,
428
- PROVIDERS.GEMINI,
429
- true
430
- );
431
- }
432
- if (msg.includes("SAFETY") || msg.includes("blocked")) {
433
- throw new AIServiceError(
434
- `Gemini blocked this request due to safety filters. Try rephrasing your prompt.`,
435
- ERROR_CODES.SAFETY_BLOCKED,
436
- PROVIDERS.GEMINI,
437
- false
438
- );
439
- }
440
- if (signal?.aborted) return null; // Caller cancelled
441
- throw new AIServiceError(
442
- `Gemini SDK error: ${msg}`,
443
- ERROR_CODES.PROVIDER_ERROR,
444
- PROVIDERS.GEMINI,
445
- false
446
- );
447
- }
448
-
449
- const rawText = response?.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
450
- return rawText;
451
- }
452
-
453
- // ── 5b. GROQ (OpenAI-compatible REST) ────────────────────────────────────────
454
- async function callGroq({ apiKey, model, userPrompt, signal }) {
455
- const config = PROVIDER_CONFIG[PROVIDERS.GROQ];
456
-
457
- const body = {
458
- model,
459
- messages: [
460
- { role: "system", content: SYSTEM_PROMPT },
461
- { role: "user", content: userPrompt },
462
- ],
463
- temperature: 0.2,
464
- max_tokens: 8192,
465
- // Groq supports OpenAI-style JSON mode
466
- response_format: { type: "json_object" },
467
- };
468
-
469
- let res;
470
- try {
471
- res = await fetch(config.endpoint, {
472
- method: "POST",
473
- headers: {
474
- "Content-Type": "application/json",
475
- Authorization: `Bearer ${apiKey}`,
476
- },
477
- body: JSON.stringify(body),
478
- signal,
479
- });
480
- } catch (err) {
481
- if (err.name === "AbortError") return null;
482
- throw new AIServiceError(
483
- `Network error contacting Groq: ${err.message}. Check your internet connection.`,
484
- ERROR_CODES.NETWORK_ERROR,
485
- PROVIDERS.GROQ,
486
- true
487
- );
488
- }
489
-
490
- const data = await res.json().catch(() => ({}));
491
-
492
- if (!res.ok) {
493
- throw mapHttpError(res.status, data, PROVIDERS.GROQ);
494
- }
495
-
496
- return data?.choices?.[0]?.message?.content ?? "";
497
- }
498
-
499
- // ── 5c. OPENROUTER (OpenAI-compatible REST, multi-model gateway) ─────────────
500
- async function callOpenRouter({ apiKey, model, userPrompt, signal }) {
501
- const config = PROVIDER_CONFIG[PROVIDERS.OPENROUTER];
502
-
503
- const body = {
504
- model,
505
- messages: [
506
- { role: "system", content: SYSTEM_PROMPT },
507
- { role: "user", content: userPrompt },
508
- ],
509
- temperature: 0.2,
510
- max_tokens: 8192,
511
- response_format: { type: "json_object" },
512
- };
513
-
514
- let res;
515
- try {
516
- res = await fetch(config.endpoint, {
517
- method: "POST",
518
- headers: {
519
- "Content-Type": "application/json",
520
- Authorization: `Bearer ${apiKey}`,
521
- ...config.extraHeaders,
522
- },
523
- body: JSON.stringify(body),
524
- signal,
525
- });
526
- } catch (err) {
527
- if (err.name === "AbortError") return null;
528
- throw new AIServiceError(
529
- `Network error contacting OpenRouter: ${err.message}. Check your internet connection.`,
530
- ERROR_CODES.NETWORK_ERROR,
531
- PROVIDERS.OPENROUTER,
532
- true
533
- );
534
- }
535
-
536
- const data = await res.json().catch(() => ({}));
537
-
538
- if (!res.ok) {
539
- throw mapHttpError(res.status, data, PROVIDERS.OPENROUTER);
540
- }
541
-
542
- return data?.choices?.[0]?.message?.content ?? "";
543
- }
544
-
545
- // ─────────────────────────────────────────────────────────────────────────────
546
- // 6. PROVIDER DISPATCHER
547
- // Routes a generation request to the correct provider caller.
548
- // ─────────────────────────────────────────────────────────────────────────────
549
-
550
- const PROVIDER_CALLERS = {
551
- [PROVIDERS.GEMINI]: callGemini,
552
- [PROVIDERS.GROQ]: callGroq,
553
- [PROVIDERS.OPENROUTER]: callOpenRouter,
554
- };
555
-
556
- // ─────────────────────────────────────────────────────────────────────────────
557
- // 7. THE HOOK — useAIService
558
- // ─────────────────────────────────────────────────────────────────────────────
559
-
560
- /**
561
- * useAIService — Core AI generation hook for Ethrix-Forge.
562
- *
563
- * @param {Object} [options]
564
- * @param {string} [options.initialProvider="gemini"] - Starting provider key
565
- * @param {string} [options.initialApiKey=""] - Starting API key
566
- * @param {string} [options.initialModel] - Override default model
567
- * @param {number} [options.retryAttempts=2] - Auto-retry count on retryable errors
568
- * @param {number} [options.retryDelayMs=1500] - Base delay between retries (doubles each attempt)
569
- *
570
- * @returns {AIServiceHookResult}
571
- */
572
- export function useAIService({
573
- initialProvider = PROVIDERS.GEMINI,
574
- initialApiKey = "",
575
- initialModel = null,
576
- retryAttempts = 2,
577
- retryDelayMs = 1500,
578
- } = {}) {
579
-
580
- // ── State ──────────────────────────────────────────────────────────────────
581
- const [activeProvider, setActiveProviderState] = useState(initialProvider);
582
- const [apiKey, setApiKey] = useState(initialApiKey);
583
- const [activeModel, setActiveModel] = useState(
584
- initialModel || PROVIDER_CONFIG[initialProvider]?.defaultModel
585
- );
586
  const [isLoading, setIsLoading] = useState(false);
587
- const [error, setError] = useState(null); // AIServiceError | null
588
- const [lastRawResponse, setLastRawResponse] = useState(null);
589
- const [lastGeneratedFiles, setLastGeneratedFiles] = useState([]);
590
- const [requestCount, setRequestCount] = useState(0); // Useful for analytics/rate tracking
591
-
592
- // AbortController ref — allows cancellation of in-flight requests
593
- const abortControllerRef = useRef(null);
594
 
595
- // Store the last prompt for retry functionality
596
- const lastPromptRef = useRef(null);
597
-
598
- // ── Provider Switcher ──────────────────────────────────────────────────────
599
- /**
600
- * Switch the active provider. Automatically resets the model to the
601
- * new provider's default so you never end up with a model/provider mismatch.
602
- */
603
- const setActiveProvider = useCallback((providerKey) => {
604
- if (!PROVIDER_CONFIG[providerKey]) {
605
- console.warn(`[Ethrix-Forge] Unknown provider: "${providerKey}". Ignoring.`);
606
- return;
607
- }
608
- setActiveProviderState(providerKey);
609
- setActiveModel(PROVIDER_CONFIG[providerKey].defaultModel);
610
- setError(null);
611
- }, []);
612
-
613
- // ── Core Generator ───────────────────────────────��────────────────────────
614
- /**
615
- * Generates files from a natural language prompt.
616
- *
617
- * @param {string} userPrompt - The user's coding request
618
- * @param {Object} [overrides] - Optional per-call overrides
619
- * @param {string} [overrides.provider] - Use a different provider for this call
620
- * @param {string} [overrides.model] - Use a different model for this call
621
- * @param {string} [overrides.apiKey] - Use a different key for this call
622
- *
623
- * @returns {Promise<Array<{id, filename, language, code}>>} Parsed file array
624
- */
625
- const generate = useCallback(
626
- async (userPrompt, overrides = {}) => {
627
- const provider = overrides.provider ?? activeProvider;
628
- const model = overrides.model ?? activeModel;
629
- const key = overrides.apiKey ?? apiKey;
630
-
631
- // ── Pre-flight validation ──────────────────────────────────────────
632
- if (!key || key.trim().length === 0) {
633
- const err = new AIServiceError(
634
- `No API key provided for ${PROVIDER_CONFIG[provider]?.label || provider}. Please add your key in Settings.`,
635
- ERROR_CODES.NO_API_KEY,
636
- provider,
637
- false
638
- );
639
- setError(err);
640
- throw err;
641
- }
642
-
643
- if (!userPrompt || userPrompt.trim().length === 0) {
644
- const err = new AIServiceError(
645
- "Prompt cannot be empty.",
646
- ERROR_CODES.UNKNOWN,
647
- provider,
648
- false
649
- );
650
- setError(err);
651
- throw err;
652
- }
653
-
654
- // ── Cancel any running request ─────────────────────────────────────
655
- if (abortControllerRef.current) {
656
- abortControllerRef.current.abort();
657
- }
658
- abortControllerRef.current = new AbortController();
659
- const { signal } = abortControllerRef.current;
660
-
661
- lastPromptRef.current = { userPrompt, overrides };
662
-
663
- setIsLoading(true);
664
- setError(null);
665
- setLastRawResponse(null);
666
-
667
- const caller = PROVIDER_CALLERS[provider];
668
- if (!caller) {
669
- const err = new AIServiceError(
670
- `Provider "${provider}" is not registered in PROVIDER_CALLERS.`,
671
- ERROR_CODES.UNKNOWN,
672
- provider,
673
- false
674
- );
675
- setIsLoading(false);
676
- setError(err);
677
- throw err;
678
- }
679
-
680
- // ── Retry loop ──────────────────────────────────────────────────────
681
- let lastErr = null;
682
- for (let attempt = 0; attempt <= retryAttempts; attempt++) {
683
- if (signal.aborted) {
684
- setIsLoading(false);
685
- return null;
686
- }
687
-
688
- if (attempt > 0) {
689
- // Exponential backoff: 1.5s → 3s → 6s...
690
- const delay = retryDelayMs * Math.pow(2, attempt - 1);
691
- console.info(
692
- `[Ethrix-Forge] Retry ${attempt}/${retryAttempts} in ${delay}ms for provider ${provider}...`
693
- );
694
- await sleep(delay);
695
- }
696
-
697
- try {
698
- const rawText = await caller({ apiKey: key, model, userPrompt, signal });
699
-
700
- if (rawText === null) {
701
- // Request was aborted
702
- setIsLoading(false);
703
- return null;
704
- }
705
-
706
- setLastRawResponse(rawText);
707
- setRequestCount((c) => c + 1);
708
-
709
- const files = parseAIResponse(rawText, provider);
710
-
711
- setLastGeneratedFiles(files);
712
- setError(null);
713
- setIsLoading(false);
714
- return files;
715
-
716
- } catch (err) {
717
- lastErr = err;
718
-
719
- // Don't retry non-retryable errors (bad key, safety block, etc.)
720
- if (err instanceof AIServiceError && !err.retryable) {
721
- break;
722
- }
723
-
724
- // Don't retry if caller was aborted
725
- if (signal.aborted || err.name === "AbortError") {
726
- setIsLoading(false);
727
- return null;
728
- }
729
-
730
- console.warn(
731
- `[Ethrix-Forge] Attempt ${attempt + 1} failed:`,
732
- err.message
733
- );
734
- }
735
- }
736
-
737
- // All attempts exhausted
738
- const finalError =
739
- lastErr instanceof AIServiceError
740
- ? lastErr
741
- : new AIServiceError(
742
- `Unexpected error: ${lastErr?.message ?? String(lastErr)}`,
743
- ERROR_CODES.UNKNOWN,
744
- provider,
745
- false
746
- );
747
-
748
- setError(finalError);
749
  setIsLoading(false);
750
- throw finalError;
751
- },
752
- [activeProvider, activeModel, apiKey, retryAttempts, retryDelayMs]
753
- );
754
-
755
- // ── Retry Last Request ─────────────────────────────────────────────────────
756
- /**
757
- * Re-runs the exact same prompt and overrides as the last generate() call.
758
- * Useful for a "Retry" button shown alongside error messages.
759
- */
760
- const retryLastRequest = useCallback(() => {
761
- if (!lastPromptRef.current) return null;
762
- const { userPrompt, overrides } = lastPromptRef.current;
763
- return generate(userPrompt, overrides);
764
- }, [generate]);
765
-
766
- // ── Cancel ─────────────────────────────────────────────────────────────────
767
- /**
768
- * Aborts any in-flight API request immediately.
769
- */
770
- const cancelRequest = useCallback(() => {
771
- if (abortControllerRef.current) {
772
- abortControllerRef.current.abort();
773
- }
774
- setIsLoading(false);
775
- }, []);
776
-
777
- // ── Expose helpers ─────────────────────────────────────────────────────────
778
- const currentProviderConfig = PROVIDER_CONFIG[activeProvider] ?? null;
779
- const availableModels = currentProviderConfig?.availableModels ?? [];
780
-
781
- return {
782
- // Core action
783
- generate,
784
- retryLastRequest,
785
- cancelRequest,
786
-
787
- // State
788
- isLoading,
789
- error,
790
- lastRawResponse,
791
- lastGeneratedFiles,
792
- requestCount,
793
-
794
- // Provider management
795
- activeProvider,
796
- setActiveProvider,
797
- currentProviderConfig,
798
-
799
- // Model management
800
- activeModel,
801
- setActiveModel,
802
- availableModels,
803
-
804
- // API key management
805
- apiKey,
806
- setApiKey,
807
-
808
- // Utilities
809
- PROVIDERS,
810
- PROVIDER_CONFIG,
811
- ERROR_CODES,
812
- };
813
- }
814
-
815
- // ─────────────────────────────────────────────────────────────────────────────
816
- // 8. MONACO FILE INJECTOR UTILITY
817
- // A pure helper you can use to inject parsed files into your Monaco state.
818
- // ─────────────────────────────────────────────────────────────────────────────
819
-
820
- /**
821
- * Merges newly generated files into an existing Monaco file state array.
822
- *
823
- * USAGE IN App.jsx:
824
- * const files = await generate(prompt);
825
- * setMonacoFiles(prev => injectFilesIntoEditor(prev, files));
826
- *
827
- * @param {Array} existingFiles - Current files[] state from your editor
828
- * @param {Array} newFiles - Parsed files from generate()
829
- * @param {string} [strategy="replace-all"] - Merge strategy:
830
- * "replace-all" — Wipe existing files, insert only new ones
831
- * "merge-by-name" — Update files with matching names; add new ones
832
- * "append" — Add all new files (may create duplicates)
833
- * @returns {Array} New files array to set as editor state
834
- */
835
- export function injectFilesIntoEditor(existingFiles = [], newFiles = [], strategy = "replace-all") {
836
- if (!Array.isArray(newFiles) || newFiles.length === 0) return existingFiles;
837
-
838
- switch (strategy) {
839
- case "replace-all":
840
- return newFiles;
841
-
842
- case "merge-by-name": {
843
- const existingMap = new Map(existingFiles.map((f) => [f.filename, f]));
844
- for (const newFile of newFiles) {
845
- existingMap.set(newFile.filename, newFile);
846
- }
847
- return Array.from(existingMap.values());
848
  }
 
849
 
850
- case "append":
851
- return [...existingFiles, ...newFiles];
852
-
853
- default:
854
- console.warn(`[Ethrix-Forge] Unknown injection strategy: "${strategy}". Using "replace-all".`);
855
- return newFiles;
856
- }
857
- }
858
-
859
- // ─────────────────────────────────────────────────────────────────────────────
860
- // 9. USAGE EXAMPLE — Minimal App.jsx Integration
861
- // ─────────────────────────────────────────────────────────────────────────────
862
- /*
863
-
864
- // App.jsx (abbreviated integration example):
865
-
866
- import { useState } from "react";
867
- import { useAIService, injectFilesIntoEditor, PROVIDERS } from "./useAIService";
868
- import Editor from "@monaco-editor/react";
869
-
870
- export default function App() {
871
- const [monacoFiles, setMonacoFiles] = useState([]);
872
- const [activeFile, setActiveFile] = useState(null);
873
- const [prompt, setPrompt] = useState("");
874
-
875
- const {
876
- generate,
877
- isLoading,
878
- error,
879
- retryLastRequest,
880
- cancelRequest,
881
- activeProvider,
882
  setActiveProvider,
883
- apiKey,
884
- setApiKey,
885
- activeModel,
886
- setActiveModel,
887
- availableModels,
888
- PROVIDERS,
889
- } = useAIService({
890
- initialProvider: PROVIDERS.GEMINI,
891
- retryAttempts: 2,
892
- });
893
-
894
- const handleGenerate = async () => {
895
- try {
896
- const files = await generate(prompt);
897
- if (files) {
898
- setMonacoFiles(prev => injectFilesIntoEditor(prev, files, "replace-all"));
899
- setActiveFile(files[0]);
900
- }
901
- } catch (err) {
902
- // error is also set in hook state — render err.message in your UI
903
- console.error(err);
904
- }
905
  };
906
-
907
- return (
908
- <div>
909
- // Provider selector
910
- <select value={activeProvider} onChange={(e) => setActiveProvider(e.target.value)}>
911
- <option value={PROVIDERS.GEMINI}>Gemini</option>
912
- <option value={PROVIDERS.GROQ}>Groq</option>
913
- <option value={PROVIDERS.OPENROUTER}>OpenRouter</option>
914
- </select>
915
-
916
- // Model selector
917
- <select value={activeModel} onChange={(e) => setActiveModel(e.target.value)}>
918
- {availableModels.map(m => (
919
- <option key={m.id} value={m.id}>{m.label}</option>
920
- ))}
921
- </select>
922
-
923
- // API Key input
924
- <input
925
- type="password"
926
- value={apiKey}
927
- onChange={(e) => setApiKey(e.target.value)}
928
- placeholder="Enter API Key..."
929
- />
930
-
931
- // Prompt area
932
- <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} />
933
- <button onClick={handleGenerate} disabled={isLoading}>
934
- {isLoading ? "Generating..." : "Generate"}
935
- </button>
936
- {isLoading && <button onClick={cancelRequest}>Cancel</button>}
937
-
938
- // Error display
939
- {error && (
940
- <div>
941
- <p>{error.message}</p>
942
- {error.retryable && <button onClick={retryLastRequest}>Retry</button>}
943
- </div>
944
- )}
945
-
946
- // File tabs
947
- {monacoFiles.map(file => (
948
- <button key={file.id} onClick={() => setActiveFile(file)}>
949
- {file.filename}
950
- </button>
951
- ))}
952
-
953
- // Monaco Editor
954
- {activeFile && (
955
- <Editor
956
- language={activeFile.language}
957
- value={activeFile.code}
958
- theme="vs-dark"
959
- options={{ fontSize: 14 }}
960
- />
961
- )}
962
- </div>
963
- );
964
- }
965
-
966
- */
967
-
968
- // ─────────────────────────────────────────────────────────────────────────────
969
- // HELPERS
970
- // ─────────────────────────────────────────────────────────────────────────────
971
-
972
- function sleep(ms) {
973
- return new Promise((resolve) => setTimeout(resolve, ms));
974
- }
 
1
+ import { useState, useCallback } from "react";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ // TUMHARA CLOUD BACKEND URL
4
+ const BACKEND_URL = "https://shantanupathak94-ai-code-editor.hf.space";
 
 
 
 
 
5
 
6
  export const PROVIDERS = {
7
  GEMINI: "gemini",
8
  GROQ: "groq",
9
+ OPENROUTER: "openrouter"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  };
11
 
12
+ export function useAIService({ initialProvider = PROVIDERS.GEMINI } = {}) {
13
+ const [activeProvider, setActiveProvider] = useState(initialProvider);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  const [isLoading, setIsLoading] = useState(false);
15
+ const [apiKey, setApiKey] = useState(""); // Ab API key frontend mein zaroori nahi, par purane UI ke liye state rakh li hai
 
 
 
 
 
 
16
 
17
+ const generate = useCallback(async (userPrompt) => {
18
+ setIsLoading(true);
19
+ try {
20
+ // 🚀 Direct tumhare FastAPI server ko request bhej rahe hain!
21
+ const response = await fetch(`${BACKEND_URL}/ai/generate`, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({
25
+ prompt: userPrompt,
26
+ provider: activeProvider
27
+ })
28
+ });
29
+
30
+ if (!response.ok) {
31
+ const errData = await response.json();
32
+ throw new Error(errData.detail || "Backend server error 🥺");
33
+ }
34
+
35
+ const data = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  setIsLoading(false);
37
+
38
+ // Backend ne already JSON parse karke files array de diya hai! ✨
39
+ return data.files;
40
+ } catch (error) {
41
+ setIsLoading(false);
42
+ throw error;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
+ }, [activeProvider]);
45
 
46
+ return {
47
+ generate,
48
+ isLoading,
49
+ activeProvider,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  setActiveProvider,
51
+ apiKey, // UI break na ho isliye rakha hai
52
+ setApiKey
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  };
54
+ }