rocky1410 commited on
Commit
2b19ed8
·
1 Parent(s): 8239d81

feat(api): move Gemini API calls to backend server

Browse files

- Replace client-side Gemini API calls with backend endpoints
- Add Express server to handle API requests securely
- Remove API key exposure from client-side code
- Maintain same client interface for backward compatibility

Files changed (3) hide show
  1. Dockerfile +6 -12
  2. server.js +211 -0
  3. src/services/gemini.ts +59 -128
Dockerfile CHANGED
@@ -11,23 +11,17 @@ RUN npm install
11
  # Copy the rest of the application source code
12
  COPY . .
13
 
14
- # Hugging Face automatically provides secrets as build arguments.
15
- # We need to ensure Vite can access it during the build.
16
- # The ARG instruction makes the secret available during the build.
17
- ARG VITE_GEMINI_API_KEY
18
-
19
- # This is the key part: we must explicitly set it as an environment variable
20
- # so Vite's build process can replace import.meta.env.VITE_GEMINI_API_KEY
21
- ENV VITE_GEMINI_API_KEY=${VITE_GEMINI_API_KEY}
22
 
23
  # Build the application
24
  RUN npm run build
25
 
26
- # Install 'serve' to run the built application
27
- RUN npm install --production serve
 
 
28
 
29
  # Expose the port that Hugging Face Spaces uses by default
30
  EXPOSE 7860
31
 
32
- # Command to start the server, serving the 'dist' folder
33
- CMD ["npx", "serve", "-s", "dist", "-l", "7860"]
 
11
  # Copy the rest of the application source code
12
  COPY . .
13
 
 
 
 
 
 
 
 
 
14
 
15
  # Build the application
16
  RUN npm run build
17
 
18
+ # Expect GEMINI_API_KEY to be provided at runtime by Hugging Face Spaces (Settings -> Secrets)
19
+
20
+ # Install only production dependencies needed to run the server
21
+ RUN npm install --production express
22
 
23
  # Expose the port that Hugging Face Spaces uses by default
24
  EXPOSE 7860
25
 
