Vrda commited on
Commit
8b9f7d9
·
1 Parent(s): 6da29de

Deploy Learn Pathophysiology WC3 Edition

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +13 -0
  2. .gitattributes +8 -33
  3. Dockerfile +45 -0
  4. README.md +87 -6
  5. backend/api.py +441 -0
  6. backend/requirements.txt +8 -0
  7. backend/static/any/hud_time_indicator.png +3 -0
  8. backend/static/any/resource_icons/gold.png +3 -0
  9. backend/static/any/resource_icons/lumber.png +3 -0
  10. backend/static/any/resource_icons/supply.png +3 -0
  11. backend/static/assets/index-BSpkv-VB.js +0 -0
  12. backend/static/assets/index-BzhPXs6E.css +1 -0
  13. backend/static/hum/btn_default.png +3 -0
  14. backend/static/hum/btn_disabled.png +3 -0
  15. backend/static/hum/btn_hover_bg.png +3 -0
  16. backend/static/hum/btn_pressed.png +3 -0
  17. backend/static/hum/cursor.png +3 -0
  18. backend/static/hum/hud_footer.png +3 -0
  19. backend/static/hum/hud_header.png +3 -0
  20. backend/static/hum/hud_inv_mock.png +3 -0
  21. backend/static/hum/hud_inv_mock_slot.png +3 -0
  22. backend/static/index.html +17 -0
  23. backend/static/nel/btn_default.png +3 -0
  24. backend/static/nel/btn_disabled.png +3 -0
  25. backend/static/nel/btn_hover_bg.png +3 -0
  26. backend/static/nel/btn_pressed.png +3 -0
  27. backend/static/nel/cursor.png +3 -0
  28. backend/static/nel/hud_footer.png +3 -0
  29. backend/static/nel/hud_header.png +3 -0
  30. backend/static/nel/hud_inv_mock.png +3 -0
  31. backend/static/nel/hud_inv_mock_slot.png +3 -0
  32. backend/static/orc/btn_default.png +3 -0
  33. backend/static/orc/btn_disabled.png +3 -0
  34. backend/static/orc/btn_hover_bg.png +3 -0
  35. backend/static/orc/btn_pressed.png +3 -0
  36. backend/static/orc/cursor.png +3 -0
  37. backend/static/orc/hud_footer.png +3 -0
  38. backend/static/orc/hud_header.png +3 -0
  39. backend/static/orc/hud_inv_mock.png +3 -0
  40. backend/static/orc/hud_inv_mock_slot.png +3 -0
  41. backend/static/und/btn_default.png +3 -0
  42. backend/static/und/btn_disabled.png +3 -0
  43. backend/static/und/btn_hover_bg.png +3 -0
  44. backend/static/und/btn_pressed.png +3 -0
  45. backend/static/und/cursor.png +3 -0
  46. backend/static/und/hud_footer.png +3 -0
  47. backend/static/und/hud_header.png +3 -0
  48. backend/static/und/hud_inv_mock.png +3 -0
  49. backend/static/und/hud_inv_mock_slot.png +3 -0
  50. 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
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
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: blue
5
- colorTo: gray
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ![WC3 Style](https://img.shields.io/badge/Style-Warcraft%203-orange)
16
+ ![AI Powered](https://img.shields.io/badge/AI-Gemini-blue)
17
+ ![Language](https://img.shields.io/badge/Language-Croatian-red)
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

  • SHA256: 3278868b0512fc4c6feaeef519d0c337e6e77f215859ac4ab97993e1c189c864
  • Pointer size: 131 Bytes
  • Size of remote file: 131 kB
backend/static/any/resource_icons/gold.png ADDED

Git LFS Details

  • SHA256: cc3ff9674247028ebe34ac1e8c84bd97ca821199e1bfa4f7c0c635cca3fe0cd4
  • Pointer size: 129 Bytes
  • Size of remote file: 1.84 kB
backend/static/any/resource_icons/lumber.png ADDED

Git LFS Details

  • SHA256: 650c574ce17ab7766f9d8cbf2bd6c3b5b1281954925d7d35bba9219dc8c81896
  • Pointer size: 129 Bytes
  • Size of remote file: 1.59 kB
backend/static/any/resource_icons/supply.png ADDED

Git LFS Details

  • SHA256: 11202af9a7fc0b73be772be538b5ab1f142f915c49a81a762b12623e90074309
  • Pointer size: 129 Bytes
  • Size of remote file: 1.41 kB
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

  • SHA256: b416cf7964b1cdc7314d69371f104aaf6c29b848c9a0081bf10dbdd205c240f6
  • Pointer size: 129 Bytes
  • Size of remote file: 3.17 kB
backend/static/hum/btn_disabled.png ADDED

Git LFS Details

  • SHA256: 39fc64b1fba442258573c9bc5d7cdeff62bdc12b87094102dbd6e4fc2ce3c090
  • Pointer size: 129 Bytes
  • Size of remote file: 2.37 kB
backend/static/hum/btn_hover_bg.png ADDED

Git LFS Details

  • SHA256: e9aac267b934c1bfe003de12535ae96c6d7ecc0b59092b77cf4096f20754de31
  • Pointer size: 129 Bytes
  • Size of remote file: 1.31 kB
backend/static/hum/btn_pressed.png ADDED

Git LFS Details

  • SHA256: 3238ac55d25929e5758809e494f8b66da0e21620d0fe4a2fa6d98c9b76573e67
  • Pointer size: 129 Bytes
  • Size of remote file: 2.66 kB
backend/static/hum/cursor.png ADDED

Git LFS Details

  • SHA256: f172e678d25df73e367fbe6fc2a134014c71b4951c6a4404370b5a4211adf004
  • Pointer size: 130 Bytes
  • Size of remote file: 18 kB
backend/static/hum/hud_footer.png ADDED

Git LFS Details

  • SHA256: d7194e7ebd0766414715c48726143a91435e6f11b4cdcf0eff769a3c53c584de
  • Pointer size: 131 Bytes
  • Size of remote file: 213 kB
backend/static/hum/hud_header.png ADDED

Git LFS Details

  • SHA256: b2e75eda4fe37055da8f956a28947f98d9ff8892c6b69df3512c7ee768cc9b30
  • Pointer size: 130 Bytes
  • Size of remote file: 31.7 kB
backend/static/hum/hud_inv_mock.png ADDED

Git LFS Details

  • SHA256: 5099d37633d83fa5feba9279ceb1f087e4e696efbc0bd462c62c0bc7b38e8732
  • Pointer size: 130 Bytes
  • Size of remote file: 95.5 kB
backend/static/hum/hud_inv_mock_slot.png ADDED

Git LFS Details

  • SHA256: 7504e8e821ee070fb6a1cd53521bbfdab6be788bfe184905959dfc95b8a03cc4
  • Pointer size: 129 Bytes
  • Size of remote file: 4.49 kB
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

  • SHA256: c7a3600ade27dfc4df2e7b14739efc9b53f3d8cd772f0f0b7836f4e75d6bfbb9
  • Pointer size: 129 Bytes
  • Size of remote file: 3.08 kB
backend/static/nel/btn_disabled.png ADDED

Git LFS Details

  • SHA256: 8e86445a348b78733d086f210b51c0e723212f9c1ae7a8d5b7186330558e2db7
  • Pointer size: 129 Bytes
  • Size of remote file: 1.77 kB
backend/static/nel/btn_hover_bg.png ADDED

Git LFS Details

  • SHA256: 7f7e43d86e4bd6918ee37ab7eb1f6780e8f0424c4a452bf06da4511bc601becd
  • Pointer size: 129 Bytes
  • Size of remote file: 1.33 kB
backend/static/nel/btn_pressed.png ADDED

Git LFS Details

  • SHA256: 6e15a1ea2bd8a3ac4420feb03e97969330cae16b64e4332a815740d391566da9
  • Pointer size: 129 Bytes
  • Size of remote file: 2.69 kB
backend/static/nel/cursor.png ADDED

Git LFS Details

  • SHA256: 0ca3b098a8516e5a770280965e10b0708637e811514ae986a4888e59d5a52ca8
  • Pointer size: 130 Bytes
  • Size of remote file: 15.2 kB
backend/static/nel/hud_footer.png ADDED

Git LFS Details

  • SHA256: dd0d75f008dffad62ad2e961f70ab4bfc10e9a9f468d6e94d7173a41699e4752
  • Pointer size: 131 Bytes
  • Size of remote file: 264 kB
backend/static/nel/hud_header.png ADDED

Git LFS Details

  • SHA256: 6f479ba78e5d64fb0a97f3edae8b8757d8bc8296ae5332763088666d9b9836df
  • Pointer size: 130 Bytes
  • Size of remote file: 92.7 kB
backend/static/nel/hud_inv_mock.png ADDED

Git LFS Details

  • SHA256: d25e96c77ba51df9d16a636d675a3eb23278ce9daef42acbde170a62aa9f83b1
  • Pointer size: 130 Bytes
  • Size of remote file: 49.4 kB
backend/static/nel/hud_inv_mock_slot.png ADDED

Git LFS Details

  • SHA256: 6f4504fe032ec6e1931e04e8bd85f1dabb2426fa1b7cfd9e4485fb1c2546d5e4
  • Pointer size: 129 Bytes
  • Size of remote file: 4.64 kB
backend/static/orc/btn_default.png ADDED

Git LFS Details

  • SHA256: 0ad8feca70abe0d0e6a7aa948c9dc9d66283e3720649b84e2970dfcad3590eb3
  • Pointer size: 129 Bytes
  • Size of remote file: 2.78 kB
backend/static/orc/btn_disabled.png ADDED

Git LFS Details

  • SHA256: 953bf14a95dcb6b6120175ce4ed4b0f850fd8a1fbf65cd5f12b3a23ce5b4dbe2
  • Pointer size: 129 Bytes
  • Size of remote file: 2.11 kB
backend/static/orc/btn_hover_bg.png ADDED

Git LFS Details

  • SHA256: aa21591323e8830d6aeb5ff09e192c0023032cc133d088730e334d8e3f0059f1
  • Pointer size: 129 Bytes
  • Size of remote file: 1.43 kB
backend/static/orc/btn_pressed.png ADDED

Git LFS Details

  • SHA256: c014794f3f81d2509b47fef837a8bcd733d858c3bda04ce91ca96c742e9eb3eb
  • Pointer size: 129 Bytes
  • Size of remote file: 2.52 kB
backend/static/orc/cursor.png ADDED

Git LFS Details

  • SHA256: cbc2565a8a6cff22a1ea7bebcbc671d350b0bf96fd4bf5b7aeedf5a10696a5ce
  • Pointer size: 130 Bytes
  • Size of remote file: 15.1 kB
backend/static/orc/hud_footer.png ADDED

Git LFS Details

  • SHA256: 5625f71d24f621a80e4b653d57bae65251d7016a09004491731205743ab8a8e1
  • Pointer size: 131 Bytes
  • Size of remote file: 281 kB
backend/static/orc/hud_header.png ADDED

Git LFS Details

  • SHA256: 3db8b4b92958cd53c42aecc535e9729df0d7a778b4b063bdca8b621a41680b5b
  • Pointer size: 130 Bytes
  • Size of remote file: 39.4 kB
backend/static/orc/hud_inv_mock.png ADDED

Git LFS Details

  • SHA256: 05bcdfd6f0bc91cec360252704579c866da8dbcd42bac7dcd5a6b29e04bb39f1
  • Pointer size: 130 Bytes
  • Size of remote file: 52.5 kB
backend/static/orc/hud_inv_mock_slot.png ADDED

Git LFS Details

  • SHA256: c20e037e2cb9a5f1b6e7ccfada6ac58f4eb0fa9b95dfe991c802bc50dcce3b2e
  • Pointer size: 129 Bytes
  • Size of remote file: 4.31 kB
backend/static/und/btn_default.png ADDED

Git LFS Details

  • SHA256: fe550270021fc6e1604d7e0bb682576027715c4de76f1f69cab6830cbc8835f2
  • Pointer size: 129 Bytes
  • Size of remote file: 2.73 kB
backend/static/und/btn_disabled.png ADDED

Git LFS Details

  • SHA256: fce814c28c0ed48785d5c3b007ac4e4d8f6c765934f5dcdceb385901fcfe579e
  • Pointer size: 129 Bytes
  • Size of remote file: 2.05 kB
backend/static/und/btn_hover_bg.png ADDED

Git LFS Details

  • SHA256: 31995399159e873676e69b7841d79a505c292830003ca8db5d142c0a4e9845e7
  • Pointer size: 129 Bytes
  • Size of remote file: 1.18 kB
backend/static/und/btn_pressed.png ADDED

Git LFS Details

  • SHA256: 0869ec9b3b9a2bedc9a3c245b4e3c7d35ff31891bb7cc7d6680f42f5650d4dc1
  • Pointer size: 129 Bytes
  • Size of remote file: 2.43 kB
backend/static/und/cursor.png ADDED

Git LFS Details

  • SHA256: 10b68c7b7c875df547097add45409b8391e07a6d21df789a4b4700d18896e621
  • Pointer size: 130 Bytes
  • Size of remote file: 12.7 kB
backend/static/und/hud_footer.png ADDED

Git LFS Details

  • SHA256: b07074fdec92b009badb60d64f39aab2f26b5ab96f1983d551562309094bb725
  • Pointer size: 131 Bytes
  • Size of remote file: 292 kB
backend/static/und/hud_header.png ADDED

Git LFS Details

  • SHA256: 3129bf0f86a4d8e48f3d5fd817bd90d29545483f595c762da95ad2cc20f52d45
  • Pointer size: 130 Bytes
  • Size of remote file: 29.9 kB
backend/static/und/hud_inv_mock.png ADDED

Git LFS Details

  • SHA256: ad3ca01b9ef3306c65f55c7c4057fb7573b9816e533d6332d2046fb3fbce714a
  • Pointer size: 130 Bytes
  • Size of remote file: 53.5 kB
backend/static/und/hud_inv_mock_slot.png ADDED

Git LFS Details

  • SHA256: 0c459be05833a5878f656f32d226bc8d73522b5f6bccc79479e28e0dd6f2795a
  • Pointer size: 129 Bytes
  • Size of remote file: 3.66 kB
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