Link gallery uploads back to chat

This commit is contained in:
pewdiepie-archdaemon
2026-06-29 02:34:11 +00:00
parent e2c8b8eb37
commit 4a7c03d536
4 changed files with 72 additions and 11 deletions
+21 -6
View File
@@ -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"],
+1 -1
View File
@@ -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);
}
+2 -1
View File
@@ -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(() => {
+48 -3
View File
@@ -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) {
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
Edit
</button>
<button class="gallery-detail-back" id="gallery-chat-photo-btn" title="${img.session_id ? 'Open source chat' : 'Start a new chat with this photo'}" aria-label="${img.session_id ? 'Open source chat' : 'Discuss photo'}" style="display:inline-flex;align-items:center;gap:4px;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></svg>
${img.session_id ? 'Open chat' : 'Discuss'}
</button>
<button class="gallery-detail-back gallery-detail-fav-header${img.favorite ? ' active' : ''}" id="gallery-detail-fav-header" title="${img.favorite ? 'Unfavorite' : 'Favorite'}" aria-label="Favorite" aria-pressed="${img.favorite ? 'true' : 'false'}" style="display:inline-flex;align-items:center;justify-content:center;padding:4px 8px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="${img.favorite ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</button>
@@ -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 = 'Lets 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() {
<option value="">All sources</option>
</select>
<select class="gallery-sort" id="gallery-sort">
<option value="shuffle"${_sort === 'shuffle' ? ' selected' : ''}>Random</option>
<option value="recent"${_sort === 'recent' ? ' selected' : ''}>Recent</option>
<option value="oldest"${_sort === 'oldest' ? ' selected' : ''}>Oldest</option>
<option value="shuffle"${_sort === 'shuffle' ? ' selected' : ''}>Random order</option>
<option value="recent"${_sort === 'recent' ? ' selected' : ''}>↓ Newest first</option>
<option value="oldest"${_sort === 'oldest' ? ' selected' : ''}>Oldest first</option>
</select>
<button class="gallery-select-btn gallery-toolbar-action" id="gallery-select-btn" title="Select for bulk actions"><span style="position:relative;top:1px;">Select</span></button>
</div>