From 6305d82e958f172ce67714f8637b89afcf7e59c9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 02:54:44 +0200 Subject: [PATCH] fix: card generation, the reader's own styles, and Tools that owned other people's work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Card generation was failing every time.** The model configured for the flashcard task — and for extraction and keyword — was `ds-deepseek-v4-flash`, which the AI proxy no longer serves. All three now point at the v4.1 model that does exist, and the dead rows are gone. The reason was reachable only by reading the job record: the panel said "Card generation failed." and nothing else, while the proxy's actual answer sat in a field nothing displayed. A failed job now says which model is missing and where to change it. **The reader had no styles of its own.** Its appearance lived in ArticlesPage.css, imported by the reading page — so an article rendered correctly there and as unstyled boxes anywhere else it was used. The split pane inside AI Mode is where that finally showed: section headings drew as bare bordered rectangles. The reader imports what it needs now. **Settings → Tools listed the question workbench and the taxonomy**, which are question work and belong beside the questions. Both are on the question manager's own bar now, the taxonomy's way back leads to Questions rather than to a settings page nobody was on, and the workbench's does too. Also: air under AI Mode's composer, which sat flush against the footer. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- backend/app/tasks/quiz_tasks.py | 26 +++++++++++++++++++++- frontend/src/components/ArticleReader.jsx | 5 +++++ frontend/src/pages/AiModePage.css | 4 +++- frontend/src/pages/CategoriesPage.jsx | 5 ++++- frontend/src/pages/QuestionManagerPage.jsx | 4 ++++ frontend/src/pages/SettingsPage.jsx | 6 +++-- frontend/src/pages/ToolsPage.jsx | 2 +- 7 files changed, 46 insertions(+), 6 deletions(-) diff --git a/backend/app/tasks/quiz_tasks.py b/backend/app/tasks/quiz_tasks.py index b1843dd..c4ad423 100644 --- a/backend/app/tasks/quiz_tasks.py +++ b/backend/app/tasks/quiz_tasks.py @@ -1,6 +1,7 @@ """Async quiz extraction task with step-by-step progress reporting via Redis.""" import json import logging +import re import time import os @@ -954,6 +955,25 @@ class ArticleDraftError(RuntimeError): """A refusal with a sentence the educator can act on.""" +def _why(exc: Exception) -> str: + """One sentence a person can act on, out of whatever the provider said. + + A proxy answers a missing model with three hundred characters of JSON. The + part that matters is which model, and that it does not exist — everything + else belongs in the log. + """ + text = str(exc) + found = re.search(r"Invalid model name passed in model=([\w.:-]+)", text) + if found: + return (f"The model configured for this job — {found.group(1)} — is not on " + "the AI proxy any more. Pick another in Admin → Models.") + if "429" in text or "rate limit" in text.lower(): + return "The AI provider is rate-limiting us. Try again in a minute." + if "timed out" in text.lower() or "timeout" in text.lower(): + return "The model took too long to answer." + return text[:160] + + def _draft_grounding(r, job_id: str, topic: str, use_library: bool, use_pubmed: bool): """Source material for a draft, and the references that come with it. @@ -1221,6 +1241,10 @@ def generate_article_cards(self, job_id: str, user_id: int, article_id: int, db.rollback() r.set(f"extraction:status:{job_id}", "failed", ex=EXPIRE_SECONDS) r.set(f"extraction:error:{job_id}", str(exc)[:300], ex=EXPIRE_SECONDS) - _push_step(r, job_id, "error", "Card generation failed.") + # The reason, on the line somebody actually reads. "Card generation + # failed." was all the jobs panel showed for a fortnight while the + # answer — a model the proxy had dropped — sat in a field nothing + # displayed. + _push_step(r, job_id, "error", f"Card generation failed. {_why(exc)}") finally: db.close() diff --git a/frontend/src/components/ArticleReader.jsx b/frontend/src/components/ArticleReader.jsx index 37a3eba..4b1fdab 100644 --- a/frontend/src/components/ArticleReader.jsx +++ b/frontend/src/components/ArticleReader.jsx @@ -1,4 +1,9 @@ import { useEffect, useRef, useState } from 'react' +// The reader carries its own appearance. These rules lived only in +// ArticlesPage.css, so the reader rendered correctly on the reading page and +// as unstyled boxes anywhere else it was used — the split pane inside AI Mode +// being where that finally showed. +import '../pages/ArticlesPage.css' import useMediaQuery from '../hooks/useMediaQuery' import useHeaderOffset from '../hooks/useHeaderOffset' import { useSessionDrawer } from '../context/SessionDrawer' diff --git a/frontend/src/pages/AiModePage.css b/frontend/src/pages/AiModePage.css index 2b5ca2d..5293fbe 100644 --- a/frontend/src/pages/AiModePage.css +++ b/frontend/src/pages/AiModePage.css @@ -1,6 +1,8 @@ /* AI Mode: a rail of threads beside the conversation. */ -.ai-page { display: grid; grid-template-columns: 250px 1fr; gap: 18px; align-items: start; max-width: 1060px; margin: 0 auto; } +/* A little air under the composer. It sat flush against the footer, so the + box you type into looked like part of the site's furniture. */ +.ai-page { display: grid; grid-template-columns: 250px 1fr; gap: 18px; align-items: start; max-width: 1060px; margin: 0 auto; padding-bottom: 28px; } /* Folded, the rail keeps only its two controls, and the conversation takes the width back rather than leaving a column of nothing beside it. */ .ai-page.is-folded { grid-template-columns: 52px 1fr; } diff --git a/frontend/src/pages/CategoriesPage.jsx b/frontend/src/pages/CategoriesPage.jsx index 1acab9e..3710c07 100644 --- a/frontend/src/pages/CategoriesPage.jsx +++ b/frontend/src/pages/CategoriesPage.jsx @@ -383,7 +383,10 @@ export default function CategoriesPage() { return (
- Tools + {/* Back to the questions, because that is what a taxonomy is for and + where everybody arrives from. It used to lead to Tools, which is a + settings page nobody was on. */} + Questions

Taxonomy

diff --git a/frontend/src/pages/QuestionManagerPage.jsx b/frontend/src/pages/QuestionManagerPage.jsx index 07ba80a..8274d26 100644 --- a/frontend/src/pages/QuestionManagerPage.jsx +++ b/frontend/src/pages/QuestionManagerPage.jsx @@ -174,6 +174,10 @@ export default function QuestionManagerPage() {
Taxonomy + {/* The workbench is question work and lives beside the questions. It + was listed under Settings → Tools, which is a place nobody is + when they are thinking about a PDF full of questions. */} + Workbench + New question diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index 16c2453..b35b34c 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -247,8 +247,10 @@ function ToolsSection() {
{[ { to: '/handbook', icon: '📖', label: 'Handbook', desc: 'How the parts that are not obvious work' }, - { to: '/tools', icon: '🧪', label: 'Question workbench', desc: 'PDFs in, drafts read, questions out' }, - { to: '/categories', icon: '🗂️', label: 'Taxonomy', desc: 'Topics, systems, symptoms, diseases' }, + /* Not the workbench and not the taxonomy. Both are question work, + both are one press from Questions in the main menu, and listing + them here as well is a second door into the same room — which is + how somebody ends up in Settings wondering how to get back. */ { to: '/access', icon: '🔑', label: 'Access', desc: 'Who may edit what' }, { to: '/trash', icon: '🗑️', label: 'Trash', desc: 'Restore deleted questions' }, { to: '/jobs', icon: '📋', label: 'Extraction jobs', desc: 'Extraction history' }, diff --git a/frontend/src/pages/ToolsPage.jsx b/frontend/src/pages/ToolsPage.jsx index cb9066c..aea8ec1 100644 --- a/frontend/src/pages/ToolsPage.jsx +++ b/frontend/src/pages/ToolsPage.jsx @@ -56,7 +56,7 @@ export default function ToolsPage() { {/* The way back. This page is a workbench rather than part of the study flow, and without a trail out of it you are somewhere that looks like a different application. */} - Tools in Settings + Questions

Tools

A document goes in, a model proposes questions, you read them, and the