From 8249a44b2b7553c1717713bb5b738b06cf3115ed Mon Sep 17 00:00:00 2001 From: ifindev Date: Wed, 24 Sep 2025 14:57:19 +0700 Subject: [PATCH 1/3] chore: update package.json and pnpm-lock.yaml to include @opentelemetry/api@1.9.0 as a dependency --- package.json | 6 ------ pnpm-lock.yaml | 23 ++++++++++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 1be4e63..7c5f3e8 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,14 @@ "dev": "next dev", "build": "next build", "start": "next start", - "wrangler:dev": "npx wrangler dev", "dev:cf": "npx @opennextjs/cloudflare build && wrangler dev", "dev:remote": "npx @opennextjs/cloudflare build && wrangler dev --remote", - "build:cf": "npx @opennextjs/cloudflare build", "deploy:cf": "npx @opennextjs/cloudflare deploy", "preview:cf": "npx @opennextjs/cloudflare preview", - "deploy": "npx @opennextjs/cloudflare deploy", "deploy:preview": "npx @opennextjs/cloudflare deploy --env preview", - "db:generate": "drizzle-kit generate", "db:generate:named": "drizzle-kit generate --name", "db:migrate:local": "wrangler d1 migrations apply next-cf-app --local", @@ -25,12 +21,10 @@ "db:migrate:prod": "wrangler d1 migrations apply next-cf-app --remote", "db:studio": "drizzle-kit studio", "db:studio:local": "drizzle-kit studio --config=drizzle.local.config.ts", - "db:inspect:local": "wrangler d1 execute next-cf-app --local --command=\"SELECT name FROM sqlite_master WHERE type='table';\"", "db:inspect:preview": "wrangler d1 execute next-cf-app --env preview --command=\"SELECT name FROM sqlite_master WHERE type='table';\"", "db:inspect:prod": "wrangler d1 execute next-cf-app --remote --command=\"SELECT name FROM sqlite_master WHERE type='table';\"", "db:reset:local": "wrangler d1 execute next-cf-app --local --command=\"DROP TABLE IF EXISTS todos;\" && pnpm run db:migrate:local", - "cf:secret": "npx wrangler secret put", "cf-typegen": "pnpm exec wrangler types && pnpm exec wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts", "lint": "npx biome format --write" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f47a1e..11a3696 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,16 +52,16 @@ importers: version: 17.2.2 drizzle-orm: specifier: ^0.44.5 - version: 0.44.5(@libsql/client@0.15.15)(better-sqlite3@12.2.0)(kysely@0.28.5) + version: 0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(kysely@0.28.5) drizzle-zod: specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.44.5(@libsql/client@0.15.15)(better-sqlite3@12.2.0)(kysely@0.28.5))(zod@4.1.8) + version: 0.8.3(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(kysely@0.28.5))(zod@4.1.8) lucide-react: specifier: ^0.544.0 version: 0.544.0(react@19.1.0) next: specifier: 15.4.6 - version: 15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 15.4.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: 19.1.0 version: 19.1.0 @@ -1477,6 +1477,10 @@ packages: peerDependencies: wrangler: ^4.24.4 + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@peculiar/asn1-android@2.5.0': resolution: {integrity: sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==} @@ -5551,6 +5555,9 @@ snapshots: - encoding - supports-color + '@opentelemetry/api@1.9.0': + optional: true + '@peculiar/asn1-android@2.5.0': dependencies: '@peculiar/asn1-schema': 2.5.0 @@ -6835,15 +6842,16 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.5(@libsql/client@0.15.15)(better-sqlite3@12.2.0)(kysely@0.28.5): + drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(kysely@0.28.5): optionalDependencies: '@libsql/client': 0.15.15 + '@opentelemetry/api': 1.9.0 better-sqlite3: 12.2.0 kysely: 0.28.5 - drizzle-zod@0.8.3(drizzle-orm@0.44.5(@libsql/client@0.15.15)(better-sqlite3@12.2.0)(kysely@0.28.5))(zod@4.1.8): + drizzle-zod@0.8.3(drizzle-orm@0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(kysely@0.28.5))(zod@4.1.8): dependencies: - drizzle-orm: 0.44.5(@libsql/client@0.15.15)(better-sqlite3@12.2.0)(kysely@0.28.5) + drizzle-orm: 0.44.5(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(better-sqlite3@12.2.0)(kysely@0.28.5) zod: 4.1.8 dunder-proto@1.0.1: @@ -7415,7 +7423,7 @@ snapshots: negotiator@1.0.0: {} - next@15.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next@15.4.6(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@next/env': 15.4.6 '@swc/helpers': 0.5.15 @@ -7433,6 +7441,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.4.6 '@next/swc-win32-arm64-msvc': 15.4.6 '@next/swc-win32-x64-msvc': 15.4.6 + '@opentelemetry/api': 1.9.0 sharp: 0.34.3 transitivePeerDependencies: - '@babel/core' From 7bfc088fa608467c5eb49edc46f17da2294c7fda Mon Sep 17 00:00:00 2001 From: ifindev Date: Wed, 24 Sep 2025 14:57:33 +0700 Subject: [PATCH 2/3] chore: update Cloudflare environment and worker configuration to include VECTORIZE and AI bindings --- cloudflare-env.d.ts | 4 +++- worker-configuration.d.ts | 4 +++- wrangler.jsonc | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cloudflare-env.d.ts b/cloudflare-env.d.ts index c78fae0..722e1a7 100644 --- a/cloudflare-env.d.ts +++ b/cloudflare-env.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts` (hash: 27c9b47fcb70f49031934291d4d721fa) +// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts` (hash: 25474971f7656c5a2eec353ec5d54b13) // Runtime types generated with workerd@1.20250906.0 2025-03-01 global_fetch_strictly_public,nodejs_compat declare namespace Cloudflare { interface Env { @@ -14,6 +14,8 @@ declare namespace Cloudflare { GOOGLE_CLIENT_SECRET: string; next_cf_app_bucket: R2Bucket; next_cf_app: D1Database; + VECTORIZE: VectorizeIndex; + AI: Ai; ASSETS: Fetcher; } } diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index be762cf..c141039 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 4d9fbe91ef2bed9c40c9c0cb3efc4f85) +// Generated by Wrangler by running `wrangler types` (hash: a50fafb18c1897e17a9987365ec61465) // Runtime types generated with workerd@1.20250906.0 2025-03-01 global_fetch_strictly_public,nodejs_compat declare namespace Cloudflare { interface Env { @@ -14,6 +14,8 @@ declare namespace Cloudflare { GOOGLE_CLIENT_SECRET: string; next_cf_app_bucket: R2Bucket; next_cf_app: D1Database; + VECTORIZE: VectorizeIndex; + AI: Ai; ASSETS: Fetcher; } } diff --git a/wrangler.jsonc b/wrangler.jsonc index badd9d5..525553b 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -26,9 +26,19 @@ "r2_buckets": [ { "bucket_name": "next-cf-app-bucket", - "binding": "next_cf_app_bucket" + "binding": "next_cf_app_bucket", + "preview_bucket_name": "next-cf-app-dev-bucket" } - ] + ], + "vectorize": [ + { + "binding": "VECTORIZE", + "index_name": "next-cf-app-index" + } + ], + "ai": { + "binding": "AI" + } /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement From 3594fdf1cf1e70682d96abcd96b6067553d2eb69 Mon Sep 17 00:00:00 2001 From: ifindev Date: Wed, 24 Sep 2025 14:58:37 +0700 Subject: [PATCH 3/3] feat: add AI summarization service and update README for new features - Introduced a new API endpoint for text summarization using Cloudflare Workers AI. - Added error handling for API requests and validation for input data. - Updated README.md to include AI features, usage instructions, and configuration details. --- README.md | 95 ++++++++++++++++++++++++- src/app/api/summarize/route.ts | 78 ++++++++++++++++++++ src/lib/api-error.ts | 46 ++++++++++++ src/services/summarizer.service.ts | 110 +++++++++++++++++++++++++++++ 4 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 src/app/api/summarize/route.ts create mode 100644 src/lib/api-error.ts create mode 100644 src/services/summarizer.service.ts diff --git a/README.md b/README.md index 6b39291..5cd8564 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Combined with **Next.js 15**, you get modern React features, Server Components, - ๐ŸŒ **Cloudflare Workers** - Serverless edge compute platform - ๐Ÿ—ƒ๏ธ **Cloudflare D1** - Distributed SQLite database at the edge - ๐Ÿ“ฆ **Cloudflare R2** - S3-compatible object storage +- ๐Ÿค– **Cloudflare Workers AI** - Edge AI inference with OpenSource models - ๐Ÿ”‘ **Better Auth** - Modern authentication with Google OAuth - ๐Ÿ› ๏ธ **Drizzle ORM** - TypeScript-first database toolkit @@ -45,6 +46,7 @@ Combined with **Next.js 15**, you get modern React features, Server Components, ### ๐Ÿ“Š **Data Flow Architecture** - **Fetching**: Server Actions + React Server Components for optimal performance - **Mutations**: Server Actions with automatic revalidation +- **AI Processing**: Edge AI inference with Cloudflare Workers AI - **Type Safety**: End-to-end TypeScript from database to UI - **Caching**: Built-in Next.js caching with Cloudflare edge caching @@ -64,14 +66,16 @@ Create an API token for Wrangler authentication: 2. Select **Create Token** > find **Edit Cloudflare Workers** > select **Use Template** 3. Customize your token name (e.g., "Next.js Cloudflare Template") 4. Scope your token to your account and zones (if using custom domains) -5. **Add additional permissions** for D1 database access: +5. **Add additional permissions** for D1 database and AI access: - Account - D1:Edit - Account - D1:Read + - Account - Cloudflare Workers AI:Read **Final Token Permissions:** - All permissions from "Edit Cloudflare Workers" template - Account - D1:Edit (for database operations) - Account - D1:Read (for database queries) +- Account - Cloudflare Workers AI:Read (for AI inference) ### 3. Clone and Setup @@ -182,7 +186,10 @@ Update `wrangler.jsonc` with your resource IDs: "bucket_name": "your-app-bucket", "binding": "FILES" } - ] + ], + "ai": { + "binding": "AI" + } } ``` @@ -465,6 +472,7 @@ src/ โ”œโ”€โ”€ app/ # Next.js App Router โ”‚ โ”œโ”€โ”€ (auth)/ # Auth-related pages โ”‚ โ”œโ”€โ”€ api/ # API routes (for external access) +โ”‚ โ”‚ โ””โ”€โ”€ summarize/ # AI summarization endpoint โ”‚ โ”œโ”€โ”€ dashboard/ # Dashboard pages โ”‚ โ””โ”€โ”€ globals.css # Global styles โ”œโ”€โ”€ components/ # Shared UI components @@ -487,6 +495,8 @@ src/ โ”‚ โ”œโ”€โ”€ components/ # Todo components โ”‚ โ”œโ”€โ”€ models/ # Todo models โ”‚ โ””โ”€โ”€ schemas/ # Todo schemas +โ”œโ”€โ”€ services/ # Business logic services +โ”‚ โ””โ”€โ”€ summarizer.service.ts # AI summarization service โ””โ”€โ”€ drizzle/ # Database migrations ``` @@ -497,6 +507,72 @@ src/ - ๐Ÿ›ก๏ธ **Type Safety** - End-to-end TypeScript from database to UI - ๐Ÿงช **Testable** - Clear separation of concerns makes testing easier +## ๐Ÿค– AI Development & Testing + +### Testing the AI API + +**โš ๏ธ Authentication Required**: Login to your app first, then test the API. + +**Browser Console (Easiest):** +1. Login at `http://localhost:3000` +2. Open DevTools Console (F12) +3. Run: +```javascript +fetch('/api/summarize', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + text: "Your text to summarize here...", + config: { maxLength: 100, style: "concise" } + }) +}).then(r => r.json()).then(console.log); +``` + +**cURL (with session cookies):** +1. Login in browser first +2. DevTools โ†’ Application โ†’ Cookies โ†’ Copy `better-auth.session_token` +3. Use cookie in cURL: +```bash +curl -X POST http://localhost:3000/api/summarize \ + -H "Content-Type: application/json" \ + -H "Cookie: better-auth.session_token=your-token-here" \ + -d '{"text": "Your text here...", "config": {"maxLength": 100}}' +``` + +**Postman:** +1. Login in browser, copy session cookie from DevTools +2. Add header: `Cookie: better-auth.session_token=your-token-here` + +**Unauthenticated Request Response:** +```json +{ + "success": false, + "error": "Authentication required", + "data": null +} +``` + + +### AI Service Architecture + +The AI integration follows a clean service-based architecture: + +1. **API Route** (`/api/summarize`) - Handles HTTP requests, authentication, and validation +2. **Authentication Layer** - Validates user session before processing requests +3. **SummarizerService** - Encapsulates AI business logic +4. **Error Handling** - Comprehensive error responses with proper status codes +5. **Type Safety** - Full TypeScript support with Zod validation + +### AI Model Options + +Cloudflare Workers AI supports various models: +- **@cf/meta/llama-3.2-1b-instruct** - Text generation (current) +- **@cf/meta/llama-3.2-3b-instruct** - More capable text generation +- **@cf/meta/m2m100-1.2b** - Translation +- **@cf/baai/bge-base-en-v1.5** - Text embeddings +- **@cf/microsoft/resnet-50** - Image classification + ## ๐Ÿ”ง Advanced Configuration ### Database Schema Changes @@ -573,10 +649,25 @@ pnpm run deploy:preview ## โœ๏ธ Todos +### ๐Ÿค– AI Features +- [ ] Add text translation service with `@cf/meta/m2m100-1.2b` +- [ ] Implement text embeddings for semantic search with `@cf/baai/bge-base-en-v1.5` +- [ ] Add image classification API with `@cf/microsoft/resnet-50` +- [ ] Create chat/conversation API with conversation memory +- [ ] Add content moderation with AI classification +- [ ] Implement sentiment analysis for user feedback + +### ๐Ÿ’ณ Payments & Communication - [ ] Implement email sending with [Resend](https://resend.com/) & [Cloudflare Email Routing](https://www.cloudflare.com/developer-platform/products/email-routing/) - [ ] Implement international payment gateway with [Polar.sh](https://polar.sh/) - [ ] Implement Indonesian payment gateway either with [Xendit](https://www.xendit.co/en-id/), [Midtrans](https://midtrans.com/en), or [Duitku](https://www.duitku.com/) +### ๐Ÿ“Š Analytics & Performance +- [ ] Add Cloudflare Analytics integration +- [ ] Implement custom metrics tracking +- [ ] Add performance monitoring dashboard +- [ ] Create AI usage analytics and cost tracking + ## ๐Ÿค Contributing diff --git a/src/app/api/summarize/route.ts b/src/app/api/summarize/route.ts new file mode 100644 index 0000000..5fed70d --- /dev/null +++ b/src/app/api/summarize/route.ts @@ -0,0 +1,78 @@ +import { getCloudflareContext } from "@opennextjs/cloudflare"; +import { headers } from "next/headers"; +import handleApiError from "@/lib/api-error"; +import { getAuthInstance } from "@/modules/auth/utils/auth-utils"; +import { + SummarizerService, + summarizeRequestSchema, +} from "@/services/summarizer.service"; + +export async function POST(request: Request) { + try { + // Check authentication + const auth = await getAuthInstance(); + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + return new Response( + JSON.stringify({ + success: false, + error: "Authentication required", + data: null, + }), + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } + + const { env } = await getCloudflareContext(); + + if (!env.AI) { + return new Response( + JSON.stringify({ + success: false, + error: "AI service is not available", + data: null, + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } + + // parse request body + const body = await request.json(); + const validated = summarizeRequestSchema.parse(body); + + const summarizerService = new SummarizerService(env.AI); + const result = await summarizerService.summarize( + validated.text, + validated.config, + ); + + return new Response( + JSON.stringify({ + success: true, + data: result, + error: null, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } catch (error) { + return handleApiError(error); + } +} diff --git a/src/lib/api-error.ts b/src/lib/api-error.ts new file mode 100644 index 0000000..5bb0505 --- /dev/null +++ b/src/lib/api-error.ts @@ -0,0 +1,46 @@ +import { z } from "zod/v4"; + +export default function handleApiError(error: unknown) { + if (error instanceof z.ZodError) { + const firstError = error.issues[0]; + return new Response( + JSON.stringify({ + success: false, + error: firstError.message, + field: firstError.path.join("."), + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Handle JSON parsing errors + if (error instanceof SyntaxError) { + return new Response( + JSON.stringify({ + success: false, + error: "Invalid JSON format", + }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Handle AI/service errors + const message = + error instanceof Error ? error.message : "Internal server error"; + return new Response( + JSON.stringify({ + success: false, + error: message, + }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); +} diff --git a/src/services/summarizer.service.ts b/src/services/summarizer.service.ts new file mode 100644 index 0000000..3084ef4 --- /dev/null +++ b/src/services/summarizer.service.ts @@ -0,0 +1,110 @@ +import { z } from "zod/v4"; + +export const summarizerConfigSchema = z.object({ + maxLength: z.number().int().min(50).max(1000).optional().default(200), + style: z + .enum(["concise", "detailed", "bullet-points"]) + .optional() + .default("concise"), + language: z.string().min(1).max(50).optional().default("English"), +}); + +export const summarizeRequestSchema = z.object({ + text: z + .string() + .trim() + .min(50, "Text too short to summarize (minimum 50 characters)") + .max(50000, "Text too long (maximum 50,000 characters)"), + config: summarizerConfigSchema.optional(), +}); + +type SummaryStyles = z.infer["style"]; +export type SummarizeRequest = z.infer; +export type SummarizerConfig = z.infer; +export type SummaryResult = { + summary: string; + originalLength: number; + summaryLength: number; + tokensUsed: { + input: number; + output: number; + }; +}; + +export class SummarizerService { + constructor(private readonly ai: Ai) {} + + async summarize( + text: string, + config?: SummarizerConfig, + ): Promise { + const { + maxLength = 200, + style = "concise", + language = "English", + } = config || {}; + + const systemPrompt = this.buildSystenPrompt(maxLength, style, language); + + // Estimate tokens (rough calculation: 1 token โ‰ˆ 4 characters) + const inputTokens = Math.ceil((systemPrompt.length + text.length) / 4); + + const response = await this.ai.run("@cf/meta/llama-3.2-1b-instruct", { + messages: [ + { role: "system", content: systemPrompt }, + { + role: "user", + content: `Please summarize the following text: ${text}`, + }, + ], + }); + + const summary = response.response?.trim() ?? ""; + const outputTokens = Math.ceil(summary.length / 4); + + return { + summary, + originalLength: text.length, + summaryLength: summary.length, + tokensUsed: { + input: inputTokens, + output: outputTokens, + }, + }; + } + + private buildSystenPrompt( + maxLength: number, + style: string, + language: string, + ): string { + const styleInstructructions: Record = { + concise: + "Create a brief, concise summary focusing on the main points.", + detailed: + "Create a comprehensive summary that covers key details and context.", + "bullet-points": + "Create a summary using bullet points to highlight key information.", + }; + + return `You are a professional text summarizer. ${styleInstructructions[style as keyof typeof styleInstructructions]} + + Instructions: + - Summarize in ${language} + - Keep the summary under ${maxLength} words + - Focus on the most important information + - Maintain the original meaning and context + - Use clear, readable language + - Do not add your own opinions or interpretations + - DO NOT START THE SUMMARY WITH the following phrases: + - "Here is a summary of the text" + - "Here is a summary of the text:" + - "Here is a summary of the text in bullet points" + - "Here is the summary:" + - "Here is the summary in bullet points:" + - "Here is the summary in ${language}:" + - "Here is the summary in ${language} in bullet points:" + + Output only the summary, nothing else.`; + } +}