Initial commit: AI-driven visual novel scaffold

- Monorepo (pnpm workspace): apps/web + packages/{types,ai-client,engine}
- Next.js 16 web app with three-stage AI orchestration
- Three independently configurable providers: text LLM, image generator, vision model
- Warm minimalist editorial UI design
- One-click Vercel deploy ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuanzonghao
2026-05-09 13:29:58 +08:00
commit cbd95bbea2
45 changed files with 1855 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@dada/ai-client",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dada/types": "workspace:*"
}
}
+41
View File
@@ -0,0 +1,41 @@
import type { ProviderConfig } from "@dada/types";
export type ChatMessage = {
role: "system" | "user" | "assistant";
content: string;
};
export async function chat(
config: ProviderConfig,
messages: ChatMessage[],
opts?: { temperature?: number; responseFormat?: "json_object" | "text" },
): Promise<string> {
const url = `${config.baseUrl.replace(/\/$/, "")}/chat/completions`;
const body: Record<string, unknown> = {
model: config.model,
messages,
temperature: opts?.temperature ?? 0.9,
};
if (opts?.responseFormat === "json_object") {
body.response_format = { type: "json_object" };
}
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Chat API error ${res.status}: ${text}`);
}
const json = (await res.json()) as {
choices: { message: { content: string } }[];
};
return json.choices[0]?.message.content ?? "";
}
+44
View File
@@ -0,0 +1,44 @@
import type { ProviderConfig } from "@dada/types";
export async function generateImage(
config: ProviderConfig,
prompt: string,
opts?: { size?: string; quality?: "low" | "medium" | "high" | "auto" },
): Promise<string> {
const url = `${config.baseUrl.replace(/\/$/, "")}/images/generations`;
const body: Record<string, unknown> = {
model: config.model,
prompt,
size: opts?.size ?? "1024x1536",
quality: opts?.quality ?? "medium",
n: 1,
};
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Image API error ${res.status}: ${text}`);
}
const json = (await res.json()) as {
data: { b64_json?: string; url?: string }[];
};
const item = json.data[0];
if (!item) throw new Error("Image API returned no data");
if (item.b64_json) return item.b64_json;
if (item.url) {
const imgRes = await fetch(item.url);
const buf = await imgRes.arrayBuffer();
return Buffer.from(buf).toString("base64");
}
throw new Error("Image API returned neither b64_json nor url");
}
+4
View File
@@ -0,0 +1,4 @@
export { chat } from "./chat";
export { generateImage } from "./image";
export { interpretClick } from "./vision";
export type { ChatMessage } from "./chat";
+46
View File
@@ -0,0 +1,46 @@
import type { ProviderConfig } from "@dada/types";
export async function interpretClick(
config: ProviderConfig,
imageBase64: string,
prompt: string,
): Promise<string> {
const url = `${config.baseUrl.replace(/\/$/, "")}/chat/completions`;
const body = {
model: config.model,
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{
type: "image_url",
image_url: { url: `data:image/png;base64,${imageBase64}` },
},
],
},
],
temperature: 0.2,
response_format: { type: "json_object" },
};
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Vision API error ${res.status}: ${text}`);
}
const json = (await res.json()) as {
choices: { message: { content: string } }[];
};
return json.choices[0]?.message.content ?? "";
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src/**/*"]
}