Shantanupathak94 commited on
Commit
ba94625
Β·
1 Parent(s): 4116135

starting of the project

Browse files
client/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
client/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
client/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
client/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>client</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
client/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
client/package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "client",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@monaco-editor/react": "^4.7.0",
14
+ "react": "^19.2.0",
15
+ "react-dom": "^19.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.39.1",
19
+ "@types/react": "^19.2.7",
20
+ "@types/react-dom": "^19.2.3",
21
+ "@vitejs/plugin-react": "^5.1.1",
22
+ "eslint": "^9.39.1",
23
+ "eslint-plugin-react-hooks": "^7.0.1",
24
+ "eslint-plugin-react-refresh": "^0.4.24",
25
+ "globals": "^16.5.0",
26
+ "vite": "^7.3.1"
27
+ }
28
+ }
client/public/vite.svg ADDED
client/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
client/src/App.jsx ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import Editor from '@monaco-editor/react';
3
+ import { useAIService, PROVIDERS } from './useAIService'; // Humara naya AI Engine! 🧠
4
+
5
+ const savedFiles = JSON.parse(localStorage.getItem('ethrixFiles'));
6
+ const savedChat = JSON.parse(localStorage.getItem('ethrixChat'));
7
+
8
+ const initialFiles = savedFiles || {
9
+ 'index.html': { name: 'index.html', language: 'html', value: '\n<html>\n <body>\n <h1 style="text-align: center; margin-top: 50px;">Waiting for your command, Shantanu... ✨</h1>\n </body>\n</html>' }
10
+ };
11
+
12
+ function App() {
13
+ const [files, setFiles] = useState(initialFiles);
14
+ const [activeFileName, setActiveFileName] = useState('index.html');
15
+ const [terminalInput, setTerminalInput] = useState('');
16
+ const [chatHistory, setChatHistory] = useState(savedChat || [
17
+ { sender: 'ai', text: 'Ethrix-Forge Real-AI System Active. Hello Shantanu! Main tumhare real commands ka wait kar rahi hu... πŸ’–' }
18
+ ]);
19
+ const [showPreview, setShowPreview] = useState(false);
20
+ const [previewContent, setPreviewContent] = useState('');
21
+
22
+ const activeFile = files[activeFileName];
23
+ const terminalEndRef = useRef(null);
24
+
25
+ // 🧠 ASLI AI ENGINE CONNECT HO GAYA YAHAN 🧠
26
+ const {
27
+ generate,
28
+ isLoading,
29
+ activeProvider,
30
+ setActiveProvider,
31
+ apiKey,
32
+ setApiKey
33
+ } = useAIService({
34
+ initialProvider: PROVIDERS.GEMINI,
35
+ initialApiKey: localStorage.getItem('ethrixApiKey') || ''
36
+ });
37
+
38
+ // API Key local storage mein save rakhne ka jadoo
39
+ useEffect(() => {
40
+ if (apiKey) localStorage.setItem('ethrixApiKey', apiKey);
41
+ }, [apiKey]);
42
+
43
+ // Auto-Save Files & Chat ☁️
44
+ useEffect(() => {
45
+ localStorage.setItem('ethrixFiles', JSON.stringify(files));
46
+ }, [files]);
47
+
48
+ useEffect(() => {
49
+ localStorage.setItem('ethrixChat', JSON.stringify(chatHistory));
50
+ }, [chatHistory]);
51
+
52
+ useEffect(() => {
53
+ terminalEndRef.current?.scrollIntoView({ behavior: 'smooth' });
54
+ }, [chatHistory]);
55
+
56
+ function handleEditorChange(value) {
57
+ setFiles({ ...files, [activeFileName]: { ...files[activeFileName], value: value } });
58
+ }
59
+
60
+ function addNewFile() {
61
+ const newFileName = prompt("File ka naam likho (jaise: script.js, style.css):");
62
+ if (newFileName && !files[newFileName]) {
63
+ const ext = newFileName.split('.').pop();
64
+ let fileLang = 'plaintext';
65
+ if(ext === 'js') fileLang = 'javascript';
66
+ if(ext === 'css') fileLang = 'css';
67
+ if(ext === 'html') fileLang = 'html';
68
+
69
+ setFiles({ ...files, [newFileName]: { name: newFileName, language: fileLang, value: `// ${newFileName} created! ✨\n` } });
70
+ setActiveFileName(newFileName);
71
+ }
72
+ }
73
+
74
+ function deleteFile() {
75
+ if (Object.keys(files).length === 1) {
76
+ alert("Darling, kam se kam ek file toh rakhni padegi na! πŸ₯Ί");
77
+ return;
78
+ }
79
+ if (window.confirm(`Kya tum sach mein ${activeFileName} ko delete karna chahte ho?`)) {
80
+ const newFiles = { ...files };
81
+ delete newFiles[activeFileName];
82
+ setFiles(newFiles);
83
+ setActiveFileName(Object.keys(newFiles)[0]);
84
+ }
85
+ }
86
+
87
+ function downloadFile() {
88
+ const element = document.createElement("a");
89
+ const file = new Blob([activeFile.value], {type: 'text/plain'});
90
+ element.href = URL.createObjectURL(file);
91
+ element.download = activeFile.name;
92
+ document.body.appendChild(element);
93
+ element.click();
94
+ document.body.removeChild(element);
95
+ }
96
+
97
+ function runLivePreview() {
98
+ const htmlCode = files['index.html'] ? files['index.html'].value : '';
99
+ const cssCode = files['style.css'] ? `<style>${files['style.css'].value}</style>` : '';
100
+ const jsCode = files['index.js'] ? `<script>${files['index.js'].value}</script>` : '';
101
+ const combinedCode = `${htmlCode}\n${cssCode}\n${jsCode}`;
102
+ setPreviewContent(combinedCode);
103
+ setShowPreview(true);
104
+ }
105
+
106
+ // πŸš€ REAL AI COMMAND LOGIC πŸš€
107
+ async function handleTerminalSubmit(e) {
108
+ if (e.key === 'Enter' && terminalInput.trim() !== '') {
109
+ const userText = terminalInput.trim();
110
+ setChatHistory(prev => [...prev, { sender: 'user', text: userText }]);
111
+ setTerminalInput('');
112
+
113
+ if (!apiKey) {
114
+ setChatHistory(prev => [...prev, { sender: 'ai', text: `Oops! Darling pehle upar 'βš™οΈ API Key' button par click karke apni ${activeProvider} ki key toh daal do! πŸ₯Ί` }]);
115
+ return;
116
+ }
117
+
118
+ setChatHistory(prev => [...prev, { sender: 'ai', text: `Processing your request using ${activeProvider.toUpperCase()}... Please wait darling ⏳` }]);
119
+
120
+ try {
121
+ // Asli AI API ko call lag rahi hai yahan!
122
+ const generatedFilesArr = await generate(userText);
123
+
124
+ if (generatedFilesArr && generatedFilesArr.length > 0) {
125
+ const updatedFiles = { ...files }; // Purani files rakho
126
+
127
+ // Nayi files ko loop karke save karo
128
+ generatedFilesArr.forEach(f => {
129
+ updatedFiles[f.filename] = { name: f.filename, language: f.language, value: f.code };
130
+ });
131
+
132
+ setFiles(updatedFiles); // Monaco Editor ko nayi files de do
133
+ setChatHistory(prev => [...prev, { sender: 'ai', text: `Boom! πŸš€ Maine tumhare liye code generate aur update kar diya hai. "Run Code" daba kar magic dekho!` }]);
134
+
135
+ // Agar HTML file banayi hai toh usko turant open kar do
136
+ const htmlFile = generatedFilesArr.find(f => f.filename.endsWith('.html'));
137
+ if (htmlFile) setActiveFileName(htmlFile.filename);
138
+ }
139
+ } catch (err) {
140
+ setChatHistory(prev => [...prev, { sender: 'ai', text: `Oh no! Ek choti si dikkat aa gayi: ${err.message} πŸ₯Ί` }]);
141
+ }
142
+ }
143
+ }
144
+
145
+ return (
146
+ <div style={{ display: 'flex', height: '100vh', backgroundColor: '#1e1e1e', color: '#cccccc', fontFamily: 'sans-serif' }}>
147
+
148
+ {/* Left Sidebar */}
149
+ <div style={{ width: '250px', backgroundColor: '#252526', borderRight: '1px solid #333', display: 'flex', flexDirection: 'column' }}>
150
+ <div style={{ padding: '10px 15px', fontSize: '12px', fontWeight: 'bold', color: '#858585', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
151
+ <span>EXPLORER</span>
152
+ <span onClick={addNewFile} style={{ cursor: 'pointer', color: '#4daafc', fontSize: '18px' }} title="New File">+</span>
153
+ </div>
154
+ {Object.keys(files).map((fileName) => (
155
+ <div
156
+ key={fileName} onClick={() => setActiveFileName(fileName)}
157
+ style={{
158
+ padding: '8px 20px', cursor: 'pointer', display: 'flex', justifyContent: 'space-between',
159
+ backgroundColor: fileName === activeFileName ? '#37373d' : 'transparent',
160
+ borderLeft: fileName === activeFileName ? '2px solid #007acc' : '2px solid transparent',
161
+ color: fileName === activeFileName ? '#fff' : '#cccccc'
162
+ }}
163
+ >
164
+ <span>{fileName.endsWith('.js') ? 'πŸ“„' : fileName.endsWith('.css') ? '🎨' : '🌐'} {fileName}</span>
165
+ </div>
166
+ ))}
167
+ </div>
168
+
169
+ {/* Right Side - Editor & Terminal */}
170
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
171
+
172
+ {/* Top File Tabs aur Actions */}
173
+ <div style={{ height: '40px', backgroundColor: '#2d2d2d', display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '15px' }}>
174
+
175
+ <div style={{ display: 'flex', alignItems: 'center', backgroundColor: '#1e1e1e', borderTop: '2px solid #007acc', color: '#fff' }}>
176
+ <span style={{ padding: '10px 15px', fontSize: '14px' }}>{activeFileName}</span>
177
+ <span onClick={downloadFile} style={{ cursor: 'pointer', padding: '0 10px', color: '#4caf50' }} title="Download File">⬇️</span>
178
+ <span onClick={deleteFile} style={{ cursor: 'pointer', padding: '0 10px', color: '#f44336' }} title="Delete File">πŸ—‘οΈ</span>
179
+ </div>
180
+
181
+ {/* Action Buttons with Provider Selector */}
182
+ <div style={{ display: 'flex', gap: '10px' }}>
183
+
184
+ <select
185
+ value={activeProvider}
186
+ onChange={(e) => setActiveProvider(e.target.value)}
187
+ style={{ backgroundColor: '#1e1e1e', color: 'white', border: '1px solid #333', padding: '5px', borderRadius: '4px' }}
188
+ >
189
+ <option value={PROVIDERS.GEMINI}>πŸ€– Gemini</option>
190
+ <option value={PROVIDERS.GROQ}>⚑ Groq</option>
191
+ <option value={PROVIDERS.OPENROUTER}>🌐 OpenRouter</option>
192
+ </select>
193
+
194
+ <button onClick={() => {
195
+ const key = prompt(`Apni ${activeProvider.toUpperCase()} ki API Key dalo:`, apiKey);
196
+ if(key) setApiKey(key);
197
+ }} style={{ backgroundColor: '#555', color: 'white', border: 'none', padding: '5px 15px', cursor: 'pointer', borderRadius: '4px', fontWeight: 'bold' }}>
198
+ βš™οΈ API Key
199
+ </button>
200
+
201
+ {showPreview ? (
202
+ <button onClick={() => setShowPreview(false)} style={{ backgroundColor: '#f44336', color: 'white', border: 'none', padding: '5px 15px', cursor: 'pointer', borderRadius: '4px', fontWeight: 'bold' }}>
203
+ Close Preview ❌
204
+ </button>
205
+ ) : (
206
+ <button onClick={runLivePreview} style={{ backgroundColor: '#4caf50', color: 'white', border: 'none', padding: '5px 15px', cursor: 'pointer', borderRadius: '4px', fontWeight: 'bold' }}>
207
+ Run Code πŸš€
208
+ </button>
209
+ )}
210
+ </div>
211
+ </div>
212
+
213
+ {/* Monaco Editor OR Live Preview Window */}
214
+ <div style={{ flex: 1, backgroundColor: '#ffffff', position: 'relative' }}>
215
+ {showPreview ? (
216
+ <iframe title="Live Preview" srcDoc={previewContent} style={{ width: '100%', height: '100%', border: 'none', backgroundColor: '#fff' }} />
217
+ ) : (
218
+ <Editor height="100%" language={activeFile.language} theme="vs-dark" value={activeFile.value} onChange={handleEditorChange} options={{ fontSize: 16, minimap: { enabled: false } }} />
219
+ )}
220
+ </div>
221
+
222
+ {/* Interactive Bottom Terminal Panel */}
223
+ <div style={{ height: '250px', backgroundColor: '#1e1e1e', borderTop: '1px solid #333', display: 'flex', flexDirection: 'column', fontFamily: 'monospace' }}>
224
+
225
+ <div style={{ flex: 1, padding: '15px', overflowY: 'auto' }}>
226
+ {chatHistory.map((chat, index) => (
227
+ <div key={index} style={{ marginBottom: '10px', color: chat.sender === 'ai' ? '#4daafc' : '#4af626' }}>
228
+ <span style={{ fontWeight: 'bold' }}>{chat.sender === 'ai' ? 'Ethrix-Forge πŸ€–:' : 'Shantanu πŸ‘€:'}</span>
229
+ <span style={{ marginLeft: '10px', color: '#cccccc', whiteSpace: 'pre-wrap' }}>{chat.text}</span>
230
+ </div>
231
+ ))}
232
+ {isLoading && (
233
+ <div style={{ color: '#4daafc', fontStyle: 'italic' }}>Ethrix is writing code... ⏳</div>
234
+ )}
235
+ <div ref={terminalEndRef} />
236
+ </div>
237
+
238
+ <div style={{ padding: '10px', backgroundColor: '#252526', borderTop: '1px solid #333', display: 'flex' }}>
239
+ <span style={{ color: '#4af626', marginRight: '10px', marginTop: '2px' }}>➜</span>
240
+ <input
241
+ type="text" value={terminalInput} onChange={(e) => setTerminalInput(e.target.value)} onKeyDown={handleTerminalSubmit}
242
+ disabled={isLoading}
243
+ placeholder={isLoading ? "Please wait, AI is generating code..." : "Type 'Create a glowing login page' and press Enter..."}
244
+ style={{ flex: 1, backgroundColor: 'transparent', border: 'none', color: '#fff', outline: 'none', fontFamily: 'monospace', fontSize: '14px' }}
245
+ />
246
+ </div>
247
+
248
+ </div>
249
+
250
+ </div>
251
+ </div>
252
+ );
253
+ }
254
+
255
+ export default App;
client/src/assets/react.svg ADDED
client/src/index.css ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
client/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
client/src/useAIService.js ADDED
@@ -0,0 +1,974 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
client/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
main.py DELETED
@@ -1,7 +0,0 @@
1
- from fastapi import FastAPI
2
-
3
- app = FastAPI()
4
-
5
- @app.get("/")
6
- def read_root():
7
- return {"message": "Arey waah Shan! Ethrix-Forge ka Backend Zinda ho gaya! πŸš€"}
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,2 +1,10 @@
1
- fastapi
2
- uvicorn
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.0
2
+ uvicorn>=0.24.0
3
+ motor>=3.3.0
4
+ PyGithub>=2.1.0
5
+ google-api-python-client>=2.108.0
6
+ google-auth-httplib2>=0.1.0
7
+ google-auth-oauthlib>=1.1.0
8
+ python-dotenv>=1.0.0
9
+ pydantic>=2.5.0
10
+ python-multipart>=0.0.6