Skip to content

Desktop app: one interface, two hosts — plus in-process ONNX and document extraction - #19

Merged
peopleworks merged 8 commits into
mainfrom
feat/desktop-app
Jul 30, 2026
Merged

Desktop app: one interface, two hosts — plus in-process ONNX and document extraction#19
peopleworks merged 8 commits into
mainfrom
feat/desktop-app

Conversation

@peopleworks

Copy link
Copy Markdown
Owner

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.Web keeps 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.Onnx extracts the perplexity and embedding engines out of the HTTP server into a
library, so a host can run them in-process instead of over the network. Moved, not rewritten.

SignsOfAI.Documents reads PDF, DOCX, ODT, EPUB, RTF, TXT and Markdown, with page/paragraph
structure so a finding maps back to somewhere the reader can look.

Two host differences that are handled, not papered over

  • Static assets. A native HttpClient cannot reach a WebView's virtual host, so the i18n loader
    reads through a JS fetch helper. 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.EnsureInitializedAsync is
    idempotent and App.razor holds the router back until it completes, so neither host renders
    half-translated.

Two things reviewers should know

SignsOfAI.Desktop is deliberately not in SignsOfAI.slnx. It is WPF, and three workflows build
that solution on ubuntu-latest. It lives in SignsOfAI.Desktop.slnx with its own windows-latest
job, 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.yml now names its solution explicitly: a second .slnx makes a bare dotnet test stop with
MSB1011.

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

  • 204 tests pass (125 Core + 72 Documents + 7 ONNX). Both new test projects were previously outside
    any integrated build — that is how a test assertion ending in || true survived; it is fixed and
    now asserts the real behaviour.
  • Both solutions build in Release with no warnings.
  • The web app is unchanged: every asset resolves, the EN/ES switch works, and the English sample
    still scores 94/100 with 23 recommendations.
  • The perplexity API still answers after the extraction, with real inference through the new
    library — ppl 27.33, predictability 0.859, very-predictable, 411ms.
  • The desktop app launches and renders the shared interface.

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.Onnx into it (offline
perplexity, 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

peopleworks and others added 8 commits July 30, 2026 10:38
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
peopleworks merged commit c7bf695 into main Jul 30, 2026
3 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant