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
21 changes: 21 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,36 @@ jobs:
- name: Publish
run: dotnet publish src/SignsOfAI.Web -c Release -o publish

# The Word task pane is its own WebAssembly app and ships under /word/. Office loads a task
# pane by URL, so an add-in whose SourceLocation 404s does not fail quietly: Word says "this
# add-in may not load properly" and the pane shows whatever the fallback served. That is
# exactly what happened the first time the manifest was sideloaded, because this step did not
# exist yet and Pages answered the pane's URL with the web app's SPA fallback.
- name: Publish the Word task pane
run: dotnet publish src/SignsOfAI.Word -c Release -o publish-word

# Project sites live at /<repo>/, so rewrite the base href from "/" to "/<repo>/".
- name: Rewrite base href
run: |
sed -i 's|<base href="/" />|<base href="/${{ github.event.repository.name }}/" />|g' publish/wwwroot/index.html
sed -i 's|<base href="/" />|<base href="/${{ github.event.repository.name }}/word/" />|g' publish-word/wwwroot/index.html

# SPA deep links (e.g. /catalog) 404 on Pages without a fallback — serve index.html as 404.
- name: SPA fallback
run: cp publish/wwwroot/index.html publish/wwwroot/404.html

- name: Place the task pane under /word/
run: cp -r publish-word/wwwroot publish/wwwroot/word

# The pane is loaded by URL from inside Word, where a 404 shows as an add-in error rather than
# a missing page. Fail the deploy here instead of finding out in Word.
- name: The task pane's own entry point exists
run: |
test -f publish/wwwroot/word/index.html
grep -q 'base href="/${{ github.event.repository.name }}/word/"' publish/wwwroot/word/index.html
grep -q '_framework/blazor.webassembly\.' publish/wwwroot/word/index.html
echo "word/index.html is in the artifact, with its own base href and a real bootstrap name."

- uses: actions/upload-pages-artifact@v5
with:
path: publish/wwwroot
Expand Down
1 change: 1 addition & 0 deletions SignsOfAI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<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" />
<Project Path="src/SignsOfAI.Word/SignsOfAI.Word.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/SignsOfAI.Calibration/SignsOfAI.Calibration.csproj" />
Expand Down
14 changes: 14 additions & 0 deletions src/SignsOfAI.UI/Services/HostCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,18 @@ public sealed class HostCapabilities
RuntimeKey = "footer.runtime.desktop",
Version = version,
};

