Skip to content

Commit 2fb53e7

Browse files
mrviduusclaude
andauthored
The conversation moves to the reader's own assistant (#596)
* feat(mcp): a connect key a reader can actually paste The remote MCP endpoint had no credential of its own. Its only supported bearer was a 60-minute access token minted for a different transport, obtained by installing a .NET CLI, running a device flow in a terminal and copying a JWT out of a cache file — then repeating within the hour. That is the reason zero insights were ever written, not the quality of the feature, and it is why the whole thing is unreachable from a phone. McpAccessKey is long-lived, revoked in place rather than deleted, and stores only a SHA-256 of the key via the existing DeviceCodes.HashToken — the same helper behind PasswordResetToken and DeviceAuthorization. A fast hash is right here: this is 32 bytes of CSPRNG output, not a human-chosen password. Resolution is middleware, not a change to GetUserId: that method is synchronous and called from 102 places, and a key lookup needs the database. It sits ABOVE the rate limiter, unlike GuestActivityMiddleware which is deliberately below — highlight-write is the one policy partitioned by user id rather than IP, precisely because MCP traffic arrives from a single container address, and it cannot pick that partition before the key is resolved. LastUsedAt is written at most hourly. An assistant issues one request per tool call, so writing it per request would put an UPDATE in front of every read of the user's own library. The device flow is untouched. It is still correct for the local stdio tool, which caches a refresh token and renews itself. Still to come before this is usable: the page that creates and shows a key, on web and mobile, and an integration test for the revoke-then-401 and LastUsedAt paths — GuestActivityMiddleware is dead code today precisely because nobody wrote that second test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * refactor: delete Study Buddy Zero usage across its whole life, and unlike the other AI surfaces that is not a product verdict — it is orphaned code. StudyBuddyPanel is referenced exactly once in the repository, by its own declaration. The selection-toolbar button that looks like its entry point routes to handleAskAboutThis and always has, and its aria-label already reads reader.selectionToolbar.askAboutThis, so reader.selectionToolbar.studyBuddy was a string nothing rendered. The prop chain stays because the button is real and useful, but it is renamed onStudyBuddy → onAskAbout (and handleStudyBuddy, StudyBuddyIcon with it). A prop named after a deleted feature is how the next person concludes the feature is still there. BookToolTriggers survives: it is the shared detector behind the Explain pre-router, not just the agent's tool gate. Its test kept the property it pinned — a self-contained passage triggers nothing — with the passages inlined now that the golden corpus is gone. Its eval runner scored a surface with no entry point, so it goes with it. The model registry, traces, shadow routing and AiQualityPage stay: they route translate, explain and the SEO crews, which are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * refactor: delete the Librarian 48 runs, 3.5 cents of inference, one user. It answered "what should I read" in natural language over the catalog — a real idea, but it is the surface an outside assistant with the connector attached does better, over a catalog it can already search with search_books. Goes with it: the agent, its DTOs, RetrievedCatalog, the eval runner and golden set, both clients (web api/hooks/components + DiscoverPage, mobile screen and the entry card on Search), the rate-limit policy, the model route, and 25 web + 22 shared locale keys. DiscoverMenu stays — it is the genres/authors/books navigation dropdown and only one of its links pointed here. The /discover route and its legacy redirect go with the page they resolved to. LibrarySearchService stays: it is the seam behind SearchLibraryTool and SearchLibrarySemanticTool, which are their own decision. The librarian.openBook entry in the locale override registry goes too — it documented a deliberate web/mobile divergence for a key that no longer exists, and a registry that outlives its keys stops being a registry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * refactor: delete the in-app book chat 16 conversations and 26 messages over its whole life, from one person — and book_conversation is upserted on read, so those 16 counted sheet-opens, not conversations. The engagement number was smaller than it looked, not larger. The reason it goes is not the usage, and not cost: measured on production, the six chat surfaces together spent 16 cents. It goes because the conversation is better somewhere else. The reader already pays for an assistant that beats anything we can afford to serve, and it already holds their profile and a year of history, none of which is copyable here. What has to come home is the conclusion, and BookInsight already carries it. Removed end to end: the endpoints, ConversationSummarizer, BookChatHistory, both entities and their tables (DropBookChat is annotated with what it destroys and the counts taken before writing it), both clients, and the reader chrome that opened them — AskPanel and AskSheet, the top-bar button on both platforms, the "Ask" action in the selection bars, and the citation-jump machinery that existed only to land a chat citation on its page. useRagIndex goes with the panel that owned it, which settles the guest-403 dead-end it had: a bare catch turned a guest's 403 into "Could not index this book" plus a Retry that reissued the same 403. Deleting beat fixing. ask_book and AskEndpoints are untouched. They are the MCP tool's backing route, not this, and they are their own decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * docs(handoff): record what shipped, what is left, and four decisions the RAG cut surfaces Written before continuing the deletion so the findings survive the session: the similar-books rail dies with the chunks (4 editions of 1423 have an embedding, so it is already blank on 99.7% of pages), three tools are RAG-backed and two serve features that stay, Tutor loses its grounded example sentence, and RagIndexStatus is 7 columns on each of two entities. Also the honest not-done list: the key has no UI on either client, so it can only be minted with curl today, and its revoke/LastUsedAt integration test is missing — the exact gap that left GuestActivityMiddleware dead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * refactor: delete the retrieval spine 7 books of 1498 were ever indexed — 4 catalog editions of 1423, 3 uploads of 75. Vision PDF transcription bought those seven and cost $4.14, which is 94% of the project's $4.39 lifetime LLM spend. Everything downstream of it was reasoning we were paying to make possible, so that an outside assistant could do it better for free by reading the chapter as plain text. Gone: Ai.Rag entire, Application/Rag, Infrastructure/Rag, both chunk tables, pgvector, editions.embedding, the seven RAG-state columns on each of Edition and UserBook, the indexing and embedding workers, /ask and /index on both halves, /admin/rag, the eval runners, and ask_book from the bridge (14 tools → 13). Two things went that were NOT chat, and both were already dead: * "Similar books" — SimilarBooksRail is fed by editions.embedding, and 4 of 1423 editions had one, so the rail was blank on 99.7% of book pages. * Semantic catalog search (HybridCatalogSearch, semantic=true) blended FTS with the same vectors and degraded to FTS everywhere else. Keyword search is untouched: it runs on the Postgres FTS search_vector. Three tools were RAG-backed. search_book was reachable from Explain's EarlierReference signal — Explain's tool path last fired 2026-06-13, so the signal now maps to nothing rather than to something broken. get_example_sentence leaves Tutor without a worked example on a miss; worth noting VocabularyWord.Sentence already stores the sentence a word was saved from, so that capability is a rewire away with no retrieval at all. find_earlier_definition had no live caller. DropRagSpine is annotated with what it destroys and the counts taken first. Reading PDFs never went through any of this: the reader renders the original document (ADR-012) and extraction is deterministic, in TextStack.Extraction, covered in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * docs: say 13 tools everywhere, and stop advertising what was deleted mcp.md is the canonical reference — the NuGet README and the /mcp landing page both point at it — and it still described 14 tools including ask_book, told readers a bookId passed to ask_book 404s, and printed a chain ending in it. The landing page listed the tool by name and the entitlement comments still named librarian / ask / book chat / study buddy as the paid-inference surface. mcp.md now says plainly that there is no question-answering tool and why: your assistant reads get_chapter as plain text and reasons over it better than our retrieval did, on a subscription you already pay for. STATUS.md's "AI platform: Phases 1-12 complete, RAG, agents" and "Book Chat: web + mobile at parity" sat twelve inches from assistant-handoff.md saying the opposite. Replaced with what is actually live, plus an In flight entry carrying the measurements, the not-done list, and the two unapplied destructive migrations. Also removed here: ReadingProgressGate, whose only caller was RagContextService. MaxChapterNumber itself stays and is now documented as deliberately write-only — it keeps accumulating correctly and get_book_progress is the reader it is waiting for. Re-deriving that history later would be worse than carrying one int. One correction to the previous commit's wording: pgvector the extension is NOT gone. DropRagSpine drops the chunk tables, editions.embedding and its HNSW index; vector columns remain on vocabulary_words.embedding and the drift centroids, so Infrastructure still references Pgvector. Noted in assistant-handoff.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E * test: two assertions I missed when ask_book and the chat routes went Both failed in CI's docker job, and neither is a break: the API came up healthy and 174 integration tests passed. McpManifestEndpointTests still expected 14 tools and listed ask_book. I updated the drift test and the stdio smoke test and missed this one — three places assert the tool count and only two were in my head. GuestSessionEndpointTests' paid-inference matrix still named /ask, /me/chat and /index. They now 404 rather than 403, so the test reported 'paid inference reachable by a guest' when the truth was 'route deleted'. Reduced to its one surviving entry, with a comment saying why one entry is still worth a matrix: the next paid surface has to be added here on the day it ships, and a stale row reads like a pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d8d6e86 commit 2fb53e7

