Spaces:
Running
Running
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
- Dockerfile +6 -12
- server.js +211 -0
- 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 |
-
#
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# Expose the port that Hugging Face Spaces uses by default
|
| 30 |
EXPOSE 7860
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
CMD ["
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
const GEMINI_API_KEY = import.meta.env.VITE_GEMINI_API_KEY;
|
| 5 |
-
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY || '');
|
| 6 |
|
| 7 |
-
//
|
| 8 |
-
|
| 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 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
if (!
|
| 72 |
-
|
| 73 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
|
|
|
|
|
|
| 75 |
try {
|
| 76 |
-
const
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 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 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
}
|
| 112 |
-
|
| 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 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
} catch (error) {
|
| 125 |
-
console.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
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 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 |
-
|
| 185 |
-
|
|
|
|
| 186 |
|
| 187 |
-
|
|
|
|
| 188 |
} catch (error) {
|
| 189 |
-
console.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 |
};
|