Clean up presigned URL implementation: remove dead fields, fix error handling

- Remove unused image_url from upload response and TS type
- Remove bare except in serve_image that masked config errors as 404s
- Add error state and broken-image placeholder in QuestionBubble

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:49:01 -04:00
parent b62a8b6b3f
commit 64dab18428
3 changed files with 17 additions and 13 deletions

View File

@@ -123,22 +123,13 @@ async def upload_image():
await s3_upload_image(processed_bytes, key, output_content_type) await s3_upload_image(processed_bytes, key, output_content_type)
return jsonify( return jsonify({"image_key": key})
{
"image_key": key,
"image_url": f"/api/conversation/image/{key}",
}
)
@conversation_blueprint.get("/image/<path:image_key>") @conversation_blueprint.get("/image/<path:image_key>")
@jwt_refresh_token_required @jwt_refresh_token_required
async def serve_image(image_key: str): async def serve_image(image_key: str):
try:
url = await s3_presigned_url(image_key) url = await s3_presigned_url(image_key)
except Exception:
return jsonify({"error": "Image not found"}), 404
return jsonify({"url": url}) return jsonify({"url": url})

View File

@@ -125,7 +125,7 @@ class ConversationService {
async uploadImage( async uploadImage(
file: File, file: File,
conversationId: string, conversationId: string,
): Promise<{ image_key: string; image_url: string }> { ): Promise<{ image_key: string }> {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("conversation_id", conversationId); formData.append("conversation_id", conversationId);

View File

@@ -9,10 +9,17 @@ type QuestionBubbleProps = {
export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => { export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
const [imageUrl, setImageUrl] = useState<string | null>(null); const [imageUrl, setImageUrl] = useState<string | null>(null);
const [imageError, setImageError] = useState(false);
useEffect(() => { useEffect(() => {
if (!image_key) return; if (!image_key) return;
conversationService.getPresignedImageUrl(image_key).then(setImageUrl).catch(() => {}); conversationService
.getPresignedImageUrl(image_key)
.then(setImageUrl)
.catch((err) => {
console.error("Failed to load image:", err);
setImageError(true);
});
}, [image_key]); }, [image_key]);
return ( return (
@@ -25,6 +32,12 @@ export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
"shadow-sm shadow-leaf/10", "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 && ( {imageUrl && (
<img <img
src={imageUrl} src={imageUrl}