Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# The NuGet workflow also runs the tests, but only when a release is published — too late to help a
# contributor. This runs on the pull request itself, which matters most for the community-contributed
# UI translations in src/SignsOfAI.Web/wwwroot/i18n: the locale tests report a mistyped key, a lost
# UI translations in src/SignsOfAI.UI/wwwroot/i18n: the locale tests report a mistyped key, a lost
# {0} placeholder or a duplicated entry by name, so a translation PR can be reviewed on evidence
# rather than by reading JSON side by side. See Docs/TRANSLATING.md.
name: CI
Expand Down Expand Up @@ -38,3 +38,23 @@ jobs:

- name: Test
run: dotnet test SignsOfAI.slnx -c Release --no-build --nologo

# The desktop app is WPF, so it needs a Windows runner and it is not in SignsOfAI.slnx — see the
# comment at the top of that file. Its own job keeps the build above on Linux, where it stays fast
# for the translation PRs that are the reason this workflow exists. Build only: the interface is
# the shared SignsOfAI.UI library, and the tests over it already ran in the job above.
desktop:
name: Build desktop (Windows)
runs-on: windows-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"

- name: Build
run: dotnet build SignsOfAI.Desktop.slnx -c Release --nologo
4 changes: 3 additions & 1 deletion .github/workflows/nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ jobs:
dotnet-version: "10.0.x"

# A broken build must never reach NuGet — published versions can't be deleted.
# Name the solution explicitly: the repository now has two (the second one carries the
# Windows-only desktop app), and left to discover one on its own dotnet stops with MSB1011.
- name: Test
run: dotnet test -c Release --nologo
run: dotnet test SignsOfAI.slnx -c Release --nologo

- name: Pack
run: |
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ dotnet run --project src/SignsOfAI.Cli -- check some-file.md
| Project | What it is |
| --- | --- |
| `src/SignsOfAI.Core` | The engine. Pure .NET, no browser, no I/O. Everything else is a shell around it. |
| `src/SignsOfAI.Web` | Blazor WebAssembly app. |
| `src/SignsOfAI.UI` | The interface itself — pages, components, UI services. Both hosts below render it, so a fix reaches web and desktop together. |
| `src/SignsOfAI.Web` | Host: the Blazor WebAssembly app in the browser. |
| `src/SignsOfAI.Desktop` | Host: the WPF + WebView2 app. Windows-only, so it lives in `SignsOfAI.Desktop.slnx` rather than the main solution, which is built on Linux. |
| `src/SignsOfAI.Cli` | `signsofai` — terminal reports and CI gating. |
| `src/SignsOfAI.Mcp` | MCP server over stdio. |
| `src/SignsOfAI.Perplexity.Api` | The optional server for perplexity and paraphrase. |
Expand Down
4 changes: 2 additions & 2 deletions Docs/TRANSLATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ If you speak a language this tool doesn't, you can add it. That's the whole idea
Everything lives in one folder:

