Uh oh!
There was an error while loading. Please reload this page.
Add image preview to ChatPanel - #551
Conversation
Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
The latest updates on your projects. Learn more about Vercel for GitHub.
|
CLAassistant
commented
Feb 26, 2026
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro 📒 Files selected for processing (1)
📜 Recent review details🔇 Additional comments (2)
WalkthroughThe change adds image preview functionality to the chat panel component by introducing state to manage blob URLs, creating preview URLs when images are attached via effect hook, and enhancing the UI to conditionally display image previews with appropriate test identifiers. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Main concerns are around maintainability and consistency: the PR introduces a targeted ESLint suppression (@next/next/no-img-element) where next/image would be a better fit, and the current effect-driven previewUrl state can be simplified to reduce render churn and avoid Strict Mode edge-case flicker. Functionally the blob URL is correctly revoked, so issues are not correctness-blocking.
Additional notes (2)
- Performance |
components/chat-panel.tsx:160-168
UsingURL.createObjectURLis fine, but the effect currently sets state (setPreviewUrl(url)) and also returns a cleanup that revokes the previousurl. In React Strict Mode, effects run twice on mount in development, which can surface timing issues in UI tests (e.g., brief flicker) and can complicate debugging.
You can avoid storing the blob URL in React state entirely by deriving it from selectedFile with useMemo and a cleanup effect that only revokes when it changes. This reduces render churn and makes the lifecycle clearer.
- Readability |
components/chat-panel.tsx:301-306
The attachment container is rendered for anyselectedFile, butdata-testid="attached-image"implies it only applies to images. Since the UI supports non-image files (andpreviewUrlis conditionally null), this test id name can create confusion and brittle tests.
Either rename it to something file-agnostic (e.g., attached-file) or only apply it when the selected file is an image.
Summary of changes
What changed
- Added image preview state to
ChatPanelviapreviewUrl: string | null. - Introduced a
useEffectthat:- Creates a blob URL with
URL.createObjectURL(selectedFile)when the attachment is an image. - Stores it in state and revokes it on cleanup to avoid leaking blob URLs.
- Clears the preview when the attachment is removed or not an image.
- Creates a blob URL with
- Updated the attachment UI to:
- Render a thumbnail (
<img src={previewUrl} ... />) when available. - Add
data-testidhooks (attached-image,image-preview). - Add an ESLint suppression for
@next/next/no-img-element.
- Render a thumbnail (
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||
| <img | ||
| src={previewUrl} | ||
| alt="Preview" | ||
| className="h-16 w-16 object-cover rounded-md border border-border" | ||
| data-testid="image-preview" | ||
| /> |
There was a problem hiding this comment.
The inline // eslint-disable-next-line @next/next/no-img-element is a pretty broad escape hatch and will likely spread. Since this is a preview of a local blob URL, you can still use next/image safely (it supports blob: URLs) and avoid disabling the rule.
This also gives you better defaults (lazy loading/decoding) and consistent sizing behavior.
Suggestion
Replace the raw <img> with next/image and remove the ESLint disable. Example:
importImagefrom"next/image"{previewUrl ? (<divclassName="relative mr-2 h-16 w-16"><Imagesrc={previewUrl}alt={`Preview of ${selectedFile.name}`}fillclassName="object-cover rounded-md border border-border"unoptimized/></div>) : null}Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this change.
ngoiyaeric
commented
Feb 26, 2026
@coderabbitai the system is not responding tokens for the image attachment plus prompt |
ngoiyaeric
commented
Feb 26, 2026
@coderabbitai this is how the images are not being previewed, analyze the error
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 10653 🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 1093 🏁 Script executed: Length of output: 505 🏁 Script executed: Length of output: 25039 🏁 Script executed: Length of output: 27 🏁 Script executed: Length of output: 2732 🏁 Script executed: Length of output: 25039 🏁 Script executed: Length of output: 6425 🏁 Script executed: Length of output: 6425 🏁 Script executed: Length of output: 18036 🏁 Script executed: Length of output: 21 🏁 Script executed: Length of output: 1956 🏁 Script executed: Length of output: 26317 🏁 Script executed: Length of output: 5512
Looking at the code flow:
For example, FixUpdate your exportasyncfunctiongetModel(requireVision: boolean=false){constselectedModel=awaitgetSelectedModel();constxaiApiKey=process.env.XAI_API_KEY;constgemini3ProApiKey=process.env.GEMINI_3_PRO_API_KEY;constawsAccessKeyId=process.env.AWS_ACCESS_KEY_ID;constawsSecretAccessKey=process.env.AWS_SECRET_ACCESS_KEY;constawsRegion=process.env.AWS_REGION;constbedrockModelId=process.env.BEDROCK_MODEL_ID||'anthropic.claude-3-5-sonnet-20241022-v2:0';constopenaiApiKey=process.env.OPENAI_API_KEY;if(selectedModel){switch(selectedModel){case'Grok 4.2':
if(xaiApiKey){constxai=createXai({apiKey: xaiApiKey,baseURL: 'https://api.x.ai/v1',});try{// Use vision-capable model when images are presentreturnxai(requireVision ? 'grok-vision-beta' : 'grok-4-fast-non-reasoning');}catch(error){console.error('Selected model "Grok 4.2" is configured but failed to initialize.',error);thrownewError('Failed to initialize selected model.');}}else{console.error('User selected "Grok 4.2" but XAI_API_KEY is not set.');thrownewError('Selected model is not configured.');}case'Gemini 3':
if(gemini3ProApiKey){constgoogle=createGoogleGenerativeAI({apiKey: gemini3ProApiKey,});try{// Gemini 3 Pro already supports visionreturngoogle('gemini-3-pro-preview');}catch(error){console.error('Selected model "Gemini 3" is configured but failed to initialize.',error);thrownewError('Failed to initialize selected model.');}}else{console.error('User selected "Gemini 3" but GEMINI_3_PRO_API_KEY is not set.');thrownewError('Selected model is not configured.');}case'GPT-5.1':
if(openaiApiKey){constopenai=createOpenAI({apiKey: openaiApiKey,});// GPT-4o already supports visionreturnopenai('gpt-4o');}else{console.error('User selected "GPT-5.1" but OPENAI_API_KEY is not set.');thrownewError('Selected model is not configured.');}}}// Default behavior with vision supportif(xaiApiKey){constxai=createXai({apiKey: xaiApiKey,baseURL: 'https://api.x.ai/v1',});try{// Use vision model when images are presentreturnxai(requireVision ? 'grok-vision-beta' : 'grok-4-fast-non-reasoning');}catch(error){console.warn('xAI API unavailable, falling back to next provider:');}}if(gemini3ProApiKey){constgoogle=createGoogleGenerativeAI({apiKey: gemini3ProApiKey,});try{// Gemini 3 Pro supports visionreturngoogle('gemini-3-pro-preview');}catch(error){console.warn('Gemini 3 Pro API unavailable, falling back to next provider:',error);}}if(awsAccessKeyId&&awsSecretAccessKey){constbedrock=createAmazonBedrock({bedrockOptions: {region: awsRegion,credentials: {accessKeyId: awsAccessKeyId,secretAccessKey: awsSecretAccessKey,},},});// Claude 3.5 Sonnet supports visionconstmodel=bedrock(bedrockModelId,{additionalModelRequestFields: {top_k: 350},});returnmodel;}constopenai=createOpenAI({apiKey: openaiApiKey,});// GPT-4o supports visionreturnopenai('gpt-4o');}Note: You'll need to verify the correct model identifiers for vision-capable variants. For Grok, I used After applying this fix, your image attachments should work properly and the AI will respond to both your prompt and image! |
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain
🏁 Script executed: Length of output: 10653
🏁 Script executed: Length of output: 0 🏁 Script executed: Length of output: 25650
The ProblemOn Line 115 in image: URL.createObjectURL(selectedFile)// Creates blob:http://...Blob URLs only work in the browser and cannot be accessed by the backend. The backend filters out these invalid URLs (see The SolutionConvert the image to base64 before adding it to the content array. Here's the fix: Why This Fixes Both Issues
ImplementationReplace your The image preview thumbnail should already be working (that uses the separate Let me know if you need help implementing this fix! 🏁 Script executed: Length of output: 1350
The ProblemThe Comment image shows content about Mountain Gorillas in Kahuzi-Biega National Park, Kinshasa Cityscape, and Okapi Wildlife Reserve - this appears to be test content you're trying to analyze with image attachments. On Line 111-115 in image: URL.createObjectURL(selectedFile)// Creates blob:http://...Blob URLs only work in the browser and cannot be accessed by the backend. The backend filters out these invalid URLs (see The SolutionConvert the image to base64 in the browser before adding it to the content array. Here's the complete fix: consthandleSubmit=async(e: React.FormEvent<HTMLFormElement>)=>{e.preventDefault()if(!input.trim()&&!selectedFile){return}constcontent: ({type: 'text';text: string}|{type: 'image';image: string})[]=[]if(input){content.push({type: 'text',text: input})}// FIX: Convert image to base64 instead of blob URL (browser-compatible)if(selectedFile&&selectedFile.type.startsWith('image/')){constbuffer=awaitselectedFile.arrayBuffer()constbase64=btoa(newUint8Array(buffer).reduce((data,byte)=>data+String.fromCharCode(byte),''))constdataUrl=`data:${selectedFile.type};base64,${base64}`content.push({type: 'image',image: dataUrl// Now using data URL instead of blob URL})}setMessages(currentMessages=>[
...currentMessages,{id: nanoid(),component: <UserMessagecontent={content}/>}])constformData=newFormData(e.currentTarget)if(selectedFile){formData.append('file',selectedFile)}// Include drawn features in the form dataformData.append('drawnFeatures',JSON.stringify(mapData.drawnFeatures||[]))setInput('')clearAttachment()constresponseMessage=awaitsubmit(formData)setMessages(currentMessages=>[...currentMessages,responseMessageasany])}What ChangedBefore (broken): image: URL.createObjectURL(selectedFile)// blob:http://localhost:3000/abc123After (fixed): constbuffer=awaitselectedFile.arrayBuffer()constbase64=btoa(newUint8Array(buffer).reduce((data,byte)=>data+String.fromCharCode(byte),''))constdataUrl=`data:${selectedFile.type};base64,${base64}`
image: dataUrl// data:image/png;base64,iVBORw0KGgo...Why This Fixes Both Issues
Replace the |
ngoiyaeric
commented
Jul 9, 2026
Closing this pull request as part of a bulk cleanup of pending PRs. |

Added image preview logic using URL.createObjectURL and updated UI to show thumbnails for attached images.
PR created automatically by Jules for task 6818274094371257842 started by @ngoiyaeric
Summary by CodeRabbit