Conversation
abubahmed
commented
Mar 23, 2026
- Added multimodal support for PDFs, audio files, and images
- Hardcoded modals that can support each type (OpenRouter doesn't provide this info)
- All media types are stored in storage, but OpenRouter doesn't take URLs for audio (passed in as base64 encoded data)
- Copy paste, drag and drop, and file upload support
- For audio, users can record audio directly into the website
|
@claude review this |
|
Claude finished @DIodide's task in 2m 7s —— View job Code Review: Abu/multimodal PR
Overall this is a clean implementation. The hook/component separation is good, URL revocation is handled correctly, and the Convex auth gates are in place. A few bugs and a couple of security/UX concerns worth addressing before merge: Bugs1. Can't send attachments-only (no text)
if (!content || !activeHarness) return;If a user drops an image and hits send without typing any text, the message is silently blocked. The guard should be 2.
const recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });Safari doesn't support 3. MediaRecorder stream leak on component unmount
If a user navigates away mid-recording, 4. Attachments silently dropped when message is enqueued during streaming
clearAttachments();
if (isStreaming && conversationId) {
onEnqueue(content); // attachments are gone, only text enqueued
return;
}If the user sends while streaming is active, attachments are cleared but the queued message only carries text. The attachments are lost silently. This probably needs a design decision, but at minimum users should be warned or the enqueue path should not clear attachments. Security5. No server-side file size enforcement
6.
Any authenticated user who knows a Performance7.
Every time a user sends a message, the code re-fetches signed URLs for all attachments in the entire conversation history. For a long conversation with many images, this fires many Convex queries in series. Consider caching resolved URLs (they're already being fetched reactively for display in Minor / Nits8.
export function modelSupportsMedia(model: string | undefined): boolean {
return modelHas(model, "image");
}The name implies "any media" but it only gates on image support. This means a hypothetical audio-only model would not show the attachment UI at all. Consider renaming to 9.
If somehow a file with a MIME type that doesn't match any branch ends up in |