Desktop app: one interface, two hosts — plus in-process ONNX and document extraction - #19
Merged
Conversation
Adds a new class library that extracts analysable plain text from 7 document formats, with positional structure (page/paragraph) so AI-writing findings can be mapped back to where the user can see them. Extractors: - TXT: UTF-8/16 with BOM detection, ISO-8859-1 fallback - MD: strips all Markdown formatting (headers, emphasis, links, code blocks, blockquotes, lists, horizontal rules, HTML) via regex - DOCX: wraps the existing zero-dependency DocxTextExtractor from Core - ODT: ZIP + content.xml, zero dependencies (same trick as DOCX) - EPUB: ZIP + OPF spine reading order + XHTML chapter extraction, zero deps - RTF: hand-written control-word stripper (state machine), zero dependencies - PDF: UglyToad.PdfPig (Apache-2.0, pure managed, MIT-compatible), page-by-page Design: - IDocumentExtractor interface with CanHandle + ExtractAsync - ExtractionResult carries Text, Paragraphs (with PageNumber), and Warnings - ExtractionFailure is a typed result for encrypted/corrupt/oversize files - DocumentExtractorFacade is the registry + safety net: no exception from a single file can escape and kill a batch - ExtractionOptions with MaxSizeBytes (default 100 MB) and MaxPages (2k) Hard constraints satisfied: - Never adds PackageReference to Core (Core stays dependency-free for WASM) - MIT-compatible only (PdfPig is Apache-2.0; no iText/GPL/AGPL) - A bad file never kills a batch (facade catches all exceptions) - ExtractionOptions guards against resource exhaustion - Did not touch Web, Perplexity, Cli, Mcp, slnx, README, or modify Core Tests: 72 tests with programmatically-generated fixtures (2-page PDF, ODT, EPUB with 2 chapters, RTF, UTF-16 BOM, deliberately broken files), all run with no network. 125 existing tests still pass. Known limitations (honest): - EPUB nested navigation (NCX with sub-items) is not handled - RTF: ignorable destinations (\*\ prefix) not fully tracked across nested groups — complex RTF with embedded images/OLE may leave artifacts - Markdown: reference-style link definitions on separate lines may leave leftover URLs; only inline references are converted Co-Authored-By: Claude <noreply@anthropic.com>
… web Extracts every page, component and UI service out of the WebAssembly project into SignsOfAI.UI, a Razor class library, and adds SignsOfAI.Desktop — a WPF window whose entire content is a WebView rendering those same components. A change to the interface now lands in both hosts at once. Two things genuinely differ between a browser tab and a WebView, and both are handled rather than papered over: - Static assets. A native HttpClient cannot reach a WebView's virtual host, so the i18n loader reads through a JS fetch helper instead. That is one code path for both hosts rather than an abstraction with two implementations, and it removed the HttpClient bound to HostEnvironment.BaseAddress entirely. - Startup order. The web app resolves the language before the first render; the desktop cannot, because JS interop only wakes once the WebView has booted. Loc exposes an idempotent EnsureInitializedAsync and App.razor holds the router back until it completes, so neither host renders half-translated. The desktop app is deliberately absent from SignsOfAI.slnx: it is WPF, and three workflows build that solution on ubuntu-latest. It lives in its own SignsOfAI.Desktop.slnx with a windows-latest CI job, which keeps the main build on Linux where it stays fast for translation pull requests. nuget.yml now names its solution explicitly, because a second .slnx makes a bare `dotnet test` stop with MSB1011. The TFM pins a Windows platform version on purpose: BlazorWebView hosts the page in WebView2's composition control, which reaches for the Windows SDK WinRT projections. Without it the app builds clean and dies at the first render. Verified: 125 tests pass, both solutions build with no warnings, and the web app is unchanged — every asset resolves, the EN/ES switch works, and the English sample still scores 94/100 with 23 recommendations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test for a non-.docx ZIP ended in `|| true`, which made the assertion impossible to fail. It counted towards the suite total and protected nothing. Checked whether it was hiding a defect: it was not. DocxExtractor catches the InvalidOperationException that Core's extractor throws on a ZIP with no word/document.xml and turns it into a CorruptFile warning with empty text. The test simply never said so. It now asserts that behaviour, the way the ODT, PDF and RTF tests already do, so a regression here would surface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both landed as projects nobody's integrated build compiled, which is exactly how a test assertion ending in `|| true` survived review — it counted towards a green suite that no CI run ever executed. In the solution, all three test projects run together: 204 tests. SignsOfAI.Desktop stays out, for the reason at the top of this file. The desktop host does not reference the two new libraries yet; that comes with the features they unlock (in-process perplexity, folders of documents). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
peopleworks
added a commit
that referenced
this pull request
Jul 30, 2026
temp/ is local scratch — session handoffs, working notes, drafts and screenshots. It reached the default branch of a public repository through a `git add -A` run from the repository root, and PR #19 merged before the branch fix landed. The .gitignore rule is the actual fix; deleting the files without it only buys time until the next `git add -A`. This takes the files out of the tree, not out of the history: they stay reachable in 90ab44a for anyone with the hash. Only GitHub Support can purge that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Reddit user asked for a desktop version. This is it, plus the two libraries that will make it
worth downloading.
What changed
The interface moved out of the web project. Every page, component and UI service now lives in
SignsOfAI.UI, a Razor class library.SignsOfAI.Webkeeps only what a host owns —index.html,the icons, the deployment config, the WASM bootstrap — and the new
SignsOfAI.Desktop(WPF +WebView2) renders the very same components in a window. A change to the interface lands in both at
once; there is no second copy to keep in step.
SignsOfAI.Onnxextracts the perplexity and embedding engines out of the HTTP server into alibrary, so a host can run them in-process instead of over the network. Moved, not rewritten.
SignsOfAI.Documentsreads PDF, DOCX, ODT, EPUB, RTF, TXT and Markdown, with page/paragraphstructure so a finding maps back to somewhere the reader can look.
Two host differences that are handled, not papered over
HttpClientcannot reach a WebView's virtual host, so the i18n loaderreads through a JS fetch helper. One code path for both hosts rather than an abstraction with two
implementations — and it removed the
HttpClientbound toHostEnvironment.BaseAddressentirely.because JS interop only wakes once the WebView has booted.
Loc.EnsureInitializedAsyncisidempotent and
App.razorholds the router back until it completes, so neither host rendershalf-translated.
Two things reviewers should know
SignsOfAI.Desktopis deliberately not inSignsOfAI.slnx. It is WPF, and three workflows buildthat solution on
ubuntu-latest. It lives inSignsOfAI.Desktop.slnxwith its ownwindows-latestjob, which keeps the main build on Linux where it stays fast for translation pull requests. There is
a comment at the top of the solution file saying so, because Visual Studio will offer to add it.
nuget.ymlnow names its solution explicitly: a second.slnxmakes a baredotnet teststop withMSB1011.
The desktop TFM pins a Windows platform version on purpose. BlazorWebView hosts the page in
WebView2's composition control, which reaches for the Windows SDK WinRT projections. Without the
pin the app builds clean and dies at the first render.
Verification
any integrated build — that is how a test assertion ending in
|| truesurvived; it is fixed andnow asserts the real behaviour.
still scores 94/100 with 23 recommendations.
library —
ppl 27.33, predictability 0.859, very-predictable, 411ms.What CI is being asked here that a Windows machine could not answer: the two new projects have
never been built on Linux. The ONNX test project conditions its RID and native package on
IsOSPlatform('Windows')and its model-dependent tests return early when the weights are absent(they are git-ignored, so CI has none). That reasoning looks right, but only this run proves it.
Not in this PR
The desktop still does nothing a browser tab cannot. Wiring
SignsOfAI.Onnxinto it (offlineperplexity, no server, nothing uploaded),
SignsOfAI.Documents(drop a folder), and local Ollama —a CORS wall from a page, an ordinary HTTP call from here — is the next step.
🤖 Generated with Claude Code