create-supyagent-app 0.1.0
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/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/package.json +24 -0
- package/templates/api-route/route.ts.tmpl +29 -0
- package/templates/base/README.md.tmpl +30 -0
- package/templates/base/next.config.ts +5 -0
- package/templates/base/postcss.config.js +8 -0
- package/templates/base/src/app/api/chats/[id]/route.ts +23 -0
- package/templates/base/src/app/api/chats/route.ts +10 -0
- package/templates/base/src/app/chat/[id]/page.tsx +33 -0
- package/templates/base/src/app/chat/page.tsx +21 -0
- package/templates/base/src/app/globals.css +27 -0
- package/templates/base/src/app/layout.tsx +24 -0
- package/templates/base/src/app/page.tsx +5 -0
- package/templates/base/src/components/chat-input.tsx +62 -0
- package/templates/base/src/components/chat-message.tsx +45 -0
- package/templates/base/src/components/chat-sidebar.tsx +76 -0
- package/templates/base/src/components/chat.tsx +72 -0
- package/templates/base/src/lib/prisma.ts +11 -0
- package/templates/base/src/lib/utils.ts +6 -0
- package/templates/base/tailwind.config.ts +15 -0
- package/templates/base/tsconfig.json +23 -0
- package/templates/env/.env.example.tmpl +8 -0
- package/templates/package-json/package.json.tmpl +37 -0
- package/templates/prisma/schema.prisma.tmpl +28 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as p2 from "@clack/prompts";
|
|
5
|
+
import pc2 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/prompts.ts
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
|
|
11
|
+
// src/utils.ts
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
function resolveProjectPath(name) {
|
|
15
|
+
return resolve(process.cwd(), name);
|
|
16
|
+
}
|
|
17
|
+
function projectExists(path) {
|
|
18
|
+
return existsSync(path);
|
|
19
|
+
}
|
|
20
|
+
var AI_PROVIDERS = {
|
|
21
|
+
anthropic: {
|
|
22
|
+
label: "Anthropic (Claude)",
|
|
23
|
+
package: "@ai-sdk/anthropic",
|
|
24
|
+
import: `import { anthropic } from '@ai-sdk/anthropic'`,
|
|
25
|
+
model: `anthropic('claude-sonnet-4-20250514')`,
|
|
26
|
+
envKey: "ANTHROPIC_API_KEY"
|
|
27
|
+
},
|
|
28
|
+
openai: {
|
|
29
|
+
label: "OpenAI (GPT)",
|
|
30
|
+
package: "@ai-sdk/openai",
|
|
31
|
+
import: `import { openai } from '@ai-sdk/openai'`,
|
|
32
|
+
model: `openai('gpt-4o')`,
|
|
33
|
+
envKey: "OPENAI_API_KEY"
|
|
34
|
+
},
|
|
35
|
+
openrouter: {
|
|
36
|
+
label: "OpenRouter (any model)",
|
|
37
|
+
package: "@ai-sdk/openrouter",
|
|
38
|
+
import: `import { openrouter } from '@ai-sdk/openrouter'`,
|
|
39
|
+
model: `openrouter('anthropic/claude-sonnet-4-20250514')`,
|
|
40
|
+
envKey: "OPENROUTER_API_KEY"
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var DB_CONFIGS = {
|
|
44
|
+
sqlite: {
|
|
45
|
+
label: "SQLite (local dev)",
|
|
46
|
+
provider: "sqlite",
|
|
47
|
+
url: "file:./dev.db"
|
|
48
|
+
},
|
|
49
|
+
postgres: {
|
|
50
|
+
label: "PostgreSQL (production)",
|
|
51
|
+
provider: "postgresql",
|
|
52
|
+
url: "postgresql://user:password@localhost:5432/mydb"
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/prompts.ts
|
|
57
|
+
async function runPrompts(argName) {
|
|
58
|
+
p.intro(pc.bgCyan(pc.black(" Create Supyagent App ")));
|
|
59
|
+
const projectName = argName || await p.text({
|
|
60
|
+
message: "Project name",
|
|
61
|
+
placeholder: "my-supyagent-app",
|
|
62
|
+
defaultValue: "my-supyagent-app",
|
|
63
|
+
validate(value) {
|
|
64
|
+
if (!value) return "Project name is required";
|
|
65
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(value)) {
|
|
66
|
+
return "Invalid project name (lowercase, alphanumeric, hyphens, dots)";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(projectName)) {
|
|
71
|
+
p.cancel("Cancelled.");
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const projectPath = resolveProjectPath(projectName);
|
|
75
|
+
if (projectExists(projectPath)) {
|
|
76
|
+
p.cancel(`Directory "${projectName}" already exists.`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const aiProvider = await p.select({
|
|
80
|
+
message: "AI provider",
|
|
81
|
+
options: [
|
|
82
|
+
{ value: "anthropic", label: AI_PROVIDERS.anthropic.label },
|
|
83
|
+
{ value: "openai", label: AI_PROVIDERS.openai.label },
|
|
84
|
+
{ value: "openrouter", label: AI_PROVIDERS.openrouter.label }
|
|
85
|
+
]
|
|
86
|
+
});
|
|
87
|
+
if (p.isCancel(aiProvider)) {
|
|
88
|
+
p.cancel("Cancelled.");
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const database = await p.select({
|
|
92
|
+
message: "Database",
|
|
93
|
+
options: [
|
|
94
|
+
{ value: "sqlite", label: DB_CONFIGS.sqlite.label },
|
|
95
|
+
{ value: "postgres", label: DB_CONFIGS.postgres.label }
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
if (p.isCancel(database)) {
|
|
99
|
+
p.cancel("Cancelled.");
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return { projectName, projectPath, aiProvider, database };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/scaffold.ts
|
|
106
|
+
import { mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
107
|
+
import { join, dirname } from "path";
|
|
108
|
+
import { fileURLToPath } from "url";
|
|
109
|
+
|
|
110
|
+
// src/template.ts
|
|
111
|
+
function applyTemplate(content, variables) {
|
|
112
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
113
|
+
return key in variables ? variables[key] : match;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/scaffold.ts
|
|
118
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
119
|
+
var TEMPLATES_DIR = join(__dirname, "..", "templates");
|
|
120
|
+
function readTemplate(relativePath) {
|
|
121
|
+
return readFileSync(join(TEMPLATES_DIR, relativePath), "utf-8");
|
|
122
|
+
}
|
|
123
|
+
function writeProject(projectPath, relativePath, content) {
|
|
124
|
+
const fullPath = join(projectPath, relativePath);
|
|
125
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
126
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
127
|
+
}
|
|
128
|
+
function scaffoldProject(config) {
|
|
129
|
+
const { projectPath, projectName, aiProvider, database } = config;
|
|
130
|
+
const ai = AI_PROVIDERS[aiProvider];
|
|
131
|
+
const db = DB_CONFIGS[database];
|
|
132
|
+
const vars = {
|
|
133
|
+
projectName,
|
|
134
|
+
aiProviderPackage: ai.package,
|
|
135
|
+
aiProviderImport: ai.import,
|
|
136
|
+
aiModel: ai.model,
|
|
137
|
+
aiProviderEnvKey: ai.envKey,
|
|
138
|
+
dbProvider: db.provider,
|
|
139
|
+
dbUrl: db.url
|
|
140
|
+
};
|
|
141
|
+
mkdirSync(projectPath, { recursive: true });
|
|
142
|
+
writeProject(projectPath, "next.config.ts", readTemplate("base/next.config.ts"));
|
|
143
|
+
writeProject(projectPath, "tsconfig.json", readTemplate("base/tsconfig.json"));
|
|
144
|
+
writeProject(projectPath, "tailwind.config.ts", readTemplate("base/tailwind.config.ts"));
|
|
145
|
+
writeProject(projectPath, "postcss.config.js", readTemplate("base/postcss.config.js"));
|
|
146
|
+
writeProject(projectPath, ".gitignore", readTemplate("base/.gitignore"));
|
|
147
|
+
writeProject(projectPath, "README.md", applyTemplate(readTemplate("base/README.md.tmpl"), vars));
|
|
148
|
+
writeProject(projectPath, "src/app/layout.tsx", readTemplate("base/src/app/layout.tsx"));
|
|
149
|
+
writeProject(projectPath, "src/app/page.tsx", readTemplate("base/src/app/page.tsx"));
|
|
150
|
+
writeProject(projectPath, "src/app/globals.css", readTemplate("base/src/app/globals.css"));
|
|
151
|
+
writeProject(projectPath, "src/app/chat/page.tsx", readTemplate("base/src/app/chat/page.tsx"));
|
|
152
|
+
writeProject(projectPath, "src/app/chat/[id]/page.tsx", readTemplate("base/src/app/chat/[id]/page.tsx"));
|
|
153
|
+
writeProject(
|
|
154
|
+
projectPath,
|
|
155
|
+
"src/app/api/chat/route.ts",
|
|
156
|
+
applyTemplate(readTemplate("api-route/route.ts.tmpl"), vars)
|
|
157
|
+
);
|
|
158
|
+
writeProject(projectPath, "src/app/api/chats/route.ts", readTemplate("base/src/app/api/chats/route.ts"));
|
|
159
|
+
writeProject(projectPath, "src/app/api/chats/[id]/route.ts", readTemplate("base/src/app/api/chats/[id]/route.ts"));
|
|
160
|
+
writeProject(projectPath, "src/components/chat.tsx", readTemplate("base/src/components/chat.tsx"));
|
|
161
|
+
writeProject(projectPath, "src/components/chat-sidebar.tsx", readTemplate("base/src/components/chat-sidebar.tsx"));
|
|
162
|
+
writeProject(projectPath, "src/components/chat-message.tsx", readTemplate("base/src/components/chat-message.tsx"));
|
|
163
|
+
writeProject(projectPath, "src/components/chat-input.tsx", readTemplate("base/src/components/chat-input.tsx"));
|
|
164
|
+
writeProject(projectPath, "src/lib/utils.ts", readTemplate("base/src/lib/utils.ts"));
|
|
165
|
+
writeProject(projectPath, "src/lib/prisma.ts", readTemplate("base/src/lib/prisma.ts"));
|
|
166
|
+
writeProject(
|
|
167
|
+
projectPath,
|
|
168
|
+
"prisma/schema.prisma",
|
|
169
|
+
applyTemplate(readTemplate("prisma/schema.prisma.tmpl"), vars)
|
|
170
|
+
);
|
|
171
|
+
writeProject(
|
|
172
|
+
projectPath,
|
|
173
|
+
".env.example",
|
|
174
|
+
applyTemplate(readTemplate("env/.env.example.tmpl"), vars)
|
|
175
|
+
);
|
|
176
|
+
writeProject(
|
|
177
|
+
projectPath,
|
|
178
|
+
"package.json",
|
|
179
|
+
applyTemplate(readTemplate("package-json/package.json.tmpl"), vars)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/post-install.ts
|
|
184
|
+
import { detectPackageManager, installDependencies } from "nypm";
|
|
185
|
+
async function installDeps(projectPath) {
|
|
186
|
+
const pm = await detectPackageManager(projectPath);
|
|
187
|
+
await installDependencies({
|
|
188
|
+
cwd: projectPath,
|
|
189
|
+
packageManager: pm?.name
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/index.ts
|
|
194
|
+
async function main() {
|
|
195
|
+
const argName = process.argv[2];
|
|
196
|
+
const config = await runPrompts(argName);
|
|
197
|
+
if (!config) {
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const s = p2.spinner();
|
|
201
|
+
s.start("Scaffolding project...");
|
|
202
|
+
scaffoldProject(config);
|
|
203
|
+
s.stop("Scaffolded project");
|
|
204
|
+
s.start("Installing dependencies...");
|
|
205
|
+
try {
|
|
206
|
+
await installDeps(config.projectPath);
|
|
207
|
+
s.stop("Installed dependencies");
|
|
208
|
+
} catch {
|
|
209
|
+
s.stop("Failed to install dependencies \u2014 run install manually");
|
|
210
|
+
}
|
|
211
|
+
p2.note(
|
|
212
|
+
[
|
|
213
|
+
`cd ${config.projectName}`,
|
|
214
|
+
`cp .env.example .env.local ${pc2.dim("# Add your API keys")}`,
|
|
215
|
+
`pnpm db:setup ${pc2.dim("# Initialize database")}`,
|
|
216
|
+
`pnpm dev ${pc2.dim("# Start development server")}`
|
|
217
|
+
].join("\n"),
|
|
218
|
+
"Next steps"
|
|
219
|
+
);
|
|
220
|
+
p2.outro(pc2.green("Done!"));
|
|
221
|
+
}
|
|
222
|
+
main().catch(console.error);
|
|
223
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/prompts.ts","../src/utils.ts","../src/scaffold.ts","../src/template.ts","../src/post-install.ts"],"sourcesContent":["import * as p from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport { runPrompts } from \"./prompts.js\";\nimport { scaffoldProject } from \"./scaffold.js\";\nimport { installDeps } from \"./post-install.js\";\n\nasync function main() {\n const argName = process.argv[2];\n const config = await runPrompts(argName);\n\n if (!config) {\n process.exit(1);\n }\n\n const s = p.spinner();\n\n s.start(\"Scaffolding project...\");\n scaffoldProject(config);\n s.stop(\"Scaffolded project\");\n\n s.start(\"Installing dependencies...\");\n try {\n await installDeps(config.projectPath);\n s.stop(\"Installed dependencies\");\n } catch {\n s.stop(\"Failed to install dependencies — run install manually\");\n }\n\n p.note(\n [\n `cd ${config.projectName}`,\n `cp .env.example .env.local ${pc.dim(\"# Add your API keys\")}`,\n `pnpm db:setup ${pc.dim(\"# Initialize database\")}`,\n `pnpm dev ${pc.dim(\"# Start development server\")}`,\n ].join(\"\\n\"),\n \"Next steps\"\n );\n\n p.outro(pc.green(\"Done!\"));\n}\n\nmain().catch(console.error);\n","import * as p from \"@clack/prompts\";\nimport pc from \"picocolors\";\nimport type { ProjectConfig } from \"./utils.js\";\nimport { AI_PROVIDERS, DB_CONFIGS, resolveProjectPath, projectExists } from \"./utils.js\";\n\nexport async function runPrompts(argName?: string): Promise<ProjectConfig | null> {\n p.intro(pc.bgCyan(pc.black(\" Create Supyagent App \")));\n\n const projectName = argName || await p.text({\n message: \"Project name\",\n placeholder: \"my-supyagent-app\",\n defaultValue: \"my-supyagent-app\",\n validate(value) {\n if (!value) return \"Project name is required\";\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(value)) {\n return \"Invalid project name (lowercase, alphanumeric, hyphens, dots)\";\n }\n },\n }) as string;\n\n if (p.isCancel(projectName)) {\n p.cancel(\"Cancelled.\");\n return null;\n }\n\n const projectPath = resolveProjectPath(projectName);\n\n if (projectExists(projectPath)) {\n p.cancel(`Directory \"${projectName}\" already exists.`);\n return null;\n }\n\n const aiProvider = await p.select({\n message: \"AI provider\",\n options: [\n { value: \"anthropic\", label: AI_PROVIDERS.anthropic.label },\n { value: \"openai\", label: AI_PROVIDERS.openai.label },\n { value: \"openrouter\", label: AI_PROVIDERS.openrouter.label },\n ],\n }) as \"anthropic\" | \"openai\" | \"openrouter\";\n\n if (p.isCancel(aiProvider)) {\n p.cancel(\"Cancelled.\");\n return null;\n }\n\n const database = await p.select({\n message: \"Database\",\n options: [\n { value: \"sqlite\", label: DB_CONFIGS.sqlite.label },\n { value: \"postgres\", label: DB_CONFIGS.postgres.label },\n ],\n }) as \"sqlite\" | \"postgres\";\n\n if (p.isCancel(database)) {\n p.cancel(\"Cancelled.\");\n return null;\n }\n\n return { projectName, projectPath, aiProvider, database };\n}\n","import { existsSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport function resolveProjectPath(name: string): string {\n return resolve(process.cwd(), name);\n}\n\nexport function projectExists(path: string): boolean {\n return existsSync(path);\n}\n\nexport interface ProjectConfig {\n projectName: string;\n projectPath: string;\n aiProvider: \"anthropic\" | \"openai\" | \"openrouter\";\n database: \"sqlite\" | \"postgres\";\n}\n\nexport const AI_PROVIDERS = {\n anthropic: {\n label: \"Anthropic (Claude)\",\n package: \"@ai-sdk/anthropic\",\n import: `import { anthropic } from '@ai-sdk/anthropic'`,\n model: `anthropic('claude-sonnet-4-20250514')`,\n envKey: \"ANTHROPIC_API_KEY\",\n },\n openai: {\n label: \"OpenAI (GPT)\",\n package: \"@ai-sdk/openai\",\n import: `import { openai } from '@ai-sdk/openai'`,\n model: `openai('gpt-4o')`,\n envKey: \"OPENAI_API_KEY\",\n },\n openrouter: {\n label: \"OpenRouter (any model)\",\n package: \"@ai-sdk/openrouter\",\n import: `import { openrouter } from '@ai-sdk/openrouter'`,\n model: `openrouter('anthropic/claude-sonnet-4-20250514')`,\n envKey: \"OPENROUTER_API_KEY\",\n },\n} as const;\n\nexport const DB_CONFIGS = {\n sqlite: {\n label: \"SQLite (local dev)\",\n provider: \"sqlite\",\n url: \"file:./dev.db\",\n },\n postgres: {\n label: \"PostgreSQL (production)\",\n provider: \"postgresql\",\n url: \"postgresql://user:password@localhost:5432/mydb\",\n },\n} as const;\n","import { mkdirSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyTemplate } from \"./template.js\";\nimport { AI_PROVIDERS, DB_CONFIGS, type ProjectConfig } from \"./utils.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst TEMPLATES_DIR = join(__dirname, \"..\", \"templates\");\n\nfunction readTemplate(relativePath: string): string {\n return readFileSync(join(TEMPLATES_DIR, relativePath), \"utf-8\");\n}\n\nfunction writeProject(projectPath: string, relativePath: string, content: string): void {\n const fullPath = join(projectPath, relativePath);\n mkdirSync(dirname(fullPath), { recursive: true });\n writeFileSync(fullPath, content, \"utf-8\");\n}\n\nexport function scaffoldProject(config: ProjectConfig): void {\n const { projectPath, projectName, aiProvider, database } = config;\n const ai = AI_PROVIDERS[aiProvider];\n const db = DB_CONFIGS[database];\n\n const vars: Record<string, string> = {\n projectName,\n aiProviderPackage: ai.package,\n aiProviderImport: ai.import,\n aiModel: ai.model,\n aiProviderEnvKey: ai.envKey,\n dbProvider: db.provider,\n dbUrl: db.url,\n };\n\n mkdirSync(projectPath, { recursive: true });\n\n // ── Base files ──\n writeProject(projectPath, \"next.config.ts\", readTemplate(\"base/next.config.ts\"));\n writeProject(projectPath, \"tsconfig.json\", readTemplate(\"base/tsconfig.json\"));\n writeProject(projectPath, \"tailwind.config.ts\", readTemplate(\"base/tailwind.config.ts\"));\n writeProject(projectPath, \"postcss.config.js\", readTemplate(\"base/postcss.config.js\"));\n writeProject(projectPath, \".gitignore\", readTemplate(\"base/.gitignore\"));\n writeProject(projectPath, \"README.md\", applyTemplate(readTemplate(\"base/README.md.tmpl\"), vars));\n\n // ── Source files ──\n writeProject(projectPath, \"src/app/layout.tsx\", readTemplate(\"base/src/app/layout.tsx\"));\n writeProject(projectPath, \"src/app/page.tsx\", readTemplate(\"base/src/app/page.tsx\"));\n writeProject(projectPath, \"src/app/globals.css\", readTemplate(\"base/src/app/globals.css\"));\n writeProject(projectPath, \"src/app/chat/page.tsx\", readTemplate(\"base/src/app/chat/page.tsx\"));\n writeProject(projectPath, \"src/app/chat/[id]/page.tsx\", readTemplate(\"base/src/app/chat/[id]/page.tsx\"));\n\n // API routes\n writeProject(\n projectPath,\n \"src/app/api/chat/route.ts\",\n applyTemplate(readTemplate(\"api-route/route.ts.tmpl\"), vars)\n );\n writeProject(projectPath, \"src/app/api/chats/route.ts\", readTemplate(\"base/src/app/api/chats/route.ts\"));\n writeProject(projectPath, \"src/app/api/chats/[id]/route.ts\", readTemplate(\"base/src/app/api/chats/[id]/route.ts\"));\n\n // Components\n writeProject(projectPath, \"src/components/chat.tsx\", readTemplate(\"base/src/components/chat.tsx\"));\n writeProject(projectPath, \"src/components/chat-sidebar.tsx\", readTemplate(\"base/src/components/chat-sidebar.tsx\"));\n writeProject(projectPath, \"src/components/chat-message.tsx\", readTemplate(\"base/src/components/chat-message.tsx\"));\n writeProject(projectPath, \"src/components/chat-input.tsx\", readTemplate(\"base/src/components/chat-input.tsx\"));\n\n // Lib\n writeProject(projectPath, \"src/lib/utils.ts\", readTemplate(\"base/src/lib/utils.ts\"));\n writeProject(projectPath, \"src/lib/prisma.ts\", readTemplate(\"base/src/lib/prisma.ts\"));\n\n // ── Prisma schema ──\n writeProject(\n projectPath,\n \"prisma/schema.prisma\",\n applyTemplate(readTemplate(\"prisma/schema.prisma.tmpl\"), vars)\n );\n\n // ── Env example ──\n writeProject(\n projectPath,\n \".env.example\",\n applyTemplate(readTemplate(\"env/.env.example.tmpl\"), vars)\n );\n\n // ── package.json ──\n writeProject(\n projectPath,\n \"package.json\",\n applyTemplate(readTemplate(\"package-json/package.json.tmpl\"), vars)\n );\n}\n","/**\n * Replace {{variable}} placeholders in template content.\n */\nexport function applyTemplate(\n content: string,\n variables: Record<string, string>\n): string {\n return content.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key) => {\n return key in variables ? variables[key] : match;\n });\n}\n","import { detectPackageManager, installDependencies } from \"nypm\";\n\nexport async function installDeps(projectPath: string): Promise<void> {\n const pm = await detectPackageManager(projectPath);\n await installDependencies({\n cwd: projectPath,\n packageManager: pm?.name,\n });\n}\n"],"mappings":"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","names":["p","pc","pc"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-supyagent-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a supyagent-powered chatbot app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-supyagent-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist", "templates"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"clean": "rm -rf dist"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@clack/prompts": "^0.8.0",
|
|
16
|
+
"nypm": "^0.4.0",
|
|
17
|
+
"picocolors": "^1.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"tsup": "^8.3.0",
|
|
21
|
+
"typescript": "^5.7.0"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
|
|
2
|
+
{{aiProviderImport}};
|
|
3
|
+
import { supyagent } from '@supyagent/sdk';
|
|
4
|
+
import { createPrismaAdapter } from '@supyagent/sdk/prisma';
|
|
5
|
+
import { prisma } from '@/lib/prisma';
|
|
6
|
+
|
|
7
|
+
const client = supyagent({ apiKey: process.env.SUPYAGENT_API_KEY! });
|
|
8
|
+
const adapter = createPrismaAdapter(prisma);
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json();
|
|
12
|
+
|
|
13
|
+
await adapter.saveChat(chatId, messages);
|
|
14
|
+
|
|
15
|
+
const tools = await client.tools({ cache: 300 });
|
|
16
|
+
|
|
17
|
+
const result = streamText({
|
|
18
|
+
model: {{aiModel}},
|
|
19
|
+
system: 'You are a helpful assistant. Use your tools when asked to interact with connected services.',
|
|
20
|
+
messages: convertToModelMessages(messages),
|
|
21
|
+
tools,
|
|
22
|
+
maxSteps: 5,
|
|
23
|
+
onFinish: async ({ response }) => {
|
|
24
|
+
await adapter.saveChat(chatId, [...messages, ...response.messages as unknown as UIMessage[]]);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return result.toUIMessageStreamResponse();
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A chatbot powered by [Supyagent](https://supyagent.com) with connected integrations.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env.local # Add your API keys
|
|
9
|
+
pnpm db:setup # Initialize database
|
|
10
|
+
pnpm dev # Start development server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Getting API Keys
|
|
14
|
+
|
|
15
|
+
1. **Supyagent API Key**: Sign up at [app.supyagent.com](https://app.supyagent.com) and create an API key
|
|
16
|
+
2. **AI Provider Key**: Get your key from your AI provider's dashboard
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- Chat with an AI assistant that can use your connected integrations
|
|
21
|
+
- Persistent chat history
|
|
22
|
+
- Rich tool result visualization (emails, calendar events, Slack messages, etc.)
|
|
23
|
+
|
|
24
|
+
## Stack
|
|
25
|
+
|
|
26
|
+
- [Next.js](https://nextjs.org) — React framework
|
|
27
|
+
- [Vercel AI SDK](https://sdk.vercel.ai) — AI integration
|
|
28
|
+
- [Supyagent](https://supyagent.com) — Third-party service integrations
|
|
29
|
+
- [Prisma](https://prisma.io) — Database ORM
|
|
30
|
+
- [Tailwind CSS](https://tailwindcss.com) — Styling
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { prisma } from "@/lib/prisma";
|
|
2
|
+
import { createPrismaAdapter } from "@supyagent/sdk/prisma";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
|
|
5
|
+
const adapter = createPrismaAdapter(prisma);
|
|
6
|
+
|
|
7
|
+
export async function GET(
|
|
8
|
+
_req: Request,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const messages = await adapter.loadChat(id);
|
|
13
|
+
return NextResponse.json({ messages });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function DELETE(
|
|
17
|
+
_req: Request,
|
|
18
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
19
|
+
) {
|
|
20
|
+
const { id } = await params;
|
|
21
|
+
await adapter.deleteChat(id);
|
|
22
|
+
return NextResponse.json({ ok: true });
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { prisma } from "@/lib/prisma";
|
|
2
|
+
import { createPrismaAdapter } from "@supyagent/sdk/prisma";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
|
|
5
|
+
const adapter = createPrismaAdapter(prisma);
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const chats = await adapter.listChats();
|
|
9
|
+
return NextResponse.json({ chats });
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { use, useEffect, useState } from "react";
|
|
4
|
+
import { Chat } from "@/components/chat";
|
|
5
|
+
|
|
6
|
+
interface ChatMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
parts: Array<{ type: string; [key: string]: unknown }>;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function ChatPage({ params }: { params: Promise<{ id: string }> }) {
|
|
14
|
+
const { id } = use(params);
|
|
15
|
+
const [initialMessages, setInitialMessages] = useState<ChatMessage[] | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
fetch(`/api/chats/${id}`)
|
|
19
|
+
.then((res) => (res.ok ? res.json() : { messages: [] }))
|
|
20
|
+
.then((data) => setInitialMessages(data.messages || []))
|
|
21
|
+
.catch(() => setInitialMessages([]));
|
|
22
|
+
}, [id]);
|
|
23
|
+
|
|
24
|
+
if (initialMessages === null) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex h-screen items-center justify-center">
|
|
27
|
+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-zinc-700 border-t-zinc-300" />
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return <Chat chatId={id} initialMessages={initialMessages} />;
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useEffect, useId } from "react";
|
|
5
|
+
|
|
6
|
+
export default function NewChatPage() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const id = useId().replace(/:/g, "");
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
// Generate a unique chat ID and redirect
|
|
12
|
+
const chatId = `chat_${id}_${Date.now().toString(36)}`;
|
|
13
|
+
router.replace(`/chat/${chatId}`);
|
|
14
|
+
}, [router, id]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex h-screen items-center justify-center">
|
|
18
|
+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-zinc-700 border-t-zinc-300" />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
body {
|
|
7
|
+
@apply bg-zinc-950 text-zinc-100;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Scrollbar styling */
|
|
12
|
+
::-webkit-scrollbar {
|
|
13
|
+
width: 6px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
::-webkit-scrollbar-track {
|
|
17
|
+
background: transparent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
::-webkit-scrollbar-thumb {
|
|
21
|
+
background: rgb(63 63 70);
|
|
22
|
+
border-radius: 3px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
::-webkit-scrollbar-thumb:hover {
|
|
26
|
+
background: rgb(82 82 91);
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: "Supyagent Chat",
|
|
9
|
+
description: "AI chat with connected integrations",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className="dark">
|
|
19
|
+
<body className={`${inter.className} bg-zinc-950 text-zinc-100 antialiased`}>
|
|
20
|
+
{children}
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowUp, Square } from "lucide-react";
|
|
4
|
+
import type { ChangeEvent, FormEvent } from "react";
|
|
5
|
+
|
|
6
|
+
interface ChatInputProps {
|
|
7
|
+
input: string;
|
|
8
|
+
handleInputChange: (e: ChangeEvent<HTMLTextAreaElement>) => void;
|
|
9
|
+
handleSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ChatInput({
|
|
15
|
+
input,
|
|
16
|
+
handleInputChange,
|
|
17
|
+
handleSubmit,
|
|
18
|
+
isLoading,
|
|
19
|
+
stop,
|
|
20
|
+
}: ChatInputProps) {
|
|
21
|
+
return (
|
|
22
|
+
<form
|
|
23
|
+
onSubmit={handleSubmit}
|
|
24
|
+
className="relative flex items-end rounded-xl border border-zinc-800 bg-zinc-900 focus-within:border-zinc-700"
|
|
25
|
+
>
|
|
26
|
+
<textarea
|
|
27
|
+
value={input}
|
|
28
|
+
onChange={handleInputChange}
|
|
29
|
+
placeholder="Send a message..."
|
|
30
|
+
rows={1}
|
|
31
|
+
className="flex-1 resize-none bg-transparent px-4 py-3 text-sm text-zinc-200 placeholder-zinc-500 outline-none"
|
|
32
|
+
onKeyDown={(e) => {
|
|
33
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (input.trim()) {
|
|
36
|
+
handleSubmit(e as unknown as FormEvent<HTMLFormElement>);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
<div className="p-2">
|
|
42
|
+
{isLoading ? (
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={stop}
|
|
46
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors"
|
|
47
|
+
>
|
|
48
|
+
<Square className="h-3.5 w-3.5" />
|
|
49
|
+
</button>
|
|
50
|
+
) : (
|
|
51
|
+
<button
|
|
52
|
+
type="submit"
|
|
53
|
+
disabled={!input.trim()}
|
|
54
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-200 text-zinc-900 disabled:opacity-30 disabled:cursor-not-allowed hover:bg-white transition-colors"
|
|
55
|
+
>
|
|
56
|
+
<ArrowUp className="h-4 w-4" />
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</form>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { UIMessage } from "ai";
|
|
4
|
+
import { SupyagentToolCall, SupyagentToolResult } from "@supyagent/sdk/react";
|
|
5
|
+
|
|
6
|
+
interface ChatMessageProps {
|
|
7
|
+
message: UIMessage;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ChatMessage({ message }: ChatMessageProps) {
|
|
11
|
+
const isUser = message.role === "user";
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
|
|
15
|
+
<div
|
|
16
|
+
className={`max-w-[85%] space-y-2 ${
|
|
17
|
+
isUser
|
|
18
|
+
? "rounded-2xl rounded-br-md bg-zinc-800 px-4 py-2.5"
|
|
19
|
+
: ""
|
|
20
|
+
}`}
|
|
21
|
+
>
|
|
22
|
+
{message.parts.map((part, i) => {
|
|
23
|
+
if (part.type === "text") {
|
|
24
|
+
return (
|
|
25
|
+
<p key={i} className="text-sm text-zinc-200 whitespace-pre-wrap">
|
|
26
|
+
{part.text}
|
|
27
|
+
</p>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (part.type === "tool-invocation") {
|
|
32
|
+
return (
|
|
33
|
+
<div key={i} className="space-y-2">
|
|
34
|
+
<SupyagentToolCall part={part as Record<string, unknown>} />
|
|
35
|
+
<SupyagentToolResult part={part as Record<string, unknown>} />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { MessageSquare, Plus, Trash2 } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
interface ChatSummary {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ChatSidebarProps {
|
|
15
|
+
currentChatId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ChatSidebar({ currentChatId }: ChatSidebarProps) {
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const [chats, setChats] = useState<ChatSummary[]>([]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
fetch("/api/chats")
|
|
24
|
+
.then((res) => res.json())
|
|
25
|
+
.then((data) => setChats(data.chats || []))
|
|
26
|
+
.catch(() => {});
|
|
27
|
+
}, [currentChatId]);
|
|
28
|
+
|
|
29
|
+
const deleteChat = async (id: string) => {
|
|
30
|
+
await fetch(`/api/chats/${id}`, { method: "DELETE" });
|
|
31
|
+
setChats((prev) => prev.filter((c) => c.id !== id));
|
|
32
|
+
if (id === currentChatId) {
|
|
33
|
+
router.push("/chat");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex h-full w-64 flex-col border-r border-zinc-800 bg-zinc-900/50">
|
|
39
|
+
<div className="flex items-center justify-between p-4">
|
|
40
|
+
<span className="text-sm font-medium text-zinc-300">Chats</span>
|
|
41
|
+
<button
|
|
42
|
+
onClick={() => router.push("/chat")}
|
|
43
|
+
className="rounded-md p-1.5 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200 transition-colors"
|
|
44
|
+
>
|
|
45
|
+
<Plus className="h-4 w-4" />
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="flex-1 overflow-y-auto px-2">
|
|
50
|
+
{chats.map((chat) => (
|
|
51
|
+
<div
|
|
52
|
+
key={chat.id}
|
|
53
|
+
className={`group mb-1 flex items-center rounded-lg px-3 py-2 cursor-pointer transition-colors ${
|
|
54
|
+
chat.id === currentChatId
|
|
55
|
+
? "bg-zinc-800 text-zinc-200"
|
|
56
|
+
: "text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-300"
|
|
57
|
+
}`}
|
|
58
|
+
onClick={() => router.push(`/chat/${chat.id}`)}
|
|
59
|
+
>
|
|
60
|
+
<MessageSquare className="mr-2 h-3.5 w-3.5 shrink-0" />
|
|
61
|
+
<span className="flex-1 truncate text-sm">{chat.title}</span>
|
|
62
|
+
<button
|
|
63
|
+
onClick={(e) => {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
deleteChat(chat.id);
|
|
66
|
+
}}
|
|
67
|
+
className="ml-1 hidden rounded p-1 text-zinc-500 hover:bg-zinc-700 hover:text-zinc-300 group-hover:block"
|
|
68
|
+
>
|
|
69
|
+
<Trash2 className="h-3 w-3" />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useChat } from "@ai-sdk/react";
|
|
4
|
+
import { ChatMessage } from "./chat-message";
|
|
5
|
+
import { ChatInput } from "./chat-input";
|
|
6
|
+
import { ChatSidebar } from "./chat-sidebar";
|
|
7
|
+
import { useRef, useEffect } from "react";
|
|
8
|
+
|
|
9
|
+
interface ChatProps {
|
|
10
|
+
chatId: string;
|
|
11
|
+
initialMessages: Parameters<typeof useChat>[0] extends { initialMessages?: infer M } ? M : never;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Chat({ chatId, initialMessages }: ChatProps) {
|
|
15
|
+
const { messages, input, handleInputChange, handleSubmit, isLoading, stop } =
|
|
16
|
+
useChat({
|
|
17
|
+
api: "/api/chat",
|
|
18
|
+
body: { chatId },
|
|
19
|
+
initialMessages: initialMessages as Parameters<typeof useChat>[0]["initialMessages"],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (scrollRef.current) {
|
|
26
|
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
27
|
+
}
|
|
28
|
+
}, [messages]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex h-screen">
|
|
32
|
+
<ChatSidebar currentChatId={chatId} />
|
|
33
|
+
|
|
34
|
+
<div className="flex flex-1 flex-col">
|
|
35
|
+
<div
|
|
36
|
+
ref={scrollRef}
|
|
37
|
+
className="flex-1 overflow-y-auto px-4 py-6"
|
|
38
|
+
>
|
|
39
|
+
<div className="mx-auto max-w-3xl space-y-6">
|
|
40
|
+
{messages.length === 0 && (
|
|
41
|
+
<div className="flex h-full min-h-[60vh] items-center justify-center">
|
|
42
|
+
<div className="text-center">
|
|
43
|
+
<h1 className="text-2xl font-semibold text-zinc-200">
|
|
44
|
+
Supyagent Chat
|
|
45
|
+
</h1>
|
|
46
|
+
<p className="mt-2 text-sm text-zinc-500">
|
|
47
|
+
Ask me anything — I can use your connected integrations.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
{messages.map((message) => (
|
|
53
|
+
<ChatMessage key={message.id} message={message} />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="border-t border-zinc-800 px-4 py-4">
|
|
59
|
+
<div className="mx-auto max-w-3xl">
|
|
60
|
+
<ChatInput
|
|
61
|
+
input={input}
|
|
62
|
+
handleInputChange={handleInputChange}
|
|
63
|
+
handleSubmit={handleSubmit}
|
|
64
|
+
isLoading={isLoading}
|
|
65
|
+
stop={stop}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
|
|
3
|
+
const globalForPrisma = globalThis as unknown as {
|
|
4
|
+
prisma: PrismaClient | undefined;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
|
8
|
+
|
|
9
|
+
if (process.env.NODE_ENV !== "production") {
|
|
10
|
+
globalForPrisma.prisma = prisma;
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
content: [
|
|
5
|
+
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
|
6
|
+
"./node_modules/@supyagent/sdk/dist/**/*.{js,mjs}",
|
|
7
|
+
],
|
|
8
|
+
darkMode: "class",
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {},
|
|
11
|
+
},
|
|
12
|
+
plugins: [],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default config;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint",
|
|
10
|
+
"db:generate": "prisma generate",
|
|
11
|
+
"db:push": "prisma db push",
|
|
12
|
+
"db:setup": "prisma generate && prisma db push",
|
|
13
|
+
"db:studio": "prisma studio"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@ai-sdk/react": "^1.1.0",
|
|
17
|
+
"{{aiProviderPackage}}": "^1.1.0",
|
|
18
|
+
"@prisma/client": "^6.2.0",
|
|
19
|
+
"@supyagent/sdk": "^0.1.0",
|
|
20
|
+
"ai": "^4.1.0",
|
|
21
|
+
"clsx": "^2.1.0",
|
|
22
|
+
"lucide-react": "^0.468.0",
|
|
23
|
+
"next": "^15.1.0",
|
|
24
|
+
"react": "^19.0.0",
|
|
25
|
+
"react-dom": "^19.0.0",
|
|
26
|
+
"tailwind-merge": "^2.6.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"postcss": "^8.4.0",
|
|
33
|
+
"prisma": "^6.2.0",
|
|
34
|
+
"tailwindcss": "^3.4.0",
|
|
35
|
+
"typescript": "^5.7.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "{{dbProvider}}"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model Chat {
|
|
11
|
+
id String @id @default(cuid())
|
|
12
|
+
title String @default("New Chat")
|
|
13
|
+
createdAt DateTime @default(now())
|
|
14
|
+
updatedAt DateTime @updatedAt
|
|
15
|
+
messages Message[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
model Message {
|
|
19
|
+
id String @id @default(cuid())
|
|
20
|
+
chatId String
|
|
21
|
+
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
|
22
|
+
role String
|
|
23
|
+
parts String
|
|
24
|
+
metadata String?
|
|
25
|
+
createdAt DateTime @default(now())
|
|
26
|
+
|
|
27
|
+
@@index([chatId])
|
|
28
|
+
}
|