Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copy this file to .env and fill in real values.
# .env is gitignored — never commit real credentials.

# Postgres (Docker Compose defaults work for local dev)
POSTGRES_USER=summarization
POSTGRES_PASSWORD=localdev
POSTGRES_DB=summarization

# Better Auth — generate with: openssl rand -hex 32
BETTER_AUTH_SECRET=

# Login allowlist — comma-separated emails, no spaces. Shared by both the
# backend and the auth sidecar (single source of truth). Empty = allow all (dev mode).
ALLOWED_EMAILS=

# GitHub OAuth App (optional — email/password auth works without it)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# Azure Document Intelligence
# Azure Portal → your Cognitive Services resource → Keys and Endpoint
AZURE_DOC_INTELLIGENCE_ENDPOINT=
AZURE_DOC_INTELLIGENCE_KEY=

# Azure OpenAI (AI Foundry)
# Azure Portal → your AI Foundry project → Deployments
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_KEY=
AZURE_OPENAI_DEPLOYMENT=
AZURE_OPENAI_MODEL_NAME=
AZURE_OPENAI_API_VERSION=2025-04-01-preview

# Upload limits — enforced by the backend at /api/upload
MAX_UPLOAD_SIZE_MB=50
MAX_PAGES=500
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,9 @@ htmlcov/
# Local docker-compose overrides (never commit)
docker-compose.override.yml

# Operational docs with live credentials — never commit
docs/deployment-runbook.md
docs/session-transcript.md

# External repo checkout — separate repository, not part of this project
VLLM-Service/
33 changes: 29 additions & 4 deletions backend/api/files/router.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""File management API endpoints with organized storage and deduplication"""

import os

import fitz
from fastapi import APIRouter, File, UploadFile, HTTPException, Request, Depends
from fastapi.responses import JSONResponse, Response
from typing import Optional
Expand Down Expand Up @@ -48,10 +51,13 @@ async def upload_file(
"""
print(f"[UPLOAD] Request headers: {request.headers}")

max_upload_mb = int(os.environ.get("MAX_UPLOAD_SIZE_MB", "50"))
max_upload_bytes = max_upload_mb * 1024 * 1024

