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:
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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 ?? "";
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { chat } from "./chat";
|
||||
export { generateImage } from "./image";
|
||||
export { interpretClick } from "./vision";
|
||||
export type { ChatMessage } from "./chat";
|
||||
@@ -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 ?? "";
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user