/// <summary>
/// A Word task pane. The third host, and the one this class's opening comment was written for.
///
/// It is a browser in every way that matters here — an embedded WebView, sandboxed, unable to
/// reach a plain-HTTP port on the machine — so the capabilities match the tab. What differs is
/// only what it may say about itself: "runs 100% in your browser" is false inside Word, the
/// same way it was false inside a WPF window before the desktop host got its own key.
/// </summary>
public static HostCapabilities WordTaskPane { get; } = new()
{
ReachesLocalServices = false,
RuntimeKey = "footer.runtime.word",
};
}
9 changes: 9 additions & 0 deletions src/SignsOfAI.UI/wwwroot/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@
"dl.updates.detail": "At most one check a day. No text, no account, no identifier — and nothing is downloaded for you.",
"footer.runtime.browser": "built with .NET 10 and Blazor WebAssembly · runs 100% in your browser",
"footer.runtime.desktop": "built with .NET 10, Blazor and WebView2 · runs on this machine",
"footer.runtime.word": "built with .NET 10 and Blazor WebAssembly · runs inside Word, on this machine",
"word.starting": "Starting…",
"word.outside": "This pane is open outside Word, so there is no document to read. Paste text to try the engine.",
"word.paste.placeholder": "Paste text here",
"word.analyze": "Analyse",
"word.analyze.document": "Analyse this document",
"word.reading": "Reading the document…",
"word.stats": "{0} words · {1} sentences · {2} signals",
"word.more": "and {0} more",
"nav.catalog": "Catalog",
"nav.builtwith": "Built with .NET 10 · Blazor",
"footer.role": "<a href=\"https://mvp.microsoft.com/en-US/mvp/profile/24060a02-dbc6-44ec-bca5-c213ff9835c5\" target=\"_blank\" rel=\"noopener\">Microsoft MVP for .NET</a>",
Expand Down
9 changes: 9 additions & 0 deletions src/SignsOfAI.UI/wwwroot/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@
"dl.updates.detail": "Como mucho una comprobación al día. Sin texto, sin cuenta, sin identificador — y no se descarga nada por ti.",
"footer.runtime.browser": "hecho con .NET 10 y Blazor WebAssembly · funciona 100% en tu navegador",
"footer.runtime.desktop": "hecho con .NET 10, Blazor y WebView2 · funciona en esta máquina",
"footer.runtime.word": "hecho con .NET 10 y Blazor WebAssembly · funciona dentro de Word, en esta máquina",
"word.starting": "Iniciando…",
"word.outside": "Este panel está abierto fuera de Word, así que no hay documento que leer. Pega un texto para probar el motor.",
"word.paste.placeholder": "Pega el texto aquí",
"word.analyze": "Analizar",
"word.analyze.document": "Analizar este documento",
"word.reading": "Leyendo el documento…",
"word.stats": "{0} palabras · {1} frases · {2} señales",
"word.more": "y {0} más",
"nav.catalog": "Catálogo",
"nav.builtwith": "Hecho con .NET 10 · Blazor",
"footer.role": "<a href=\"https://mvp.microsoft.com/en-US/mvp/profile/24060a02-dbc6-44ec-bca5-c213ff9835c5\" target=\"_blank\" rel=\"noopener\">Microsoft MVP en .NET</a>",
Expand Down
20 changes: 20 additions & 0 deletions src/SignsOfAI.Word/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SignsOfAI.UI;
using SignsOfAI.UI.Services;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<SignsOfAI.Word.TaskPane>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// Same registration call as the other two hosts, so a service added for one is present here too.
builder.Services.AddSignsOfAiUi();

// Registered after AddSignsOfAiUi so it wins over the browser default. A task pane runs in an
// embedded browser: it is sandboxed exactly as a tab is, and it reaches no local service.
builder.Services.AddSingleton(HostCapabilities.WordTaskPane);

var host = builder.Build();
await host.Services.GetRequiredService<Loc>().EnsureInitializedAsync();
await host.RunAsync();
121 changes: 121 additions & 0 deletions src/SignsOfAI.Word/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Signs of AI Writing — Word task pane

**Status: it loads in Word.** Sideloaded into Word on the web on 7 September: the manifest is
accepted, the ribbon shows a **Signs of AI** group with a **Read the signs** button, and the pane
opens. What it showed was the wrong page — see below, it was a 404 and it is fixed.

Still untested: that `Word.run` returns the body of a real document. That call is written and has
only ever run against nothing.

## What this is

The third host. Every rule, every component and the engine arrive through `SignsOfAI.UI`, the same
Razor class library the web app and the desktop window render. This project is the task pane shell
and about sixty lines of Office.js glue.

That matters more here than it does anywhere else the project runs. **A task pane is a browser**, so
the WebAssembly engine runs inside Word, on the machine, and the document is never uploaded. Every
other add-in in this category posts your document to an API. The manifest asks for `ReadDocument`
rather than `ReadWriteDocument`, so Word itself enforces that this add-in only reads — rather than
asking anyone to trust a sentence on a website.

## What it does with a short document

The honest thing, and it is worth seeing before deciding what to build next. A 92-word paste scores
90/100 and the pane says:

> **No verdict at this length.** This text is 92 words. The boundary was measured only on texts of
> 649 words and longer, so no verdict is given — the score is neither evidence that a machine wrote
> this nor evidence that a person did. Everything below is unaffected.

The named signals, the character scan and the citation cross-check all still appear, because those
carry no threshold. An essay clears 649 words comfortably; this is mostly a note about what a
PowerPoint deck would get, and why a PowerPoint add-in is a different product rather than the same
one with a different manifest.

## Trying it

### In a browser, without Word

