338 lines
11 KiB
Python
338 lines
11 KiB
Python
from fastapi import APIRouter, Depends, Request, Form, HTTPException, Body
|
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
|
import httpx
|
|
from typing import Optional, List, Dict, Any
|
|
from pydantic import BaseModel
|
|
from deps import current_user
|
|
from services.config_loader import ConfigLoader
|
|
from services.localization import merge_role_localization
|
|
from config import get_settings
|
|
|
|
router = APIRouter()
|
|
settings = get_settings()
|
|
|
|
@router.get("/crumbforest/roles", response_class=HTMLResponse)
|
|
async def roles_dashboard(req: Request, user = Depends(current_user)):
|
|
"""
|
|
Show available roles based on user's group.
|
|
"""
|
|
# Determine user group
|
|
if not user:
|
|
user_group = 'home'
|
|
else:
|
|
user_group = user.get('user_group', 'demo')
|
|
|
|
# Get Language
|
|
lang = req.query_params.get("lang") or req.session.get("lang") or "de"
|
|
req.session["lang"] = lang
|
|
|
|
# Load config
|
|
config = ConfigLoader.load_config()
|
|
all_roles_raw = config.get('roles', {})
|
|
|
|
# Localize roles
|
|
all_roles = merge_role_localization(all_roles_raw, lang)
|
|
|
|
# Filter roles by group access
|
|
available_roles = {
|
|
rid: role for rid, role in all_roles.items()
|
|
if user_group in role.get('group_access', [])
|
|
}
|
|
|
|
# Get group config for template rendering
|
|
group_config = config.get('groups', {}).get(user_group, {})
|
|
|
|
return req.app.state.render(
|
|
req,
|
|
"crumbforest/roles_dashboard.html",
|
|
roles=available_roles,
|
|
user_group=user_group,
|
|
group_config=group_config,
|
|
lang=lang
|
|
)
|
|
|
|
@router.get("/crumbforest/roles/{role_id}", response_class=HTMLResponse)
|
|
async def role_chat(req: Request, role_id: str, user = Depends(current_user)):
|
|
"""
|
|
Chat interface for a specific role.
|
|
"""
|
|
# Get Language
|
|
lang = req.query_params.get("lang") or req.session.get("lang") or "de"
|
|
req.session["lang"] = lang
|
|
|
|
config = ConfigLoader.load_config()
|
|
|
|
# Localize single role lookup
|
|
# Only need to localize the specific role, but easiest to localize all then pick
|
|
all_roles = merge_role_localization(config.get('roles', {}), lang)
|
|
role = all_roles.get(role_id)
|
|
|
|
if not role:
|
|
raise HTTPException(404, "Role not found")
|
|
|
|
# Determine user group
|
|
if not user:
|
|
user_group = 'home'
|
|
else:
|
|
user_group = user.get('user_group', 'demo')
|
|
|
|
# Check access
|
|
if user_group not in role.get('group_access', []):
|
|
# Redirect to login if not logged in, else 403
|
|
if not user:
|
|
# Fix: Use /{lang}/login instead of /login to prevent matching {lang}=login
|
|
return RedirectResponse(f"/{lang}/login?next={req.url.path}", status_code=302)
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
group_config = config.get('groups', {}).get(user_group, {})
|
|
|
|
return req.app.state.render(
|
|
req,
|
|
"crumbforest/role_chat.html",
|
|
role=role,
|
|
group_config=group_config,
|
|
lang=lang
|
|
)
|
|
|
|
@router.post("/crumbforest/roles/{role_id}/ask")
|
|
async def ask_role(
|
|
req: Request,
|
|
role_id: str,
|
|
question: str = Form(...),
|
|
user = Depends(current_user)
|
|
):
|
|
"""
|
|
Send question to role and get AI response.
|
|
"""
|
|
config = ConfigLoader.load_config()
|
|
role = config.get('roles', {}).get(role_id)
|
|
|
|
if not role:
|
|
raise HTTPException(404, "Role not found")
|
|
|
|
# Access check (simplified for API)
|
|
if not user:
|
|
raise HTTPException(401, "Login required")
|
|
|
|
user_group = user.get('user_group', 'demo')
|
|
if user_group not in role.get('group_access', []):
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
# Get Language from session (persisted by dashboard/chat page view)
|
|
lang = req.session.get("lang", "de")
|
|
|
|
# Merge localization for this role again to be sure (in case session changed or not applied)
|
|
# Actually, config loaded above might not be localized if we used ConfigLoader directly.
|
|
# Let's import merge_role_localization
|
|
from services.localization import merge_role_localization
|
|
localized_roles = merge_role_localization(config.get('roles', {}), lang)
|
|
localized_role = localized_roles.get(role_id)
|
|
if localized_role:
|
|
role = localized_role
|
|
|
|
# Initialize RAG Service
|
|
from services.provider_factory import ProviderFactory
|
|
from utils.rag_chat import RAGChatService
|
|
from deps import get_qdrant_client
|
|
|
|
if not settings.openrouter_api_key:
|
|
return JSONResponse({
|
|
"answer": "⚠️ OpenRouter API Key missing.",
|
|
"error": "config_error"
|
|
}, status_code=500)
|
|
|
|
try:
|
|
embedding_provider = ProviderFactory.create_provider("openrouter", settings)
|
|
completion_provider = ProviderFactory.create_provider("openrouter", settings)
|
|
qdrant_client = get_qdrant_client()
|
|
|
|
rag_service = RAGChatService(
|
|
qdrant_client=qdrant_client,
|
|
embedding_provider=embedding_provider,
|
|
completion_provider=completion_provider
|
|
)
|
|
|
|
# Generate answer with RAG
|
|
# We pass role['system_prompt'] which is now localized!
|
|
result = rag_service.chat_with_context(
|
|
question=question,
|
|
character_name=role['name'],
|
|
character_prompt=role['system_prompt'],
|
|
context_limit=3,
|
|
lang=lang,
|
|
model_override=role.get('model') # Support specific models per role
|
|
)
|
|
|
|
answer = result['answer']
|
|
|
|
# Update history (Session based for now)
|
|
history_key = f'role_history_{role_id}'
|
|
history = req.session.get(history_key, [])
|
|
history.append({"role": "user", "content": question})
|
|
history.append({"role": "assistant", "content": answer})
|
|
req.session[history_key] = history[-10:]
|
|
|
|
# Log interaction (for Admin Logs)
|
|
try:
|
|
from utils.chat_logger import ChatLogger
|
|
logger = ChatLogger()
|
|
session_id = req.session.get("session_id", "anonymous")
|
|
|
|
logger.log_interaction(
|
|
character_id=role_id,
|
|
character_name=role.get('name', role_id),
|
|
user_id=user.get("id") if user else "anonymous",
|
|
user_role=user.get("role") if user else "anonymous",
|
|
question=question,
|
|
answer=answer,
|
|
model=result.get("model", "unknown"),
|
|
provider=result.get("provider", "unknown"),
|
|
context_found=result.get("context_found", False),
|
|
sources_count=len(result.get("sources", [])),
|
|
lang=lang,
|
|
session_id=session_id
|
|
)
|
|
except Exception as log_err:
|
|
print(f"⚠️ Logger failed: {log_err}")
|
|
|
|
return JSONResponse({
|
|
"role": role_id,
|
|
"question": question,
|
|
"answer": answer,
|
|
"usage": {}, # RAG service might not return usage exactly same way, or we skip
|
|
"sources": result.get("sources", []) # Return sources!
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"Exception in ask_role: {e}")
|
|
return JSONResponse({
|
|
"answer": "⚠️ Internal server error (RAG).",
|
|
"details": str(e)
|
|
}, status_code=500)
|
|
|
|
# --- Settings Endpoints ---
|
|
|
|
@router.get("/settings", response_class=HTMLResponse)
|
|
async def settings_page(req: Request, user = Depends(current_user)):
|
|
"""
|
|
User settings page.
|
|
"""
|
|
if not user:
|
|
return RedirectResponse(f"/login?next={req.url.path}", status_code=302)
|
|
|
|
config = ConfigLoader.load_config()
|
|
user_group = user.get('user_group', 'demo')
|
|
group_config = config.get('groups', {}).get(user_group, {})
|
|
theme_variants = config.get('theme_variants', {})
|
|
|
|
return req.app.state.render(
|
|
req,
|
|
"pages/settings.html",
|
|
group_config=group_config,
|
|
theme_variants=theme_variants
|
|
)
|
|
|
|
@router.post("/settings/theme")
|
|
async def update_theme(
|
|
req: Request,
|
|
theme: str = Form(...),
|
|
user = Depends(current_user)
|
|
):
|
|
"""
|
|
Update user theme preference.
|
|
"""
|
|
if not user:
|
|
raise HTTPException(401, "Login required")
|
|
|
|
# Verify theme exists
|
|
config = ConfigLoader.load_config()
|
|
if theme not in config.get('theme_variants', {}):
|
|
raise HTTPException(400, "Invalid theme")
|
|
|
|
# Update session
|
|
user['theme'] = theme
|
|
req.session['user'] = user
|
|
|
|
# Update DB
|
|
try:
|
|
from ..deps import get_db
|
|
from pymysql.cursors import DictCursor
|
|
with get_db().cursor(DictCursor) as cur:
|
|
cur.execute("UPDATE users SET theme=%s WHERE id=%s", (theme, user['id']))
|
|
except Exception as e:
|
|
print(f"Error updating theme in DB: {e}")
|
|
# Continue anyway, session is updated
|
|
|
|
return RedirectResponse("/settings", status_code=302)
|
|
|
|
@router.post("/settings/accessibility")
|
|
async def update_accessibility(
|
|
req: Request,
|
|
high_contrast: Optional[bool] = Form(None),
|
|
animation_reduced: Optional[bool] = Form(None),
|
|
font_size: str = Form("normal"),
|
|
user = Depends(current_user)
|
|
):
|
|
"""
|
|
Update accessibility settings.
|
|
"""
|
|
if not user:
|
|
raise HTTPException(401, "Login required")
|
|
|
|
import json
|
|
|
|
accessibility = {
|
|
"high_contrast": bool(high_contrast),
|
|
"animation_reduced": bool(animation_reduced),
|
|
"font_size": font_size
|
|
}
|
|
|
|
# Update session
|
|
user['accessibility'] = accessibility
|
|
|
|
# If high contrast is enabled, force that theme, otherwise revert to group default or keep current?
|
|
# For now, we just set the accessibility flags. The theme switcher handles the main theme.
|
|
# However, if high_contrast is ON, we might want to auto-switch to pico-high-contrast.
|
|
if accessibility['high_contrast']:
|
|
user['theme'] = 'pico-high-contrast'
|
|
elif user['theme'] == 'pico-high-contrast' and not accessibility['high_contrast']:
|
|
# Revert to default if turning off high contrast
|
|
user['theme'] = 'pico-default'
|
|
|
|
req.session['user'] = user
|
|
|
|
# Update DB
|
|
try:
|
|
from deps import get_db
|
|
from pymysql.cursors import DictCursor
|
|
with get_db().cursor(DictCursor) as cur:
|
|
cur.execute(
|
|
"UPDATE users SET accessibility=%s, theme=%s WHERE id=%s",
|
|
(json.dumps(accessibility), user['theme'], user['id'])
|
|
)
|
|
except Exception as e:
|
|
print(f"Error updating accessibility in DB: {e}")
|
|
|
|
return RedirectResponse("/settings", status_code=302)
|
|
|
|
@router.get("/crumbforest/roles/{role_id}/export")
|
|
async def export_history(req: Request, role_id: str, user = Depends(current_user)):
|
|
"""
|
|
Export chat history as JSON.
|
|
"""
|
|
if not user:
|
|
raise HTTPException(401, "Login required")
|
|
|
|
history_key = f'role_history_{role_id}'
|
|
history = req.session.get(history_key, [])
|
|
|
|
return JSONResponse({
|
|
"role": role_id,
|
|
"exported_at": str(import_datetime().now()),
|
|
"history": history
|
|
})
|
|
|
|
def import_datetime():
|
|
from datetime import datetime
|
|
return datetime
|