diff --git a/routes/upload_routes.py b/routes/upload_routes.py
index cba32f0f7..7df194e9a 100644
--- a/routes/upload_routes.py
+++ b/routes/upload_routes.py
@@ -6,11 +6,11 @@ import asyncio
import shutil
import uuid
from pathlib import Path
-from fastapi import APIRouter, Request, File, UploadFile, HTTPException
-from typing import List
+from fastapi import APIRouter, Request, File, UploadFile, HTTPException, Form
+from typing import List, Optional
import logging
from core.middleware import require_admin
-from core.database import SessionLocal, GalleryImage
+from core.database import SessionLocal, GalleryImage, Session as DbSession
from src.auth_helpers import effective_user
from src.constants import GENERATED_IMAGES_DIR
from src.upload_handler import count_recent_uploads
@@ -56,7 +56,17 @@ def setup_upload_routes(upload_handler):
raise HTTPException(404, "File not found")
- def _promote_chat_image_to_gallery(meta: dict, owner: str | None) -> str | None:
+ def _valid_session_id_for_owner(db, session_id: str | None, owner: str | None) -> str | None:
+ if not session_id:
+ return None
+ sess = db.query(DbSession).filter(DbSession.id == session_id).first()
+ if not sess:
+ return None
+ if owner and sess.owner and sess.owner != owner:
+ return None
+ return session_id
+
+ def _promote_chat_image_to_gallery(meta: dict, owner: str | None, session_id: str | None = None) -> str | None:
"""Make chat-uploaded images visible in Gallery without changing chat storage."""
is_image_file = getattr(upload_handler, "is_image_file", None)
if not callable(is_image_file):
@@ -105,6 +115,7 @@ def setup_upload_routes(upload_handler):
prompt=meta.get("name") or "Chat upload",
model="chat-upload",
owner=owner,
+ session_id=_valid_session_id_for_owner(db, session_id, owner),
file_hash=file_hash,
width=meta.get("width"),
height=meta.get("height"),
@@ -120,7 +131,11 @@ def setup_upload_routes(upload_handler):
db.close()
@router.post("")
- async def api_upload(request: Request, files: List[UploadFile] = File(...)):
+ async def api_upload(
+ request: Request,
+ files: List[UploadFile] = File(...),
+ session_id: Optional[str] = Form(None),
+ ):
"""Upload files with enhanced security and organization."""
if not files:
raise HTTPException(400, "No files uploaded")
@@ -148,7 +163,7 @@ def setup_upload_routes(upload_handler):
try:
owner = effective_user(request)
meta = upload_handler.save_upload(u, client_ip, owner=owner)
- gallery_id = _promote_chat_image_to_gallery(meta, owner)
+ gallery_id = _promote_chat_image_to_gallery(meta, owner, session_id)
item = {
"id": meta["id"],
"name": meta["name"],
diff --git a/static/js/chat.js b/static/js/chat.js
index d7b19dd85..5c3e98ec1 100644
--- a/static/js/chat.js
+++ b/static/js/chat.js
@@ -686,7 +686,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
let ids = [];
try {
- ids = await fileHandlerModule.uploadPending();
+ ids = await fileHandlerModule.uploadPending({ sessionId: sessionModule.getCurrentSessionId() });
} catch(e) {
console.error('upload failed', e);
}
diff --git a/static/js/fileHandler.js b/static/js/fileHandler.js
index 529075bc7..09cb8caf7 100644
--- a/static/js/fileHandler.js
+++ b/static/js/fileHandler.js
@@ -142,7 +142,7 @@ export function removePending(idx) {
/**
* Upload all pending files to server
*/
-export async function uploadPending() {
+export async function uploadPending(opts = {}) {
if (pendingFiles.length === 0) return [];
_lastUploadCancelled = false;
@@ -169,6 +169,7 @@ export async function uploadPending() {
const fd = new FormData();
pendingFiles.forEach(f => fd.append('files', f, f.name || 'paste.png'));
+ if (opts.sessionId) fd.append('session_id', opts.sessionId);
_uploadAbortCtrl = new AbortController();
_uploading = true;
const timeoutId = setTimeout(() => {
diff --git a/static/js/gallery.js b/static/js/gallery.js
index 09abbe322..b74a90f53 100644
--- a/static/js/gallery.js
+++ b/static/js/gallery.js
@@ -8,6 +8,8 @@ import spinnerModule from './spinner.js';
import { makeWindowDraggable } from './windowDrag.js';
import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
import { topPortalZ } from './toolWindowZOrder.js';
+import sessionModule from './sessions.js';
+import fileHandlerModule from './fileHandler.js';
const API_BASE = window.location.origin;
let _open = false;
@@ -1356,6 +1358,10 @@ function _openDetail(img) {
Edit
+
@@ -1462,6 +1468,45 @@ function _openDetail(img) {
detail.style.display = 'none';
});
+ document.getElementById('gallery-chat-photo-btn')?.addEventListener('click', async () => {
+ if (img.session_id) {
+ closeGallery();
+ try {
+ await sessionModule.selectSession(img.session_id);
+ } catch (e) {
+ console.error('Open source chat failed:', e);
+ uiModule.showError && uiModule.showError('Could not open source chat');
+ }
+ return;
+ }
+
+ try {
+ const dcRes = await fetch('/api/default-chat', { credentials: 'same-origin' });
+ const dc = await dcRes.json();
+ if (!dc?.endpoint_url || !dc?.model) {
+ uiModule.showError && uiModule.showError('Pick a chat model first');
+ return;
+ }
+ sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id);
+ closeGallery();
+ const res = await fetch(img.url, { credentials: 'same-origin' });
+ if (!res.ok) throw new Error('image fetch ' + res.status);
+ const blob = await res.blob();
+ const name = img.filename || 'gallery-photo.jpg';
+ const file = new File([blob], name, { type: blob.type || 'image/jpeg' });
+ fileHandlerModule.addFiles([file]);
+ const input = document.getElementById('message');
+ if (input) {
+ input.value = 'Let’s discuss this photo.';
+ input.dispatchEvent(new Event('input'));
+ input.focus();
+ }
+ } catch (e) {
+ console.error('Discuss photo failed:', e);
+ uiModule.showError && uiModule.showError('Could not start photo chat');
+ }
+ });
+
// Clickable tag chips — both AI Tags and User Tags. Clicking a chip
// closes the detail, sets the tag filter on the main grid, and
// re-fetches so the user sees other photos with that tag.
@@ -1967,9 +2012,9 @@ export function openGallery() {