275 files changed

Lines changed: 18548 additions & 22376 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CLAUDE.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ Upload EPUB/PDF → BookFile (stored) → IngestionJob (queued)
241241
- Clients must send `Authorization` on the four merge entry points (`/auth/register`, `/auth/login`, `/auth/google`, `/auth/apple`) — and must **refresh an expiring token first** (`packages/shared/src/api/tokenExpiry.ts`). An expired bearer is worse than none: the server ignores it and answers 200 with nothing merged.
242242
- `apps/mobile/src/lib/capabilities.ts` is the single source of guest policy (`capabilitiesFor(user)`). Account-only: AI, identity editing, account deletion, cross-device sync, silent sign-out. Deliberately *not*: reading, translation, dictionary, saving vocabulary. **Upload was account-only until 2026-09-06 and is now open to a guest** (ADR-014 §3a) — `canUpload` is the one capability that is a *session* predicate (`hasSession`), so it is false only with no session at all, which on mobile means an install that has never opened a book. `isAuthenticated` stays `user !== null` (a guest **has** a session); only account questions go through capabilities. `capabilityLiterals.test.ts` fails the build on inline `user?.isGuest` re-derivation.
243243
- A guest's Sign Out is a **destructive confirm**: the three SecureStore keys are the only handle on the row, which `GuestCleanupWorker` then keeps forever, unreachable.
244-
- Server-side enforcement, not just UI: `RequireAiAccount()` (`Api/Extensions/AiAccountPolicy.cs`) returns **403 `account_required`** (distinct from 401 — "sign up" vs "sign in") on the paid-inference surface (librarian, tutor, ask, book chat, study buddy, RAG indexing). `GET /me/chat` is inside it because it upserts on read. Never applied to translate/dictionary/TTS.
244+
- Server-side enforcement, not just UI: `RequireAiAccount()` (`Api/Extensions/AiAccountPolicy.cs`) returns **403 `account_required`** (distinct from 401 — "sign up" vs "sign in") on the paid-inference surface (tutor). Librarian, ask, book chat, study buddy and RAG indexing were deleted 2026-09-10. Never applied to translate/dictionary/TTS.
245245
- Entitlements: `Entitlements:Tiers:Guest` = `{ StorageLimitBytes: 50MB, MaxBooks: 1, DailyEnrichmentCap: 50, AiEnabled: false }`. The tier is the only thing metering a guest upload since the 2026-09-06 reversal — the client used to block it by product choice as well, and no longer does. `DailyEnrichmentCap` clamps the user's own daily vocabulary cap (`DailyCapService.EffectiveCap`) and is also checked by `PromoteLookup`. Unset / `<=0` means unlimited/allowed — a config typo costs money, never an outage.
246246
- GuestLimitsContext (web) holds the last-read book and the word-count threshold that triggers minting. There are no client-side usage limits.
247247
- GuestCleanupWorker: every **2h**, deletes guests inactive **30d** — but only those holding nothing durable (vocab, highlights, bookmarks, library, uploads, notes, progress). Engaged guests live indefinitely. ReadingSessions are deliberately excluded from that filter.
@@ -514,7 +514,9 @@ Supported formats: EPUB, PDF. Processing order: Spelling → Hyphenation → Typ
514514

