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() {