1 Commits

Author SHA1 Message Date
Ryan Chen
b6576fb2fd Fix images not sending in existing conversations
Add missing pendingImage, onImageSelect, and onClearImage props to the
MessageInput rendered in the active chat footer, matching the homepage version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 09:07:21 -04:00
5 changed files with 25 additions and 48 deletions

View File

@@ -3,7 +3,7 @@ import json
import logging
import uuid
from quart import Blueprint, jsonify, make_response, request
from quart import Blueprint, Response, jsonify, make_response, request
from quart_jwt_extended import (
get_jwt_identity,
jwt_refresh_token_required,
@@ -12,7 +12,6 @@ from quart_jwt_extended import (
import blueprints.users.models
from utils.image_process import analyze_user_image
from utils.image_upload import ImageValidationError, process_image
from utils.s3_client import generate_presigned_url as s3_presigned_url
from utils.s3_client import get_image as s3_get_image
from utils.s3_client import upload_image as s3_upload_image
@@ -123,14 +122,27 @@ async def upload_image():
await s3_upload_image(processed_bytes, key, output_content_type)
return jsonify({"image_key": key})
return jsonify(
{
"image_key": key,
"image_url": f"/api/conversation/image/{key}",
}
)
@conversation_blueprint.get("/image/<path:image_key>")
@jwt_refresh_token_required
async def serve_image(image_key: str):
url = await s3_presigned_url(image_key)
return jsonify({"url": url})
try:
image_bytes, content_type = await s3_get_image(image_key)
except Exception:
return jsonify({"error": "Image not found"}), 404
return Response(
image_bytes,
content_type=content_type,
headers={"Cache-Control": "private, max-age=3600"},
)
@conversation_blueprint.post("/stream-query")

View File

@@ -125,7 +125,7 @@ class ConversationService {
async uploadImage(
file: File,
conversationId: string,
): Promise<{ image_key: string }> {
): Promise<{ image_key: string; image_url: string }> {
const formData = new FormData();
formData.append("file", file);
formData.append("conversation_id", conversationId);
@@ -147,15 +147,8 @@ class ConversationService {
return await response.json();
}
async getPresignedImageUrl(imageKey: string): Promise<string> {
const response = await userService.fetchWithRefreshToken(
`${this.conversationBaseUrl}/image/${imageKey}`,
);
if (!response.ok) {
throw new Error("Failed to get image URL");
}
const data = await response.json();
return data.url;
getImageUrl(imageKey: string): string {
return `/api/conversation/image/${imageKey}`;
}
async streamQuery(

View File

@@ -425,6 +425,9 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => {
handleQuestionSubmit={handleQuestionSubmit}
setSimbaMode={setSimbaMode}
isLoading={isLoading}
pendingImage={pendingImage}
onImageSelect={(file) => setPendingImage(file)}
onClearImage={() => setPendingImage(null)}
/>
</div>
</footer>

View File

@@ -1,4 +1,3 @@
import { useEffect, useState } from "react";
import { cn } from "../lib/utils";
import { conversationService } from "../api/conversationService";
@@ -8,20 +7,6 @@ type QuestionBubbleProps = {
};
export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [imageError, setImageError] = useState(false);
useEffect(() => {
if (!image_key) return;
conversationService
.getPresignedImageUrl(image_key)
.then(setImageUrl)
.catch((err) => {
console.error("Failed to load image:", err);
setImageError(true);
});
}, [image_key]);
return (
<div className="flex justify-end message-enter">
<div
@@ -32,15 +17,9 @@ export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
"shadow-sm shadow-leaf/10",
)}
>
{imageError && (
<div className="flex items-center gap-2 text-xs text-charcoal/50 bg-charcoal/5 rounded-xl px-3 py-2 mb-2">
<span>🖼</span>
<span>Image failed to load</span>
</div>
)}
{imageUrl && (
{image_key && (
<img
src={imageUrl}
src={conversationService.getImageUrl(image_key)}
alt="Uploaded image"
className="max-w-full rounded-xl mb-2"
/>

View File

@@ -47,16 +47,6 @@ async def get_image(key: str) -> tuple[bytes, str]:
return body, content_type
async def generate_presigned_url(key: str, expires_in: int = 3600) -> str:
async with _get_client() as client:
url = await client.generate_presigned_url(
"get_object",
Params={"Bucket": S3_BUCKET_NAME, "Key": key},
ExpiresIn=expires_in,
)
return url
async def delete_image(key: str) -> None:
async with _get_client() as client:
await client.delete_object(Bucket=S3_BUCKET_NAME, Key=key)