fix: card generation, the reader's own styles, and Tools that owned other people's work
**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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
93a3b74011
commit
6305d82e95
7 changed files with 46 additions and 6 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -383,7 +383,10 @@ export default function CategoriesPage() {
|
|||
|
||||
return (
|
||||
<div className="cat-page">
|
||||
<BackLink to="/settings?s=tools">Tools</BackLink>
|
||||
{/* 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. */}
|
||||
<BackLink to="/questions/manage">Questions</BackLink>
|
||||
<div className="cat-header">
|
||||
<div>
|
||||
<h1>Taxonomy</h1>
|
||||
|
|
|
|||
|
|
@ -174,6 +174,10 @@ export default function QuestionManagerPage() {
|
|||
</div>
|
||||
<div className="qm-header-actions">
|
||||
<Link className="btn btn-secondary" to="/categories">Taxonomy</Link>
|
||||
{/* 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. */}
|
||||
<Link className="btn btn-secondary" to="/tools">Workbench</Link>
|
||||
<QuestionImport onImported={refresh} selectedIds={selected} />
|
||||
<Link className="btn btn-primary" to="/questions/new"
|
||||
state={{ from: '/questions/manage', label: 'Questions' }}>+ New question</Link>
|
||||
|
|
|
|||
|
|
@ -247,8 +247,10 @@ function ToolsSection() {
|
|||
<div className="set-cards">
|
||||
{[
|
||||
{ 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' },
|
||||
|
|
|
|||
|
|
@ -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. */}
|
||||
<BackLink to="/settings?s=tools">Tools in Settings</BackLink>
|
||||
<BackLink to="/questions/manage">Questions</BackLink>
|
||||
<h1>Tools</h1>
|
||||
<p>
|
||||
A document goes in, a model proposes questions, you read them, and the
|
||||
|
|
|
|||
Loading…
Reference in a new issue