Compare commits
1 Commits
fix/image-
...
feat/makef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3671926430 |
5
Makefile
5
Makefile
@@ -1,8 +1,11 @@
|
|||||||
.PHONY: deploy build up down restart logs migrate migrate-new frontend test
|
.PHONY: deploy redeploy build up down restart logs migrate migrate-new frontend test
|
||||||
|
|
||||||
# Build and deploy
|
# Build and deploy
|
||||||
deploy: build up
|
deploy: build up
|
||||||
|
|
||||||
|
redeploy:
|
||||||
|
git pull && $(MAKE) down && $(MAKE) up
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker compose build raggr
|
docker compose build raggr
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from quart import Blueprint, jsonify, make_response, request
|
from quart import Blueprint, Response, jsonify, make_response, request
|
||||||
from quart_jwt_extended import (
|
from quart_jwt_extended import (
|
||||||
get_jwt_identity,
|
get_jwt_identity,
|
||||||
jwt_refresh_token_required,
|
jwt_refresh_token_required,
|
||||||
@@ -12,7 +12,6 @@ from quart_jwt_extended import (
|
|||||||
import blueprints.users.models
|
import blueprints.users.models
|
||||||
from utils.image_process import analyze_user_image
|
from utils.image_process import analyze_user_image
|
||||||
from utils.image_upload import ImageValidationError, process_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 get_image as s3_get_image
|
||||||
from utils.s3_client import upload_image as s3_upload_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)
|
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>")
|
@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):
|
||||||
url = await s3_presigned_url(image_key)
|
try:
|
||||||
return jsonify({"url": url})
|
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")
|
@conversation_blueprint.post("/stream-query")
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class ConversationService {
|
|||||||
async uploadImage(
|
async uploadImage(
|
||||||
file: File,
|
file: File,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
): Promise<{ image_key: string }> {
|
): Promise<{ image_key: string; image_url: 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);
|
||||||
@@ -147,15 +147,8 @@ class ConversationService {
|
|||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPresignedImageUrl(imageKey: string): Promise<string> {
|
getImageUrl(imageKey: string): string {
|
||||||
const response = await userService.fetchWithRefreshToken(
|
return `/api/conversation/image/${imageKey}`;
|
||||||
`${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(
|
async streamQuery(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { conversationService } from "../api/conversationService";
|
import { conversationService } from "../api/conversationService";
|
||||||
|
|
||||||
@@ -8,20 +7,6 @@ type QuestionBubbleProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const QuestionBubble = ({ text, image_key }: 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 (
|
return (
|
||||||
<div className="flex justify-end message-enter">
|
<div className="flex justify-end message-enter">
|
||||||
<div
|
<div
|
||||||
@@ -32,15 +17,9 @@ export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
|
|||||||
"shadow-sm shadow-leaf/10",
|
"shadow-sm shadow-leaf/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{imageError && (
|
{image_key && (
|
||||||
<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 && (
|
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={conversationService.getImageUrl(image_key)}
|
||||||
alt="Uploaded image"
|
alt="Uploaded image"
|
||||||
className="max-w-full rounded-xl mb-2"
|
className="max-w-full rounded-xl mb-2"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -47,16 +47,6 @@ async def get_image(key: str) -> tuple[bytes, str]:
|
|||||||
return body, content_type
|
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 def delete_image(key: str) -> None:
|
||||||
async with _get_client() as client:
|
async with _get_client() as client:
|
||||||
await client.delete_object(Bucket=S3_BUCKET_NAME, Key=key)
|
await client.delete_object(Bucket=S3_BUCKET_NAME, Key=key)
|
||||||
|
|||||||
Reference in New Issue
Block a user