515515
## MCP Server (`backend/src/Ai/TextStack.Ai.Mcp/`)
516516

517-
Thin, stateless MCP↔HTTP bridge (Phase 8) — every tool call becomes an HTTP request to the public TextStack API (no DB/EF/OpenAI). 14 tools. Public catalog: `search_books`, `get_book`, `get_chapter`. The user's own uploads (Bearer): `search_my_library`, `get_my_book`, `get_my_chapter`, `save_my_highlight`, `list_my_book_highlights` — keyed by `bookId` (`UserBook.Id`), which is NOT an `editionId` and does not work in the edition-scoped tools. Write-back, either book type (Bearer): `save_insight`, `get_my_insights` — conclusions from an outside assistant, keyed by chapter **slug** (`BookInsight`, table `book_insight`), one per (user, book, chapter) so a re-run replaces rather than accumulates. Everything else (Bearer): `list_my_highlights`, `list_my_vocabulary`, `ask_book`, `save_highlight`.
517+
Thin, stateless MCP↔HTTP bridge (Phase 8) — every tool call becomes an HTTP request to the public TextStack API (no DB/EF/OpenAI). 13 tools. Public catalog: `search_books`, `get_book`, `get_chapter`. The user's own uploads (Bearer): `search_my_library`, `get_my_book`, `get_my_chapter`, `save_my_highlight`, `list_my_book_highlights` — keyed by `bookId` (`UserBook.Id`), which is NOT an `editionId` and does not work in the edition-scoped tools. Write-back, either book type (Bearer): `save_insight`, `get_my_insights` — conclusions from an outside assistant, keyed by chapter **slug** (`BookInsight`, table `book_insight`), one per (user, book, chapter) so a re-run replaces rather than accumulates. Everything else (Bearer): `list_my_highlights`, `list_my_vocabulary`, `save_highlight`.
518+
519+
**There is no question-answering tool.** `ask_book` and the retrieval spine behind it were deleted 2026-09-10: 7 books of 1498 were ever indexed, and the vision PDF transcription that fed the index was 94% of the project's lifetime LLM spend. An assistant reads `get_chapter` as plain text and reasons over it better, on the reader's own subscription.
518520