26
+ # Start Node server that serves static files and proxies API calls
27
+ CMD ["node", "server.js"]
server.js ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const { GoogleGenerativeAI, SchemaType } = require('@google/generative-ai');
3
+ const path = require('path');
4
+
5
+ const app = express();
6
+ const port = process.env.PORT || 7860;
7
+
8
+ // Middleware
9
+ app.use(express.json({ limit: '50mb' }));
10
+ app.use(express.static(path.join(__dirname, 'dist')));
11
+
12
+ // Initialize Gemini with server-side env var (no VITE_ prefix)
13
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
14
+ const genAI = GEMINI_API_KEY ? new GoogleGenerativeAI(GEMINI_API_KEY) : null;
15
+
16
+ // Define the same schema as your client code
17
+ const fileSchema = {
18
+ type: SchemaType.OBJECT,
19
+ properties: {
20
+ files: {
21
+ type: SchemaType.ARRAY,
22
+ items: {
23
+ type: SchemaType.OBJECT,
24
+ properties: {
25
+ fileName: { type: SchemaType.STRING },
26
+ code: { type: SchemaType.STRING }
27
+ },
28
+ required: ["fileName", "code"]
29
+ }
30
+ }
31
+ },
32
+ required: ["files"]
33
+ };
34
+
35
+ // Helper function to format HTML (same as client)
36
+ const { html: formatHtml } = require('js-beautify');
37
+
38
+ const formatCode = (code) => {
39
+ try {
40
+ return formatHtml(code, {
41
+ indent_size: 2,
42
+ indent_char: ' ',
43
+ max_preserve_newlines: 2,
44
+ preserve_newlines: true,
45
+ indent_inner_html: true,
46
+ brace_style: 'collapse',
47
+ indent_scripts: 'normal',
48
+ wrap_line_length: 120,
49
+ unformatted: ['code', 'pre', 'em', 'strong', 'span'],
50
+ content_unformatted: ['pre', 'code'],
51
+ });
52
+ } catch (error) {
53
+ console.error('Error formatting code:', error);
54
+ return code;
55
+ }
56
+ };
57
+
58
+ const transformApiResponse = (response) => {
59
+ const filesRecord = {};
60
+ if (response.files && Array.isArray(response.files)) {
61
+ for (const file of response.files) {
62
+ if (file.fileName && file.code) {
63
+ filesRecord[file.fileName] = formatCode(file.code);
64
+ }
65
+ }
66
+ }
67
+ return filesRecord;
68
+ };
69
+
70
+ // API endpoint for generating code
71
+ app.post('/api/generate-code', async (req, res) => {
72
+ if (!GEMINI_API_KEY) {
73
+ return res.status(500).json({
74
+ error: "Gemini API key is not configured. Please set GEMINI_API_KEY in your environment variables."
75
+ });
76
+ }
77
+
78
+ try {
79
+ const { messages } = req.body;
80
+
81
+ const model = genAI.getGenerativeModel({
82
+ model: "gemini-2.5-flash",
83
+ generationConfig: {
84
+ responseMimeType: "application/json",
85
+ responseSchema: fileSchema
86
+ }
87
+ });
88
+
89
+ const result = await model.generateContent({
90
+ contents: [{
91
+ role: "user",
92
+ parts: [{
93
+ text: `You are an expert frontend developer. Your task is to generate complete HTML pages based on user requests.
94
+
95
+ **YOUR PRIMARY JOB:**
96
+ - Understand the user's intent to create a website with one or more pages.
97
+ - Generate complete, valid HTML files that fulfill this intent.
98
+ - Your output will be structured as an array of objects, where each object has a "fileName" and a "code" property.
99
+
100
+ **CRITICAL RULES ON FILE CREATION:**
101
+ - **ONLY create full HTML page files** (e.g., \`index.html\`, \`about.html\`).
102
+ - **DO NOT create separate files for components** like navbars or footers. These must be part of their respective HTML page files.
103
+ - **DO NOT create separate .css or .js files.** All styling (via Tailwind classes) and scripts (via \`<script>\` tags for Alpine.js) must be self-contained within each HTML file.
104
+
105
+ **TECHNOLOGY CONSTRAINTS:**
106
+ - Use Tailwind CSS via CDN: \`<script src="https://cdn.tailwindcss.com"></script>\` in the <head>.
107
+ - Use Alpine.js for interactivity via CDN: \`<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>\` in the <head>.
108
+ - Ensure links between pages use relative paths (e.g., \`<a href="./about.html">\`).
109
+
110
+ **RULES:**
111
+ - Each HTML file must be a complete document (DOCTYPE, html, head, body).
112
+ - Ensure all pages are responsive and visually consistent.
113
+
114
+ User request: ${messages[messages.length - 1].content}`
115
+ }]
116
+ }],
117
+ safetySettings: [
118
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
119
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
120
+ { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
121
+ { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" }
122
+ ]
123
+ });
124
+
125
+ const response = await result.response;
126
+ const parsedResponse = JSON.parse(response.text());
127
+
128
+ res.json(transformApiResponse(parsedResponse));
129
+ } catch (error) {
130
+ console.error("Error in generateCode:", error);
131
+ res.status(500).json({ error: error.message });
132
+ }
133
+ });
134
+
135
+ // API endpoint for modifying code
136
+ app.post('/api/modify-code', async (req, res) => {
137
+ if (!GEMINI_API_KEY) {
138
+ return res.status(500).json({
139
+ error: "Gemini API key is not configured. Please set GEMINI_API_KEY in your environment variables."
140
+ });
141
+ }
142
+
143
+ try {
144
+ const { currentFiles, modificationRequest } = req.body;
145
+
146
+ const model = genAI.getGenerativeModel({
147
+ model: "gemini-2.5-flash",
148
+ generationConfig: {
149
+ responseMimeType: "application/json",
150
+ responseSchema: fileSchema
151
+ }
152
+ });
153
+
154
+ const result = await model.generateContent({
155
+ contents: [{
156
+ role: "user",
157
+ parts: [{
158
+ text: `You are an expert frontend developer working on a website modification task.
159
+
160
+ **YOUR PRIMARY JOB:**
161
+ - Understand the user's intent to modify their existing website.
162
+ - Make precise changes to fulfill that intent, which may involve editing multiple files and/or creating new files.
163
+ - Your output will be structured as an array of objects, where each object has a "fileName" and a "code" property for each file that was changed or created.
164
+
165
+ **CRITICAL SAFETY RULE:**
166
+ Your primary goal is to prevent data loss. When a user asks to move content to a new file or create a new file, you MUST provide the content for all new files in your response. It is unacceptable to only show the file where content was removed. Your response MUST be a complete fulfillment of the user's request, including both the modified original files and the newly created files.
167
+
168
+ **CRITICAL RULES ON FILE CREATION & MODIFICATION:**
169
+ - **ONLY create or modify full HTML page files** (e.g., \`index.html\`, \`about.html\`).
170
+ - **DO NOT create separate files for components** like navbars or footers. Integrate them into the main HTML pages.
171
+ - **DO NOT create separate .css or .js files.** All styling and scripts must be self-contained within each HTML file.
172
+ - If you create a new page, you MUST also modify existing pages to add navigation links to it.
173
+
174
+ **RULES:**
175
+ - If a request involves changes that logically affect multiple files (e.g., updating a navigation menu), you MUST include ALL of those files in your response.
176
+
177
+ **Current Project Files:**
178
+ ${JSON.stringify(currentFiles, null, 2)}
179
+
180
+ **Modification Request:** ${modificationRequest}`
181
+ }]
182
+ }],
183
+ safetySettings: [
184
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
185
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
186
+ { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
187
+ { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" }
188
+ ]
189
+ });
190
+
191
+ const response = await result.response;
192
+ const parsedResponse = JSON.parse(response.text());
193
+
194
+ res.json(transformApiResponse(parsedResponse));
195
+ } catch (error) {
196
+ console.error("Error in modifyCode:", error);
197
+ res.status(500).json({ error: error.message });
198
+ }
199
+ });
200
+
201
+ // Serve React app for all other routes
202
+ app.get('*', (req, res) => {
203
+ res.sendFile(path.join(__dirname, 'dist', 'index.html'));
204
+ });
205
+
206
+ app.listen(port, '0.0.0.0', () => {
207
+ console.log(`Server running on port ${port}`);
208
+ if (!GEMINI_API_KEY) {
209
+ console.warn('Warning: GEMINI_API_KEY environment variable is not set');
210
+ }
211
+ });
src/services/gemini.ts CHANGED
@@ -1,27 +1,8 @@
1
- import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
2
- import { html as formatHtml } from "js-beautify";
3
-
4
- const GEMINI_API_KEY = import.meta.env.VITE_GEMINI_API_KEY;
5
- const genAI = new GoogleGenerativeAI(GEMINI_API_KEY || '');
6
 
7
- // Define a valid schema that the Google API understands: an array of file objects.
8
- const fileSchema = {
9
- type: SchemaType.OBJECT,
10
- properties: {
11
- files: {
12
- type: SchemaType.ARRAY,
13
- items: {
14
- type: SchemaType.OBJECT,
15
- properties: {
16
- fileName: { type: SchemaType.STRING },
17
- code: { type: SchemaType.STRING }
18
- },
19
- required: ["fileName", "code"]
20
- }
21
- }
22
- },
23
- required: ["files"]
24
- };
25
 
26
  export interface ChatMessage {
27
  role: string;
@@ -31,7 +12,6 @@ export interface ChatMessage {
31
  // Helper function to format HTML with proper indentation
32
  const formatCode = (code: string): string => {
33
  try {
34
- // Use js-beautify for proper HTML formatting
35
  return formatHtml(code, {
36
  indent_size: 2,
37
  indent_char: ' ',
@@ -46,7 +26,6 @@ const formatCode = (code: string): string => {
46
  });
47
  } catch (error) {
48
  console.error('Error formatting code:', error);
49
- // Return original code if formatting fails
50
  return code;
51
  }
52
  };
@@ -54,10 +33,9 @@ const formatCode = (code: string): string => {
54
  // Helper function to transform the AI's array response into the object format our app uses.
55
  const transformApiResponse = (response: { files: { fileName: string; code: string }[] }): Record<string, string> => {
56
  const filesRecord: Record<string, string> = {};
57
- if (response.files && Array.isArray(response.files)) {
58
- for (const file of response.files) {
59
  if (file.fileName && file.code) {
60
- // Format the code with proper indentation
61
  filesRecord[file.fileName] = formatCode(file.code);
62
  }
63
  }
@@ -65,64 +43,58 @@ const transformApiResponse = (response: { files: { fileName: string; code: strin
65
  return filesRecord;
66
  };
67
 
68
- export const generateCode = async (
69
- messages: ChatMessage[]
70
- ): Promise<Record<string, string>> => {
71
- if (!GEMINI_API_KEY) {
72
- throw new Error("Gemini API key is not configured. Please set VITE_GEMINI_API_KEY in your environment variables.");
73
  }
 
 
 
 
 
 
 
74
 
 
 
75
  try {
76
- const model = genAI.getGenerativeModel({
77
- model: "gemini-2.5-flash",
78
- generationConfig: {
79
- responseMimeType: "application/json",
80
- responseSchema: fileSchema
81
- }
82
- });
83
-
84
- const result = await model.generateContent({
85
- contents: [{
86
- role: "user",
87
- parts: [{
88
- text: `You are an expert frontend developer. Your task is to generate complete HTML pages based on user requests.
89
-
90
- **YOUR PRIMARY JOB:**
91
- - Understand the user's intent to create a website with one or more pages.
92
- - Generate complete, valid HTML files that fulfill this intent.
93
- - Your output will be structured as an array of objects, where each object has a "fileName" and a "code" property.
94
 
95
- **CRITICAL RULES ON FILE CREATION:**
96
- - **ONLY create full HTML page files** (e.g., \`index.html\`, \`about.html\`).
97
- - **DO NOT create separate files for components** like navbars or footers. These must be part of their respective HTML page files.
98
- - **DO NOT create separate .css or .js files.** All styling (via Tailwind classes) and scripts (via \`<script>\` tags for Alpine.js) must be self-contained within each HTML file.
99
 
100
- **TECHNOLOGY CONSTRAINTS:**
101
- - Use Tailwind CSS via CDN: \`<script src="https://cdn.tailwindcss.com"></script>\` in the <head>.
102
- - Use Alpine.js for interactivity via CDN: \`<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>\` in the <head>.
103
- - Ensure links between pages use relative paths (e.g., \`<a href="./about.html">\`).
104
 
105
- **RULES:**
106
- - Each HTML file must be a complete document (DOCTYPE, html, head, body).
107
- - Ensure all pages are responsive and visually consistent.
108
-
109
- User request: ${messages[messages.length - 1].content}`
110
- }]
111
- }],
112
- safetySettings: [
113
- { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
114
- { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
115
- { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
116
- { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" }
117
- ]
118
  });
119
 
120
- const response = await result.response;
121
- const parsedResponse = JSON.parse(response.text());
122
-
123
- return transformApiResponse(parsedResponse);
 
 
124
  } catch (error) {
125
- console.error("Error in generateCode:", error);
126
  throw error;
127
  }
128
  };
@@ -131,62 +103,21 @@ export const modifyCode = async (
131
  currentFiles: Record<string, string>,
132
  modificationRequest: string
133
  ): Promise<Record<string, string>> => {
134
- if (!GEMINI_API_KEY) {
135
- throw new Error("Gemini API key is not configured. Please set VITE_GEMINI_API_KEY in your environment variables.");
136
- }
137
-
138
  try {
139
- const model = genAI.getGenerativeModel({
140
- model: "gemini-2.5-flash",
141
- generationConfig: {
142
- responseMimeType: "application/json",
143
- responseSchema: fileSchema
144
- }
145
- });
146
-
147
- const result = await model.generateContent({
148
- contents: [{
149
- role: "user",
150
- parts: [{
151
- text: `You are an expert frontend developer working on a website modification task.
152
-
153
- **YOUR PRIMARY JOB:**
154
- - Understand the user's intent to modify their existing website.
155
- - Make precise changes to fulfill that intent, which may involve editing multiple files and/or creating new files.
156
- - Your output will be structured as an array of objects, where each object has a "fileName" and a "code" property for each file that was changed or created.
157
-
158
- **CRITICAL SAFETY RULE:**
159
- Your primary goal is to prevent data loss. When a user asks to move content to a new file or create a new file, you MUST provide the content for all new files in your response. It is unacceptable to only show the file where content was removed. Your response MUST be a complete fulfillment of the user's request, including both the modified original files and the newly created files.
160
-
161
- **CRITICAL RULES ON FILE CREATION & MODIFICATION:**
162
- - **ONLY create or modify full HTML page files** (e.g., \`index.html\`, \`about.html\`).
163
- - **DO NOT create separate files for components** like navbars or footers. Integrate them into the main HTML pages.
164
- - **DO NOT create separate .css or .js files.** All styling and scripts must be self-contained within each HTML file.
165
- - If you create a new page, you MUST also modify existing pages to add navigation links to it.
166
-
167
- **RULES:**
168
- - If a request involves changes that logically affect multiple files (e.g., updating a navigation menu), you MUST include ALL of those files in your response.
169
-
170
- **Current Project Files:**
171
- ${JSON.stringify(currentFiles, null, 2)}
172
-
173
- **Modification Request:** ${modificationRequest}`
174
- }]
175
- }],
176
- safetySettings: [
177
- { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
178
- { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
179
- { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
180
- { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" }
181
- ]
182
  });
183
 
184
- const response = await result.response;
185
- const parsedResponse = JSON.parse(response.text());
 
186
 
187
- return transformApiResponse(parsedResponse);
 
188
  } catch (error) {
189
- console.error("Error in modifyCode:", error);
190
  throw error;
191
  }
192
  };
 
1
+ // Removed direct use of @google/generative-ai on the client
2
+ // and switched to calling backend endpoints that securely use the API key at runtime.
 
 
 
3
 
4
+ // Keep js-beautify formatting utilities and transformers for compatibility
5
+ import { html as formatHtml } from "js-beautify";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export interface ChatMessage {
8
  role: string;
 
12
  // Helper function to format HTML with proper indentation
13
  const formatCode = (code: string): string => {
14
  try {
 
15
  return formatHtml(code, {
16
  indent_size: 2,
17
  indent_char: ' ',
 
26
  });
27
  } catch (error) {
28
  console.error('Error formatting code:', error);
 
29
  return code;
30
  }
31
  };
 
33
  // Helper function to transform the AI's array response into the object format our app uses.
34
  const transformApiResponse = (response: { files: { fileName: string; code: string }[] }): Record<string, string> => {
35
  const filesRecord: Record<string, string> = {};
36
+ if (response && (response as any).files && Array.isArray((response as any).files)) {
37
+ for (const file of (response as any).files) {
38
  if (file.fileName && file.code) {
 
39
  filesRecord[file.fileName] = formatCode(file.code);
40
  }
41
  }
 
43
  return filesRecord;
44
  };
45
 
46
+ // Normalize server response to the same shape as before
47
+ const normalizeResponse = (data: any): Record<string, string> => {
48
+ // If server already returns a Record<string, string>
49
+ if (data && typeof data === 'object' && !Array.isArray(data) && !('files' in data)) {
50
+ return data as Record<string, string>;
51
  }
52
+ // If server returns { files: [...] }
53
+ if (data && typeof data === 'object' && 'files' in data) {
54
+ return transformApiResponse(data as { files: { fileName: string; code: string }[] });
55
+ }
56
+ // Fallback to empty object
57
+ return {};
58
+ };
59
 
60
+ const handleError = async (res: Response) => {
61
+ let message = 'Unknown error';
62
  try {
63
+ const body = await res.json();
64
+ if (body && body.error) {
65
+ message = String(body.error);
66
+ }
67
+ } catch {
68
+ // ignore JSON parse errors
69
+ message = res.statusText || message;
70
+ }
 
 
 
 
 
 
 
 
 
 
71
 
72
+ // Preserve prior UX message for missing key to avoid UI changes
73
+ if (message.toLowerCase().includes('gemini api key is not configured')) {
74
+ throw new Error('Gemini API key is not configured. Please set VITE_GEMINI_API_KEY in your environment variables.');
75
+ }
76
 
77
+ throw new Error(message);
78
+ };
 
 
79
 
80
+ export const generateCode = async (
81
+ messages: ChatMessage[]
82
+ ): Promise<Record<string, string>> => {
83
+ try {
84
+ const res = await fetch('/api/generate-code', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ messages }),
 
 
 
 
 
88
  });
89
 
90
+ if (!res.ok) {
91
+ await handleError(res);
92
+ }
93
+
94
+ const data = await res.json();
95
+ return normalizeResponse(data);
96
  } catch (error) {
97
+ console.error('Error in generateCode:', error);
98
  throw error;
99
  }
100
  };
 
103
  currentFiles: Record<string, string>,
104
  modificationRequest: string
105
  ): Promise<Record<string, string>> => {
 
 
 
 
106
  try {
107
+ const res = await fetch('/api/modify-code', {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ currentFiles, modificationRequest }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  });
112
 
113
+ if (!res.ok) {
114
+ await handleError(res);
115
+ }
116
 
117
+ const data = await res.json();
118
+ return normalizeResponse(data);
119
  } catch (error) {
120
+ console.error('Error in modifyCode:', error);
121
  throw error;
122
  }
123
  };