diff --git a/examples/chat/app/(app)/chat/[id]/page.tsx b/examples/chat/app/(app)/chat/[id]/page.tsx index c3a6211..bd8d4df 100644 --- a/examples/chat/app/(app)/chat/[id]/page.tsx +++ b/examples/chat/app/(app)/chat/[id]/page.tsx @@ -138,15 +138,30 @@ export default function ConversationPage({ } if (input.trim()) textParts.push(input.trim()); + const compactUrl = (url?: string) => { + if (!url || url.startsWith("data:")) return undefined; + return url; + }; + // Images are sent via experimental_attachments — the Vercel AI SDK's supported // multimodal API for useChat. This ensures images reach the model correctly // instead of being silently dropped by the SDK's string-content serialization. const imageAttachments = atts - .filter((att) => att.mimeType.startsWith("image/") && att.url) - .map((att) => ({ url: att.url!, contentType: att.mimeType, name: att.name })); + .map((att) => ({ + att, + url: compactUrl(att.url), + })) + .filter(({ att, url }) => att.mimeType.startsWith("image/") && url) + .map(({ att, url }) => ({ url: url!, contentType: att.mimeType, name: att.name })); // Attachment metadata in the request body for provenance recording - const attachmentMeta = atts.map(({ cid, url, mimeType, name, governance }) => ({ cid, url, mimeType, name, governance })); + const attachmentMeta = atts.map(({ cid, url, mimeType, name, governance }) => ({ + cid, + url: compactUrl(url), + mimeType, + name, + governance, + })); append( { role: "user", content: textParts.join("\n\n") || " " }, diff --git a/examples/chat/app/api/media/upload/route.ts b/examples/chat/app/api/media/upload/route.ts index 017db34..73f7864 100644 --- a/examples/chat/app/api/media/upload/route.ts +++ b/examples/chat/app/api/media/upload/route.ts @@ -3,7 +3,7 @@ * * Accepts a multipart form with a `file` field (and optional `userId`). * Uploads to Pinata/IPFS and returns a persistent gateway URL + CID. - * Falls back to base64 data URL if PINATA_JWT is not configured (local dev). + * Falls back to a short, server-hosted media URL if PINATA_JWT is unavailable. * * NOTE: This route intentionally does NOT register the file in ProvenanceKit. * Provenance registration happens downstream via the FileProvenanceTag component: @@ -19,6 +19,7 @@ */ import { NextRequest, NextResponse } from "next/server"; +import { putGeneratedImage } from "@/lib/generated-image-cache"; export const runtime = "nodejs"; @@ -106,19 +107,22 @@ export async function POST(req: NextRequest) { } } - // Fallback: base64 data URL (local dev without Pinata configured). - // In production, PINATA_JWT must be set — base64 is too large for MongoDB. + // Fallback: store a transient copy and return a compact public URL. + // Do not return base64 data URLs here: they make /api/chat requests exceed + // Vercel body limits and can prevent the LLM from receiving image inputs. const buffer = Buffer.from(await file.arrayBuffer()); - const base64 = buffer.toString("base64"); - const dataUrl = `data:${file.type};base64,${base64}`; + const cached = await putGeneratedImage(buffer, file.type); + const origin = process.env.NEXT_PUBLIC_APP_URL ?? new URL(req.url).origin; + const url = new URL(cached.url, origin).toString(); return NextResponse.json({ - url: dataUrl, + url, cid: undefined, mimeType: file.type, name: file.name, size: file.size, isImage, textContent, + transient: true, }); }