519521
**Assistant write ceiling**: `HighlightsEndpoints.MaxAssistantHighlightsPerBook` (200) caps how many highlights one book may receive over MCP, counted by `anchor_json->>'source' = 'mcp'` — the field `SynthesizeAnchor` writes. A person highlighting in the reader is never counted and never capped. `POST /me/highlights` is additionally rate-limited by the `highlight-write` policy, which is the **only policy partitioned by user id rather than IP**: the MCP bridge reaches the API from one container address, so an IP key would let one looping client throttle every other MCP user.
520522

@@ -524,7 +526,7 @@ The write-back exists because the reasoning happens in Claude/ChatGPT — where
524526

525527
**Dual transport** (env `MCP_TRANSPORT`: `stdio` default | `http`; `--http` flag also selects http). Shared wiring (tool catalog handlers, typed `TextStackApiClient`) in `McpBridgeCore`; the two host builders in `McpHosts`.
526528
- **stdio** (local, single identity): `Host.CreateApplicationBuilder`, **logs→stderr** (stdout is JSON-RPC only — never `Console.Write*`), singleton DI, token from `TEXTSTACK_MCP_TOKEN` (static) or the device flow (`DeviceFlowTokenProvider`, AI-050). Byte-identical to the pre-049 server.
527-
- **http** (AI-049, remote, **multi-user**): `WebApplication`, `.WithHttpTransport(o => o.Stateless = true)`, `app.MapMcp("/mcp")` + `GET /health`. Each connection authenticates with its OWN `Authorization: Bearer <token>` — the AI-050 device-flow JWT pasted into the client config — read per-request by `HttpContextTokenProvider` (SCOPED; `McpToolCatalog` + provider scoped so no identity leaks across connections). NEVER touches the device-flow cache. Package: `ModelContextProtocol.AspNetCore` 1.4.0 (matches the pinned `ModelContextProtocol`).
529+
- **http** (AI-049, remote, **multi-user**): `WebApplication`, `.WithHttpTransport(o => o.Stateless = true)`, `app.MapMcp("/mcp")` + `GET /health`. Each connection authenticates with its OWN `Authorization: Bearer <token>` — a connect key (`tsk_…`, `McpAccessKey`) or the AI-050 device-flow JWT — read per-request by `HttpContextTokenProvider` (SCOPED; `McpToolCatalog` + provider scoped so no identity leaks across connections). NEVER touches the device-flow cache. Package: `ModelContextProtocol.AspNetCore` 1.4.0 (matches the pinned `ModelContextProtocol`).
528530

