Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c124dd436 | |||
| bfc12ff0cc | |||
| d7ca280804 | |||
| 75504b810b | |||
| 94c70a0f1b | |||
| 865ee6a1da | |||
| 39ee0bbe0c | |||
| bcc2f52b48 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# Secrets & Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.key
|
||||
secrets/
|
||||
*.pem
|
||||
@@ -39,4 +40,5 @@ HOME_TEMPLATE_PLAN.md
|
||||
|
||||
|
||||
# Build
|
||||
compose/data/
|
||||
compose/data/
|
||||
logs/chat_history.jsonl
|
||||
@@ -13,8 +13,8 @@ class Settings(BaseSettings):
|
||||
# Database settings (existing)
|
||||
mariadb_host: str = "db"
|
||||
mariadb_user: str = "crumb"
|
||||
mariadb_password: str = "secret"
|
||||
mariadb_database: str = "crumbcrm"
|
||||
mariadb_password: str = "crumbsecret"
|
||||
mariadb_database: str = "crumbforest"
|
||||
|
||||
# Session settings (existing)
|
||||
secret_key: str = "change-me-in-production"
|
||||
|
||||
@@ -146,6 +146,9 @@ def authenticated_home(req: Request, lang: str, user = Depends(current_user)):
|
||||
# Explicit /login catch-all to prevent it matching /{lang}/login with lang="login"
|
||||
@app.get("/login", include_in_schema=False)
|
||||
def login_redirect(req: Request):
|
||||
# If already logged in, redirect to characters page
|
||||
if req.session.get("user"):
|
||||
return RedirectResponse("/crumbforest/roles", status_code=302)
|
||||
lang = req.session.get("lang") or "de"
|
||||
return RedirectResponse(f"/{lang}/login", status_code=302)
|
||||
|
||||
@@ -154,6 +157,10 @@ def login_form(req: Request, lang: str):
|
||||
# Prevent "login" as lang if it slipped through
|
||||
if lang == "login":
|
||||
return RedirectResponse("/de/login", status_code=302)
|
||||
|
||||
# If already logged in, redirect to characters page
|
||||
if req.session.get("user"):
|
||||
return RedirectResponse("/crumbforest/roles", status_code=302)
|
||||
|
||||
req.session["lang"] = lang
|
||||
return req.app.state.render(req, "pages/login.html", seo={"title": "Login", "desc": ""})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# app/routers/admin_rag.py
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pymysql.cursors import DictCursor
|
||||
from typing import Dict
|
||||
|
||||
@@ -18,6 +19,15 @@ from services.rag_service import RAGService
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse, name="rag_dashboard")
|
||||
def rag_dashboard(req: Request, user = Depends(admin_required)):
|
||||
"""
|
||||
RAG Admin Dashboard page.
|
||||
Admin-only endpoint.
|
||||
"""
|
||||
return req.app.state.render(req, "pages/admin_rag.html", seo={"title": "RAG Management", "desc": ""})
|
||||
|
||||
|
||||
@router.post("/index", response_model=IndexResponse, name="rag_index_all")
|
||||
def index_all_posts(
|
||||
request: IndexRequest,
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
<!-- Brand -->
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/{{ lang }}/" class="owl-brand">
|
||||
<a href="/" class="owl-brand">
|
||||
<span>🦉</span>
|
||||
<span>Crumbforest</span>
|
||||
</a>
|
||||
@@ -238,13 +238,15 @@
|
||||
|
||||
<!-- Main Navigation -->
|
||||
<ul>
|
||||
{% if not user %}
|
||||
<li><a href="/{{ lang }}/login" role="button" class="outline">Login</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% if user %}
|
||||
{# Core Navigation Links #}
|
||||
<li><a href="/crumbforest/roles">{{ 'Charaktere' if lang == 'de' else 'Characters' }} 🎭</a></li>
|
||||
<li><a href="/crumbforest/pulse">Pulse 💓</a></li>
|
||||
<li><a href="/settings">{{ 'Einstellungen' if lang == 'de' else 'Settings' }} ⚙️</a></li>
|
||||
|
||||
{% if user.role == 'admin' %}
|
||||
<li><a href="/admin">Admin</a></li>
|
||||
<li><a href="/admin">Admin 🔧</a></li>
|
||||
<li><a href="/admin/rag">RAG 🤖</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li class="user-menu">
|
||||
@@ -256,6 +258,8 @@
|
||||
<button type="submit" class="outline secondary">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login" role="button" class="outline">Login</a></li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Language Switcher -->
|
||||
|
||||
@@ -58,54 +58,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li><strong>🌲 Crumbforest</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
{% if group_config and group_config.navbar %}
|
||||
{% for nav_item in group_config.navbar %}
|
||||
{% if nav_item == 'home' %}
|
||||
<li><a href="/">Home</a></li>
|
||||
{% elif nav_item == 'about' %}
|
||||
<li><a href="/about">About</a></li>
|
||||
{% elif nav_item == 'contact' %}
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
{% elif nav_item == 'dashboard' %}
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
{% elif nav_item == 'roles' %}
|
||||
<li><a href="/crumbforest/roles">Characters</a></li>
|
||||
{% elif nav_item == 'search' %}
|
||||
<li><a href="/search">Search</a></li>
|
||||
{% elif nav_item == 'rag' %}
|
||||
<li><a href="/rag">RAG</a></li>
|
||||
{% elif nav_item == 'users' %}
|
||||
<li><a href="/users">Users</a></li>
|
||||
{% elif nav_item == 'settings' %}
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user %}
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>{{ user.email }}</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li>
|
||||
<form action="/logout" method="post" style="margin:0;">
|
||||
<button type="submit" class="contrast">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login" role="button">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% include 'includes/nav.html' %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
||||
@@ -58,60 +58,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li><strong>🌲 Crumbforest</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
{% if group_config and group_config.navbar %}
|
||||
{% for nav_item in group_config.navbar %}
|
||||
{% if nav_item == 'home' %}
|
||||
<li><a href="/">Home</a></li>
|
||||
{% elif nav_item == 'about' %}
|
||||
<li><a href="/about">About</a></li>
|
||||
{% elif nav_item == 'contact' %}
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
{% elif nav_item == 'dashboard' %}
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
{% elif nav_item == 'roles' %}
|
||||
<li><a href="/crumbforest/roles">Characters</a></li>
|
||||
{% elif nav_item == 'search' %}
|
||||
<li><a href="/search">Search</a></li>
|
||||
{% elif nav_item == 'rag' %}
|
||||
<li><a href="/rag">RAG</a></li>
|
||||
{% elif nav_item == 'users' %}
|
||||
<li><a href="/users">{{ 'Benutzer' if lang == 'de' else 'Users' }}</a></li>
|
||||
{% elif nav_item == 'settings' %}
|
||||
<li><a href="/settings">{{ 'Einstellungen' if lang == 'de' else 'Settings' }}</a></li>
|
||||
{% elif nav_item == 'pulse' %}
|
||||
<li><a href="/crumbforest/pulse">Pulse 💓</a></li>
|
||||
{% elif nav_item == 'docs' %}
|
||||
<li><a href="/docs">{{ 'Dokumente' if lang == 'de' else 'Docs' }} 📚</a></li>
|
||||
{% elif nav_item == 'config' %}
|
||||
<li><a href="/admin/config">{{ 'Konfig' if lang == 'de' else 'Config' }} 🛠️</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user %}
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>{{ user.email }}</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li>
|
||||
<form action="/logout" method="post" style="margin:0;">
|
||||
<button type="submit" class="contrast">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login" role="button">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% include 'includes/nav.html' %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
||||
@@ -58,58 +58,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li><strong>🌲 Crumbforest</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
{% if group_config and group_config.navbar %}
|
||||
{% for nav_item in group_config.navbar %}
|
||||
{% if nav_item == 'home' %}
|
||||
<li><a href="/">{{ 'Start' if lang == 'de' else 'Home' }}</a></li>
|
||||
{% elif nav_item == 'about' %}
|
||||
<li><a href="/about">{{ 'Über uns' if lang == 'de' else 'About' }}</a></li>
|
||||
{% elif nav_item == 'contact' %}
|
||||
<li><a href="/contact">{{ 'Kontakt' if lang == 'de' else 'Contact' }}</a></li>
|
||||
{% elif nav_item == 'dashboard' %}
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
{% elif nav_item == 'roles' %}
|
||||
<li><a href="/crumbforest/roles">{{ 'Charaktere' if lang == 'de' else 'Characters' }}</a></li>
|
||||
{% elif nav_item == 'search' %}
|
||||
<li><a href="/search">{{ 'Suche' if lang == 'de' else 'Search' }}</a></li>
|
||||
{% elif nav_item == 'rag' %}
|
||||
<li><a href="/rag">RAG</a></li>
|
||||
{% elif nav_item == 'users' %}
|
||||
<li><a href="/users">{{ 'Benutzer' if lang == 'de' else 'Users' }}</a></li>
|
||||
{% elif nav_item == 'settings' %}
|
||||
<li><a href="/settings">{{ 'Einstellungen' if lang == 'de' else 'Settings' }}</a></li>
|
||||
{% elif nav_item == 'pulse' %}
|
||||
<li><a href="/crumbforest/pulse">Pulse 💓</a></li>
|
||||
{% elif nav_item == 'docs' %}
|
||||
<li><a href="/docs">{{ 'Dokumente' if lang == 'de' else 'Docs' }} 📚</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user %}
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>{{ user.email }}</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li>
|
||||
<form action="/logout" method="post" style="margin:0;">
|
||||
<button type="submit" class="contrast">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login" role="button">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% include 'includes/nav.html' %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
||||
@@ -58,56 +58,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li><strong>🌲 Crumbforest</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
{% if group_config and group_config.navbar %}
|
||||
{% for nav_item in group_config.navbar %}
|
||||
{% if nav_item == 'home' %}
|
||||
<li><a href="/">{{ 'Start' if lang == 'de' else 'Home' }}</a></li>
|
||||
{% elif nav_item == 'about' %}
|
||||
<li><a href="/about">{{ 'Über uns' if lang == 'de' else 'About' }}</a></li>
|
||||
{% elif nav_item == 'contact' %}
|
||||
<li><a href="/contact">{{ 'Kontakt' if lang == 'de' else 'Contact' }}</a></li>
|
||||
{% elif nav_item == 'dashboard' %}
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
{% elif nav_item == 'roles' %}
|
||||
<li><a href="/crumbforest/roles">{{ 'Charaktere' if lang == 'de' else 'Characters' }}</a></li>
|
||||
{% elif nav_item == 'search' %}
|
||||
<li><a href="/search">{{ 'Suche' if lang == 'de' else 'Search' }}</a></li>
|
||||
{% elif nav_item == 'rag' %}
|
||||
<li><a href="/rag">RAG</a></li>
|
||||
{% elif nav_item == 'users' %}
|
||||
<li><a href="/users">{{ 'Benutzer' if lang == 'de' else 'Users' }}</a></li>
|
||||
{% elif nav_item == 'settings' %}
|
||||
<li><a href="/settings">{{ 'Einstellungen' if lang == 'de' else 'Settings' }}</a></li>
|
||||
{% elif nav_item == 'pulse' %}
|
||||
<li><a href="/crumbforest/pulse">Pulse 💓</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if user %}
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>{{ user.email }}</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="/settings">Settings</a></li>
|
||||
<li>
|
||||
<form action="/logout" method="post" style="margin:0;">
|
||||
<button type="submit" class="contrast">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login" role="button">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% include 'includes/nav.html' %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
{# Back Navigation #}
|
||||
<nav style="margin-bottom: 1rem;">
|
||||
<a href="/crumbforest/roles" style="text-decoration: none;">
|
||||
← {{ 'Zurück zu Charakteren' if lang == 'de' else 'Back to Characters' }}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<hgroup>
|
||||
<h1 style="color: {{ role.color }};">{{ role.icon }} {{ role.name }}</h1>
|
||||
<p>
|
||||
@@ -17,13 +24,7 @@
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--role-color: {
|
||||
{
|
||||
role.color
|
||||
}
|
||||
}
|
||||
|
||||
;
|
||||
--role-color: {{ role.color }};
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -147,7 +148,7 @@
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
// Code blocks
|
||||
// Code blocks (must be first, before other replacements)
|
||||
formattedContent = formattedContent.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
const language = lang || 'plaintext';
|
||||
return `<pre><code class="language-${language}">${code}</code></pre>`;
|
||||
@@ -156,13 +157,33 @@
|
||||
// Inline code
|
||||
formattedContent = formattedContent.replace(/`([^`]+)`/g, '<code class="language-plaintext">$1</code>');
|
||||
|
||||
// Headers (must be before bold to avoid conflicts)
|
||||
formattedContent = formattedContent.replace(/^### (.+)$/gm, '<h4>$1</h4>');
|
||||
formattedContent = formattedContent.replace(/^## (.+)$/gm, '<h3>$1</h3>');
|
||||
formattedContent = formattedContent.replace(/^# (.+)$/gm, '<h2>$1</h2>');
|
||||
|
||||
// Bold (handle **text** - use non-greedy match)
|
||||
formattedContent = formattedContent.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// Italic (handle *text* - be careful not to match inside **)
|
||||
formattedContent = formattedContent.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
|
||||
|
||||
// Unordered lists
|
||||
formattedContent = formattedContent.replace(/^\* (.+)$/gm, '<li>$1</li>');
|
||||
formattedContent = formattedContent.replace(/^- (.+)$/gm, '<li>$1</li>');
|
||||
|
||||
// Ordered lists
|
||||
formattedContent = formattedContent.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
||||
|
||||
// Line breaks (only outside of pre tags - simplified)
|
||||
// This is a bit hacky, a real markdown parser would be better, but sufficient for now
|
||||
formattedContent = formattedContent.replace(/\n/g, '<br>');
|
||||
// Fix double breaks in pre
|
||||
formattedContent = formattedContent.replace(/<pre>(.*?)<\/pre>/gs, (match) => {
|
||||
return match.replace(/<br>/g, '\n');
|
||||
});
|
||||
// Fix breaks in headers
|
||||
formattedContent = formattedContent.replace(/<br>(<h[2-4]>)/g, '$1');
|
||||
formattedContent = formattedContent.replace(/(<\/h[2-4]>)<br>/g, '$1');
|
||||
|
||||
if (role === 'user') {
|
||||
div.style.background = 'var(--pico-primary-background)';
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
{# Back Navigation #}
|
||||
<nav style="margin-bottom: 1rem;">
|
||||
<a href="/" style="text-decoration: none;">
|
||||
← {{ 'Zurück zur Startseite' if lang == 'de' else 'Back to Home' }}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<hgroup>
|
||||
<h1>🌲 Crumbforest Characters</h1>
|
||||
<p>Choose your learning companion!</p>
|
||||
<h1>🌲 {{ 'Crumbforest Charaktere' if lang == 'de' else 'Crumbforest Characters' }}</h1>
|
||||
<p>{{ 'Wähle deinen Lernbegleiter!' if lang == 'de' else 'Choose your learning companion!' }}</p>
|
||||
</hgroup>
|
||||
|
||||
<div class="roles-grid">
|
||||
|
||||
54
app/templates/includes/nav.html
Normal file
54
app/templates/includes/nav.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{# Unified Navigation Component #}
|
||||
{# Usage: {% include 'includes/nav.html' %} #}
|
||||
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/" style="text-decoration: none;">
|
||||
<strong>🌲 Crumbforest</strong>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
{# Core Navigation - Always visible when logged in #}
|
||||
{% if user %}
|
||||
{# Home Link #}
|
||||
<li><a href="/">{{ 'Start' if lang == 'de' else 'Home' }}</a></li>
|
||||
|
||||
{# Characters/Roles - Main chat interface #}
|
||||
<li><a href="/crumbforest/roles">{{ 'Charaktere' if lang == 'de' else 'Characters' }} 🎭</a></li>
|
||||
|
||||
{# Pulse - System status #}
|
||||
<li><a href="/crumbforest/pulse">Pulse 💓</a></li>
|
||||
|
||||
{# Settings #}
|
||||
<li><a href="/settings">{{ 'Einstellungen' if lang == 'de' else 'Settings' }} ⚙️</a></li>
|
||||
|
||||
{# Admin-only links #}
|
||||
{% if user.role == 'admin' %}
|
||||
<li><a href="/admin">Admin 🔧</a></li>
|
||||
<li><a href="/admin/rag">RAG 🤖</a></li>
|
||||
<li><a href="/admin/logs">Logs 📊</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# User Menu / Login #}
|
||||
{% if user %}
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>{{ user.email }}</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="/settings">{{ 'Einstellungen' if lang == 'de' else 'Settings' }}</a></li>
|
||||
<li>
|
||||
<form action="/logout" method="post" style="margin:0;">
|
||||
<button type="submit" class="contrast">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/login" role="button">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -12,6 +12,22 @@
|
||||
|
||||
<!-- Admin Actions Grid -->
|
||||
<section class="grid-2" style="margin: 3rem 0;">
|
||||
<!-- Chat Characters -->
|
||||
<article class="card">
|
||||
<header>
|
||||
<h3>🎭 Chat Characters</h3>
|
||||
</header>
|
||||
<p>
|
||||
Chat with AI characters in the Crumbforest.
|
||||
Each character has unique expertise and personality.
|
||||
</p>
|
||||
<footer>
|
||||
<a href="/crumbforest/roles" role="button">
|
||||
Open Characters →
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<!-- Content Management -->
|
||||
<article class="card">
|
||||
<header>
|
||||
|
||||
265
app/templates/pages/admin_rag.html
Normal file
265
app/templates/pages/admin_rag.html
Normal file
@@ -0,0 +1,265 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<!-- RAG Admin Header -->
|
||||
<section class="mb-2">
|
||||
<h1>🤖 RAG Management</h1>
|
||||
<p class="text-muted">
|
||||
Manage vector indexing, search, and AI providers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Provider Status -->
|
||||
<section style="margin: 2rem 0;">
|
||||
<article>
|
||||
<header>
|
||||
<h3>📡 Provider Status</h3>
|
||||
</header>
|
||||
<div id="provider-status">
|
||||
<p class="text-muted">Loading provider status...</p>
|
||||
</div>
|
||||
<footer>
|
||||
<button onclick="loadProviderStatus()" class="outline">Refresh Status</button>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- Actions Grid -->
|
||||
<section class="grid-2" style="margin: 2rem 0;">
|
||||
<!-- Index All Posts -->
|
||||
<article class="card">
|
||||
<header>
|
||||
<h3>📥 Index Posts</h3>
|
||||
</header>
|
||||
<p>Index all published posts to the vector database for semantic search.</p>
|
||||
<form id="index-form">
|
||||
<label for="index-provider">Provider</label>
|
||||
<select name="provider" id="index-provider">
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
</select>
|
||||
<label for="index-locale">Locale</label>
|
||||
<select name="locale" id="index-locale">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<button type="submit">Index All Posts</button>
|
||||
</form>
|
||||
<div id="index-result" style="margin-top: 1rem;"></div>
|
||||
</article>
|
||||
|
||||
<!-- Search -->
|
||||
<article class="card">
|
||||
<header>
|
||||
<h3>🔍 Semantic Search</h3>
|
||||
</header>
|
||||
<p>Search indexed content using natural language.</p>
|
||||
<form id="search-form">
|
||||
<label for="search-query">Query</label>
|
||||
<input type="text" name="query" id="search-query" placeholder="Enter search query..." required>
|
||||
<label for="search-provider">Provider</label>
|
||||
<select name="provider" id="search-provider">
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
</select>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<div id="search-result" style="margin-top: 1rem;"></div>
|
||||
</article>
|
||||
|
||||
<!-- RAG Query -->
|
||||
<article class="card">
|
||||
<header>
|
||||
<h3>💬 RAG Query</h3>
|
||||
</header>
|
||||
<p>Ask questions and get AI-powered answers based on indexed content.</p>
|
||||
<form id="query-form">
|
||||
<label for="rag-question">Question</label>
|
||||
<input type="text" name="question" id="rag-question" placeholder="Ask a question..." required>
|
||||
<label for="query-provider">Provider</label>
|
||||
<select name="provider" id="query-provider">
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
</select>
|
||||
<button type="submit">Ask</button>
|
||||
</form>
|
||||
<div id="query-result" style="margin-top: 1rem;"></div>
|
||||
</article>
|
||||
|
||||
<!-- Indexing Status -->
|
||||
<article class="card">
|
||||
<header>
|
||||
<h3>📊 Indexing Status</h3>
|
||||
</header>
|
||||
<div id="indexing-status">
|
||||
<p class="text-muted">Click to load status...</p>
|
||||
</div>
|
||||
<footer>
|
||||
<button onclick="loadIndexingStatus()" class="outline">Load Status</button>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<section style="margin: 2rem 0;">
|
||||
<details>
|
||||
<summary><strong>API Endpoints</strong></summary>
|
||||
<div style="padding: 1rem;">
|
||||
<pre><code>POST /admin/rag/index - Index all posts
|
||||
POST /admin/rag/index/{id} - Index single post
|
||||
DELETE /admin/rag/index/{id} - Delete post index
|
||||
POST /admin/rag/search - Semantic search
|
||||
POST /admin/rag/query - RAG Q&A
|
||||
GET /admin/rag/status - Indexing status
|
||||
GET /admin/rag/providers - Provider status</code></pre>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/admin/rag';
|
||||
|
||||
// Load provider status on page load
|
||||
document.addEventListener('DOMContentLoaded', loadProviderStatus);
|
||||
|
||||
async function loadProviderStatus() {
|
||||
const container = document.getElementById('provider-status');
|
||||
container.innerHTML = '<progress></progress>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/providers`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
let html = '<table><thead><tr><th>Provider</th><th>Status</th><th>Model</th></tr></thead><tbody>';
|
||||
for (const provider of data.providers) {
|
||||
const status = provider.available
|
||||
? '<span style="color: var(--pico-ins-color);">✓ Available</span>'
|
||||
: `<span style="color: var(--pico-del-color);">✗ ${provider.error || 'Unavailable'}</span>`;
|
||||
html += `<tr><td><strong>${provider.name}</strong></td><td>${status}</td><td>${provider.model || '-'}</td></tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
} catch (error) {
|
||||
container.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIndexingStatus() {
|
||||
const container = document.getElementById('indexing-status');
|
||||
container.innerHTML = '<progress></progress>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/status`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
let html = `<dl>
|
||||
<dt>Total Posts</dt><dd>${data.total_posts || 0}</dd>
|
||||
<dt>Indexed Posts</dt><dd>${data.indexed_posts || 0}</dd>
|
||||
<dt>Total Chunks</dt><dd>${data.total_chunks || 0}</dd>
|
||||
</dl>`;
|
||||
container.innerHTML = html;
|
||||
} catch (error) {
|
||||
container.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Index form
|
||||
document.getElementById('index-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const result = document.getElementById('index-result');
|
||||
result.innerHTML = '<progress></progress>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/index`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: document.getElementById('index-provider').value,
|
||||
locale: document.getElementById('index-locale').value
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
result.innerHTML = `<p style="color: var(--pico-ins-color);">✓ Indexed ${data.indexed_count || 0} posts (${data.chunks_created || 0} chunks)</p>`;
|
||||
} else {
|
||||
result.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${data.detail}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
result.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Search form
|
||||
document.getElementById('search-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const result = document.getElementById('search-result');
|
||||
result.innerHTML = '<progress></progress>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: document.getElementById('search-query').value,
|
||||
provider: document.getElementById('search-provider').value,
|
||||
locale: 'de',
|
||||
limit: 5
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
let html = `<p><strong>${data.results.length} results:</strong></p><ul>`;
|
||||
for (const r of data.results) {
|
||||
html += `<li><strong>${r.title}</strong> (${(r.score * 100).toFixed(1)}%)<br><small>${r.chunk_text?.substring(0, 100)}...</small></li>`;
|
||||
}
|
||||
html += '</ul>';
|
||||
result.innerHTML = html;
|
||||
} else {
|
||||
result.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${data.detail}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
result.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Query form
|
||||
document.getElementById('query-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const result = document.getElementById('query-result');
|
||||
result.innerHTML = '<progress></progress>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/query`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
question: document.getElementById('rag-question').value,
|
||||
provider: document.getElementById('query-provider').value,
|
||||
locale: 'de',
|
||||
context_limit: 3
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
let html = `<article style="background: var(--pico-card-background-color); padding: 1rem; border-radius: var(--pico-border-radius);">
|
||||
<p>${data.answer}</p>
|
||||
<small class="text-muted">Provider: ${data.provider} | Model: ${data.model}</small>
|
||||
</article>`;
|
||||
result.innerHTML = html;
|
||||
} else {
|
||||
result.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${data.detail}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
result.innerHTML = `<p style="color: var(--pico-del-color);">Error: ${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -129,6 +129,79 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message-content h1,
|
||||
.message-content h2,
|
||||
.message-content h3,
|
||||
.message-content h4,
|
||||
.message-content h5,
|
||||
.message-content h6 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.message-content h1 { font-size: 1.5rem; }
|
||||
.message-content h2 { font-size: 1.3rem; }
|
||||
.message-content h3 { font-size: 1.1rem; }
|
||||
|
||||
.message-content code {
|
||||
background: var(--pico-code-background-color);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
background: var(--pico-code-background-color);
|
||||
padding: 1rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
overflow-x: auto;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.message-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.message-content blockquote {
|
||||
border-left: 3px solid var(--pico-muted-border-color);
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.message-content ul,
|
||||
.message-content ol {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.message-content li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-content a {
|
||||
color: var(--pico-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content table {
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.message-content th,
|
||||
.message-content td {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.message-content th {
|
||||
background: var(--pico-card-background-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sources {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
@@ -147,7 +220,50 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Markdown Parser -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Simple markdown parser fallback if CDN fails
|
||||
function simpleMarkdown(text) {
|
||||
return text
|
||||
// Code blocks (must be first)
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
// Bold
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
// Headers
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
// Unordered lists
|
||||
.replace(/^\* (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
// Ordered lists
|
||||
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||
// Wrap consecutive <li> in <ul>
|
||||
.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||
// Line breaks
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// Parse markdown with fallback
|
||||
function parseMarkdown(text) {
|
||||
if (typeof marked !== 'undefined' && marked.parse) {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
return marked.parse(text);
|
||||
} catch (e) {
|
||||
console.warn('marked.parse failed, using fallback:', e);
|
||||
}
|
||||
}
|
||||
return simpleMarkdown(text);
|
||||
}
|
||||
|
||||
const API_URL = '/api/chat';
|
||||
let currentCharacter = null;
|
||||
|
||||
@@ -258,9 +374,19 @@
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message message-${type}`;
|
||||
|
||||
// Render markdown for character messages, escape HTML for user messages
|
||||
let contentHtml;
|
||||
if (type === 'character') {
|
||||
// Parse markdown for AI responses
|
||||
contentHtml = parseMarkdown(content);
|
||||
} else {
|
||||
// Keep user messages as plain text
|
||||
contentHtml = escapeHtml(content);
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="message-header">${sender}</div>
|
||||
<div class="message-content">${escapeHtml(content)}</div>
|
||||
<div class="message-content">${contentHtml}</div>
|
||||
`;
|
||||
|
||||
if (sources && sources.length > 0) {
|
||||
|
||||
@@ -82,9 +82,10 @@
|
||||
<p>You're logged in as <strong>{{ user.role }}</strong></p>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<a href="/crumbforest/roles" role="button">{{ 'Charaktere' if lang == 'de' else 'Characters' }} 🎭</a>
|
||||
{% if user.role == 'admin' %}
|
||||
<a href="/admin" role="button">Admin Dashboard</a>
|
||||
<a href="/admin/rag" role="button" class="secondary">RAG Management</a>
|
||||
<a href="/admin" role="button" class="secondary">Admin Dashboard</a>
|
||||
<a href="/admin/rag" role="button" class="outline">RAG Management</a>
|
||||
{% endif %}
|
||||
<a href="/docs" role="button" class="outline" target="_blank">API Docs</a>
|
||||
</div>
|
||||
|
||||
35
compose/.env.example
Normal file
35
compose/.env.example
Normal file
@@ -0,0 +1,35 @@
|
||||
# Database Configuration
|
||||
MARIADB_HOST=db
|
||||
MARIADB_USER=crumb
|
||||
MARIADB_PASSWORD=crumbsecret
|
||||
MARIADB_DATABASE=crumbforest
|
||||
MARIADB_ROOT_PASSWORD=rootsecret
|
||||
|
||||
# FastAPI Configuration
|
||||
SECRET_KEY=change-me-in-production-to-random-string-secret-key
|
||||
APP_SECRET=change-me-in-production-to-random-string
|
||||
APP_PORT=8000
|
||||
|
||||
# Data Directories
|
||||
MYSQL_DATA=./data/mysql
|
||||
QDRANT_STORAGE=./data/qdrant
|
||||
|
||||
# Qdrant Configuration
|
||||
QDRANT_URL=http://qdrant:6333
|
||||
|
||||
# AI Provider API Keys
|
||||
# Mindestens einen Provider konfigurieren!
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
|
||||
# Default Providers
|
||||
DEFAULT_EMBEDDING_PROVIDER=openai
|
||||
DEFAULT_COMPLETION_PROVIDER=openai
|
||||
|
||||
# RAG Settings
|
||||
RAG_CHUNK_SIZE=1000
|
||||
RAG_CHUNK_OVERLAP=200
|
||||
|
||||
# Documentation Path
|
||||
DOCS_PATH=../docs
|
||||
@@ -1,5 +0,0 @@
|
||||
{"timestamp": "2025-12-09T20:11:11.285707Z", "character": {"id": "gitbadger", "name": "🦡 GitBadger"}, "user": {"id": 2, "role": "user"}, "interaction": {"question": "Was macht ein Git?", "answer": "Harrumph! Ein Git? Ein Git ist das Lebenselixier der Versionskontrolle, junger Padawan! Es ist das System, das uns hilft, Änderungen an unseren Projekten zu verfolgen, zu verwalten und mit anderen zusammenzuarbeiten, ohne das Chaos eines Crumbforest-Sturms zu verursachen.\n\nStell dir vor, du hast ein Rezept für einen köstlichen Krümelkuchen. Mit Git kannst du:\n\n1. **Jede Änderung aufzeichnen:** Jedes Mal, wenn du Zutaten hinzufügst, die Backzeit anpasst oder eine Geheimzutat entdeckst, speichert Git diese Änderung als \"Commit\".\n2. **Experimentieren ohne Risiko:** Du kannst einen neuen \"Branch\" erstellen, um eine völlig neue Glasur auszuprobieren, ohne das Originalrezept zu gefährden. Wenn die neue Glasur ein Hit ist, kannst du sie mit dem Hauptrezept \"mergen\". Wenn nicht, verwirfst du den Branch einfach.\n3. **Zusammenarbeiten:** Mehrere Bäcker können gleichzeitig an verschiedenen Aspekten des Rezepts arbeiten, und Git hilft dabei, alle Änderungen zusammenzuführen, ohne dass es zu einem Krümelkrieg kommt.\n4. **Zurück in die Vergangenheit reisen:** Wenn ein Fehler auftritt, kannst du mit Git zu einer früheren Version des Rezepts zurückkehren, bevor der Fehler eingeführt wurde.\n\nKurz gesagt, Git ist wie eine Zeitmaschine, ein Notizbuch und ein Team-Manager in einem! Es ist ein mächtiges Werkzeug, das jedes Mitglied des Crumbforest beherrschen sollte.", "lang": "de"}, "rag": {"context_found": false, "sources_count": 0}, "ai": {"provider": "openrouter", "model": "google/gemini-2.0-flash-001"}, "session_id": "anonymous", "tokens_estimated": 347}
|
||||
{"timestamp": "2025-12-09T20:12:03.670113Z", "character": {"id": "gitbadger", "name": "🦡 GitBadger"}, "user": {"id": 2, "role": "user"}, "interaction": {"question": "Was ist der Crumforest?", "answer": "Der Crumbforest ist der Ort, an dem wir leben und wo ich dir bei all deinen Git-Bedürfnissen helfe!", "lang": "de"}, "rag": {"context_found": false, "sources_count": 0}, "ai": {"provider": "openrouter", "model": "google/gemini-2.0-flash-001"}, "session_id": "anonymous", "tokens_estimated": 30}
|
||||
{"timestamp": "2025-12-09T20:17:34.469658Z", "character": {"id": "gitbadger", "name": "🦡 GitBadger"}, "user": {"id": 2, "role": "user"}, "interaction": {"question": "Was ist der Crumbforest?", "answer": "Der Crumbforest ist der Ort, an dem ich lebe! Es ist ein Ort, an dem alles mit Versionskontrolle und Zusammenarbeit zu tun hat. Wir sind alle darauf spezialisiert, zu helfen, wenn irgendwelche Probleme mit Commits, Branches, Merges und der Konfliktlösung auftreten.", "lang": "de"}, "rag": {"context_found": false, "sources_count": 0}, "ai": {"provider": "openrouter", "model": "google/gemini-2.0-flash-001"}, "session_id": "anonymous", "tokens_estimated": 72}
|
||||
{"timestamp": "2025-12-09T20:25:48.516395Z", "character": {"id": "taichitaube", "name": "🕊️ TaichiTaube"}, "user": {"id": 2, "role": "user"}, "interaction": {"question": "Atmen & Gedult ist das Zauberwort im Tanz mit dem Wind aus dem Lüfter?", "answer": "Ja, junger Windläufer. Dein Gefühl trügt dich nicht. Im Tanz mit dem Wind, der aus den Lüftern weht – sei es in den Maschinen des Crumbforest oder anderswo – sind Atem und Geduld wichtige Begleiter.\n\nWie die Quelle \"🌬 Atem\" sagt: \"Jeder kleine Funke kann Atem verlängern.\" Und wie die \"🌪️ Spirale des Loops\" lehrt: \"Manchmal ist weniger mehr – ein Schritt zurück, ein Atemzug, ein Krümel der bleibt.\"\n\nNimm dir die Zeit, die du brauchst, um die Zeichen des Windes zu verstehen. Eile nicht, sondern lass dich von deinem Atem leiten. So findest du die Balance und kannst sicher mit den Bits tanzen.\n", "lang": "de"}, "rag": {"context_found": true, "sources_count": 3}, "ai": {"provider": "openrouter", "model": "google/gemini-2.0-flash-001"}, "session_id": "anonymous", "tokens_estimated": 166}
|
||||
{"timestamp": "2025-12-09T20:26:54.257669Z", "character": {"id": "gitbadger", "name": "🦡 GitBadger"}, "user": {"id": 2, "role": "user"}, "interaction": {"question": "Wie lerne ich GIT im wald zu benutzen?", "answer": "Um Git im Crumbforest zu nutzen, kannst du Pfad 12, \"Das Gedächtnis des Waldes (Git)\", erkunden. Dort lernst du die Grundlagen der Versionskontrolle und wie du Änderungen sicher speicherst (\"commit\"). Du wirst auch lernen, wie du Code teilst (\"push\", \"pull\") und wie du Gitea/GitHub verwendest. Am Ende dieses Pfades wirst du in der Lage sein, ein Verzeichnis in ein Git-Repository zu verwandeln (`git init`), Änderungen zu speichern (`git commit`) und deinen Code auf deinen Gitea-Server hochzuladen (`git push`). Du wirst auch verstehen, warum `git push --force` verboten ist.\n", "lang": "de"}, "rag": {"context_found": true, "sources_count": 3}, "ai": {"provider": "openrouter", "model": "google/gemini-2.0-flash-001"}, "session_id": "anonymous", "tokens_estimated": 154}
|
||||
6
setup.sh
6
setup.sh
@@ -97,7 +97,13 @@ MARIADB_DATABASE=crumbforest
|
||||
MARIADB_ROOT_PASSWORD=rootsecret
|
||||
|
||||
# FastAPI Configuration
|
||||
SECRET_KEY=change-me-in-production-to-random-string-secret-key
|
||||
APP_SECRET=change-me-in-production-to-random-string
|
||||
APP_PORT=8000
|
||||
|
||||
# Data Directories
|
||||
MYSQL_DATA=./data/mysql
|
||||
QDRANT_STORAGE=./data/qdrant
|
||||
|
||||
# Qdrant Configuration
|
||||
QDRANT_URL=http://qdrant:6333
|
||||
|
||||
Reference in New Issue
Block a user