```
src/SignsOfAI.Web/wwwroot/i18n/
src/SignsOfAI.UI/wwwroot/i18n/
├── locales.json ← the list of available languages
├── en.json ← English (the reference)
└── es.json ← Spanish
Expand Down Expand Up @@ -41,7 +41,7 @@ Say you want French (`fr`).
**1. Copy the reference.**

```bash
cp src/SignsOfAI.Web/wwwroot/i18n/en.json src/SignsOfAI.Web/wwwroot/i18n/fr.json
cp src/SignsOfAI.UI/wwwroot/i18n/en.json src/SignsOfAI.UI/wwwroot/i18n/fr.json
```

**2. Translate the values in `fr.json`.** Change only the text on the right of each colon. Never
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,11 @@ SignsOfAI.slnx
│ │ ├─ Rules/Packs/ # rules.en.json, rules.es.json (embedded, community-extensible)
│ │ ├─ Text/ # Tokenizer, sentence splitter, language detector, statistics
│ │ └─ AiWritingAnalyzer # Public facade: Analyze(text, language)
│ ├─ SignsOfAI.Web # Blazor WebAssembly front end (Analyze, Originality, Catalog)
│ ├─ SignsOfAI.UI # The whole interface (Analyze, Originality, Catalog) — shared by
│ │ │ # both hosts below, so a change lands in web and desktop at once
│ │ └─ wwwroot/i18n/ # UI translations: en.json, es.json + locales.json (community-extensible)
│ ├─ SignsOfAI.Web # Host: Blazor WebAssembly, runs in the browser
│ ├─ SignsOfAI.Desktop # Host: WPF + WebView2, runs offline and reaches local models
│ ├─ SignsOfAI.Cli # `dotnet tool` for CI pipelines
│ ├─ SignsOfAI.Mcp # MCP server (stdio): the engine as tools for Claude Desktop / any client
│ └─ SignsOfAI.Perplexity.Api # Optional ASP.NET Core server: predictability + embeddings
Expand Down Expand Up @@ -291,7 +294,7 @@ field is simply reported and never auto-edited, which is why every built-in rule
**If you speak a language this tool doesn't, you can add it — and you don't need to know C#.**

The interface is plain JSON: one file per language in
[`src/SignsOfAI.Web/wwwroot/i18n/`](src/SignsOfAI.Web/wwwroot/i18n), plus a `locales.json` manifest.
[`src/SignsOfAI.UI/wwwroot/i18n/`](src/SignsOfAI.UI/wwwroot/i18n), plus a `locales.json` manifest.
Adding a language is *copy `en.json`, translate the values on the right, add one line to the manifest*.
No build step, no code to read, and the language switch picks it up on its own.

Expand Down
21 changes: 21 additions & 0 deletions SignsOfAI.Desktop.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Solution>

<!--
The desktop app and what it needs, in its own solution.

Why not just add it to SignsOfAI.slnx: that solution is built on ubuntu-latest by three
workflows, and a WPF project cannot be built there. Keeping the Windows-only head in a solution
of its own is what lets the main build stay on Linux — which matters, because that is the build
that gives a translation contributor fast feedback on a pull request.
-->

<Folder Name="/src/">
<Project Path="src/SignsOfAI.Core/SignsOfAI.Core.csproj" />
<Project Path="src/SignsOfAI.Desktop/SignsOfAI.Desktop.csproj" />
<Project Path="src/SignsOfAI.UI/SignsOfAI.UI.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SignsOfAI.Core.Tests/SignsOfAI.Core.Tests.csproj" />
</Folder>

</Solution>
13 changes: 13 additions & 0 deletions SignsOfAI.slnx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<Solution>

<!--
src/SignsOfAI.Desktop is deliberately NOT listed here, and Visual Studio will offer to add it —
please don't. It is a WPF app on net10.0-windows, and three workflows build this solution on
ubuntu-latest (ci, codeql, nuget), so adding it turns all three red on the next push. Open
SignsOfAI.Desktop.slnx for desktop work; CI builds it in its own windows-latest job.
-->

<Folder Name="/src/">
<Project Path="src/SignsOfAI.Cli/SignsOfAI.Cli.csproj" />
<Project Path="src/SignsOfAI.Core/SignsOfAI.Core.csproj" />
<Project Path="src/SignsOfAI.Documents/SignsOfAI.Documents.csproj" />
<Project Path="src/SignsOfAI.Onnx/SignsOfAI.Onnx.csproj" />
<Project Path="src/SignsOfAI.Perplexity.Api/SignsOfAI.Perplexity.Api.csproj" />
<Project Path="src/SignsOfAI.UI/SignsOfAI.UI.csproj" />
<Project Path="src/SignsOfAI.Web/SignsOfAI.Web.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SignsOfAI.Core.Tests/SignsOfAI.Core.Tests.csproj" />
<Project Path="tests/SignsOfAI.Documents.Tests/SignsOfAI.Documents.Tests.csproj" />
<Project Path="tests/SignsOfAI.Onnx.Tests/SignsOfAI.Onnx.Tests.csproj" />
</Folder>
</Solution>
4 changes: 4 additions & 0 deletions src/SignsOfAI.Desktop/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Application x:Class="SignsOfAI.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml" />
9 changes: 9 additions & 0 deletions src/SignsOfAI.Desktop/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SignsOfAI.Desktop;

/// <summary>
/// The WPF application shell. Deliberately empty: the window is the whole of the desktop-specific
/// code, and the interface itself comes from the shared SignsOfAI.UI library.
/// </summary>
public partial class App : System.Windows.Application
{
}
26 changes: 26 additions & 0 deletions src/SignsOfAI.Desktop/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Window x:Class="SignsOfAI.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
xmlns:web="clr-namespace:Microsoft.AspNetCore.Components.Web;assembly=Microsoft.AspNetCore.Components.Web"
xmlns:ui="clr-namespace:SignsOfAI.UI;assembly=SignsOfAI.UI"
Title="Signs of AI Writing"
Height="900" Width="1440"
MinHeight="600" MinWidth="900"
WindowStartupLocation="CenterScreen">

<!--
The entire window is one WebView hosting the shared Blazor components — the same App root the
web app mounts, so there is no second copy of the interface to keep in step. HeadOutlet is
registered for parity with the web host: it makes <PageTitle> a no-op rather than nothing.
-->
<Grid>
<blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type ui:App}" />
<blazor:RootComponent Selector="head::after" ComponentType="{x:Type web:HeadOutlet}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</Grid>

</Window>
30 changes: 30 additions & 0 deletions src/SignsOfAI.Desktop/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using SignsOfAI.UI;

namespace SignsOfAI.Desktop;

public partial class MainWindow : Window
{
public MainWindow()
{
var services = new ServiceCollection();
services.AddWpfBlazorWebView();
#if DEBUG
// Right-click → Inspect inside the WebView. Debug builds only: shipping it would put dev
// tools in front of end users.
services.AddBlazorWebViewDeveloperTools();
#endif

// The single registration call the web app also makes — see SignsOfAI.UI/UiServices.cs.
// Anything the interface needs arrives here automatically, so a service added for the web
// can never be missing on the desktop.
services.AddSignsOfAiUi();

// The XAML binds Services="{DynamicResource services}", so the provider has to be in
// Resources before InitializeComponent builds the visual tree.
Resources.Add("services", services.BuildServiceProvider());

InitializeComponent();
}
}
45 changes: 45 additions & 0 deletions src/SignsOfAI.Desktop/SignsOfAI.Desktop.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--
The desktop host. It contains no interface of its own: it opens a window, hands the shared
SignsOfAI.UI components to a WebView2 and gets out of the way. Everything the user sees is the
same code the web app serves.

Why a desktop build exists at all, when the web app already runs entirely on the visitor's
machine: three things a browser tab cannot do. It can reach a local model server (Ollama on
localhost is a CORS wall from a page, an ordinary HTTP call from here), it can run the ONNX
perplexity model in-process instead of calling a server, and it can be pointed at a folder of
documents rather than one pasted passage.

The SDK is Microsoft.NET.Sdk.Razor, not the plain SDK: that is what brings in the static web
assets pipeline, so the class library's wwwroot lands under _content/SignsOfAI.UI/ where the page
expects it.
-->
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<!-- The platform version is load-bearing, not decoration. BlazorWebView hosts the page in
WebView2's composition control, which reaches for the Windows SDK WinRT projections
(Microsoft.Windows.SDK.NET); a bare net10.0-windows does not bring those in, and the app
builds cleanly and then dies at the first render with a FileNotFoundException. -->
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<!-- Compile against the 19041 surface but keep running on 1809, the oldest Windows 10 that
still gets WebView2 — no reason to lock out those machines. -->
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>SignsOfAI.Desktop</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<!-- Per-monitor DPI awareness comes from app.manifest; without it the WebView renders blurry
on a scaled display, which is most laptops. -->
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Wpf" Version="10.0.90" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SignsOfAI.UI\SignsOfAI.UI.csproj" />
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions src/SignsOfAI.Desktop/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">

<assemblyIdentity version="1.0.0.0" name="SignsOfAI.Desktop" />

<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- Without this the WebView is bitmap-stretched on any scaled display and the text looks
soft — which is most laptops, and the first thing a reviewer notices. -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
</windowsSettings>
</application>

</assembly>
29 changes: 29 additions & 0 deletions src/SignsOfAI.Desktop/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Signs of AI Writing</title>
<base href="/" />
<!-- The interface and its assets come from the shared SignsOfAI.UI class library, which serves
its wwwroot under _content/{assembly}/ — the same paths the web app uses. -->
<link rel="stylesheet" href="_content/SignsOfAI.UI/css/app.css" />
<link rel="stylesheet" href="SignsOfAI.Desktop.styles.css" />
</head>

<body>
<div id="app"></div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

<script src="_content/SignsOfAI.UI/js/lang.js"></script>
<script src="_content/SignsOfAI.UI/js/share-card.js"></script>
<script src="_framework/blazor.webview.js"></script>
</body>

</html>
Loading
Loading