diff --git a/apps/rowboat/app/api/uploaded-images/route.ts b/apps/rowboat/app/api/tmp-images/upload/route.ts similarity index 60% rename from apps/rowboat/app/api/uploaded-images/route.ts rename to apps/rowboat/app/api/tmp-images/upload/route.ts index 25556ccd..21b41e27 100644 --- a/apps/rowboat/app/api/uploaded-images/route.ts +++ b/apps/rowboat/app/api/tmp-images/upload/route.ts @@ -1,16 +1,13 @@ import { NextRequest, NextResponse } from 'next/server'; -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; -import crypto from 'crypto'; +import { requireAuth } from '@/app/lib/auth'; import { tempBinaryCache } from '@/src/application/services/temp-binary-cache'; import { GoogleGenerativeAI } from '@google/generative-ai'; import { UsageTracker, getCustomerForUserId, logUsage as libLogUsage } from '@/app/lib/billing'; -import { requireAuth } from '@/app/lib/auth'; import { USE_AUTH, USE_BILLING } from '@/app/lib/feature_flags'; -// POST /api/uploaded-images +// POST /api/tmp-images/upload // Accepts an image file (multipart/form-data, field name: "file") -// Stores it either in S3 (if configured) under uploaded_images///. -// or in the in-memory temp cache. Returns a JSON with a URL that the agent can fetch. +// Stores it in the in-memory temp cache and returns a temporary URL. export async function POST(request: NextRequest) { try { // Require authentication if enabled @@ -18,7 +15,7 @@ export async function POST(request: NextRequest) { if (USE_AUTH) { try { currentUser = await requireAuth(); - } catch (_) { + } catch { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } } @@ -54,7 +51,7 @@ export async function POST(request: NextRequest) { const response: any = result.response as any; descriptionMarkdown = response?.text?.() || null; - // Track usage similar to agents-runtime + // Track usage similar to rag-worker try { const inputTokens = response?.usageMetadata?.promptTokenCount || 0; const outputTokens = response?.usageMetadata?.candidatesTokenCount || 0; @@ -63,9 +60,9 @@ export async function POST(request: NextRequest) { modelName: 'gemini-2.5-flash', inputTokens, outputTokens, - context: 'uploaded_images.upload_with_description', + context: 'tmp_images.upload_with_description', }); - } catch (_) { + } catch { // ignore usage tracking errors } } @@ -73,56 +70,11 @@ export async function POST(request: NextRequest) { console.warn('Gemini description failed', e); } - // If S3 configured, upload there - const s3Bucket = process.env.RAG_UPLOADS_S3_BUCKET || ''; - if (s3Bucket) { - const s3Region = process.env.RAG_UPLOADS_S3_REGION || 'us-east-1'; - const s3 = new S3Client({ - region: s3Region, - credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? { - accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, - } : undefined, - }); - - const ext = mime === 'image/jpeg' ? '.jpg' : mime === 'image/webp' ? '.webp' : mime === 'image/png' ? '.png' : '.bin'; - const imageId = crypto.randomUUID(); - const last2 = imageId.slice(-2).padStart(2, '0'); - const dirA = last2.charAt(0); - const dirB = last2.charAt(1); - const key = `uploaded_images/${dirA}/${dirB}/${imageId}${ext}`; - - await s3.send(new PutObjectCommand({ - Bucket: s3Bucket, - Key: key, - Body: buf, - ContentType: mime, - })); - - const url = `/api/uploaded-images/${imageId}${ext}`; - - // Log usage to billing similar to rag-worker - try { - if (USE_BILLING && currentUser) { - const customer = await getCustomerForUserId(currentUser.id); - if (customer) { - const items = usageTracker.flush(); - if (items.length > 0) { - await libLogUsage(customer.id, { items }); - } - } - } - } catch (_) { - // ignore billing logging errors - } - - return NextResponse.json({ url, storage: 's3', id: `${imageId}${ext}`, mimeType: mime, description: descriptionMarkdown }); - } - - // Otherwise store in temp cache and return temp URL + // Store in temp cache and return temp URL const ttlSec = 10 * 60; // 10 minutes const id = tempBinaryCache.put(buf, mime, ttlSec * 1000); const url = `/api/tmp-images/${id}`; + // Log usage to billing similar to rag-worker try { if (USE_BILLING && currentUser) { @@ -134,13 +86,14 @@ export async function POST(request: NextRequest) { } } } - } catch (_) { + } catch { // ignore billing logging errors } return NextResponse.json({ url, storage: 'temp', id, mimeType: mime, expiresInSec: ttlSec, description: descriptionMarkdown }); } catch (e) { - console.error('upload image error', e); + console.error('tmp image upload error', e); return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); } } + diff --git a/apps/rowboat/app/api/uploaded-images/[id]/route.ts b/apps/rowboat/app/api/uploaded-images/[id]/route.ts index a89fc757..fd723f99 100644 --- a/apps/rowboat/app/api/uploaded-images/[id]/route.ts +++ b/apps/rowboat/app/api/uploaded-images/[id]/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; -import { S3Client, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; +import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { Readable } from 'stream'; import { requireAuth } from '@/app/lib/auth'; -// Serves uploaded images from S3 by UUID-only path: /api/uploaded-images/{id} +// Serves uploaded images from S3 at path: /api/uploaded-images/{uuid}.{ext} // Reconstructs the S3 key using the same sharding logic as image upload. export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) { // Require authentication (handles guest mode internally when USE_AUTH is disabled) diff --git a/apps/rowboat/components/common/compose-box-playground.tsx b/apps/rowboat/components/common/compose-box-playground.tsx index 09723521..2d32d60f 100644 --- a/apps/rowboat/components/common/compose-box-playground.tsx +++ b/apps/rowboat/components/common/compose-box-playground.tsx @@ -87,7 +87,6 @@ export function ComposeBoxPlayground({ } const controller = new AbortController(); uploadAbortRef.current = controller; - let usedFallback = false; try { // 1) Request a presigned S3 upload URL via server action const { getUploadUrlForImage } = await import('@/app/actions/uploaded-images.actions'); @@ -126,12 +125,11 @@ export function ComposeBoxPlayground({ } } } catch (err: any) { - // On local, S3 may be unconfigured. Fallback to legacy temp upload endpoint. if (err?.name === 'AbortError') throw err; - usedFallback = true; + // Fallback to temp in-memory upload for local/dev without S3 const form = new FormData(); form.append('file', file); - const res = await fetch('/api/uploaded-images', { + const res = await fetch('/api/tmp-images/upload', { method: 'POST', body: form, signal: controller.signal,