Spaces:
Sleeping
Sleeping
Deploy Learn Pathophysiology WC3 Edition
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +13 -0
- .gitattributes +8 -33
- Dockerfile +45 -0
- README.md +87 -6
- backend/api.py +441 -0
- backend/requirements.txt +8 -0
- backend/static/any/hud_time_indicator.png +3 -0
- backend/static/any/resource_icons/gold.png +3 -0
- backend/static/any/resource_icons/lumber.png +3 -0
- backend/static/any/resource_icons/supply.png +3 -0
- backend/static/assets/index-BSpkv-VB.js +0 -0
- backend/static/assets/index-BzhPXs6E.css +1 -0
- backend/static/hum/btn_default.png +3 -0
- backend/static/hum/btn_disabled.png +3 -0
- backend/static/hum/btn_hover_bg.png +3 -0
- backend/static/hum/btn_pressed.png +3 -0
- backend/static/hum/cursor.png +3 -0
- backend/static/hum/hud_footer.png +3 -0
- backend/static/hum/hud_header.png +3 -0
- backend/static/hum/hud_inv_mock.png +3 -0
- backend/static/hum/hud_inv_mock_slot.png +3 -0
- backend/static/index.html +17 -0
- backend/static/nel/btn_default.png +3 -0
- backend/static/nel/btn_disabled.png +3 -0
- backend/static/nel/btn_hover_bg.png +3 -0
- backend/static/nel/btn_pressed.png +3 -0
- backend/static/nel/cursor.png +3 -0
- backend/static/nel/hud_footer.png +3 -0
- backend/static/nel/hud_header.png +3 -0
- backend/static/nel/hud_inv_mock.png +3 -0
- backend/static/nel/hud_inv_mock_slot.png +3 -0
- backend/static/orc/btn_default.png +3 -0
- backend/static/orc/btn_disabled.png +3 -0
- backend/static/orc/btn_hover_bg.png +3 -0
- backend/static/orc/btn_pressed.png +3 -0
- backend/static/orc/cursor.png +3 -0
- backend/static/orc/hud_footer.png +3 -0
- backend/static/orc/hud_header.png +3 -0
- backend/static/orc/hud_inv_mock.png +3 -0
- backend/static/orc/hud_inv_mock_slot.png +3 -0
- backend/static/und/btn_default.png +3 -0
- backend/static/und/btn_disabled.png +3 -0
- backend/static/und/btn_hover_bg.png +3 -0
- backend/static/und/btn_pressed.png +3 -0
- backend/static/und/cursor.png +3 -0
- backend/static/und/hud_footer.png +3 -0
- backend/static/und/hud_header.png +3 -0
- backend/static/und/hud_inv_mock.png +3 -0
- backend/static/und/hud_inv_mock_slot.png +3 -0
- chroma_db/34c32620-31ab-4a2f-bd92-3fcfd7be3432/data_level0.bin +3 -0
.dockerignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
.Python
|
| 5 |
+
*.egg
|
| 6 |
+
*.egg-info
|
| 7 |
+
.env
|
| 8 |
+
.venv
|
| 9 |
+
venv/
|
| 10 |
+
node_modules/
|
| 11 |
+
*.log
|
| 12 |
+
.DS_Store
|
| 13 |
+
.git
|
.gitattributes
CHANGED
|
@@ -1,35 +1,10 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
chroma_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
chroma_db/**/*.bin filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
chroma_db/*.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
*.pickle filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ===========================================================================
|
| 2 |
+
# Learn Pathophysiology — WC3 Edition
|
| 3 |
+
# Multi-stage build: Vue frontend + FastAPI backend
|
| 4 |
+
# Deploy: HuggingFace Spaces (Docker SDK)
|
| 5 |
+
# ===========================================================================
|
| 6 |
+
|
| 7 |
+
# ---------- Stage 1: Build Vue Frontend ----------
|
| 8 |
+
FROM node:20-slim AS frontend-build
|
| 9 |
+
|
| 10 |
+
WORKDIR /build
|
| 11 |
+
COPY frontend/package.json frontend/package-lock.json* ./
|
| 12 |
+
RUN npm install
|
| 13 |
+
COPY frontend/ ./
|
| 14 |
+
RUN npm run build
|
| 15 |
+
# Output goes to /build/dist/
|
| 16 |
+
|
| 17 |
+
# ---------- Stage 2: Python Backend + Serve Static ----------
|
| 18 |
+
FROM python:3.11-slim
|
| 19 |
+
|
| 20 |
+
WORKDIR /app
|
| 21 |
+
|
| 22 |
+
# Install Python deps
|
| 23 |
+
COPY backend/requirements.txt .
|
| 24 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Copy backend code
|
| 27 |
+
COPY backend/ ./
|
| 28 |
+
|
| 29 |
+
# Copy built frontend from stage 1
|
| 30 |
+
COPY --from=frontend-build /build/dist ./static/
|
| 31 |
+
|
| 32 |
+
# Copy chroma_db (must be present in docker build context)
|
| 33 |
+
COPY chroma_db/ ./chroma_db/
|
| 34 |
+
|
| 35 |
+
# Environment
|
| 36 |
+
ENV PORT=7860
|
| 37 |
+
ENV CHROMA_DIR=/app/chroma_db
|
| 38 |
+
# Auth (set via HuggingFace Spaces secrets)
|
| 39 |
+
# ENV GOOGLE_CLIENT_ID=...
|
| 40 |
+
# ENV JWT_SECRET=...
|
| 41 |
+
# ENV GEMINI_API_KEY=...
|
| 42 |
+
|
| 43 |
+
EXPOSE 7860
|
| 44 |
+
|
| 45 |
+
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,12 +1,93 @@
|
|
| 1 |
---
|
| 2 |
title: Learn Pathophysiology
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
-
license: apache-2.0
|
| 9 |
-
short_description: MVP for an AI enabled pathophysiology learning app
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Learn Pathophysiology
|
| 3 |
+
emoji: ⚔️
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Learn Pathophysiology - WC3 Edition ⚔️
|
| 12 |
+
|
| 13 |
+
AI-powered chatbot for learning **Pathophysiology** with authentic **Warcraft 3** styling!
|
| 14 |
+
|
| 15 |
+

|
| 16 |
+

|
| 17 |
+

|
| 18 |
+
|
| 19 |
+
## 🎮 Features
|
| 20 |
+
|
| 21 |
+
### 💬 AI Chat
|
| 22 |
+
Ask questions about Pathophysiology in Croatian and get detailed, RAG-powered answers grounded in the official Gamulin textbook.
|
| 23 |
+
|
| 24 |
+
### 📸 Image Analysis
|
| 25 |
+
Upload textbook pages and get AI-powered analysis, explanations, and Q&A.
|
| 26 |
+
|
| 27 |
+
### 🎨 WC3-Themed Interface
|
| 28 |
+
Choose from 4 authentic Warcraft 3 race themes:
|
| 29 |
+
- **Human** - Blue/silver Alliance theme
|
| 30 |
+
- **Orc** - Red/brown Horde theme
|
| 31 |
+
- **Night Elf** - Purple/teal nature theme
|
| 32 |
+
- **Undead** - Green/dark Scourge theme
|
| 33 |
+
|
| 34 |
+
### 🤖 Multi-Model Support
|
| 35 |
+
- **Gemini 3 Flash Preview** - Fastest, newest model
|
| 36 |
+
- **Gemini 2.5 Flash** - Fast and reliable
|
| 37 |
+
- **Gemini 2.5 Pro** - Most capable for complex questions
|
| 38 |
+
|
| 39 |
+
### 📚 RAG-Powered
|
| 40 |
+
All answers are grounded in the official **Patofiziologija (Gamulin, Marušić, Kovač)** textbook, with citations to page numbers.
|
| 41 |
+
|
| 42 |
+
## 🏗️ Technology Stack
|
| 43 |
+
|
| 44 |
+
- **Frontend**: Vue 3 + TypeScript + Custom WC3 UI library
|
| 45 |
+
- **Backend**: FastAPI (Python)
|
| 46 |
+
- **AI**: Google Gemini API
|
| 47 |
+
- **RAG**: ChromaDB vector database
|
| 48 |
+
- **Deployment**: Docker
|
| 49 |
+
|
| 50 |
+
## 🎓 For Medical Students
|
| 51 |
+
|
| 52 |
+
This app is specifically designed for medical students at **University of Split School of Medicine** to help master Pathophysiology through:
|
| 53 |
+
|
| 54 |
+
1. **Interactive Q&A** - Ask anything about Pathophysiology
|
| 55 |
+
2. **Contextual Learning** - Get explanations grounded in your textbook
|
| 56 |
+
3. **Image Analysis** - Understand complex diagrams and pages
|
| 57 |
+
4. **Citation Support** - See exactly where information comes from
|
| 58 |
+
|
| 59 |
+
## 🚀 Usage
|
| 60 |
+
|
| 61 |
+
Simply:
|
| 62 |
+
1. Select your preferred AI model (top-left sidebar)
|
| 63 |
+
2. Choose your favorite WC3 race theme
|
| 64 |
+
3. Start chatting or upload an image!
|
| 65 |
+
|
| 66 |
+
### Example Questions (in Croatian):
|
| 67 |
+
- "Što je hipertenzija i kako nastaje?"
|
| 68 |
+
- "Objasni patofiziologiju dijabetesa tipa 2"
|
| 69 |
+
- "Kako funkcionira renin-angiotenzin-aldosteron sustav?"
|
| 70 |
+
|
| 71 |
+
## 🌐 Open Source
|
| 72 |
+
|
| 73 |
+
This project is open source! Check out the code on GitHub:
|
| 74 |
+
- Frontend: Vue 3 + TypeScript with custom WC3 UI components
|
| 75 |
+
- Backend: FastAPI with Gemini AI integration
|
| 76 |
+
- RAG: ChromaDB with Gemini embeddings
|
| 77 |
+
|
| 78 |
+
## 📝 Credits
|
| 79 |
+
|
| 80 |
+
- **UI Design**: Inspired by Warcraft 3 (Blizzard Entertainment)
|
| 81 |
+
- **Content**: Based on Patofiziologija textbook by Gamulin, Marušić, Kovač
|
| 82 |
+
- **AI**: Powered by Google Gemini
|
| 83 |
+
- **Development**: University of Split School of Medicine project
|
| 84 |
+
|
| 85 |
+
## ⚠️ Disclaimer
|
| 86 |
+
|
| 87 |
+
This is an educational tool. Always verify medical information with official sources and your professors. The AI can make mistakes!
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
**Built with ❤️ and Warcraft 3 nostalgia**
|
| 92 |
+
|
| 93 |
+
*For the Alliance! For the Horde! For passing exams!* ⚔️📚
|
backend/api.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Learn Pathophysiology - FastAPI Backend
|
| 3 |
+
|
| 4 |
+
Serves RAG + LLM API endpoints and Vue frontend static files.
|
| 5 |
+
Deploy: HuggingFace Spaces (Docker) or run locally.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import secrets
|
| 10 |
+
import logging
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from datetime import datetime, timezone, timedelta
|
| 13 |
+
|
| 14 |
+
import jwt
|
| 15 |
+
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends, Request
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
from fastapi.staticfiles import StaticFiles
|
| 18 |
+
from fastapi.responses import FileResponse
|
| 19 |
+
from pydantic import BaseModel
|
| 20 |
+
from dotenv import load_dotenv
|
| 21 |
+
from google import genai
|
| 22 |
+
from google.genai import types
|
| 23 |
+
from google.oauth2 import id_token as google_id_token
|
| 24 |
+
from google.auth.transport import requests as google_requests
|
| 25 |
+
import chromadb
|
| 26 |
+
|
| 27 |
+
load_dotenv()
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
# =============================================================================
|
| 32 |
+
# CONFIGURATION
|
| 33 |
+
# =============================================================================
|
| 34 |
+
|
| 35 |
+
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
|
| 36 |
+
CHROMA_DIR = os.environ.get("CHROMA_DIR", "../chroma_db")
|
| 37 |
+
COLLECTION_NAME = "pathophysiology"
|
| 38 |
+
EMBEDDING_MODEL = "gemini-embedding-001"
|
| 39 |
+
DEFAULT_MODEL = "gemini-3-flash-preview"
|
| 40 |
+
RAG_TOP_K = 5
|
| 41 |
+
|
| 42 |
+
# Auth
|
| 43 |
+
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
|
| 44 |
+
JWT_SECRET = os.environ.get("JWT_SECRET", secrets.token_urlsafe(32))
|
| 45 |
+
JWT_ALGORITHM = "HS256"
|
| 46 |
+
JWT_EXPIRY_DAYS = 7
|
| 47 |
+
AUTH_ENABLED = bool(GOOGLE_CLIENT_ID) # disable auth if no client ID
|
| 48 |
+
|
| 49 |
+
AVAILABLE_MODELS = {
|
| 50 |
+
"gemini-3-flash-preview": {
|
| 51 |
+
"name": "Gemini 3 Flash",
|
| 52 |
+
"description": "Najnoviji i najbrzi model",
|
| 53 |
+
"icon": "swords",
|
| 54 |
+
"wc3_name": "Blademaster",
|
| 55 |
+
"tier": "fast",
|
| 56 |
+
},
|
| 57 |
+
"gemini-2.5-flash": {
|
| 58 |
+
"name": "Gemini 2.5 Flash",
|
| 59 |
+
"description": "Brz i pouzdan",
|
| 60 |
+
"icon": "bow",
|
| 61 |
+
"wc3_name": "Shadow Hunter",
|
| 62 |
+
"tier": "fast",
|
| 63 |
+
},
|
| 64 |
+
"gemini-2.5-pro": {
|
| 65 |
+
"name": "Gemini 2.5 Pro",
|
| 66 |
+
"description": "Najpametniji za kompleksne zadatke",
|
| 67 |
+
"icon": "mage",
|
| 68 |
+
"wc3_name": "Archmage",
|
| 69 |
+
"tier": "smart",
|
| 70 |
+
},
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
SYSTEM_PROMPT = """Ti si "Learn Pathophysiology AI", strucni asistent za ucenje patofiziologije
|
| 74 |
+
za studente medicine.
|
| 75 |
+
|
| 76 |
+
ULOGA:
|
| 77 |
+
- Objasnjavaš patofiziološke koncepte jasno i precizno
|
| 78 |
+
- Koristiš primjere i analogije kad je moguce
|
| 79 |
+
- Povezuješ koncepte s klinickom praksom
|
| 80 |
+
- Odgovaraš na hrvatskom jeziku
|
| 81 |
+
|
| 82 |
+
KONTEKST IZ BAZE ZNANJA:
|
| 83 |
+
{rag_context}
|
| 84 |
+
|
| 85 |
+
PRAVILA:
|
| 86 |
+
1. Uvijek citiraj izvor kad koristiš informacije iz konteksta
|
| 87 |
+
2. Ako nisi siguran, reci to otvoreno
|
| 88 |
+
3. Koristi medicinsku terminologiju, ali objasni kompleksne termine
|
| 89 |
+
4. Budi koncizan ali potpun u odgovorima
|
| 90 |
+
5. Odgovaraj na hrvatskom jeziku"""
|
| 91 |
+
|
| 92 |
+
# =============================================================================
|
| 93 |
+
# SINGLETONS
|
| 94 |
+
# =============================================================================
|
| 95 |
+
|
| 96 |
+
_genai_client = None
|
| 97 |
+
_chroma_collection = None
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def get_client():
|
| 101 |
+
global _genai_client
|
| 102 |
+
if _genai_client is None:
|
| 103 |
+
if not GEMINI_API_KEY:
|
| 104 |
+
raise HTTPException(status_code=500, detail="GEMINI_API_KEY not configured")
|
| 105 |
+
_genai_client = genai.Client(api_key=GEMINI_API_KEY)
|
| 106 |
+
return _genai_client
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def get_collection():
|
| 110 |
+
global _chroma_collection
|
| 111 |
+
if _chroma_collection is None:
|
| 112 |
+
chroma_path = Path(CHROMA_DIR)
|
| 113 |
+
if not chroma_path.exists():
|
| 114 |
+
# Try relative to this file
|
| 115 |
+
alt_path = Path(__file__).parent.parent.parent / "chroma_db"
|
| 116 |
+
if alt_path.exists():
|
| 117 |
+
chroma_path = alt_path
|
| 118 |
+
else:
|
| 119 |
+
return None
|
| 120 |
+
try:
|
| 121 |
+
client = chromadb.PersistentClient(path=str(chroma_path))
|
| 122 |
+
_chroma_collection = client.get_collection(COLLECTION_NAME)
|
| 123 |
+
except Exception as e:
|
| 124 |
+
logger.error(f"ChromaDB error: {e}")
|
| 125 |
+
return None
|
| 126 |
+
return _chroma_collection
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# =============================================================================
|
| 130 |
+
# AUTH HELPERS
|
| 131 |
+
# =============================================================================
|
| 132 |
+
|
| 133 |
+
def create_jwt(email: str, name: str, picture: str = "") -> str:
|
| 134 |
+
payload = {
|
| 135 |
+
"sub": email,
|
| 136 |
+
"name": name,
|
| 137 |
+
"picture": picture,
|
| 138 |
+
"iat": datetime.now(timezone.utc),
|
| 139 |
+
"exp": datetime.now(timezone.utc) + timedelta(days=JWT_EXPIRY_DAYS),
|
| 140 |
+
}
|
| 141 |
+
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def decode_jwt(token: str) -> dict | None:
|
| 145 |
+
try:
|
| 146 |
+
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
| 147 |
+
except jwt.ExpiredSignatureError:
|
| 148 |
+
return None
|
| 149 |
+
except jwt.InvalidTokenError:
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
async def require_auth(request: Request):
|
| 154 |
+
"""FastAPI dependency — returns user dict or raises 401."""
|
| 155 |
+
if not AUTH_ENABLED:
|
| 156 |
+
return {"sub": "anonymous", "name": "Local User"}
|
| 157 |
+
|
| 158 |
+
auth_header = request.headers.get("Authorization", "")
|
| 159 |
+
if not auth_header.startswith("Bearer "):
|
| 160 |
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
| 161 |
+
|
| 162 |
+
token = auth_header[7:]
|
| 163 |
+
user = decode_jwt(token)
|
| 164 |
+
if not user:
|
| 165 |
+
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
| 166 |
+
return user
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# =============================================================================
|
| 170 |
+
# RAG FUNCTIONS
|
| 171 |
+
# =============================================================================
|
| 172 |
+
|
| 173 |
+
def embed_query(text: str) -> list[float]:
|
| 174 |
+
c = get_client()
|
| 175 |
+
result = c.models.embed_content(model=EMBEDDING_MODEL, contents=text)
|
| 176 |
+
return result.embeddings[0].values
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def query_rag(query_text: str, top_k: int = RAG_TOP_K):
|
| 180 |
+
coll = get_collection()
|
| 181 |
+
if coll is None:
|
| 182 |
+
return "Nema dostupnog konteksta.", []
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
query_embedding = embed_query(query_text)
|
| 186 |
+
results = coll.query(
|
| 187 |
+
query_embeddings=[query_embedding],
|
| 188 |
+
n_results=top_k,
|
| 189 |
+
include=["documents", "metadatas", "distances"]
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
contexts = []
|
| 193 |
+
citations = []
|
| 194 |
+
|
| 195 |
+
if results and results["documents"] and results["documents"][0]:
|
| 196 |
+
for idx, (doc, meta, dist) in enumerate(zip(
|
| 197 |
+
results["documents"][0],
|
| 198 |
+
results["metadatas"][0],
|
| 199 |
+
results["distances"][0]
|
| 200 |
+
)):
|
| 201 |
+
contexts.append(doc)
|
| 202 |
+
similarity = max(0, 1 - dist / 2)
|
| 203 |
+
citations.append({
|
| 204 |
+
"text": doc[:600] + "..." if len(doc) > 600 else doc,
|
| 205 |
+
"score": round(similarity, 3),
|
| 206 |
+
"source": meta.get("source", "Baza znanja"),
|
| 207 |
+
"page_num": meta.get("page_num", "?"),
|
| 208 |
+
"rank": idx + 1,
|
| 209 |
+
})
|
| 210 |
+
|
| 211 |
+
formatted = "\n\n---\n\n".join(contexts) if contexts else "Nema konteksta."
|
| 212 |
+
return formatted, citations
|
| 213 |
+
except Exception as e:
|
| 214 |
+
logger.error(f"RAG error: {e}")
|
| 215 |
+
return "Nema dostupnog konteksta.", []
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def generate_chat_response(message: str, history: list, model_name: str = ""):
|
| 219 |
+
model_name = model_name or DEFAULT_MODEL
|
| 220 |
+
if model_name not in AVAILABLE_MODELS:
|
| 221 |
+
model_name = DEFAULT_MODEL
|
| 222 |
+
|
| 223 |
+
c = get_client()
|
| 224 |
+
rag_context, citations = query_rag(message)
|
| 225 |
+
system_prompt = SYSTEM_PROMPT.format(rag_context=rag_context)
|
| 226 |
+
|
| 227 |
+
contents = [system_prompt]
|
| 228 |
+
for msg in (history or [])[-10:]:
|
| 229 |
+
role = msg.get("role", "user")
|
| 230 |
+
content = msg.get("content", "")
|
| 231 |
+
if role == "user":
|
| 232 |
+
contents.append(f"Student: {content}")
|
| 233 |
+
else:
|
| 234 |
+
contents.append(f"Asistent: {content}")
|
| 235 |
+
contents.append(f"Student: {message}")
|
| 236 |
+
|
| 237 |
+
response = c.models.generate_content(
|
| 238 |
+
model=model_name,
|
| 239 |
+
contents="\n\n".join(contents),
|
| 240 |
+
config=types.GenerateContentConfig(
|
| 241 |
+
temperature=0.7,
|
| 242 |
+
max_output_tokens=8192,
|
| 243 |
+
top_p=0.9,
|
| 244 |
+
)
|
| 245 |
+
)
|
| 246 |
+
return response.text, citations
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def do_analyze_image(image_bytes: bytes, question: str = "", model_name: str = ""):
|
| 250 |
+
model_name = model_name or DEFAULT_MODEL
|
| 251 |
+
if model_name not in AVAILABLE_MODELS:
|
| 252 |
+
model_name = DEFAULT_MODEL
|
| 253 |
+
|
| 254 |
+
c = get_client()
|
| 255 |
+
|
| 256 |
+
# Extract keywords from image
|
| 257 |
+
extract_resp = c.models.generate_content(
|
| 258 |
+
model=model_name,
|
| 259 |
+
contents=[
|
| 260 |
+
types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
|
| 261 |
+
"Izvuci glavni topic i kljucne rijeci s ove stranice. Odgovori kratko."
|
| 262 |
+
],
|
| 263 |
+
config=types.GenerateContentConfig(temperature=0.3, max_output_tokens=200)
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
rag_context, citations = query_rag(extract_resp.text, top_k=3)
|
| 267 |
+
|
| 268 |
+
if question:
|
| 269 |
+
prompt = (
|
| 270 |
+
f"Analiziraj ovu stranicu iz materijala za patofiziologiju "
|
| 271 |
+
f"i odgovori na pitanje studenta.\n\n"
|
| 272 |
+
f"PITANJE: {question}\n\n"
|
| 273 |
+
f"KONTEKST IZ BAZE ZNANJA:\n{rag_context}\n\n"
|
| 274 |
+
f"Odgovori detaljno na hrvatskom jeziku."
|
| 275 |
+
)
|
| 276 |
+
else:
|
| 277 |
+
prompt = (
|
| 278 |
+
f"Analiziraj ovu stranicu iz materijala za patofiziologiju.\n\n"
|
| 279 |
+
f"1. Prepoznaj glavni topic\n2. Izvuci kljucne pojmove\n"
|
| 280 |
+
f"3. Sazmi glavne tocke\n4. Objasni klinicku vaznost\n\n"
|
| 281 |
+
f"KONTEKST IZ BAZE ZNANJA:\n{rag_context}\n\n"
|
| 282 |
+
f"Odgovori na hrvatskom jeziku."
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
response = c.models.generate_content(
|
| 286 |
+
model=model_name,
|
| 287 |
+
contents=[
|
| 288 |
+
types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
|
| 289 |
+
prompt
|
| 290 |
+
],
|
| 291 |
+
config=types.GenerateContentConfig(temperature=0.5, max_output_tokens=8192)
|
| 292 |
+
)
|
| 293 |
+
return response.text, citations
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# =============================================================================
|
| 297 |
+
# FASTAPI APP
|
| 298 |
+
# =============================================================================
|
| 299 |
+
|
| 300 |
+
app = FastAPI(title="Learn Pathophysiology API", version="1.0.0")
|
| 301 |
+
|
| 302 |
+
app.add_middleware(
|
| 303 |
+
CORSMiddleware,
|
| 304 |
+
allow_origins=["*"],
|
| 305 |
+
allow_credentials=True,
|
| 306 |
+
allow_methods=["*"],
|
| 307 |
+
allow_headers=["*"],
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
# --- Request / Response Models ---
|
| 312 |
+
|
| 313 |
+
class ChatRequest(BaseModel):
|
| 314 |
+
message: str
|
| 315 |
+
model: str = ""
|
| 316 |
+
history: list = []
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
# --- Auth Models ---
|
| 320 |
+
|
| 321 |
+
class GoogleAuthRequest(BaseModel):
|
| 322 |
+
credential: str # Google ID token
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
# --- Auth Endpoints ---
|
| 326 |
+
|
| 327 |
+
@app.get("/api/auth/config")
|
| 328 |
+
async def auth_config():
|
| 329 |
+
"""Tell the frontend if auth is required and the Google Client ID."""
|
| 330 |
+
return {
|
| 331 |
+
"auth_enabled": AUTH_ENABLED,
|
| 332 |
+
"google_client_id": GOOGLE_CLIENT_ID if AUTH_ENABLED else None,
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
@app.post("/api/auth/google")
|
| 337 |
+
async def auth_google(req: GoogleAuthRequest):
|
| 338 |
+
"""Verify Google ID token and return a JWT session token."""
|
| 339 |
+
if not AUTH_ENABLED:
|
| 340 |
+
raise HTTPException(status_code=400, detail="Auth not enabled")
|
| 341 |
+
|
| 342 |
+
try:
|
| 343 |
+
idinfo = google_id_token.verify_oauth2_token(
|
| 344 |
+
req.credential,
|
| 345 |
+
google_requests.Request(),
|
| 346 |
+
GOOGLE_CLIENT_ID,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
email = idinfo.get("email", "")
|
| 350 |
+
name = idinfo.get("name", email)
|
| 351 |
+
picture = idinfo.get("picture", "")
|
| 352 |
+
|
| 353 |
+
token = create_jwt(email, name, picture)
|
| 354 |
+
return {
|
| 355 |
+
"token": token,
|
| 356 |
+
"user": {"email": email, "name": name, "picture": picture},
|
| 357 |
+
}
|
| 358 |
+
except ValueError as e:
|
| 359 |
+
logger.error(f"Google auth failed: {e}")
|
| 360 |
+
raise HTTPException(status_code=401, detail="Invalid Google token")
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@app.get("/api/auth/me")
|
| 364 |
+
async def auth_me(user=Depends(require_auth)):
|
| 365 |
+
"""Return the current user's info from their JWT."""
|
| 366 |
+
return {
|
| 367 |
+
"email": user.get("sub", ""),
|
| 368 |
+
"name": user.get("name", ""),
|
| 369 |
+
"picture": user.get("picture", ""),
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
# --- API Endpoints (public) ---
|
| 374 |
+
|
| 375 |
+
@app.get("/api/health")
|
| 376 |
+
async def health():
|
| 377 |
+
coll = get_collection()
|
| 378 |
+
return {
|
| 379 |
+
"status": "ok",
|
| 380 |
+
"chroma_docs": coll.count() if coll else 0,
|
| 381 |
+
"has_api_key": bool(GEMINI_API_KEY),
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
@app.get("/api/models")
|
| 386 |
+
async def list_models():
|
| 387 |
+
return {"models": AVAILABLE_MODELS, "default": DEFAULT_MODEL}
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@app.get("/api/stats")
|
| 391 |
+
async def stats():
|
| 392 |
+
coll = get_collection()
|
| 393 |
+
return {
|
| 394 |
+
"documents": coll.count() if coll else 0,
|
| 395 |
+
"collection": COLLECTION_NAME,
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
# --- API Endpoints (protected) ---
|
| 400 |
+
|
| 401 |
+
@app.post("/api/chat")
|
| 402 |
+
async def chat(req: ChatRequest, user=Depends(require_auth)):
|
| 403 |
+
try:
|
| 404 |
+
model = req.model or DEFAULT_MODEL
|
| 405 |
+
reply, citations = generate_chat_response(req.message, req.history, model)
|
| 406 |
+
return {"reply": reply, "citations": citations, "model_used": model}
|
| 407 |
+
except Exception as e:
|
| 408 |
+
logger.error(f"Chat error: {e}")
|
| 409 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
@app.post("/api/analyze-image")
|
| 413 |
+
async def analyze_image_endpoint(
|
| 414 |
+
image: UploadFile = File(...),
|
| 415 |
+
question: str = Form(""),
|
| 416 |
+
model: str = Form(""),
|
| 417 |
+
user=Depends(require_auth),
|
| 418 |
+
):
|
| 419 |
+
try:
|
| 420 |
+
model_name = model or DEFAULT_MODEL
|
| 421 |
+
image_bytes = await image.read()
|
| 422 |
+
analysis, citations = do_analyze_image(image_bytes, question, model_name)
|
| 423 |
+
return {"analysis": analysis, "citations": citations, "model_used": model_name}
|
| 424 |
+
except Exception as e:
|
| 425 |
+
logger.error(f"Image analysis error: {e}")
|
| 426 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
# --- Serve Vue Frontend (production) ---
|
| 430 |
+
static_dir = Path(__file__).parent / "static"
|
| 431 |
+
if static_dir.exists():
|
| 432 |
+
@app.get("/")
|
| 433 |
+
async def serve_index():
|
| 434 |
+
return FileResponse(str(static_dir / "index.html"))
|
| 435 |
+
|
| 436 |
+
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
if __name__ == "__main__":
|
| 440 |
+
import uvicorn
|
| 441 |
+
uvicorn.run("api:app", host="0.0.0.0", port=7860, reload=True)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.110.0
|
| 2 |
+
uvicorn[standard]>=0.27.0
|
| 3 |
+
google-genai>=1.0.0
|
| 4 |
+
google-auth>=2.0.0
|
| 5 |
+
chromadb>=0.5.0
|
| 6 |
+
python-dotenv>=1.0.0
|
| 7 |
+
python-multipart>=0.0.9
|
| 8 |
+
pyjwt>=2.0.0
|
backend/static/any/hud_time_indicator.png
ADDED
|
Git LFS Details
|
backend/static/any/resource_icons/gold.png
ADDED
|
|
Git LFS Details
|
backend/static/any/resource_icons/lumber.png
ADDED
|
|
Git LFS Details
|
backend/static/any/resource_icons/supply.png
ADDED
|
|
Git LFS Details
|
backend/static/assets/index-BSpkv-VB.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/static/assets/index-BzhPXs6E.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@charset "UTF-8";.btn{width:100%;height:100%;position:relative;background:var(--v854fa1ce) no-repeat;background-size:100% 100%;color:var(--v548a9e4c);-webkit-user-select:none;user-select:none;-webkit-mask-image:var(--v854fa1ce);-webkit-mask-size:100% 100%}.btn__capt{width:100%;position:absolute;text-align:center;bottom:0}.btn--d{font-size:14px;letter-spacing:.5px;line-height:180%}.btn--s{height:32px;line-height:32px;min-width:100px}.btn--m{height:48px;line-height:48px;min-width:200px}*:not([disabled])+.btn:hover:before{content:"";position:absolute;width:100%;height:100%;background:var(--ba36690e) no-repeat;background-size:100% 100%;left:0;top:0;mix-blend-mode:screen}*:not([disabled])+.btn:active{background-image:var(--v4e4c04fa)}*:not([disabled])+.btn:active .btn__capt{left:3px;bottom:-2px}[disabled]+.btn{background-image:var(--v512606b8);pointer-events:all!important;color:#646464}.cursored,.cursored *{cursor:none}.cursor{position:relative;width:32px;height:32px;background:var(--v7b062dde) no-repeat}.cursor__container{top:0;left:0;position:fixed;z-index:99;pointer-events:none}.cursor--default-active{animation:cursor-active .5s steps(8) infinite}.cursor--pointer{background-position:-32px -96px}.cursor--pointer-active{animation:cursor-pointer .5s steps(8) infinite}.cursor--pointer-denied{background-position:-64px -96px}.cursor--hold{background-position:-128px -96px}.cursor--arrow-top{animation:cursor-arrow .1s steps(3) infinite;transform:rotate(-90deg)}.cursor--arrow-right{animation:cursor-arrow .1s steps(3) infinite}.cursor--arrow-bottom{animation:cursor-arrow .1s steps(3) infinite;transform:rotate(90deg)}.cursor--arrow-left{animation:cursor-arrow .1s steps(3) infinite;transform:rotate(-180deg)}@keyframes cursor-active{0%{background-position:0 0}to{background-position:-256px 0}}@keyframes cursor-pointer{0%{background-position:0 -64px}to{background-position:-256px -64px}}@keyframes cursor-arrow{0%{background-position:-160px -96px}to{background-position:-256px -96px}}.chat-area[data-v-b84b5170]{flex:1;overflow:hidden;display:flex;flex-direction:column}.messages-container[data-v-b84b5170]{flex:1;overflow-y:auto;padding:1rem 1.5rem;display:flex;flex-direction:column;gap:.75rem}.login-page{width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at center,#12122a,#0a0a16,#050510)}.login-card{width:100%;max-width:420px;padding:2rem}.login-frame{background:linear-gradient(180deg,#13132a,#0d0d1f);border:2px solid var(--wc3-gold-dim, #8b7b4f);border-radius:6px;padding:2.5rem 2rem;text-align:center;box-shadow:0 0 40px #c8aa6e14,inset 0 1px #c8aa6e1a}.login-icon{font-size:3rem;margin-bottom:.75rem;filter:drop-shadow(0 0 10px rgba(200,170,110,.4))}.login-frame h1{font-family:var(--font-display, "Cinzel Decorative", serif);font-size:1.6rem;font-weight:900;background:linear-gradient(180deg,#f0d060,#c8aa6e,#8b7b4f);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:2px;margin-bottom:.25rem}.login-frame .subtitle{font-family:var(--font-body, "Crimson Text", serif);color:var(--wc3-text-dim, #7a6e5a);font-style:italic;font-size:.95rem}.login-prompt{font-family:var(--font-heading, "Cinzel", serif);color:var(--wc3-gold, #c8aa6e);font-size:.9rem;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:1.25rem}.g-btn-container{display:flex;justify-content:center;min-height:44px;margin-bottom:.5rem}.login-error{color:#f66;font-size:.85rem;margin-top:.75rem}.login-footer{font-size:.8rem;color:var(--wc3-text-dim, #7a6e5a);font-style:italic;line-height:1.6}.login-footer-dim{color:var(--wc3-text-muted, #555060);font-size:.75rem}:root{--wc3-bg: #0a0a0f;--wc3-bg-panel: #0f0f1a;--wc3-gold: #c8aa6e;--wc3-gold-bright: #f0d060;--wc3-gold-dim: #8b7b4f;--wc3-text: #d4c4a0;--wc3-text-light: #f0e6d0;--wc3-text-dim: #7a6e5a;--wc3-text-muted: #555060;--wc3-border: #5a4a2a;--wc3-border-dim: #3a2e1e;--wc3-red-soft: rgba(140, 40, 40, .25);--wc3-blue-soft: rgba(30, 50, 90, .35);--font-heading: "Cinzel", serif;--font-display: "Cinzel Decorative", "Cinzel", serif;--font-body: "Crimson Text", serif;--sidebar-w: 270px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%;overflow:hidden;background:var(--wc3-bg);color:var(--wc3-text);font-family:var(--font-body);line-height:1.6}#app{height:100vh}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:#0000004d}::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--wc3-gold-dim),#4a3a20);border-radius:2px}::-webkit-scrollbar-thumb:hover{background:var(--wc3-gold)}.app-shell{display:flex;height:100vh;overflow:hidden}.sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:linear-gradient(180deg,#0f0f22,#0a0a18);border-right:2px solid var(--wc3-border);display:flex;flex-direction:column;overflow-y:auto;padding:1rem;box-shadow:2px 0 20px #00000080}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;background:radial-gradient(ellipse at top center,#12122a,#0a0a16 60%,#070710)}.app-header{text-align:center;padding:1.25rem 1rem .5rem;flex-shrink:0}.header-frame h1{font-family:var(--font-display);font-size:2rem;font-weight:900;background:linear-gradient(180deg,#f0d060,#c8aa6e,#8b7b4f);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:3px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.8))}.header-frame .subtitle{font-family:var(--font-body);color:var(--wc3-text-dim);font-style:italic;font-size:.95rem;margin-top:.15rem}.gold-divider{height:1px;background:linear-gradient(90deg,transparent,var(--wc3-gold-dim),transparent);margin:.75rem 0;border:none}.tab-bar{display:flex;gap:.5rem;padding:.35rem 1.5rem;border-bottom:1px solid var(--wc3-border-dim);flex-shrink:0}.tab-btn-wrap{height:32px;min-width:120px}.tab-trigger.active{filter:brightness(1.3)}.msg{max-width:85%;padding:.85rem 1.15rem;border-radius:6px;line-height:1.65;font-size:1rem;border:1px solid transparent;animation:fadeSlide .25s ease}@keyframes fadeSlide{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.msg.user{align-self:flex-end;background:linear-gradient(135deg,#8c282859,#5a191940);border-color:#c850504d;color:var(--wc3-text-light)}.msg.assistant{align-self:flex-start;background:linear-gradient(135deg,#141e4173,#0f14324d);border-color:#6482c840;color:var(--wc3-text-light)}.msg h1,.msg h2,.msg h3,.msg h4{font-family:var(--font-heading);color:var(--wc3-gold);margin:.75rem 0 .35rem}.msg h1{font-size:1.3rem}.msg h2{font-size:1.15rem}.msg h3{font-size:1.05rem}.msg p{margin:.4rem 0}.msg strong{color:var(--wc3-gold-bright)}.msg em{font-style:italic}.msg ul,.msg ol{padding-left:1.25rem;margin:.4rem 0}.msg li{margin:.2rem 0}.msg code{background:#c8aa6e14;border:1px solid var(--wc3-border-dim);padding:.1rem .4rem;border-radius:3px;font-size:.9em;color:var(--wc3-gold)}.msg pre{background:#0000004d;border:1px solid var(--wc3-border-dim);border-radius:4px;padding:.75rem;overflow-x:auto;margin:.5rem 0}.msg pre code{background:none;border:none;padding:0}.msg blockquote{border-left:3px solid var(--wc3-gold-dim);padding-left:.75rem;margin:.5rem 0;color:var(--wc3-text);font-style:italic}.msg hr{border:none;height:1px;background:linear-gradient(90deg,transparent,var(--wc3-gold-dim),transparent);margin:.75rem 0}.msg-meta{font-size:.8rem;color:var(--wc3-text-dim);font-style:italic;margin-top:.4rem}.wc3-select{width:100%;padding:.5rem .75rem;font-family:var(--font-body);font-size:.95rem;color:var(--wc3-text-light);background:linear-gradient(180deg,#1a1a32,#13132a);border:1px solid var(--wc3-gold-dim);border-radius:2px;outline:none;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23c8aa6e' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center}.wc3-select:hover{border-color:var(--wc3-gold)}.wc3-select:focus{border-color:var(--wc3-gold);box-shadow:0 0 8px #c8aa6e33}.wc3-select option{background:var(--wc3-bg-panel);color:var(--wc3-text)}.wc3-input{width:100%;padding:.6rem .85rem;font-family:var(--font-body);font-size:1rem;color:var(--wc3-text-light);background:linear-gradient(180deg,#12122a,#0f0f24);border:1px solid var(--wc3-border);border-radius:2px;outline:none;box-shadow:inset 0 2px 6px #0000004d;transition:border-color .15s}.wc3-input:focus{border-color:var(--wc3-gold);box-shadow:inset 0 2px 6px #0000004d,0 0 8px #c8aa6e26}.wc3-input::placeholder{color:var(--wc3-text-muted)}.wc3-checkbox{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-family:var(--font-body);color:var(--wc3-text);font-size:.95rem}.wc3-checkbox input[type=checkbox]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:18px;height:18px;background:#13132a;border:1px solid var(--wc3-gold-dim);border-radius:2px;cursor:pointer;position:relative}.wc3-checkbox input[type=checkbox]:checked{border-color:var(--wc3-gold)}.wc3-checkbox input[type=checkbox]:checked:after{content:"✓";position:absolute;top:-1px;left:2px;color:var(--wc3-gold-bright);font-size:14px;font-weight:700}.loading{display:flex;align-items:center;gap:.6rem;padding:.75rem 1rem;color:var(--wc3-gold);font-family:var(--font-heading);font-size:.85rem;font-style:italic;animation:fadeSlide .3s ease}.dot-pulse{display:inline-flex;gap:4px}.dot-pulse span{width:6px;height:6px;border-radius:50%;background:var(--wc3-gold);animation:pulse 1.2s infinite ease-in-out}.dot-pulse span:nth-child(2){animation-delay:.2s}.dot-pulse span:nth-child(3){animation-delay:.4s}@keyframes pulse{0%,80%,to{opacity:.2;transform:scale(.8)}40%{opacity:1;transform:scale(1.1)}}.error-msg{background:#b4282826;border:1px solid #cc3333;color:#f88;padding:.6rem .85rem;border-radius:4px;font-size:.9rem}.resource-bar{background:linear-gradient(180deg,#1a1a30,#112);border:1px solid var(--wc3-border);border-radius:3px;padding:.45rem .75rem;text-align:center;font-family:var(--font-heading);font-size:.8rem;color:var(--wc3-gold);letter-spacing:1px}.sidebar h2{font-family:var(--font-heading);color:var(--wc3-gold);font-size:.85rem;letter-spacing:2px;text-transform:uppercase;text-shadow:0 0 8px rgba(200,170,110,.2);padding-bottom:.4rem;border-bottom:1px solid var(--wc3-border-dim);margin-top:.75rem;margin-bottom:.5rem}.sidebar h2:first-child{margin-top:0}.model-caption{font-size:.85rem;color:var(--wc3-text-dim);font-style:italic;margin-top:.25rem}.sidebar-footer{margin-top:auto;padding-top:.75rem;text-align:center;font-size:.75rem;color:var(--wc3-text-muted);font-style:italic;line-height:1.6}.chat-input-bar{display:flex;gap:.5rem;padding:.75rem 1.5rem;background:linear-gradient(180deg,var(--wc3-bg-panel),var(--wc3-bg));border-top:1px solid var(--wc3-border);flex-shrink:0;align-items:center}.chat-input-bar .wc3-input{flex:1}.chat-send-btn{height:38px;min-width:100px}.citations-toggle{font-family:var(--font-heading);font-size:.8rem;color:var(--wc3-gold-dim);background:none;border:1px solid var(--wc3-border-dim);border-radius:3px;padding:.35rem .75rem;cursor:pointer;margin-top:.5rem;transition:all .15s}.citations-toggle:hover{border-color:var(--wc3-gold);color:var(--wc3-gold)}.citations-panel{margin-top:.5rem;border:1px solid var(--wc3-border-dim);border-radius:4px;background:#0003;padding:.75rem;animation:fadeSlide .2s ease}.citation-item{padding:.5rem 0;border-bottom:1px solid var(--wc3-border-dim)}.citation-item:last-child{border-bottom:none}.citation-header{font-family:var(--font-heading);font-size:.8rem;color:var(--wc3-gold);margin-bottom:.25rem;display:flex;align-items:center;flex-wrap:wrap;gap:.25rem}.citation-text{font-size:.85rem;color:var(--wc3-text);font-style:italic;border-left:2px solid var(--wc3-gold-dim);padding-left:.6rem;margin-top:.25rem;line-height:1.5}.image-tab{flex:1;overflow-y:auto;padding:1.5rem}.upload-zone{border:2px dashed var(--wc3-border);border-radius:6px;padding:2rem;text-align:center;cursor:pointer;transition:all .15s;background:#00000026;margin-bottom:1rem}.upload-zone:hover{border-color:var(--wc3-gold);background:#c8aa6e08}.upload-zone input[type=file]{display:none}.upload-zone .icon{font-size:2.5rem;margin-bottom:.5rem}.upload-zone p{color:var(--wc3-text-dim);font-size:.95rem}.image-preview{max-width:100%;max-height:300px;border:1px solid var(--wc3-border);border-radius:4px;margin-bottom:1rem}.analysis-result{background:linear-gradient(135deg,#141e4173,#0f14324d);border:1px solid rgba(100,130,200,.25);border-radius:6px;padding:1rem 1.25rem;line-height:1.65}.loading-splash{width:100vw;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at center,#12122a,#0a0a16,#050510);color:var(--wc3-gold, #c8aa6e)}.loading-splash__icon{font-size:3rem;margin-bottom:1rem;animation:pulse 1.5s ease-in-out infinite}.loading-splash__text{font-family:var(--font-heading, "Cinzel", serif);font-size:1rem;letter-spacing:3px;text-transform:uppercase}@keyframes pulse{0%,to{opacity:.5;transform:scale(1)}50%{opacity:1;transform:scale(1.1)}}.user-badge{display:flex;align-items:center;gap:.6rem;padding:.4rem 0}.user-avatar{width:32px;height:32px;border-radius:50%;border:1.5px solid var(--wc3-gold-dim, #8b7b4f);flex-shrink:0}.user-info{display:flex;flex-direction:column;min-width:0}.user-name{font-family:var(--font-heading, "Cinzel", serif);color:var(--wc3-gold, #c8aa6e);font-size:.75rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.logout-btn{background:none;border:none;color:var(--wc3-text-dim, #7a6e5a);font-size:.65rem;cursor:pointer;text-align:left;padding:0;font-family:var(--font-body, "Crimson Text", serif);text-decoration:underline;text-underline-offset:2px}.logout-btn:hover{color:var(--wc3-gold, #c8aa6e)}@media(max-width:768px){.sidebar{display:none}:root{--sidebar-w: 0px}.header-frame h1{font-size:1.4rem}}
|
backend/static/hum/btn_default.png
ADDED
|
Git LFS Details
|
backend/static/hum/btn_disabled.png
ADDED
|
Git LFS Details
|
backend/static/hum/btn_hover_bg.png
ADDED
|
Git LFS Details
|
backend/static/hum/btn_pressed.png
ADDED
|
Git LFS Details
|
backend/static/hum/cursor.png
ADDED
|
Git LFS Details
|
backend/static/hum/hud_footer.png
ADDED
|
Git LFS Details
|
backend/static/hum/hud_header.png
ADDED
|
Git LFS Details
|
backend/static/hum/hud_inv_mock.png
ADDED
|
Git LFS Details
|
backend/static/hum/hud_inv_mock_slot.png
ADDED
|
Git LFS Details
|
backend/static/index.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="hr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Learn Pathophysiology — WC3 Edition</title>
|
| 7 |
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚔️</text></svg>">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=Cinzel+Decorative:wght@400;700;900&family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
| 11 |
+
<script type="module" crossorigin src="/assets/index-BSpkv-VB.js"></script>
|
| 12 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BzhPXs6E.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app"></div>
|
| 16 |
+
</body>
|
| 17 |
+
</html>
|
backend/static/nel/btn_default.png
ADDED
|
Git LFS Details
|
backend/static/nel/btn_disabled.png
ADDED
|
Git LFS Details
|
backend/static/nel/btn_hover_bg.png
ADDED
|
Git LFS Details
|
backend/static/nel/btn_pressed.png
ADDED
|
Git LFS Details
|
backend/static/nel/cursor.png
ADDED
|
Git LFS Details
|
backend/static/nel/hud_footer.png
ADDED
|
Git LFS Details
|
backend/static/nel/hud_header.png
ADDED
|
Git LFS Details
|
backend/static/nel/hud_inv_mock.png
ADDED
|
Git LFS Details
|
backend/static/nel/hud_inv_mock_slot.png
ADDED
|
Git LFS Details
|
backend/static/orc/btn_default.png
ADDED
|
Git LFS Details
|
backend/static/orc/btn_disabled.png
ADDED
|
Git LFS Details
|
backend/static/orc/btn_hover_bg.png
ADDED
|
Git LFS Details
|
backend/static/orc/btn_pressed.png
ADDED
|
Git LFS Details
|
backend/static/orc/cursor.png
ADDED
|
Git LFS Details
|
backend/static/orc/hud_footer.png
ADDED
|
Git LFS Details
|
backend/static/orc/hud_header.png
ADDED
|
Git LFS Details
|
backend/static/orc/hud_inv_mock.png
ADDED
|
Git LFS Details
|
backend/static/orc/hud_inv_mock_slot.png
ADDED
|
Git LFS Details
|
backend/static/und/btn_default.png
ADDED
|
Git LFS Details
|
backend/static/und/btn_disabled.png
ADDED
|
Git LFS Details
|
backend/static/und/btn_hover_bg.png
ADDED
|
Git LFS Details
|
backend/static/und/btn_pressed.png
ADDED
|
Git LFS Details
|
backend/static/und/cursor.png
ADDED
|
Git LFS Details
|
backend/static/und/hud_footer.png
ADDED
|
Git LFS Details
|
backend/static/und/hud_header.png
ADDED
|
Git LFS Details
|
backend/static/und/hud_inv_mock.png
ADDED
|
Git LFS Details
|
backend/static/und/hud_inv_mock_slot.png
ADDED
|
Git LFS Details
|
chroma_db/34c32620-31ab-4a2f-bd92-3fcfd7be3432/data_level0.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:81f0c6b96f2341facf3a93d68612adc9e9a439f400ecaef44b1d8cf6b7c04cee
|
| 3 |
+
size 49712000
|