The pane detects that Office.js is absent and offers a paste box instead of showing a broken panel.
That is how it was developed and how the screenshots were taken.

```
dotnet publish src/SignsOfAI.Word -c Release -o out
cd out/wwwroot && python -m http.server 8731
```

Then open <http://localhost:8731/index.html> in a window about 340px wide.

### In Word

Word will not load a task pane over plain HTTP from localhost without a certificate, so point the
manifest at a deployed copy, or serve the published folder over HTTPS.

**Word on the web** — the quickest path:

1. Open a document on <https://www.office.com>
2. **Home** → **Add-ins** → **More Add-ins** → **My Add-ins** → **Upload My Add-in**
3. Choose `manifest.xml`
4. **Home** → **Read the signs**

**Word for Windows** has no upload button; it reads a shared-folder catalogue. The full walkthrough
lives in `C:\Proyecto\PowerPointWebViewer\README.md`, which solved this once already — the steps are
identical, only the Trust Center list is per-application.

## The first sideload, and what it found

The pane came up showing the **web app's** navigation and "Sorry, the content you are looking for
does not exist", and Word warned *"This add-in may not load properly."*

Neither was a bug in the add-in. `SourceLocation` points at
`https://peopleworks.github.io/SignsofAI/word/index.html`, the Pages workflow published only the web
app, and so that URL answered **404**. Pages then served its SPA fallback — the web app's
`index.html` — whose Blazor router has no route for `/word/index.html` and correctly said Not found.
Word's warning was the 404, not the machine it was running on.

Fixed in `deploy-pages.yml`, which now publishes this project into `/word/` with its own rewritten
`<base href>` and **fails the deploy** if that entry point is missing. A task pane is loaded by URL
from inside Word, where a 404 surfaces as "this add-in may not load properly" rather than as a
missing page, so it is worth failing the deploy instead of finding out in Word a second time.

## What the spike proved, and what it did not

Proved, by running it:

- Blazor WebAssembly boots inside a 340px pane and the engine runs there.
- The shared components render in one narrow column — the score, the withheld verdict, the artifact
and citation panels, the findings list, the EN/ES switch.
- Office.js and Blazor coexist: `Office.onReady` fires before the module finishes starting, so the
bridge parks the answer in a promise the .NET side awaits, rather than a callback registered too
late to hear it.
- The absence of Word is a state, not a crash.

Proved by sideloading it:

- Word accepts the manifest and puts **Read the signs** on the Home tab.
- The pane opens and loads over HTTPS from Pages.

Not proved yet:

- That `Word.run` returns the body text of a real document — the call is written but has only ever
run against nothing, because the page that contains it never loaded.

Found while building, and worth keeping:

- The published page must reference `_framework/blazor.webassembly#[.{fingerprint}].js`. Without the
placeholder the file name only exists during development and the pane never starts.
- The shared stylesheet assumes a page with room. In a pane, anything with a minimum width pushes
content off the right edge, where there is no way to scroll to it.

## Before this could ship

- **The boot retry.** `boot.js` — the work from #73 and #74 that survives a transient 503 — lives in
`SignsOfAI.Web/wwwroot` and is not used here. A pane that hangs inside Word is worse than a tab
that hangs, because there is no obvious way to open developer tools. It should move into the
shared library first.
- **First load is about 3.4 MB** compressed — the runtime, and 2 MB of that is ICU data, which
cannot be dropped with `InvariantGlobalization` because it would silently change how Spanish is
handled. Trimming ICU to the locales this actually needs is the obvious next look.
- **The Office Store, or sideloading.** The store wants a privacy policy, a support page and review;
sideloading wants none of that and reaches nobody who has not been told about it.
22 changes: 22 additions & 0 deletions src/SignsOfAI.Word/SignsOfAI.Word.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.6" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<!-- The third host. Every rule, every component and the engine arrive through SignsOfAI.UI,
exactly as they do for the web app and the desktop window — this project is the task pane
shell and the Office.js glue, and nothing else. -->
<ProjectReference Include="..\SignsOfAI.UI\SignsOfAI.UI.csproj" />
</ItemGroup>

</Project>
Loading
Loading