@voltx/cli 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -14
- package/dist/build.d.mts +4 -3
- package/dist/build.d.ts +4 -3
- package/dist/build.js +56 -27
- package/dist/build.mjs +1 -2
- package/dist/chunk-2LHDOMF2.mjs +680 -0
- package/dist/chunk-4T26KROZ.mjs +456 -0
- package/dist/chunk-5DVBIJXU.mjs +84 -0
- package/dist/chunk-5RN2FYST.mjs +494 -0
- package/dist/chunk-65PVXS4D.mjs +894 -0
- package/dist/chunk-7JVIEGSA.mjs +141 -0
- package/dist/chunk-A4NA4AJN.mjs +786 -0
- package/dist/chunk-AAAHANST.mjs +839 -0
- package/dist/chunk-AD3WMFZF.mjs +205 -0
- package/dist/chunk-CFWXMM7Q.mjs +450 -0
- package/dist/chunk-CSSHLVYS.mjs +83 -0
- package/dist/chunk-CWOSNV5O.mjs +150 -0
- package/dist/chunk-EI6XBYKB.mjs +84 -0
- package/dist/chunk-EXMRIKIX.mjs +404 -0
- package/dist/chunk-FI2W4L4S.mjs +205 -0
- package/dist/chunk-FN7KZJ6H.mjs +363 -0
- package/dist/chunk-G2INQCCJ.mjs +907 -0
- package/dist/chunk-H2DTIOEO.mjs +150 -0
- package/dist/chunk-IS2WTE3C.mjs +138 -0
- package/dist/chunk-JECCDBYI.mjs +730 -0
- package/dist/chunk-KX2MRJUO.mjs +795 -0
- package/dist/chunk-LTGMHAZS.mjs +147 -0
- package/dist/chunk-OPO6RUFP.mjs +698 -0
- package/dist/chunk-P5FSO2UH.mjs +497 -0
- package/dist/chunk-PWQSKYAM.mjs +682 -0
- package/dist/chunk-Q5XCFN7L.mjs +1026 -0
- package/dist/chunk-QSU6FZC7.mjs +497 -0
- package/dist/chunk-RYWRFHEC.mjs +83 -0
- package/dist/chunk-SU4Q3PTH.mjs +201 -0
- package/dist/chunk-UO43CY7Y.mjs +497 -0
- package/dist/chunk-UXI3QSDN.mjs +121 -0
- package/dist/chunk-VD3CNPNP.mjs +123 -0
- package/dist/chunk-X6VOAPRJ.mjs +756 -0
- package/dist/cli.js +1023 -306
- package/dist/cli.mjs +7 -6
- package/dist/create.d.mts +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +813 -189
- package/dist/create.mjs +1 -2
- package/dist/dev.d.mts +6 -4
- package/dist/dev.d.ts +6 -4
- package/dist/dev.js +119 -46
- package/dist/dev.mjs +1 -2
- package/dist/generate.js +13 -13
- package/dist/generate.mjs +1 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1010 -294
- package/dist/index.mjs +6 -7
- package/dist/start.js +7 -17
- package/dist/start.mjs +1 -2
- package/dist/welcome.mjs +0 -1
- package/package.json +11 -3
package/dist/create.js
CHANGED
|
@@ -142,70 +142,72 @@ function printWelcomeBanner(projectName) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// src/create.ts
|
|
145
|
+
var VV = {
|
|
146
|
+
"@voltx/core": "^0.3.1",
|
|
147
|
+
"@voltx/server": "^0.3.1",
|
|
148
|
+
"@voltx/cli": "^0.3.5",
|
|
149
|
+
"@voltx/ai": "^0.3.0",
|
|
150
|
+
"@voltx/agents": "^0.3.1",
|
|
151
|
+
"@voltx/memory": "^0.3.0",
|
|
152
|
+
"@voltx/db": "^0.3.0",
|
|
153
|
+
"@voltx/rag": "^0.3.1",
|
|
154
|
+
"@voltx/auth": "^0.3.0"
|
|
155
|
+
};
|
|
156
|
+
function v(pkg) {
|
|
157
|
+
return VV[pkg] ?? "^0.3.0";
|
|
158
|
+
}
|
|
159
|
+
var TEMPLATE_DEPS = {
|
|
160
|
+
blank: { "@voltx/core": v("@voltx/core"), "@voltx/server": v("@voltx/server") },
|
|
161
|
+
chatbot: { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/memory": v("@voltx/memory") },
|
|
162
|
+
"rag-app": { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/rag": v("@voltx/rag"), "@voltx/db": v("@voltx/db") },
|
|
163
|
+
"agent-app": { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/agents": v("@voltx/agents"), "@voltx/memory": v("@voltx/memory") }
|
|
164
|
+
};
|
|
145
165
|
async function createProject(options) {
|
|
146
|
-
const { name, template = "blank", auth = "none" } = options;
|
|
166
|
+
const { name, template = "blank", auth = "none", shadcn = false } = options;
|
|
147
167
|
const targetDir = path.resolve(process.cwd(), name);
|
|
148
168
|
if (fs.existsSync(targetDir)) {
|
|
149
169
|
console.error(`[voltx] Directory "${name}" already exists.`);
|
|
150
170
|
process.exit(1);
|
|
151
171
|
}
|
|
152
172
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
const provider = template === "rag-app" ? "openai" : "cerebras";
|
|
174
|
+
const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
|
|
175
|
+
const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
|
|
176
|
+
const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
|
|
177
|
+
if (auth === "better-auth") {
|
|
178
|
+
deps["@voltx/auth"] = v("@voltx/auth");
|
|
179
|
+
deps["better-auth"] = "^1.5.0";
|
|
180
|
+
} else if (auth === "jwt") {
|
|
181
|
+
deps["@voltx/auth"] = v("@voltx/auth");
|
|
182
|
+
deps["jose"] = "^6.0.0";
|
|
183
|
+
}
|
|
184
|
+
const devDeps = { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" };
|
|
185
|
+
deps["hono"] = "^4.7.0";
|
|
186
|
+
deps["@hono/node-server"] = "^1.14.0";
|
|
187
|
+
deps["react"] = "^19.0.0";
|
|
188
|
+
deps["react-dom"] = "^19.0.0";
|
|
189
|
+
deps["tailwindcss"] = "^4.0.0";
|
|
190
|
+
devDeps["vite"] = "^6.0.0";
|
|
191
|
+
devDeps["@hono/vite-dev-server"] = "^0.7.0";
|
|
192
|
+
devDeps["@vitejs/plugin-react"] = "^4.3.0";
|
|
193
|
+
devDeps["@tailwindcss/vite"] = "^4.0.0";
|
|
194
|
+
devDeps["@types/react"] = "^19.0.0";
|
|
195
|
+
devDeps["@types/react-dom"] = "^19.0.0";
|
|
196
|
+
if (shadcn) {
|
|
197
|
+
deps["class-variance-authority"] = "^0.7.0";
|
|
198
|
+
deps["clsx"] = "^2.1.0";
|
|
199
|
+
deps["tailwind-merge"] = "^3.0.0";
|
|
200
|
+
deps["lucide-react"] = "^0.468.0";
|
|
201
|
+
}
|
|
202
|
+
fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
|
|
180
203
|
name,
|
|
181
204
|
version: "0.1.0",
|
|
182
205
|
private: true,
|
|
183
|
-
scripts: {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
dependencies: {
|
|
189
|
-
...templateDeps[template] ?? templateDeps["blank"],
|
|
190
|
-
"@voltx/cli": "^0.3.0",
|
|
191
|
-
...auth === "better-auth" ? { "@voltx/auth": "^0.3.0", "better-auth": "^1.5.0" } : {},
|
|
192
|
-
...auth === "jwt" ? { "@voltx/auth": "^0.3.0", "jose": "^6.0.0" } : {}
|
|
193
|
-
},
|
|
194
|
-
devDependencies: {
|
|
195
|
-
typescript: "^5.7.0",
|
|
196
|
-
tsx: "^4.21.0",
|
|
197
|
-
tsup: "^8.0.0",
|
|
198
|
-
"@types/node": "^22.0.0"
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
fs.writeFileSync(
|
|
202
|
-
path.join(targetDir, "package.json"),
|
|
203
|
-
JSON.stringify(packageJson, null, 2)
|
|
204
|
-
);
|
|
205
|
-
const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
|
|
206
|
-
const provider = template === "rag-app" ? "openai" : "cerebras";
|
|
207
|
-
const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
|
|
208
|
-
let configContent = `import { defineConfig } from "@voltx/core";
|
|
206
|
+
scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
|
|
207
|
+
dependencies: deps,
|
|
208
|
+
devDependencies: devDeps
|
|
209
|
+
}, null, 2));
|
|
210
|
+
let config = `import { defineConfig } from "@voltx/core";
|
|
209
211
|
|
|
210
212
|
export default defineConfig({
|
|
211
213
|
name: "${name}",
|
|
@@ -214,41 +216,63 @@ export default defineConfig({
|
|
|
214
216
|
provider: "${provider}",
|
|
215
217
|
model: "${model}",
|
|
216
218
|
},`;
|
|
217
|
-
if (hasDb)
|
|
218
|
-
configContent += `
|
|
219
|
+
if (hasDb) config += `
|
|
219
220
|
db: {
|
|
220
221
|
url: process.env.DATABASE_URL,
|
|
221
222
|
},`;
|
|
222
|
-
|
|
223
|
-
if (auth !== "none") {
|
|
224
|
-
configContent += `
|
|
223
|
+
if (auth !== "none") config += `
|
|
225
224
|
auth: {
|
|
226
225
|
provider: "${auth}",
|
|
227
226
|
},`;
|
|
228
|
-
|
|
229
|
-
configContent += `
|
|
227
|
+
config += `
|
|
230
228
|
server: {
|
|
231
|
-
routesDir: "
|
|
229
|
+
routesDir: "api",
|
|
232
230
|
staticDir: "public",
|
|
233
231
|
cors: true,
|
|
234
232
|
},
|
|
235
233
|
});
|
|
236
234
|
`;
|
|
237
|
-
fs.writeFileSync(path.join(targetDir, "voltx.config.ts"),
|
|
238
|
-
fs.mkdirSync(path.join(targetDir, "
|
|
235
|
+
fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
|
|
236
|
+
fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
|
|
239
237
|
fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
|
|
238
|
+
fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
|
|
239
|
+
fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
|
|
240
|
+
fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
|
|
241
|
+
fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
|
|
243
|
+
fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
|
|
244
|
+
fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
|
|
245
|
+
const tsconfig = {
|
|
246
|
+
compilerOptions: {
|
|
247
|
+
target: "ES2022",
|
|
248
|
+
module: "ESNext",
|
|
249
|
+
moduleResolution: "bundler",
|
|
250
|
+
strict: true,
|
|
251
|
+
esModuleInterop: true,
|
|
252
|
+
skipLibCheck: true,
|
|
253
|
+
outDir: "dist",
|
|
254
|
+
baseUrl: ".",
|
|
255
|
+
paths: { "@/*": ["./src/*"] },
|
|
256
|
+
jsx: "react-jsx"
|
|
257
|
+
},
|
|
258
|
+
include: ["src", "api", "server.ts", "voltx.config.ts"]
|
|
259
|
+
};
|
|
260
|
+
if (template === "agent-app") tsconfig.include.push("agents", "tools");
|
|
261
|
+
fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
|
|
262
|
+
fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
|
|
263
|
+
fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
|
|
264
|
+
fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
|
|
265
|
+
fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
|
|
266
|
+
fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
|
|
267
|
+
fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
|
|
268
|
+
if (shadcn) {
|
|
269
|
+
fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
|
|
270
|
+
fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
|
|
271
|
+
}
|
|
272
|
+
fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
|
|
240
273
|
fs.writeFileSync(
|
|
241
|
-
path.join(targetDir, "
|
|
242
|
-
|
|
243
|
-
import config from "../voltx.config";
|
|
244
|
-
|
|
245
|
-
const app = createApp(config);
|
|
246
|
-
app.start();
|
|
247
|
-
`
|
|
248
|
-
);
|
|
249
|
-
fs.writeFileSync(
|
|
250
|
-
path.join(targetDir, "src", "routes", "index.ts"),
|
|
251
|
-
`// GET / \u2014 Health check
|
|
274
|
+
path.join(targetDir, "api", "index.ts"),
|
|
275
|
+
`// GET /api \u2014 Health check
|
|
252
276
|
import type { Context } from "@voltx/server";
|
|
253
277
|
|
|
254
278
|
export function GET(c: Context) {
|
|
@@ -258,25 +282,22 @@ export function GET(c: Context) {
|
|
|
258
282
|
);
|
|
259
283
|
if (template === "chatbot" || template === "agent-app") {
|
|
260
284
|
fs.writeFileSync(
|
|
261
|
-
path.join(targetDir, "
|
|
285
|
+
path.join(targetDir, "api", "chat.ts"),
|
|
262
286
|
`// POST /api/chat \u2014 Streaming chat with conversation memory
|
|
263
287
|
import type { Context } from "@voltx/server";
|
|
264
288
|
import { streamText } from "@voltx/ai";
|
|
265
289
|
import { createMemory } from "@voltx/memory";
|
|
266
290
|
|
|
267
|
-
// In-memory for dev; swap to createMemory("postgres", { url }) for production
|
|
268
291
|
const memory = createMemory({ maxMessages: 50 });
|
|
269
292
|
|
|
270
293
|
export async function POST(c: Context) {
|
|
271
294
|
const { messages, conversationId = "default" } = await c.req.json();
|
|
272
295
|
|
|
273
|
-
// Store the latest user message
|
|
274
296
|
const lastMessage = messages[messages.length - 1];
|
|
275
297
|
if (lastMessage?.role === "user") {
|
|
276
298
|
await memory.add(conversationId, { role: "user", content: lastMessage.content });
|
|
277
299
|
}
|
|
278
300
|
|
|
279
|
-
// Get conversation history from memory
|
|
280
301
|
const history = await memory.get(conversationId);
|
|
281
302
|
|
|
282
303
|
const result = await streamText({
|
|
@@ -285,7 +306,6 @@ export async function POST(c: Context) {
|
|
|
285
306
|
messages: history.map((m) => ({ role: m.role, content: m.content })),
|
|
286
307
|
});
|
|
287
308
|
|
|
288
|
-
// Store assistant response after stream completes
|
|
289
309
|
result.text.then(async (text) => {
|
|
290
310
|
await memory.add(conversationId, { role: "assistant", content: text });
|
|
291
311
|
});
|
|
@@ -296,44 +316,72 @@ export async function POST(c: Context) {
|
|
|
296
316
|
);
|
|
297
317
|
}
|
|
298
318
|
if (template === "agent-app") {
|
|
299
|
-
fs.mkdirSync(path.join(targetDir, "
|
|
300
|
-
fs.mkdirSync(path.join(targetDir, "
|
|
301
|
-
fs.writeFileSync(
|
|
302
|
-
|
|
303
|
-
`import { createAgent } from "@voltx/agents";
|
|
304
|
-
import { searchTool } from "../tools/search";
|
|
319
|
+
fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
|
|
320
|
+
fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
|
|
321
|
+
fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
|
|
322
|
+
import type { Tool } from "@voltx/agents";
|
|
305
323
|
|
|
306
|
-
export const
|
|
307
|
-
name: "
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
export const calculatorTool: Tool = {
|
|
325
|
+
name: "calculator",
|
|
326
|
+
description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
|
|
327
|
+
parameters: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: { expression: { type: "string", description: "The math expression to evaluate" } },
|
|
330
|
+
required: ["expression"],
|
|
331
|
+
},
|
|
332
|
+
async execute(args: { expression: string }) {
|
|
333
|
+
try {
|
|
334
|
+
const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
|
|
335
|
+
if (args.expression.includes("Math.")) return match;
|
|
336
|
+
throw new Error("Invalid character: " + match);
|
|
337
|
+
});
|
|
338
|
+
const result = new Function("return " + safe)();
|
|
339
|
+
return \`\${args.expression} = \${result}\`;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
`);
|
|
346
|
+
fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
|
|
347
|
+
import type { Tool } from "@voltx/agents";
|
|
318
348
|
|
|
319
|
-
export const
|
|
320
|
-
name: "
|
|
321
|
-
description: "
|
|
349
|
+
export const datetimeTool: Tool = {
|
|
350
|
+
name: "datetime",
|
|
351
|
+
description: "Get the current date, time, day of week, and timezone.",
|
|
322
352
|
parameters: {
|
|
323
353
|
type: "object",
|
|
324
|
-
properties: {
|
|
325
|
-
required: ["query"],
|
|
354
|
+
properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
|
|
326
355
|
},
|
|
327
|
-
async execute(args: {
|
|
328
|
-
|
|
356
|
+
async execute(args: { timezone?: string }) {
|
|
357
|
+
const now = new Date();
|
|
358
|
+
const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
359
|
+
const formatted = now.toLocaleString("en-US", {
|
|
360
|
+
timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
|
|
361
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
|
|
362
|
+
});
|
|
363
|
+
return \`Current date/time (\${tz}): \${formatted}\`;
|
|
329
364
|
},
|
|
330
365
|
};
|
|
331
|
-
`
|
|
332
|
-
)
|
|
366
|
+
`);
|
|
367
|
+
fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
|
|
368
|
+
import { createAgent } from "@voltx/agents";
|
|
369
|
+
import { calculatorTool } from "../tools/calculator";
|
|
370
|
+
import { datetimeTool } from "../tools/datetime";
|
|
371
|
+
|
|
372
|
+
export const assistant = createAgent({
|
|
373
|
+
name: "assistant",
|
|
374
|
+
model: "${provider}:${model}",
|
|
375
|
+
instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
|
|
376
|
+
tools: [calculatorTool, datetimeTool],
|
|
377
|
+
maxIterations: 5,
|
|
378
|
+
});
|
|
379
|
+
`);
|
|
333
380
|
fs.writeFileSync(
|
|
334
|
-
path.join(targetDir, "
|
|
335
|
-
|
|
336
|
-
import {
|
|
381
|
+
path.join(targetDir, "api", "agent.ts"),
|
|
382
|
+
`// POST /api/agent \u2014 Run the AI agent
|
|
383
|
+
import type { Context } from "@voltx/server";
|
|
384
|
+
import { assistant } from "../agents/assistant";
|
|
337
385
|
|
|
338
386
|
export async function POST(c: Context) {
|
|
339
387
|
const { input } = await c.req.json();
|
|
@@ -346,36 +394,33 @@ export async function POST(c: Context) {
|
|
|
346
394
|
}
|
|
347
395
|
if (template === "rag-app") {
|
|
348
396
|
const embedModel = "openai:text-embedding-3-small";
|
|
349
|
-
fs.mkdirSync(path.join(targetDir, "
|
|
397
|
+
fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
|
|
350
398
|
fs.writeFileSync(
|
|
351
|
-
path.join(targetDir, "
|
|
399
|
+
path.join(targetDir, "api", "rag", "query.ts"),
|
|
352
400
|
`// POST /api/rag/query \u2014 Query documents with RAG
|
|
353
401
|
import type { Context } from "@voltx/server";
|
|
354
402
|
import { streamText } from "@voltx/ai";
|
|
355
403
|
import { createRAGPipeline, createEmbedder } from "@voltx/rag";
|
|
356
404
|
import { createVectorStore } from "@voltx/db";
|
|
357
405
|
|
|
358
|
-
const vectorStore = createVectorStore();
|
|
406
|
+
const vectorStore = createVectorStore();
|
|
359
407
|
const embedder = createEmbedder({ model: "${embedModel}" });
|
|
360
408
|
const rag = createRAGPipeline({ embedder, vectorStore });
|
|
361
409
|
|
|
362
410
|
export async function POST(c: Context) {
|
|
363
411
|
const { question } = await c.req.json();
|
|
364
|
-
|
|
365
412
|
const context = await rag.getContext(question, { topK: 5 });
|
|
366
|
-
|
|
367
413
|
const result = await streamText({
|
|
368
414
|
model: "${provider}:${model}",
|
|
369
|
-
system: \`Answer
|
|
415
|
+
system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
|
|
370
416
|
messages: [{ role: "user", content: question }],
|
|
371
417
|
});
|
|
372
|
-
|
|
373
418
|
return result.toSSEResponse();
|
|
374
419
|
}
|
|
375
420
|
`
|
|
376
421
|
);
|
|
377
422
|
fs.writeFileSync(
|
|
378
|
-
path.join(targetDir, "
|
|
423
|
+
path.join(targetDir, "api", "rag", "ingest.ts"),
|
|
379
424
|
`// POST /api/rag/ingest \u2014 Ingest documents into the vector store
|
|
380
425
|
import type { Context } from "@voltx/server";
|
|
381
426
|
import { createRAGPipeline, createEmbedder } from "@voltx/rag";
|
|
@@ -387,11 +432,7 @@ const rag = createRAGPipeline({ embedder, vectorStore });
|
|
|
387
432
|
|
|
388
433
|
export async function POST(c: Context) {
|
|
389
434
|
const { text, idPrefix } = await c.req.json();
|
|
390
|
-
|
|
391
|
-
if (!text || typeof text !== "string") {
|
|
392
|
-
return c.json({ error: "Missing 'text' field" }, 400);
|
|
393
|
-
}
|
|
394
|
-
|
|
435
|
+
if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
|
|
395
436
|
const result = await rag.ingest(text, idPrefix ?? "doc");
|
|
396
437
|
return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
|
|
397
438
|
}
|
|
@@ -399,12 +440,12 @@ export async function POST(c: Context) {
|
|
|
399
440
|
);
|
|
400
441
|
}
|
|
401
442
|
if (auth === "better-auth") {
|
|
402
|
-
fs.mkdirSync(path.join(targetDir, "
|
|
443
|
+
fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
|
|
403
444
|
fs.writeFileSync(
|
|
404
|
-
path.join(targetDir, "
|
|
445
|
+
path.join(targetDir, "api", "auth", "[...path].ts"),
|
|
405
446
|
`// ALL /api/auth/* \u2014 Better Auth handler
|
|
406
447
|
import type { Context } from "@voltx/server";
|
|
407
|
-
import { auth } from "
|
|
448
|
+
import { auth } from "../../src/lib/auth";
|
|
408
449
|
import { createAuthHandler } from "@voltx/auth";
|
|
409
450
|
|
|
410
451
|
const handler = createAuthHandler(auth);
|
|
@@ -413,11 +454,9 @@ export const GET = (c: Context) => handler(c);
|
|
|
413
454
|
export const POST = (c: Context) => handler(c);
|
|
414
455
|
`
|
|
415
456
|
);
|
|
416
|
-
fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
|
|
417
457
|
fs.writeFileSync(
|
|
418
458
|
path.join(targetDir, "src", "lib", "auth.ts"),
|
|
419
|
-
|
|
420
|
-
import { createAuth, createAuthMiddleware } from "@voltx/auth";
|
|
459
|
+
`import { createAuth, createAuthMiddleware } from "@voltx/auth";
|
|
421
460
|
|
|
422
461
|
export const auth = createAuth("better-auth", {
|
|
423
462
|
database: process.env.DATABASE_URL!,
|
|
@@ -431,11 +470,9 @@ export const authMiddleware = createAuthMiddleware({
|
|
|
431
470
|
`
|
|
432
471
|
);
|
|
433
472
|
} else if (auth === "jwt") {
|
|
434
|
-
fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
|
|
435
473
|
fs.writeFileSync(
|
|
436
474
|
path.join(targetDir, "src", "lib", "auth.ts"),
|
|
437
|
-
|
|
438
|
-
import { createAuth, createAuthMiddleware } from "@voltx/auth";
|
|
475
|
+
`import { createAuth, createAuthMiddleware } from "@voltx/auth";
|
|
439
476
|
|
|
440
477
|
export const jwt = createAuth("jwt", {
|
|
441
478
|
secret: process.env.JWT_SECRET!,
|
|
@@ -449,18 +486,13 @@ export const authMiddleware = createAuthMiddleware({
|
|
|
449
486
|
`
|
|
450
487
|
);
|
|
451
488
|
fs.writeFileSync(
|
|
452
|
-
path.join(targetDir, "
|
|
453
|
-
|
|
454
|
-
import
|
|
455
|
-
import { jwt } from "../../lib/auth";
|
|
489
|
+
path.join(targetDir, "api", "auth.ts"),
|
|
490
|
+
`import type { Context } from "@voltx/server";
|
|
491
|
+
import { jwt } from "../src/lib/auth";
|
|
456
492
|
|
|
457
493
|
export async function POST(c: Context) {
|
|
458
494
|
const { email, password } = await c.req.json();
|
|
459
|
-
|
|
460
|
-
if (!email || !password) {
|
|
461
|
-
return c.json({ error: "Email and password are required" }, 400);
|
|
462
|
-
}
|
|
463
|
-
|
|
495
|
+
if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
|
|
464
496
|
const token = await jwt.sign({ sub: email, email });
|
|
465
497
|
return c.json({ token });
|
|
466
498
|
}
|
|
@@ -470,69 +502,661 @@ export async function POST(c: Context) {
|
|
|
470
502
|
let envContent = "";
|
|
471
503
|
if (template === "rag-app") {
|
|
472
504
|
envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nOPENAI_API_KEY=sk-...\n\n";
|
|
473
|
-
envContent += "# \u2500\u2500\u2500 Database
|
|
474
|
-
envContent += "# \u2500\u2500\u2500 Vector Database (Pinecone) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPINECONE_API_KEY=pc-...\nPINECONE_INDEX=voltx-embeddings\n\n";
|
|
505
|
+
envContent += "# \u2500\u2500\u2500 Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
|
|
475
506
|
} else if (template === "chatbot" || template === "agent-app") {
|
|
476
507
|
envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCEREBRAS_API_KEY=csk-...\n\n";
|
|
477
|
-
if (template === "agent-app") {
|
|
478
|
-
envContent += "# \u2500\u2500\u2500 Database (Neon Postgres \u2014 optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
|
|
479
|
-
}
|
|
480
508
|
} else {
|
|
481
|
-
envContent += "# \u2500\u2500\u2500 LLM Provider
|
|
509
|
+
envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# OPENAI_API_KEY=sk-...\n# CEREBRAS_API_KEY=csk-...\n\n";
|
|
482
510
|
}
|
|
483
511
|
if (auth === "better-auth") {
|
|
484
|
-
envContent += "# \u2500\u2500\u2500 Auth (Better Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBETTER_AUTH_SECRET=your-secret-key-min-32-chars-here\nBETTER_AUTH_URL=http://localhost:3000\n";
|
|
485
|
-
if (template !== "rag-app" && template !== "agent-app") {
|
|
486
|
-
envContent += "DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require\n";
|
|
487
|
-
}
|
|
488
|
-
envContent += "# GITHUB_CLIENT_ID=\n# GITHUB_CLIENT_SECRET=\n\n";
|
|
512
|
+
envContent += "# \u2500\u2500\u2500 Auth (Better Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBETTER_AUTH_SECRET=your-secret-key-min-32-chars-here\nBETTER_AUTH_URL=http://localhost:3000\n\n";
|
|
489
513
|
} else if (auth === "jwt") {
|
|
490
514
|
envContent += "# \u2500\u2500\u2500 Auth (JWT) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nJWT_SECRET=your-jwt-secret-key\n\n";
|
|
491
515
|
}
|
|
492
516
|
envContent += "# \u2500\u2500\u2500 App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPORT=3000\nNODE_ENV=development\n";
|
|
493
517
|
fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
|
|
494
|
-
fs.writeFileSync(
|
|
495
|
-
path.join(targetDir, ".gitignore"),
|
|
496
|
-
"node_modules\ndist\n.env\n"
|
|
497
|
-
);
|
|
498
|
-
fs.writeFileSync(
|
|
499
|
-
path.join(targetDir, "tsconfig.json"),
|
|
500
|
-
JSON.stringify(
|
|
501
|
-
{
|
|
502
|
-
compilerOptions: {
|
|
503
|
-
target: "ES2022",
|
|
504
|
-
module: "ESNext",
|
|
505
|
-
moduleResolution: "bundler",
|
|
506
|
-
strict: true,
|
|
507
|
-
esModuleInterop: true,
|
|
508
|
-
skipLibCheck: true,
|
|
509
|
-
outDir: "dist",
|
|
510
|
-
rootDir: "src"
|
|
511
|
-
},
|
|
512
|
-
include: ["src"]
|
|
513
|
-
},
|
|
514
|
-
null,
|
|
515
|
-
2
|
|
516
|
-
)
|
|
517
|
-
);
|
|
518
|
+
fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
|
|
518
519
|
printWelcomeBanner(name);
|
|
519
520
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
521
|
+
function generateServerEntry(projectName, template) {
|
|
522
|
+
const imports = [];
|
|
523
|
+
const mounts = [];
|
|
524
|
+
imports.push('import { GET as healthGET } from "./api/index";');
|
|
525
|
+
mounts.push('app.get("/api", healthGET);');
|
|
526
|
+
if (template === "chatbot" || template === "agent-app") {
|
|
527
|
+
imports.push('import { POST as chatPOST } from "./api/chat";');
|
|
528
|
+
mounts.push('app.post("/api/chat", chatPOST);');
|
|
526
529
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
530
|
+
if (template === "agent-app") {
|
|
531
|
+
imports.push('import { POST as agentPOST } from "./api/agent";');
|
|
532
|
+
mounts.push('app.post("/api/agent", agentPOST);');
|
|
533
|
+
}
|
|
534
|
+
if (template === "rag-app") {
|
|
535
|
+
imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
|
|
536
|
+
imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
|
|
537
|
+
mounts.push('app.post("/api/rag/query", ragQueryPOST);');
|
|
538
|
+
mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
|
|
539
|
+
}
|
|
540
|
+
return `import { Hono } from "hono";
|
|
541
|
+
import { serve } from "@hono/node-server";
|
|
542
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
543
|
+
import { registerSSR } from "@voltx/server";
|
|
544
|
+
import { loadEnv } from "@voltx/core";
|
|
545
|
+
${imports.join("\n")}
|
|
546
|
+
|
|
547
|
+
loadEnv(process.env.NODE_ENV ?? "development");
|
|
548
|
+
|
|
549
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
550
|
+
const app = new Hono();
|
|
551
|
+
|
|
552
|
+
// \u2500\u2500 API Routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
553
|
+
${mounts.join("\n")}
|
|
554
|
+
|
|
555
|
+
// \u2500\u2500 Static assets (production) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
556
|
+
if (isProd) {
|
|
557
|
+
app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
|
|
558
|
+
app.use("/favicon.svg", serveStatic({ root: "./public/" }));
|
|
559
|
+
app.use("/robots.txt", serveStatic({ root: "./public/" }));
|
|
560
|
+
app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// \u2500\u2500 SSR catch-all \u2014 renders React on the server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
564
|
+
registerSSR(app, null, {
|
|
565
|
+
title: "${projectName}",
|
|
566
|
+
entryServer: "src/entry-server.tsx",
|
|
567
|
+
entryClient: "src/entry-client.tsx",
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
export default app;
|
|
571
|
+
|
|
572
|
+
if (isProd) {
|
|
573
|
+
const port = Number(process.env.PORT) || 3000;
|
|
574
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
575
|
+
console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
|
|
534
576
|
});
|
|
535
577
|
}
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
function generateViteConfigFile(entry) {
|
|
581
|
+
return `import { defineConfig } from "vite";
|
|
582
|
+
import devServer from "@hono/vite-dev-server";
|
|
583
|
+
import react from "@vitejs/plugin-react";
|
|
584
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
585
|
+
|
|
586
|
+
export default defineConfig({
|
|
587
|
+
resolve: {
|
|
588
|
+
alias: {
|
|
589
|
+
"@": "/src",
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
plugins: [
|
|
593
|
+
react(),
|
|
594
|
+
tailwindcss(),
|
|
595
|
+
devServer({
|
|
596
|
+
entry: "${entry}",
|
|
597
|
+
exclude: [
|
|
598
|
+
/.*\\.tsx?($|\\?)/,
|
|
599
|
+
/.*\\.(s?css|less)($|\\?)/,
|
|
600
|
+
/.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
|
|
601
|
+
/^\\/@.+$/,
|
|
602
|
+
/^\\/favicon\\.svg$/,
|
|
603
|
+
/^\\/node_modules\\/.*/,
|
|
604
|
+
/^\\/src\\/.*/,
|
|
605
|
+
],
|
|
606
|
+
injectClientScript: false,
|
|
607
|
+
}),
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
function generateEntryClient() {
|
|
613
|
+
return `import React from "react";
|
|
614
|
+
import { hydrateRoot } from "react-dom/client";
|
|
615
|
+
import Layout from "./layout";
|
|
616
|
+
import App from "./app";
|
|
617
|
+
import "./globals.css";
|
|
618
|
+
|
|
619
|
+
hydrateRoot(
|
|
620
|
+
document.getElementById("root")!,
|
|
621
|
+
<React.StrictMode>
|
|
622
|
+
<Layout>
|
|
623
|
+
<App />
|
|
624
|
+
</Layout>
|
|
625
|
+
</React.StrictMode>
|
|
626
|
+
);
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
function generateEntryServer() {
|
|
630
|
+
return `import React from "react";
|
|
631
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
632
|
+
import Layout from "./layout";
|
|
633
|
+
import App from "./app";
|
|
634
|
+
|
|
635
|
+
export async function render(_url: string): Promise<ReadableStream> {
|
|
636
|
+
const stream = await renderToReadableStream(
|
|
637
|
+
<React.StrictMode>
|
|
638
|
+
<Layout>
|
|
639
|
+
<App />
|
|
640
|
+
</Layout>
|
|
641
|
+
</React.StrictMode>,
|
|
642
|
+
{
|
|
643
|
+
onError(error: unknown) {
|
|
644
|
+
console.error("[voltx] SSR render error:", error);
|
|
645
|
+
},
|
|
646
|
+
}
|
|
647
|
+
);
|
|
648
|
+
return stream;
|
|
649
|
+
}
|
|
650
|
+
`;
|
|
651
|
+
}
|
|
652
|
+
function generateLayoutComponent(projectName) {
|
|
653
|
+
return `import React from "react";
|
|
654
|
+
|
|
655
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
656
|
+
return (
|
|
657
|
+
<div className="min-h-screen bg-background text-foreground font-sans">
|
|
658
|
+
<header className="border-b border-border px-6 py-3 flex items-center justify-between">
|
|
659
|
+
<div className="flex items-center gap-2">
|
|
660
|
+
<span className="text-xl">\u26A1</span>
|
|
661
|
+
<span className="font-semibold">${projectName}</span>
|
|
662
|
+
</div>
|
|
663
|
+
<a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-muted text-sm hover:text-foreground transition-colors">
|
|
664
|
+
Built with VoltX
|
|
665
|
+
</a>
|
|
666
|
+
</header>
|
|
667
|
+
<main>{children}</main>
|
|
668
|
+
</div>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
function generateGlobalCSS(useShadcn = false) {
|
|
674
|
+
if (useShadcn) {
|
|
675
|
+
return `@import "tailwindcss";
|
|
676
|
+
|
|
677
|
+
@theme inline {
|
|
678
|
+
--radius-sm: 0.25rem;
|
|
679
|
+
--radius-md: 0.375rem;
|
|
680
|
+
--radius-lg: 0.5rem;
|
|
681
|
+
--radius-xl: 0.75rem;
|
|
682
|
+
--color-background: hsl(var(--background));
|
|
683
|
+
--color-foreground: hsl(var(--foreground));
|
|
684
|
+
--color-card: hsl(var(--card));
|
|
685
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
686
|
+
--color-popover: hsl(var(--popover));
|
|
687
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
688
|
+
--color-primary: hsl(var(--primary));
|
|
689
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
690
|
+
--color-secondary: hsl(var(--secondary));
|
|
691
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
692
|
+
--color-muted: hsl(var(--muted));
|
|
693
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
694
|
+
--color-accent: hsl(var(--accent));
|
|
695
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
696
|
+
--color-destructive: hsl(var(--destructive));
|
|
697
|
+
--color-border: hsl(var(--border));
|
|
698
|
+
--color-input: hsl(var(--input));
|
|
699
|
+
--color-ring: hsl(var(--ring));
|
|
700
|
+
--color-chart-1: hsl(var(--chart-1));
|
|
701
|
+
--color-chart-2: hsl(var(--chart-2));
|
|
702
|
+
--color-chart-3: hsl(var(--chart-3));
|
|
703
|
+
--color-chart-4: hsl(var(--chart-4));
|
|
704
|
+
--color-chart-5: hsl(var(--chart-5));
|
|
705
|
+
--color-sidebar: hsl(var(--sidebar));
|
|
706
|
+
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
|
707
|
+
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
|
708
|
+
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
|
709
|
+
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
|
710
|
+
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
|
711
|
+
--color-sidebar-border: hsl(var(--sidebar-border));
|
|
712
|
+
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
:root {
|
|
716
|
+
--background: 0 0% 4%;
|
|
717
|
+
--foreground: 0 0% 93%;
|
|
718
|
+
--card: 0 0% 6%;
|
|
719
|
+
--card-foreground: 0 0% 93%;
|
|
720
|
+
--popover: 0 0% 6%;
|
|
721
|
+
--popover-foreground: 0 0% 93%;
|
|
722
|
+
--primary: 0 0% 93%;
|
|
723
|
+
--primary-foreground: 0 0% 6%;
|
|
724
|
+
--secondary: 0 0% 12%;
|
|
725
|
+
--secondary-foreground: 0 0% 93%;
|
|
726
|
+
--muted: 0 0% 12%;
|
|
727
|
+
--muted-foreground: 0 0% 55%;
|
|
728
|
+
--accent: 0 0% 12%;
|
|
729
|
+
--accent-foreground: 0 0% 93%;
|
|
730
|
+
--destructive: 0 62% 50%;
|
|
731
|
+
--border: 0 0% 14%;
|
|
732
|
+
--input: 0 0% 14%;
|
|
733
|
+
--ring: 0 0% 83%;
|
|
734
|
+
--chart-1: 220 70% 50%;
|
|
735
|
+
--chart-2: 160 60% 45%;
|
|
736
|
+
--chart-3: 30 80% 55%;
|
|
737
|
+
--chart-4: 280 65% 60%;
|
|
738
|
+
--chart-5: 340 75% 55%;
|
|
739
|
+
--sidebar: 0 0% 5%;
|
|
740
|
+
--sidebar-foreground: 0 0% 93%;
|
|
741
|
+
--sidebar-primary: 0 0% 93%;
|
|
742
|
+
--sidebar-primary-foreground: 0 0% 6%;
|
|
743
|
+
--sidebar-accent: 0 0% 12%;
|
|
744
|
+
--sidebar-accent-foreground: 0 0% 93%;
|
|
745
|
+
--sidebar-border: 0 0% 14%;
|
|
746
|
+
--sidebar-ring: 0 0% 83%;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
*,
|
|
750
|
+
*::before,
|
|
751
|
+
*::after {
|
|
752
|
+
box-sizing: border-box;
|
|
753
|
+
margin: 0;
|
|
754
|
+
padding: 0;
|
|
755
|
+
border-color: var(--color-border);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
html,
|
|
759
|
+
body {
|
|
760
|
+
height: 100%;
|
|
761
|
+
background: var(--color-background);
|
|
762
|
+
color: var(--color-foreground);
|
|
763
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
764
|
+
-webkit-font-smoothing: antialiased;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
#root {
|
|
768
|
+
height: 100%;
|
|
769
|
+
}
|
|
770
|
+
`;
|
|
771
|
+
}
|
|
772
|
+
return `@import "tailwindcss";
|
|
773
|
+
|
|
774
|
+
@theme {
|
|
775
|
+
--color-background: #0a0a0a;
|
|
776
|
+
--color-foreground: #ededed;
|
|
777
|
+
--color-muted: #888888;
|
|
778
|
+
--color-border: #222222;
|
|
779
|
+
--color-primary: #2563eb;
|
|
780
|
+
--color-accent: #a78bfa;
|
|
781
|
+
--font-sans: system-ui, -apple-system, sans-serif;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
*,
|
|
785
|
+
*::before,
|
|
786
|
+
*::after {
|
|
787
|
+
box-sizing: border-box;
|
|
788
|
+
margin: 0;
|
|
789
|
+
padding: 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
html,
|
|
793
|
+
body {
|
|
794
|
+
height: 100%;
|
|
795
|
+
background: var(--color-background);
|
|
796
|
+
color: var(--color-foreground);
|
|
797
|
+
font-family: var(--font-sans);
|
|
798
|
+
-webkit-font-smoothing: antialiased;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
#root {
|
|
802
|
+
height: 100%;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
a {
|
|
806
|
+
color: inherit;
|
|
807
|
+
text-decoration: none;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
a:hover {
|
|
811
|
+
text-decoration: underline;
|
|
812
|
+
}
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
function generateCnUtil() {
|
|
816
|
+
return `import { type ClassValue, clsx } from "clsx";
|
|
817
|
+
import { twMerge } from "tailwind-merge";
|
|
818
|
+
|
|
819
|
+
export function cn(...inputs: ClassValue[]) {
|
|
820
|
+
return twMerge(clsx(inputs));
|
|
821
|
+
}
|
|
822
|
+
`;
|
|
823
|
+
}
|
|
824
|
+
function generateComponentsJson() {
|
|
825
|
+
return JSON.stringify({
|
|
826
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
827
|
+
style: "new-york",
|
|
828
|
+
rsc: false,
|
|
829
|
+
tsx: true,
|
|
830
|
+
tailwind: {
|
|
831
|
+
config: "",
|
|
832
|
+
css: "src/globals.css",
|
|
833
|
+
baseColor: "neutral",
|
|
834
|
+
cssVariables: true
|
|
835
|
+
},
|
|
836
|
+
aliases: {
|
|
837
|
+
components: "@/components",
|
|
838
|
+
utils: "@/lib/utils",
|
|
839
|
+
ui: "@/components/ui",
|
|
840
|
+
lib: "@/lib",
|
|
841
|
+
hooks: "@/hooks"
|
|
842
|
+
}
|
|
843
|
+
}, null, 2) + "\n";
|
|
844
|
+
}
|
|
845
|
+
function generateAppComponent(projectName, template) {
|
|
846
|
+
if (template === "blank") {
|
|
847
|
+
return `import React, { useState, useEffect } from "react";
|
|
848
|
+
|
|
849
|
+
export default function App() {
|
|
850
|
+
const [status, setStatus] = useState<string>("checking...");
|
|
851
|
+
|
|
852
|
+
useEffect(() => {
|
|
853
|
+
fetch("/api")
|
|
854
|
+
.then((res) => res.json())
|
|
855
|
+
.then((data) => setStatus(data.status || "ok"))
|
|
856
|
+
.catch(() => setStatus("error"));
|
|
857
|
+
}, []);
|
|
858
|
+
|
|
859
|
+
return (
|
|
860
|
+
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-60px)] px-6 py-12">
|
|
861
|
+
<div className="text-center max-w-2xl w-full">
|
|
862
|
+
{/* Hero */}
|
|
863
|
+
<div className="relative mb-8">
|
|
864
|
+
<div className="absolute inset-0 blur-3xl opacity-20 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full" />
|
|
865
|
+
<div className="relative text-7xl mb-4">\u26A1</div>
|
|
866
|
+
</div>
|
|
867
|
+
<h1 className="text-5xl font-bold tracking-tight mb-3 bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
|
|
868
|
+
\${"\${projectName}"}
|
|
869
|
+
</h1>
|
|
870
|
+
<p className="text-muted text-lg mb-10">The AI-first full-stack framework</p>
|
|
871
|
+
|
|
872
|
+
{/* Status cards */}
|
|
873
|
+
<div className="flex gap-4 justify-center mb-10">
|
|
874
|
+
<div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
|
|
875
|
+
<div className="text-xs text-muted mb-1 uppercase tracking-wider">Server</div>
|
|
876
|
+
<div className={\`text-sm font-medium \${status === "ok" ? "text-emerald-400" : "text-red-400"}\`}>
|
|
877
|
+
<span className={\`inline-block w-2 h-2 rounded-full mr-2 \${status === "ok" ? "bg-emerald-400 animate-pulse" : "bg-red-400"}\`} />
|
|
878
|
+
{status}
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
<div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
|
|
882
|
+
<div className="text-xs text-muted mb-1 uppercase tracking-wider">Frontend</div>
|
|
883
|
+
<div className="text-sm font-medium text-emerald-400">
|
|
884
|
+
<span className="inline-block w-2 h-2 rounded-full mr-2 bg-emerald-400 animate-pulse" />
|
|
885
|
+
React + Vite
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
<div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
|
|
889
|
+
<div className="text-xs text-muted mb-1 uppercase tracking-wider">CSS</div>
|
|
890
|
+
<div className="text-sm font-medium text-sky-400">Tailwind v4</div>
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
|
|
894
|
+
{/* Get started */}
|
|
895
|
+
<div className="bg-white/[0.03] border border-border rounded-2xl p-8 text-left backdrop-blur-sm">
|
|
896
|
+
<h2 className="text-sm font-medium text-muted mb-6 uppercase tracking-wider">Get started</h2>
|
|
897
|
+
<div className="space-y-4">
|
|
898
|
+
<div className="flex items-start gap-4">
|
|
899
|
+
<div className="w-8 h-8 rounded-lg bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 text-sm shrink-0">1</div>
|
|
900
|
+
<div>
|
|
901
|
+
<code className="text-purple-400 text-sm">src/app.tsx</code>
|
|
902
|
+
<p className="text-muted text-sm mt-1">Edit this file to build your UI</p>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
<div className="flex items-start gap-4">
|
|
906
|
+
<div className="w-8 h-8 rounded-lg bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 text-sm shrink-0">2</div>
|
|
907
|
+
<div>
|
|
908
|
+
<code className="text-blue-400 text-sm">api/</code>
|
|
909
|
+
<p className="text-muted text-sm mt-1">Add API routes here (file-based routing)</p>
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
<div className="flex items-start gap-4">
|
|
913
|
+
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 text-sm shrink-0">3</div>
|
|
914
|
+
<div>
|
|
915
|
+
<code className="text-emerald-400 text-sm">src/components/</code>
|
|
916
|
+
<p className="text-muted text-sm mt-1">Create React components with Tailwind CSS</p>
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
{/* Links */}
|
|
923
|
+
<div className="flex gap-6 justify-center mt-8">
|
|
924
|
+
<a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
|
|
925
|
+
GitHub \u2192
|
|
926
|
+
</a>
|
|
927
|
+
<a href="https://voltx.co.in" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
|
|
928
|
+
Docs \u2192
|
|
929
|
+
</a>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
`;
|
|
936
|
+
}
|
|
937
|
+
const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
|
|
938
|
+
const isAgent = template === "agent-app";
|
|
939
|
+
const isRag = template === "rag-app";
|
|
940
|
+
let emptyStateTitle = "Start a conversation";
|
|
941
|
+
let emptyStateHint = "Type a message below to chat with AI";
|
|
942
|
+
let accentColor = "blue";
|
|
943
|
+
let inputPlaceholder = "Type a message...";
|
|
944
|
+
if (isAgent) {
|
|
945
|
+
emptyStateTitle = "Talk to your AI agent";
|
|
946
|
+
emptyStateHint = "The agent can use tools like Calculator and Date/Time";
|
|
947
|
+
accentColor = "purple";
|
|
948
|
+
inputPlaceholder = "Ask the agent anything...";
|
|
949
|
+
} else if (isRag) {
|
|
950
|
+
emptyStateTitle = "Ask your documents";
|
|
951
|
+
emptyStateHint = "Query your knowledge base \u2014 ingest docs via POST /api/rag/ingest";
|
|
952
|
+
accentColor = "emerald";
|
|
953
|
+
inputPlaceholder = "Ask a question about your documents...";
|
|
954
|
+
}
|
|
955
|
+
return `import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
956
|
+
|
|
957
|
+
interface Message {
|
|
958
|
+
id: string;
|
|
959
|
+
role: "user" | "assistant";
|
|
960
|
+
content: string;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export default function App() {
|
|
964
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
965
|
+
const [input, setInput] = useState("");
|
|
966
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
967
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
968
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
969
|
+
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
972
|
+
}, [messages]);
|
|
973
|
+
|
|
974
|
+
useEffect(() => {
|
|
975
|
+
inputRef.current?.focus();
|
|
976
|
+
}, []);
|
|
977
|
+
|
|
978
|
+
const sendMessage = useCallback(async () => {
|
|
979
|
+
const text = input.trim();
|
|
980
|
+
if (!text || isLoading) return;
|
|
981
|
+
|
|
982
|
+
const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
|
|
983
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
984
|
+
setInput("");
|
|
985
|
+
setIsLoading(true);
|
|
986
|
+
|
|
987
|
+
const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
|
|
988
|
+
setMessages((prev) => [...prev, assistantMsg]);
|
|
989
|
+
|
|
990
|
+
try {${isAgent ? `
|
|
991
|
+
const res = await fetch("${apiEndpoint}", {
|
|
992
|
+
method: "POST",
|
|
993
|
+
headers: { "Content-Type": "application/json" },
|
|
994
|
+
body: JSON.stringify({ input: text }),
|
|
995
|
+
});
|
|
996
|
+
const data = await res.json();
|
|
997
|
+
setMessages((prev) =>
|
|
998
|
+
prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
|
|
999
|
+
);` : `
|
|
1000
|
+
const res = await fetch("${apiEndpoint}", {
|
|
1001
|
+
method: "POST",
|
|
1002
|
+
headers: { "Content-Type": "application/json" },
|
|
1003
|
+
body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
if (!res.body) throw new Error("No response body");
|
|
1007
|
+
|
|
1008
|
+
const reader = res.body.getReader();
|
|
1009
|
+
const decoder = new TextDecoder();
|
|
1010
|
+
let buffer = "";
|
|
1011
|
+
let fullContent = "";
|
|
1012
|
+
|
|
1013
|
+
while (true) {
|
|
1014
|
+
const { done, value } = await reader.read();
|
|
1015
|
+
if (done) break;
|
|
1016
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1017
|
+
const lines = buffer.split("\\n");
|
|
1018
|
+
buffer = lines.pop() ?? "";
|
|
1019
|
+
|
|
1020
|
+
for (const line of lines) {
|
|
1021
|
+
if (!line.startsWith("data: ")) continue;
|
|
1022
|
+
const data = line.slice(6);
|
|
1023
|
+
if (data === "[DONE]") break;
|
|
1024
|
+
try {
|
|
1025
|
+
const parsed = JSON.parse(data);
|
|
1026
|
+
const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
|
|
1027
|
+
if (chunk) {
|
|
1028
|
+
fullContent += chunk;
|
|
1029
|
+
setMessages((prev) =>
|
|
1030
|
+
prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
} catch {}
|
|
1034
|
+
}
|
|
1035
|
+
}`}
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
setMessages((prev) =>
|
|
1038
|
+
prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
|
|
1039
|
+
);
|
|
1040
|
+
} finally {
|
|
1041
|
+
setIsLoading(false);
|
|
1042
|
+
}
|
|
1043
|
+
}, [input, isLoading, messages]);
|
|
1044
|
+
|
|
1045
|
+
return (
|
|
1046
|
+
<div className="h-[calc(100vh-60px)] flex flex-col">
|
|
1047
|
+
<main className="flex-1 overflow-y-auto px-4 py-6">
|
|
1048
|
+
<div className="max-w-3xl mx-auto">
|
|
1049
|
+
{messages.length === 0 && (
|
|
1050
|
+
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
|
1051
|
+
<div className="relative mb-6">
|
|
1052
|
+
<div className="absolute inset-0 blur-2xl opacity-20 bg-${accentColor}-500 rounded-full" />
|
|
1053
|
+
<div className="relative text-5xl">\u26A1</div>
|
|
1054
|
+
</div>
|
|
1055
|
+
<h2 className="text-2xl font-semibold mb-2">${emptyStateTitle}</h2>
|
|
1056
|
+
<p className="text-muted text-sm max-w-md">${emptyStateHint}</p>
|
|
1057
|
+
<div className="flex gap-2 mt-6">
|
|
1058
|
+
${isAgent ? `<button onClick={() => { setInput("What is 42 * 17?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
|
|
1059
|
+
What is 42 \xD7 17?
|
|
1060
|
+
</button>
|
|
1061
|
+
<button onClick={() => { setInput("What day is it today?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
|
|
1062
|
+
What day is it?
|
|
1063
|
+
</button>` : isRag ? `<button onClick={() => { setInput("Summarize the main topics"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
|
|
1064
|
+
Summarize main topics
|
|
1065
|
+
</button>
|
|
1066
|
+
<button onClick={() => { setInput("What are the key findings?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
|
|
1067
|
+
Key findings
|
|
1068
|
+
</button>` : `<button onClick={() => { setInput("Hello! What can you do?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
|
|
1069
|
+
Hello! What can you do?
|
|
1070
|
+
</button>
|
|
1071
|
+
<button onClick={() => { setInput("Tell me a fun fact"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
|
|
1072
|
+
Tell me a fun fact
|
|
1073
|
+
</button>`}
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
)}
|
|
1077
|
+
{messages.map((msg) => (
|
|
1078
|
+
<div key={msg.id} className={\`mb-6 flex \${msg.role === "user" ? "justify-end" : "justify-start"}\`}>
|
|
1079
|
+
<div className={\`flex items-start gap-3 max-w-[80%] \${msg.role === "user" ? "flex-row-reverse" : ""}\`}>
|
|
1080
|
+
<div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0 \${
|
|
1081
|
+
msg.role === "user" ? "bg-${accentColor}-500 text-white" : "bg-white/10 text-muted"
|
|
1082
|
+
}\`}>
|
|
1083
|
+
{msg.role === "user" ? "Y" : "\u26A1"}
|
|
1084
|
+
</div>
|
|
1085
|
+
<div className={\`px-4 py-3 rounded-2xl whitespace-pre-wrap leading-relaxed text-sm \${
|
|
1086
|
+
msg.role === "user"
|
|
1087
|
+
? "bg-${accentColor}-500 text-white rounded-br-md"
|
|
1088
|
+
: "bg-white/[0.05] border border-border rounded-bl-md"
|
|
1089
|
+
}\`}>
|
|
1090
|
+
{msg.content || (isLoading && msg.role === "assistant" ? (
|
|
1091
|
+
<span className="flex gap-1">
|
|
1092
|
+
<span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:0ms]" />
|
|
1093
|
+
<span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:150ms]" />
|
|
1094
|
+
<span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:300ms]" />
|
|
1095
|
+
</span>
|
|
1096
|
+
) : "")}
|
|
1097
|
+
</div>
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
))}
|
|
1101
|
+
<div ref={messagesEndRef} />
|
|
1102
|
+
</div>
|
|
1103
|
+
</main>
|
|
1104
|
+
<footer className="border-t border-border px-4 py-4">
|
|
1105
|
+
<form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} className="max-w-3xl mx-auto flex gap-3">
|
|
1106
|
+
<input
|
|
1107
|
+
ref={inputRef}
|
|
1108
|
+
value={input}
|
|
1109
|
+
onChange={(e) => setInput(e.target.value)}
|
|
1110
|
+
placeholder="${inputPlaceholder}"
|
|
1111
|
+
disabled={isLoading}
|
|
1112
|
+
className="flex-1 px-4 py-3 rounded-xl bg-white/[0.05] border border-border text-foreground placeholder:text-muted/50 outline-none focus:border-${accentColor}-500/50 focus:ring-1 focus:ring-${accentColor}-500/25 transition-all disabled:opacity-50"
|
|
1113
|
+
/>
|
|
1114
|
+
<button
|
|
1115
|
+
type="submit"
|
|
1116
|
+
disabled={isLoading || !input.trim()}
|
|
1117
|
+
className="px-6 py-3 rounded-xl font-medium text-sm transition-all disabled:opacity-30 disabled:cursor-not-allowed bg-${accentColor}-500 hover:bg-${accentColor}-400 text-white cursor-pointer"
|
|
1118
|
+
>
|
|
1119
|
+
{isLoading ? (
|
|
1120
|
+
<svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
|
|
1121
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
1122
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
1123
|
+
</svg>
|
|
1124
|
+
) : "Send"}
|
|
1125
|
+
</button>
|
|
1126
|
+
</form>
|
|
1127
|
+
</footer>
|
|
1128
|
+
</div>
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
`;
|
|
1132
|
+
}
|
|
1133
|
+
function generateFaviconSVG() {
|
|
1134
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
1135
|
+
<rect width="32" height="32" rx="6" fill="#0a0a0a"/>
|
|
1136
|
+
<path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
|
|
1137
|
+
</svg>
|
|
1138
|
+
`;
|
|
1139
|
+
}
|
|
1140
|
+
function generateRobotsTxt() {
|
|
1141
|
+
return `# https://www.robotstxt.org/robotstxt.html
|
|
1142
|
+
User-agent: *
|
|
1143
|
+
Allow: /
|
|
1144
|
+
|
|
1145
|
+
Sitemap: /sitemap.xml
|
|
1146
|
+
`;
|
|
1147
|
+
}
|
|
1148
|
+
function generateWebManifest(projectName) {
|
|
1149
|
+
return JSON.stringify({
|
|
1150
|
+
name: projectName,
|
|
1151
|
+
short_name: projectName,
|
|
1152
|
+
icons: [
|
|
1153
|
+
{ src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
|
|
1154
|
+
],
|
|
1155
|
+
theme_color: "#0a0a0a",
|
|
1156
|
+
background_color: "#0a0a0a",
|
|
1157
|
+
display: "standalone"
|
|
1158
|
+
}, null, 2) + "\n";
|
|
1159
|
+
}
|
|
536
1160
|
// Annotate the CommonJS export names for ESM import in node:
|
|
537
1161
|
0 && (module.exports = {
|
|
538
1162
|
createProject
|