# Check content-length header
if "content-length" in request.headers:
content_length = int(request.headers["content-length"])
if content_length > 25 * 1024 * 1024: # 25MB
if content_length > max_upload_bytes:
raise HTTPException(
status_code=413,
detail=(
Expand Down Expand Up @@ -86,9 +92,28 @@ async def upload_file(
detail="File is not a valid PDF. The file content does not match PDF format.",
)

# Validate file size (20MB limit)
if file_size > 20 * 1024 * 1024:
raise HTTPException(status_code=400, detail="File size exceeds 20MB limit")
# Validate file size
if file_size > max_upload_bytes:
raise HTTPException(
status_code=400, detail=f"File size exceeds {max_upload_mb}MB limit"
)

# Validate page count
max_pages = int(os.environ.get("MAX_PAGES", "500"))
try:
pdf = fitz.open(stream=content, filetype="pdf")
page_count = pdf.page_count
pdf.close()
except Exception:
raise HTTPException(
status_code=400,
detail="File is not a valid PDF. The file content does not match PDF format.",
)
if page_count > max_pages:
raise HTTPException(
status_code=400,
detail=f"PDF has {page_count} pages, exceeds the {max_pages} page limit",
)

# Get user ID if authenticated
user_id = current_user["id"] if current_user else None
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ services:
PORT: "3001"
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
ports:
- "3001:3001"
depends_on:
Expand Down Expand Up @@ -70,6 +71,9 @@ services:
# Azure Document Intelligence — sourced from .env
AZURE_DOC_INTELLIGENCE_ENDPOINT: ${AZURE_DOC_INTELLIGENCE_ENDPOINT:-}
AZURE_DOC_INTELLIGENCE_KEY: ${AZURE_DOC_INTELLIGENCE_KEY:-}
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
MAX_UPLOAD_SIZE_MB: ${MAX_UPLOAD_SIZE_MB:-50}
MAX_PAGES: ${MAX_PAGES:-500}
ports:
- "8001:8001"
depends_on:
Expand Down
31 changes: 9 additions & 22 deletions frontend/components/BatchStudySelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { DocumentData } from "../App";
import { loadStudyTypeTemplate } from "./TemplateLoader";
import { TemplatePicker, ResolvedTemplate } from "./TemplatePicker";
import { settingsManager, ModelConfig } from "./SettingsManager";
import { pickBestFromList } from "../utils/modelSelection";
import { Input } from "./ui/input";
import { Textarea } from "./ui/textarea";
import { Label } from "./ui/label";
Expand Down Expand Up @@ -114,30 +115,16 @@ export function BatchStudySelectionPage({
const models = await settingsManager.getAvailableModelsAsync();
setAvailableModels(models);

// Auto-select Gemini 2.5 Flash Lite by default only if nothing selected yet
// (including from restored session data)
// Auto-select the org default model only if nothing selected yet
// (including from restored session data). Uses the same priority
// ranking as the Chat page so both pages agree on the default.
if (selectedModels.length === 0 && !documentData.selectedModels?.length) {
const gemini25FlashLite = models.find(
(m) =>
m.id.toLowerCase().includes("gemini") &&
m.id.toLowerCase().includes("2.5") &&
(m.id.toLowerCase().includes("flash") ||
m.id.toLowerCase().includes("lite"))
);

const best = pickBestFromList(models);
const defaultModels: string[] = [];
if (gemini25FlashLite) {
defaultModels.push(gemini25FlashLite.id);
} else {
// Fallback: select first Gemini model or first model overall
const anyGemini = models.find((m) =>
m.id.toLowerCase().includes("gemini")
);
if (anyGemini) {
defaultModels.push(anyGemini.id);
} else if (models.length > 0) {
defaultModels.push(models[0].id);
}
if (best) {
defaultModels.push(best.modelId);
} else if (models.length > 0) {
defaultModels.push(models[0].id);
}

setSelectedModels(defaultModels);
Expand Down
20 changes: 15 additions & 5 deletions frontend/components/UploadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function UploadPage({
}, [processingFiles.size, uploadingFiles.size]);

const MAX_FILES = 10;
const MAX_FILE_SIZE_MB = 50;

const allParsers = [
{
Expand Down Expand Up @@ -157,12 +158,20 @@ export function UploadPage({
file.type === "application/pdf" ||
file.name.toLowerCase().endsWith(".pdf");

if (isPDF) {
console.log("File accepted:", file.name);
newFiles.push(file);
} else {
if (!isPDF) {
console.log("File rejected - not a PDF:", file.name, file.type);
continue;
}

if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
toast.warning(
`File "${file.name}" exceeds the ${MAX_FILE_SIZE_MB}MB limit.`
);
continue;
}

console.log("File accepted:", file.name);
newFiles.push(file);
}

if (newFiles.length > 0) {
Expand Down Expand Up @@ -602,7 +611,8 @@ export function UploadPage({
</Button>
</div>
<p className="text-xs text-muted-foreground mt-2">
Supports PDF files up to 20MB. Max {MAX_FILES} files.
Supports PDF files up to {MAX_FILE_SIZE_MB}MB. Max {MAX_FILES}{" "}
files.
</p>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/utils/modelSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const MODEL_PRIORITY: Array<{
match: (m: ModelConfig) => boolean;
modelType: string;
}> = [
// Tier 0 — org default
{
match: (m) => m.provider === "Azure" && m.name === "gpt-5.4-nano",
modelType: "azure",
},
// Tier 1 — frontier reasoning (cost-efficient first)
{
match: (m) => m.id?.includes("gemini-3-pro"),
Expand Down
10 changes: 10 additions & 0 deletions infra/container-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ properties:
value: "${AZURE_STORAGE_CONNECTION_STRING}"
- name: acr-password
value: "${ACR_PASSWORD}"
- name: allowed-emails
value: "${ALLOWED_EMAILS}"
registries:
- server: ${ACR_NAME}.azurecr.io
username: ${ACR_USERNAME}
Expand Down Expand Up @@ -58,6 +60,12 @@ properties:
secretRef: storage-connection-string
- name: AZURE_STORAGE_CONTAINER_NAME
value: summarization-uploads
- name: ALLOWED_EMAILS
secretRef: allowed-emails
- name: MAX_UPLOAD_SIZE_MB
value: "50"
- name: MAX_PAGES
value: "500"
probes:
- type: liveness
httpGet:
Expand Down Expand Up @@ -95,6 +103,8 @@ properties:
secretRef: github-client-secret
- name: PORT
value: "3001"
- name: ALLOWED_EMAILS
secretRef: allowed-emails
probes:
- type: liveness
httpGet:
Expand Down
1 change: 1 addition & 0 deletions infra/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ set -euo pipefail
: "${GITHUB_CLIENT_SECRET:?Need GITHUB_CLIENT_SECRET}"
: "${BETTER_AUTH_SECRET:?Need BETTER_AUTH_SECRET}"
: "${AZURE_STORAGE_CONNECTION_STRING:?Need AZURE_STORAGE_CONNECTION_STRING}"
: "${ALLOWED_EMAILS:?Need ALLOWED_EMAILS}" # comma-separated, no spaces — shared by backend + auth-sidecar

# ---------------------------------------------------------------------------
# Substitute placeholders → generate ephemeral YAML, never written to disk
Expand Down
Loading