Skip to content

Commit c4e4543

Browse files
committed
feat(image): implement OpenAI image generation; tighten setup scripts; restore strict linting
1 parent 1ba23ea commit c4e4543

File tree

6 files changed

+81
-87
lines changed

6 files changed

+81
-87
lines changed

eslint.config.js

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,30 +43,7 @@ export default tseslint.config(
4343
},
4444
},
4545
{
46-
// Relax strict rules for chat components/hooks and streaming route to allow pragmatic typing
47-
files: [
48-
"src/app/(general)/_components/chat/**/*.ts",
49-
"src/app/(general)/_components/chat/**/*.tsx",
50-
"src/app/(general)/_contexts/**/*.ts",
51-
"src/app/(general)/_contexts/**/*.tsx",
52-
"src/app/(general)/_hooks/**/*.ts",
53-
"src/app/(general)/_hooks/**/*.tsx",
54-
"src/app/api/chat/route.ts",
55-
"src/server/api/routers/sync.ts",
56-
"src/ai/image/**/*.ts",
57-
],
58-
rules: {
59-
"@typescript-eslint/no-explicit-any": "off",
60-
"@typescript-eslint/no-unsafe-assignment": "off",
61-
"@typescript-eslint/no-unsafe-member-access": "off",
62-
"@typescript-eslint/no-unsafe-return": "off",
63-
"@typescript-eslint/no-unsafe-call": "off",
64-
"@typescript-eslint/no-unnecessary-type-assertion": "off",
65-
"@typescript-eslint/prefer-optional-chain": "off",
66-
"@typescript-eslint/no-floating-promises": "off",
67-
"@typescript-eslint/no-misused-promises": "off",
68-
"react-hooks/exhaustive-deps": "warn",
69-
},
46+
// Removed earlier broad rule relaxations to preserve full type safety.
7047
},
7148
{
7249
linterOptions: {

setup/steps/3_docker.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import {
1515
// Start Docker Compose services
1616
export function startDockerServices(): void {
1717
try {
18-
// Allow skipping docker setup via env or CI
19-
if (process.env.SKIP_DOCKER === "1" || process.env.SKIP_DOCKER === "true" || process.env.CI === "true") {
20-
logInfo("Skipping Docker services setup (SKIP_DOCKER/CI detected)");
21-
return;
22-
}
18+
// Opinionated: do not allow skipping; environment is expected to support required services.
2319

2420
// Check if Docker is available
2521
const dockerCommand = checkDocker();
@@ -42,9 +38,7 @@ export function startDockerServices(): void {
4238
}
4339

4440
if (!dockerDaemonRunning(dockerCommand)) {
45-
logWarning(
46-
`${dockerCommand} daemon is not running. Skipping Docker services. Start the daemon and rerun if needed.`,
47-
);
41+
logError(`${dockerCommand} daemon is not running. Start it and rerun setup.`);
4842
return;
4943
}
5044

setup/steps/4_database.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,6 @@ import {
1313

1414
export function runMigrations(): void {
1515
try {
16-
// Allow skipping via env flags or CI
17-
if (
18-
process.env.SKIP_DB === "1" ||
19-
process.env.SKIP_DB === "true" ||
20-
// support both SKIP_DB_MIGRATIONS and SKIP_DB_MIGRATE spellings
21-
process.env.SKIP_DB_MIGRATIONS === "1" ||
22-
process.env.SKIP_DB_MIGRATIONS === "true" ||
23-
process.env.SKIP_DB_MIGRATE === "1" ||
24-
process.env.SKIP_DB_MIGRATE === "true" ||
25-
process.env.CI === "true"
26-
) {
27-
logInfo(
28-
"Skipping database migrations (SKIP_DB/SKIP_DB_MIGRATIONS/SKIP_DB_MIGRATE/CI detected)",
29-
);
30-
return;
31-
}
3216

3317
// If no local env file, warn and skip to keep setup non-blocking
3418
const envPath = join(getProjectRoot(), ".env.local");
@@ -38,20 +22,28 @@ export function runMigrations(): void {
3822
}
3923

4024
// If DATABASE_URL isn't configured, skip to avoid blocking in ephemeral/dev envs
25+
let dbUrl: string | undefined;
4126
try {
4227
const envContents = readFileSync(envPath, "utf8");
4328
const dbUrlLine = envContents
4429
.split(/\r?\n/)
4530
.find((l) => /^\s*DATABASE_URL\s*=/.test(l) && !/^\s*#/.test(l));
46-
const dbUrl = dbUrlLine?.split("=").slice(1).join("=").trim();
47-
if (!dbUrl) {
48-
logWarning(
49-
"DATABASE_URL missing in .env.local. Skipping database migrations. Configure it to enable Prisma.",
50-
);
51-
return;
52-
}
31+
dbUrl = dbUrlLine?.split("=").slice(1).join("=").trim();
5332
} catch {
54-
// If we can't read/parse for some reason, proceed; failures will be caught below
33+
// ignore, will handle below
34+
}
35+
36+
if (!dbUrl) {
37+
logWarning(
38+
"DATABASE_URL missing in .env.local. Skipping database migrations. Configure it to enable Prisma.",
39+
);
40+
return;
41+
}
42+
43+
const isLocal = /localhost|127\.0\.0\.1/.test(dbUrl);
44+
if (!isLocal) {
45+
logInfo("Remote/non-local DATABASE_URL detected; assuming migrations handled elsewhere. Skipping.");
46+
return;
5547
}
5648

5749
// Attempt to run migrations (script loads env via dotenv)

src/ai/image/generate.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,47 @@
11
import type { ImageModelProvider } from "./types";
2+
import { createOpenAI } from "@ai-sdk/openai"; // example provider (extend as needed)
3+
import { generateImage as sdkGenerateImage } from "ai";
24

3-
export type GeneratedImage = {
4-
uint8Array: Uint8Array;
5+
// We intentionally do not redefine SDK return types; we adapt to what the SDK returns.
6+
export interface GeneratedImageResult {
7+
url: string;
58
mimeType: string;
6-
};
7-
8-
export const generateImage = async (
9-
_model: `${ImageModelProvider}:${string}`,
10-
_prompt: string,
11-
): Promise<GeneratedImage> => {
12-
throw new Error(
13-
"Image generation is not configured in this build. Please set provider keys and implement a v5-compatible image generation path.",
14-
);
15-
};
9+
bytes?: Uint8Array;
10+
}
11+
12+
// Basic provider routing; extend with other providers if needed.
13+
function getProvider(model: `${ImageModelProvider}:${string}`) {
14+
const [provider] = model.split(":");
15+
switch (provider) {
16+
case "openai":
17+
return createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
18+
default:
19+
throw new Error(`Unsupported image provider: ${provider}`);
20+
}
21+
}
22+
23+
export async function generateImage(
24+
model: `${ImageModelProvider}:${string}`,
25+
prompt: string,
26+
): Promise<GeneratedImageResult> {
27+
const provider = getProvider(model);
28+
// The v5 SDK generateImage returns an iterator/stream or object depending on provider; we assume a simple call pattern.
29+
const { image } = await sdkGenerateImage({
30+
model: provider.image(model.split(":")[1]!),
31+
prompt,
32+
});
33+
34+
if (!image) throw new Error("No image returned by provider");
35+
36+
// Normalized shape. Some providers may give URL directly, others raw bytes.
37+
if (typeof image.url === "string") {
38+
return { url: image.url, mimeType: image.mimeType ?? "image/png" };
39+
}
40+
41+
if (image.bytes instanceof Uint8Array) {
42+
// Caller is responsible for persisting bytes (we convert upstream where needed)
43+
return { url: "", mimeType: image.mimeType ?? "image/png", bytes: image.bytes };
44+
}
45+
46+
throw new Error("Unrecognized image response shape");
47+
}

src/ai/language/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ProviderMetadata removed in v5; use a generic record for providerOptions
1+
// ProviderMetadata: retained for compatibility; if the upstream SDK exposes a richer type, prefer importing it.
22
export type ProviderMetadata = Record<string, unknown>;
33

44
export enum LanguageModelCapability {

src/toolkits/toolkits/image/tools/generate/server.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { put } from "@vercel/blob";
44
import { api } from "@/trpc/server";
55
import type { imageParameters } from "../../base";
66
import type z from "zod";
7-
import { generateImage, type GeneratedImage } from "@/ai/image/generate";
7+
import { generateImage } from "@/ai/image/generate";
88

99
export const generateToolConfigServer = (
1010
parameters: z.infer<typeof imageParameters>,
@@ -14,28 +14,27 @@ export const generateToolConfigServer = (
1414
> => {
1515
return {
1616
callback: async ({ prompt }) => {
17-
const image: GeneratedImage = await generateImage(parameters.model, prompt);
17+
const result = await generateImage(parameters.model, prompt);
18+
if (!result) throw new Error("No image generated");
1819

19-
if (!image) {
20-
console.error("No image generated");
21-
throw new Error("No image generated");
20+
// If we already have a remote URL, persist metadata only.
21+
if (result.url) {
22+
await api.images.createImage({
23+
url: result.url,
24+
contentType: result.mimeType,
25+
});
26+
return { url: result.url };
2227
}
2328

29+
if (!result.bytes) throw new Error("Image result missing bytes");
2430
const imageId = crypto.randomUUID();
31+
const ext = result.mimeType.split("/")[1] || "png";
32+
const fileName = `images/${imageId}.${ext}`;
33+
const file = new File([result.bytes], fileName, { type: result.mimeType });
2534

26-
const fileName = `images/${imageId}.${image.mimeType.split("/")[1]}`;
27-
const file = new File([image.uint8Array], fileName, { type: image.mimeType });
28-
29-
const { url: imageUrl } = await put(file.name, file, {
30-
access: "public",
31-
});
32-
33-
await api.images.createImage({
34-
url: imageUrl,
35-
contentType: image.mimeType,
36-
});
37-
38-
return { url: imageUrl };
35+
const { url } = await put(file.name, file, { access: "public" });
36+
await api.images.createImage({ url, contentType: result.mimeType });
37+
return { url };
3938
},
4039
};
4140
};

0 commit comments

Comments
 (0)