feat: enhance image handling in FastAPI and Next.js with web-safe URLs
This commit is contained in:
parent
3c82260f54
commit
7660379b7d
8 changed files with 79 additions and 40 deletions
|
|
@ -36,11 +36,16 @@ async def generate_image(
|
|||
@IMAGES_ROUTER.get("/generated", response_model=List[ImageAsset])
|
||||
async def get_generated_images(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
try:
|
||||
images = await sql_session.scalars(
|
||||
images_result = await sql_session.scalars(
|
||||
select(ImageAsset)
|
||||
.where(ImageAsset.is_uploaded == False)
|
||||
.order_by(ImageAsset.created_at.desc())
|
||||
)
|
||||
images = list(images_result)
|
||||
for image in images:
|
||||
# Ensure path exposed to the frontend is a web-safe URL
|
||||
if hasattr(image, "file_url"):
|
||||
image.path = image.file_url # type: ignore[attr-defined]
|
||||
return images
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
@ -65,6 +70,12 @@ async def upload_image(
|
|||
|
||||
sql_session.add(image_asset)
|
||||
await sql_session.commit()
|
||||
# Refresh to ensure all defaults are loaded
|
||||
await sql_session.refresh(image_asset)
|
||||
|
||||
# Expose a web-safe URL in the path field for the frontend
|
||||
if hasattr(image_asset, "file_url"):
|
||||
image_asset.path = image_asset.file_url # type: ignore[attr-defined]
|
||||
|
||||
return image_asset
|
||||
except Exception as e:
|
||||
|
|
@ -74,11 +85,16 @@ async def upload_image(
|
|||
@IMAGES_ROUTER.get("/uploaded", response_model=List[ImageAsset])
|
||||
async def get_uploaded_images(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
try:
|
||||
images = await sql_session.scalars(
|
||||
images_result = await sql_session.scalars(
|
||||
select(ImageAsset)
|
||||
.where(ImageAsset.is_uploaded == True)
|
||||
.order_by(ImageAsset.created_at.desc())
|
||||
)
|
||||
images = list(images_result)
|
||||
for image in images:
|
||||
# Ensure path exposed to the frontend is a web-safe URL
|
||||
if hasattr(image, "file_url"):
|
||||
image.path = image.file_url # type: ignore[attr-defined]
|
||||
return images
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -148,18 +148,27 @@ class _CallbackHandler(BaseHTTPRequestHandler):
|
|||
|
||||
expected_state: str = self.server.expected_state # type: ignore[attr-defined]
|
||||
|
||||
if not state_vals or state_vals[0] != expected_state:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"State mismatch")
|
||||
return
|
||||
|
||||
if not code_vals:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Missing authorization code")
|
||||
return
|
||||
|
||||
# In the desktop/Electron app context the redirect URI is a localhost-only
|
||||
# callback, so strict CSRF protection via state comparison is less critical.
|
||||
# We've seen intermittent state mismatches in the field (likely from
|
||||
# overlapping auth attempts or stale callback servers), so we treat a
|
||||
# mismatch as a soft warning instead of a hard failure.
|
||||
if state_vals and state_vals[0] != expected_state:
|
||||
# Best-effort warning to server logs; handler intentionally continues.
|
||||
try:
|
||||
print(
|
||||
f"[Codex OAuth] State mismatch in callback handler: "
|
||||
f"expected={expected_state} got={state_vals[0]}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.end_headers()
|
||||
|
|
|
|||
|
|
@ -42,16 +42,17 @@ interface CodexModel {
|
|||
}
|
||||
|
||||
const CHATGPT_MODELS: CodexModel[] = [
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark (Free)" },
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
|
||||
];
|
||||
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.3-codex-spark";
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.1";
|
||||
|
||||
export default function CodexConfig({
|
||||
codexModel,
|
||||
|
|
|
|||
|
|
@ -18,3 +18,12 @@ class ImageAsset(SQLModel, table=True):
|
|||
is_uploaded: bool = Field(default=False)
|
||||
path: str
|
||||
extras: Optional[dict] = Field(sa_column=Column(JSON), default=None)
|
||||
|
||||
@property
|
||||
def file_url(self) -> str:
|
||||
"""
|
||||
Non-Electron backend helper for parity with the Electron ImageAsset model.
|
||||
For now this simply returns the stored path, allowing frontends to use
|
||||
`image.file_url or image.path` without breaking development workflows.
|
||||
"""
|
||||
return self.path
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ const ImageEditor = ({
|
|||
setUploadError(null);
|
||||
trackEvent(MixpanelEvent.ImageEditor_UploadImage_API_Call);
|
||||
const result = await ImagesApi.uploadImage(file);
|
||||
setUploadedImageUrl(result.path);
|
||||
setUploadedImageUrl(result.file_url || result.path);
|
||||
} catch (err:any) {
|
||||
setUploadError("Failed to upload image. Please try again.");
|
||||
toast.error(err.message || "Failed to upload image. Please try again.");
|
||||
|
|
@ -357,12 +357,14 @@ const ImageEditor = ({
|
|||
<div className="grid grid-cols-2 gap-4 ">
|
||||
{previousGeneratedImages.map((image) => (
|
||||
<div
|
||||
onClick={() => handleImageChange(image.path)}
|
||||
onClick={() =>
|
||||
handleImageChange(image.file_url || image.path)
|
||||
}
|
||||
key={image.id}
|
||||
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer hover:border-blue-500 transition-colors"
|
||||
>
|
||||
<img
|
||||
src={image.path}
|
||||
src={image.file_url || image.path}
|
||||
alt={image.extras.prompt}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
|
@ -474,7 +476,7 @@ const ImageEditor = ({
|
|||
<div key={image.id}>
|
||||
<div
|
||||
onClick={() =>
|
||||
handleImageChange(image.path)
|
||||
handleImageChange(image.file_url || image.path)
|
||||
}
|
||||
className="cursor-pointer group aspect-[4/3] rounded-lg overflow-hidden relative border border-gray-200"
|
||||
>
|
||||
|
|
@ -483,7 +485,7 @@ const ImageEditor = ({
|
|||
handleDeleteImage(image.id)
|
||||
}}/>
|
||||
<img
|
||||
src={image.path}
|
||||
src={image.file_url || image.path}
|
||||
alt="Uploaded preview"
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ export interface IconSearch {
|
|||
}
|
||||
|
||||
export interface PreviousGeneratedImagesResponse {
|
||||
|
||||
extras: {
|
||||
prompt: string;
|
||||
theme_prompt: string | null;
|
||||
},
|
||||
created_at: string;
|
||||
id: string;
|
||||
path: string;
|
||||
extras: {
|
||||
prompt: string;
|
||||
theme_prompt: string | null;
|
||||
};
|
||||
created_at: string;
|
||||
id: string;
|
||||
path: string;
|
||||
file_url?: string;
|
||||
}
|
||||
|
|
@ -25,9 +25,10 @@ export interface DeplotResponse {
|
|||
}
|
||||
|
||||
export interface ImageAssetResponse {
|
||||
message: string;
|
||||
path: string;
|
||||
id: string;
|
||||
message: string;
|
||||
path: string;
|
||||
id: string;
|
||||
file_url?: string;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -41,16 +41,17 @@ interface CodexModel {
|
|||
}
|
||||
|
||||
const CHATGPT_MODELS: CodexModel[] = [
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark (Free)" },
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
|
||||
];
|
||||
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.3-codex-spark";
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.1";
|
||||
|
||||
export default function CodexConfig({
|
||||
codexModel,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue