diff --git a/backend/config.py b/backend/config.py index 440b52a..550caca 100644 --- a/backend/config.py +++ b/backend/config.py @@ -53,6 +53,16 @@ PROVIDERS = { "quick": "minimax/MiniMax-M2.7-highspeed", "env_key": "MINIMAX_API_KEY", }, + # Wie "minimax", aber Chat/Elemente (Rolle "fast") laufen auf M3 OHNE Thinking. + # M2.x kann Thinking nicht abschalten — nur M3 respektiert thinking:disabled. + # guide/quick bleiben identisch zur Thinking-Variante. + "minimax-direkt": { + "cli": "opencode", + "guide": "minimax/MiniMax-M3", + "fast": "minimax-direkt/MiniMax-M3", + "quick": "minimax/MiniMax-M2.7-highspeed", + "env_key": "MINIMAX_API_KEY", + }, "lokal": { "cli": "opencode", "guide": "ollama/qwen3.6:27b", diff --git a/backend/database.py b/backend/database.py index 151acdf..de6deac 100644 --- a/backend/database.py +++ b/backend/database.py @@ -42,6 +42,8 @@ CREATE TABLE IF NOT EXISTS elements ( description TEXT NOT NULL DEFAULT '', examples TEXT NOT NULL DEFAULT '[]', hints TEXT NOT NULL DEFAULT '[]', + aufgabe TEXT NOT NULL DEFAULT '', + loesung TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) @@ -68,6 +70,11 @@ async def init_db(): await db.execute("ALTER TABLE guides ADD COLUMN step INTEGER") except aiosqlite.OperationalError: pass + for col in ("aufgabe", "loesung"): # Migration für Elemente ohne Aufgabe/Lösung + try: + await db.execute(f"ALTER TABLE elements ADD COLUMN {col} TEXT NOT NULL DEFAULT ''") + except aiosqlite.OperationalError: + pass await db.execute( "UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' " "WHERE status IN ('queued', 'generating')" @@ -166,8 +173,8 @@ def _element_row(row, cursor) -> dict: async def create_element(element: dict) -> dict: db = await get_db() await db.execute( - """INSERT INTO elements (id, topic, title, description, examples, hints, created_at, updated_at) - VALUES (:id, :topic, :title, :description, :examples, :hints, :created_at, :updated_at)""", + """INSERT INTO elements (id, topic, title, description, examples, hints, aufgabe, loesung, created_at, updated_at) + VALUES (:id, :topic, :title, :description, :examples, :hints, :aufgabe, :loesung, :created_at, :updated_at)""", {**element, "examples": json.dumps(element["examples"], ensure_ascii=False), "hints": json.dumps(element["hints"], ensure_ascii=False)}, ) diff --git a/backend/generator.py b/backend/generator.py index 8168423..b81fb7f 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -1396,6 +1396,8 @@ def _element_fields(data: dict) -> dict | None: "description": str(data.get("description", "")).strip(), "examples": listen["examples"], "hints": listen["hints"], + "aufgabe": str(data.get("aufgabe", "")).strip(), + "loesung": str(data.get("loesung", "")).strip(), } @@ -1418,7 +1420,7 @@ def _topic_context(topic: str, limit: int = 12000) -> str: async def generate_element(topic: str, hint: str, provider: str = DEFAULT_PROVIDER) -> dict: """Erstellt Element-Felder per KI. Fallback: nur Titel aus dem Stichwort.""" - fallback = {"title": hint.strip() or "Neues Element", "description": "", "examples": [], "hints": []} + fallback = {"title": hint.strip() or "Neues Element", "description": "", "examples": [], "hints": [], "aufgabe": "", "loesung": ""} try: prompt = _prompt( "Element-Create", @@ -1454,7 +1456,7 @@ def _parse_suggestions(stdout: str) -> list[dict] | None: text = str(s.get("text", "")).strip() target = s.get("target") content = str(s.get("content", "")).strip() - if text and content and target in ("description", "examples", "hints"): + if text and content and target in ("description", "examples", "hints", "aufgabe", "loesung"): if target == "examples": content = _fence(content) suggestions.append({"text": text, "target": target, "content": content}) @@ -1465,7 +1467,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list """Zweischrittige Prüfung auf fehlende Infos: Recherche → Verifizieren. None bei Fehler.""" try: element_json = json.dumps( - {k: element[k] for k in ("title", "description", "examples", "hints")}, + {k: element[k] for k in ("title", "description", "examples", "hints", "aufgabe", "loesung")}, ensure_ascii=False, indent=1, ) context = _topic_context(element["topic"]) @@ -1502,7 +1504,7 @@ async def check_element(element: dict, provider: str = DEFAULT_PROVIDER) -> list def _element_json(element: dict) -> str: return json.dumps( - {k: element[k] for k in ("title", "description", "examples", "hints")}, + {k: element[k] for k in ("title", "description", "examples", "hints", "aufgabe", "loesung")}, ensure_ascii=False, indent=1, ) @@ -1518,7 +1520,7 @@ def _validate_change(c, element: dict) -> dict | None: content = str(c.get("content", "")).strip() if not text or action not in ("entfernen", "anpassen", "hinzufuegen"): return None - if target not in ("title", "description", "examples", "hints"): + if target not in ("title", "description", "examples", "hints", "aufgabe", "loesung"): return None if action in ("anpassen", "hinzufuegen") and not content: return None diff --git a/backend/models.py b/backend/models.py index 1e5e501..5c5400c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -8,7 +8,7 @@ FormatType = Literal[ "FullGuide", ] -ProviderType = Literal["claude", "minimax", "lokal"] +ProviderType = Literal["claude", "minimax", "minimax-direkt", "lokal"] class GuideCreateRequest(BaseModel): @@ -86,6 +86,8 @@ class ElementResponse(BaseModel): description: str = "" examples: list[str] = [] hints: list[str] = [] + aufgabe: str = "" + loesung: str = "" created_at: str updated_at: str @@ -101,6 +103,8 @@ class ElementUpdateRequest(BaseModel): description: str | None = None examples: list[str] | None = None hints: list[str] | None = None + aufgabe: str | None = None + loesung: str | None = None class ElementCheckRequest(BaseModel): @@ -109,7 +113,7 @@ class ElementCheckRequest(BaseModel): class ElementSuggestion(BaseModel): text: str - target: Literal["description", "examples", "hints"] + target: Literal["description", "examples", "hints", "aufgabe", "loesung"] content: str @@ -120,7 +124,7 @@ class ElementCheckResponse(BaseModel): class ElementStyleChange(BaseModel): text: str action: Literal["entfernen", "anpassen", "hinzufuegen"] - target: Literal["title", "description", "examples", "hints"] + target: Literal["title", "description", "examples", "hints", "aufgabe", "loesung"] index: int | None = None content: str = "" diff --git a/dev-ops/opencode.json b/dev-ops/opencode.json index 3e80dbb..7dba695 100644 --- a/dev-ops/opencode.json +++ b/dev-ops/opencode.json @@ -11,6 +11,22 @@ } } }, + "minimax-direkt": { + "npm": "@ai-sdk/anthropic", + "name": "MiniMax (ohne Thinking)", + "options": { + "baseURL": "https://api.minimax.io/anthropic/v1", + "apiKey": "{env:MINIMAX_API_KEY}" + }, + "models": { + "MiniMax-M3": { + "name": "MiniMax M3 (ohne Thinking)", + "options": { + "thinking": { "type": "disabled" } + } + } + } + }, "ollama": { "npm": "@ai-sdk/openai-compatible", "name": "Ollama (lokal)", diff --git a/frontend/index.html b/frontend/index.html index eb22121..edf3306 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@
- +