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