529531
**Deploy** (http mode): Docker `mcp-server` (`backend/Docker/Mcp.Dockerfile`, profile `mcp`) binds `http://+:8090`, mapped `127.0.0.1:8090`; talks to the API over the **internal** docker network (`TEXTSTACK_API_URL=http://api:8080`). nginx `location /mcp` (upstream `textstack_mcp`, zone `mcp_limit`) proxies with SSE settings (`proxy_buffering off`, `Connection ""`, relays `Authorization`, 3600s timeouts). Behind Cloudflare tunnel — no new cloud. Bring up: `docker compose --profile mcp up -d mcp-server`. nginx `/mcp` block is applied manually on the server at deploy.
530532

‎apps/admin/src/api/client.ts‎

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -585,29 +585,6 @@ export interface EnrichmentEvalResult {
585585
saidUnknown: boolean
586586
}[]
587587
}
588-
// AI-Agent-3: Librarian agent eval (recall/precision/F1 @k, hallucination-free)
589-
export interface LibrarianEvalResult {
590-
recallAtK: number
591-
precisionAtK: number
592-
f1AtK: number
593-
constraintSatisfaction: number
594-
coverageDecisionAccuracy: number
595-
hallucinationFreeRate: number
596-
avgToolCalls: number
597-
n: number
598-
cases: {
599-
query: string
600-
returned: number
601-
libraryReturned: number
602-
recallAtK: number
603-
precisionAtK: number
604-
f1AtK: number
605-
constraintsSatisfied: boolean
606-
coverageDecisionCorrect: boolean
607-
noHallucination: boolean
608-
toolCalls: number
609-
}[]
610-
}
611588
// AI-Agent-2: Tutor agent eval (due-coverage / weak-targeting / difficulty / thesis-alignment)
612589
export interface TutorEvalResult {
613590
dueCoverage: number
@@ -1367,11 +1344,6 @@ export const adminApi = {
13671344
})
13681345
},
13691346

1370-
runLibrarianEval: async (): Promise<LibrarianEvalResult> => {
1371-
return fetchJson<LibrarianEvalResult>('/admin/ai-quality/librarian/eval', {
1372-
method: 'POST',
1373-
})
1374-
},
13751347

