feat(frontend): POST for images
This commit is contained in:
@@ -26,6 +26,9 @@
|
||||
}
|
||||
|
||||
handle {
|
||||
request_body {
|
||||
max_size 50MB
|
||||
}
|
||||
reverse_proxy stealth-ai-relay-frontend:3000
|
||||
}
|
||||
}
|
||||
|
||||
12
frontend/src/app.d.ts
vendored
12
frontend/src/app.d.ts
vendored
@@ -1,12 +1,10 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
import type { ConvexHttpClient } from 'convex/browser';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
interface Locals {
|
||||
convex: ConvexHttpClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
frontend/src/hooks.server.ts
Normal file
8
frontend/src/hooks.server.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ConvexHttpClient } from 'convex/browser';
|
||||
import { PUBLIC_CONVEX_URL } from '$env/static/public';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
event.locals.convex = new ConvexHttpClient(PUBLIC_CONVEX_URL);
|
||||
return resolve(event);
|
||||
};
|
||||
91
frontend/src/routes/[mnemonic]/+server.ts
Normal file
91
frontend/src/routes/[mnemonic]/+server.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { api } from '$lib/convex/_generated/api';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
function detectImageType(bytes: Uint8Array): string | null {
|
||||
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
||||
return 'image/png';
|
||||
}
|
||||
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
|
||||
return 'image/gif';
|
||||
}
|
||||
if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {
|
||||
return 'image/webp';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request, locals }) => {
|
||||
const mnemonic = params.mnemonic;
|
||||
|
||||
const chatData = await locals.convex.query(api.chats.getByMnemonic, { mnemonic });
|
||||
if (!chatData) {
|
||||
throw error(404, 'Chat not found');
|
||||
}
|
||||
|
||||
const rawContentType = request.headers.get('content-type') || '';
|
||||
const caption = request.headers.get('x-caption') || '';
|
||||
|
||||
console.log('[POST /{mnemonic}] headers:', Object.fromEntries(request.headers.entries()));
|
||||
console.log('[POST /{mnemonic}] content-type:', rawContentType);
|
||||
|
||||
let base64: string;
|
||||
let mediaType: string;
|
||||
|
||||
if (rawContentType.includes('multipart/form-data')) {
|
||||
const formData = await request.formData();
|
||||
const keys = [...formData.keys()];
|
||||
console.log('[POST /{mnemonic}] formData keys:', keys);
|
||||
|
||||
let file: File | null = null;
|
||||
for (const key of ['file', 'image', 'photo', 'upload', 'attachment', ...keys]) {
|
||||
const value = formData.get(key);
|
||||
if (value instanceof File) {
|
||||
file = value;
|
||||
console.log('[POST /{mnemonic}] found file in field:', key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
throw error(400, `No file found in form data. Keys: ${keys.join(', ')}`);
|
||||
}
|
||||
|
||||
const buffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
base64 = Buffer.from(buffer).toString('base64');
|
||||
mediaType = detectImageType(bytes) || file.type || 'image/jpeg';
|
||||
|
||||
console.log('[POST /{mnemonic}] file:', file.name, file.type, 'size:', buffer.byteLength);
|
||||
} else if (rawContentType.includes('application/x-www-form-urlencoded')) {
|
||||
throw error(400, 'Use Form with File field, not URL-encoded form');
|
||||
} else {
|
||||
const buffer = await request.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
base64 = Buffer.from(buffer).toString('base64');
|
||||
mediaType = detectImageType(bytes) || rawContentType || 'image/jpeg';
|
||||
|
||||
console.log('[POST /{mnemonic}] raw bytes size:', buffer.byteLength);
|
||||
console.log('[POST /{mnemonic}] detected type:', mediaType);
|
||||
}
|
||||
|
||||
if (!base64 || base64.length === 0) {
|
||||
throw error(400, 'Empty image data');
|
||||
}
|
||||
|
||||
await locals.convex.mutation(api.messages.create, {
|
||||
chatId: chatData._id,
|
||||
role: 'user',
|
||||
content: caption,
|
||||
source: 'web',
|
||||
imageBase64: base64,
|
||||
imageMediaType: mediaType
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ ok: true, mediaType, size: base64.length }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user