diff --git a/blueprints/conversation/__init__.py b/blueprints/conversation/__init__.py index d19ace2..2ebc915 100644 --- a/blueprints/conversation/__init__.py +++ b/blueprints/conversation/__init__.py @@ -3,7 +3,7 @@ import json import logging import uuid -from quart import Blueprint, Response, jsonify, make_response, request +from quart import Blueprint, jsonify, make_response, request from quart_jwt_extended import ( get_jwt_identity, jwt_refresh_token_required, @@ -12,6 +12,7 @@ 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 @@ -134,15 +135,11 @@ async def upload_image(): @jwt_refresh_token_required async def serve_image(image_key: str): try: - image_bytes, content_type = await s3_get_image(image_key) + url = await s3_presigned_url(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"}, - ) + return jsonify({"url": url}) @conversation_blueprint.post("/stream-query") diff --git a/raggr-frontend/src/api/conversationService.ts b/raggr-frontend/src/api/conversationService.ts index 788089e..1135058 100644 --- a/raggr-frontend/src/api/conversationService.ts +++ b/raggr-frontend/src/api/conversationService.ts @@ -147,8 +147,15 @@ class ConversationService { return await response.json(); } - getImageUrl(imageKey: string): string { - return `/api/conversation/image/${imageKey}`; + async getPresignedImageUrl(imageKey: string): Promise { + 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; } async streamQuery( diff --git a/raggr-frontend/src/components/QuestionBubble.tsx b/raggr-frontend/src/components/QuestionBubble.tsx index b131c11..081c726 100644 --- a/raggr-frontend/src/components/QuestionBubble.tsx +++ b/raggr-frontend/src/components/QuestionBubble.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { cn } from "../lib/utils"; import { conversationService } from "../api/conversationService"; @@ -7,6 +8,13 @@ type QuestionBubbleProps = { }; export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => { + const [imageUrl, setImageUrl] = useState(null); + + useEffect(() => { + if (!image_key) return; + conversationService.getPresignedImageUrl(image_key).then(setImageUrl).catch(() => {}); + }, [image_key]); + return (
{ "shadow-sm shadow-leaf/10", )} > - {image_key && ( + {imageUrl && ( Uploaded image diff --git a/utils/s3_client.py b/utils/s3_client.py index 443ef82..4d941a5 100644 --- a/utils/s3_client.py +++ b/utils/s3_client.py @@ -47,6 +47,16 @@ 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)