13761348
runTutorEval: async (): Promise<TutorEvalResult> => {
13771349
return fetchJson<TutorEvalResult>('/admin/ai-quality/tutor/eval', {

‎apps/admin/src/pages/AiQualityPage.tsx‎

Lines changed: 1 addition & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
CriticDefectEvalResult,
1414
CrewAbEvalResult,
1515
EnrichmentEvalResult,
16-
LibrarianEvalResult,
1716
TutorEvalResult,
1817
ShadowSummary,
1918
ShadowPair,
@@ -445,7 +444,7 @@ function Section({ title, body }: { title: string; body: string | null }) {
445444
// ─────────────────────────── Transcripts ───────────────────────────
446445

447446
const RUN_PAGE = 25
448-
const AGENT_FILTERS = ['crew.autopublish', 'crew.seo', 'studybuddy']
447+
const AGENT_FILTERS = ['crew.autopublish', 'crew.seo', 'tutor']
449448

450449
function isErrorStatus(status: string, hasError?: boolean): boolean {
451450
return hasError === true || status === 'error' || status === 'budget_exhausted'
@@ -807,8 +806,6 @@ function EvalsTab() {
807806
const [crewAbResult, setCrewAbResult] = useState<CrewAbEvalResult | null>(null)
808807
const [enrichmentRunning, setEnrichmentRunning] = useState(false)
809808
const [enrichmentResult, setEnrichmentResult] = useState<EnrichmentEvalResult | null>(null)
810-
const [librarianRunning, setLibrarianRunning] = useState(false)
811-
const [librarianResult, setLibrarianResult] = useState<LibrarianEvalResult | null>(null)
812809
const [tutorRunning, setTutorRunning] = useState(false)
813810
const [tutorResult, setTutorResult] = useState<TutorEvalResult | null>(null)
814811

@@ -896,17 +893,6 @@ function EvalsTab() {
896893
}
897894
}
898895

899-
const runLibrarian = async () => {
900-
setError(null)
901-
setLibrarianRunning(true)
902-
try {
903-
setLibrarianResult(await adminApi.runLibrarianEval())
904-
} catch (e) {
905-
setError(evalError(e, 'Failed to run librarian eval'))
906-
} finally {
907-
setLibrarianRunning(false)
908-
}
909-
}
910896

911897
const runTutor = async () => {
912898
setError(null)
@@ -958,14 +944,6 @@ function EvalsTab() {
958944
Runs the real EnrichmentAgent over ~30 goldens — calibration, honest-unknown, genre/year accuracy. This can take a minute.
959945
</span>
960946
</div>
961-
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
962-
<button onClick={runLibrarian} disabled={librarianRunning} style={rangeBtn(false)}>
963-
{librarianRunning ? 'Running…' : 'Run librarian eval'}
964-
</button>
965-
<span style={{ fontSize: 12, color: '#6b7280' }}>
966-
Runs the real LibrarianAgent over the golden queries — recall/precision/F1@k + hallucination-free. This can take a minute.
967-
</span>
968-
</div>
969947
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
970948
<button onClick={runTutor} disabled={tutorRunning} style={rangeBtn(false)}>
971949
{tutorRunning ? 'Running…' : 'Run tutor eval'}
@@ -1058,47 +1036,6 @@ function EvalsTab() {
10581036
</table>
10591037
</div>
10601038
)}
1061-
{librarianResult && (
1062-
<div style={card}>
1063-
<div style={{ fontWeight: 600, fontSize: 15, color: '#111827', marginBottom: 12 }}>Librarian eval</div>
1064-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '8px 12px' }}>
1065-
<Metric label="Recall@k" value={`${(librarianResult.recallAtK * 100).toFixed(1)}%`} color="#059669" />
1066-
<Metric label="Precision@k" value={`${(librarianResult.precisionAtK * 100).toFixed(1)}%`} />
1067-
<Metric label="F1@k" value={`${(librarianResult.f1AtK * 100).toFixed(1)}%`} />
1068-
<Metric label="Constraint sat." value={`${(librarianResult.constraintSatisfaction * 100).toFixed(1)}%`} />
1069-
<Metric label="Coverage acc." value={`${(librarianResult.coverageDecisionAccuracy * 100).toFixed(1)}%`} />
1070-
<Metric label="Hallucination-free" value={`${(librarianResult.hallucinationFreeRate * 100).toFixed(1)}%`} />
1071-
<Metric label="Avg tool calls" value={librarianResult.avgToolCalls.toFixed(2)} />
1072-
<Metric label="N" value={String(librarianResult.n)} />
1073-
</div>
1074-
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12, marginTop: 12 }}>
1075-
<thead>
1076-
<tr style={{ textAlign: 'left', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>
1077-
<th style={th}>Query</th>
1078-
<th style={th}>R@k</th>
1079-
<th style={th}>P@k</th>
1080-
<th style={th}>F1</th>
1081-
<th style={th}>Constr</th>
1082-
<th style={th}>No-halluc</th>
1083-
<th style={th}>Tools</th>
1084-
</tr>
1085-
</thead>
1086-
<tbody>
1087-
{librarianResult.cases.map((c, i) => (
1088-
<tr key={i} style={{ borderBottom: '1px solid #f3f4f6' }}>
1089-
<td style={td}>{c.query}</td>
1090-
<td style={td}>{c.recallAtK.toFixed(2)}</td>
1091-
<td style={td}>{c.precisionAtK.toFixed(2)}</td>
1092-
<td style={td}>{c.f1AtK.toFixed(2)}</td>
1093-
<td style={{ ...td, color: c.constraintsSatisfied ? '#059669' : '#dc2626' }}>{c.constraintsSatisfied ? '✓' : '✗'}</td>
1094-
<td style={{ ...td, color: c.noHallucination ? '#059669' : '#dc2626' }}>{c.noHallucination ? '✓' : '✗'}</td>
1095-
<td style={td}>{c.toolCalls}</td>
1096-
</tr>
1097-
))}
1098-
</tbody>
1099-
</table>
1100-
</div>
1101-
)}
11021039
{tutorResult && (
11031040
<div style={card}>
11041041
<div style={{ fontWeight: 600, fontSize: 15, color: '#111827', marginBottom: 12 }}>Tutor eval</div>

‎apps/mobile/app/(tabs)/search.tsx‎

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default function DiscoverScreen() {
102102
const [catalogLoading, setCatalogLoading] = useState(true)
103103
// The catalog, "Recently Added" and the author row are each gated on their own
104104
// data being non-empty, so a failed fetch made all three vanish and left a
105-
// search box and an "Ask the librarian" card with no explanation. Empty and
105+
// search box with no explanation. Empty and
106106
// unreachable are different states and now look different.
107107
const [catalogError, setCatalogError] = useState<'offline' | 'failed' | null>(null)
108108
const [searchOffline, setSearchOffline] = useState(false)
@@ -323,29 +323,6 @@ export default function DiscoverScreen() {
323323
</View>
324324
</View>
325325

326-
{/* Ask the librarian — natural-language, reasoned recommendations (signed-in only; the screen gates). */}
327-
{!searched && (
328-
<TouchableOpacity
329-
style={[styles.librarianEntry, { backgroundColor: colors.surface, borderColor: colors.border }]}
330-
onPress={() => router.push('/librarian')}
331-
activeOpacity={0.85}
332-
accessibilityRole="button"
333-
accessibilityLabel={t('librarian.title')}
334-
>
335-
<View style={[styles.librarianIcon, { backgroundColor: colors.primaryLight }]}>
336-
<Ionicons name="sparkles-outline" size={18} color={colors.primary} />
337-
</View>
338-
<View style={{ flex: 1 }}>
339-
<Text style={[styles.librarianTitle, { color: colors.text, fontFamily: fonts.sansMedium }]}>
340-
{t('librarian.title')}
341-
</Text>
342-
<Text style={[styles.librarianSubtitle, { color: colors.textSecondary, fontFamily: fonts.sans }]} numberOfLines={1}>
343-
{t('librarian.entry.hint')}
344-
</Text>
345-
</View>
346-
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
347-
</TouchableOpacity>
348-
)}
349326

350327
{loading ? (
351328
<View style={styles.skeletonList}>
@@ -536,19 +513,6 @@ const styles = StyleSheet.create({
536513
container: { flex: 1 },
537514
center: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 },
538515
searchBar: { padding: 12 },
539-
librarianEntry: {
540-
flexDirection: 'row',
541-
alignItems: 'center',
542-
gap: 12,
543-
marginHorizontal: 12,
544-
marginBottom: 8,
545-
padding: 12,
546-
borderRadius: 12,
547-
borderWidth: 1,
548-
},
549-
librarianIcon: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center' },
550-
librarianTitle: { fontSize: 15 },
551-
librarianSubtitle: { fontSize: 12, marginTop: 2 },
552516
inputWrapper: {
553517
flexDirection: 'row',
554518
alignItems: 'center',

0 commit comments

Comments
 (0)