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