Skip to content

feat: add local invoice AI assistant#683

Open
arolariu wants to merge 57 commits intopreviewfrom
feat/local-ai-assistant
Open

feat: add local invoice AI assistant#683
arolariu wants to merge 57 commits intopreviewfrom
feat/local-ai-assistant

Conversation

@arolariu
Copy link
Copy Markdown
Owner

@arolariu arolariu commented Apr 25, 2026

Summary

This PR adds and hardens a fully client-side, local-only AI assistant for invoice data in sites/arolariu.ro. The assistant runs in the browser through WebLLM, uses sanitized invoice objects from local Zustand/IndexedDB state, and does not send invoice prompt data to the backend or to any remote AI service.

The feature is progressive enhancement: capable devices can explicitly download a small local model and chat about invoices; unsupported or low-end devices get localized fallback or compatibility-warning UI instead of an automatic model download.

Step-by-step: what changed

  1. Added defensive local AI hardware analysis

    • Checks WebGPU, a non-null WebGPU adapter, Web Worker support, storage quota, device-memory hints, and logical CPU cores.
    • Blocks known hard failures such as missing WebGPU/adapter/Worker, known low storage, known low memory, or known low CPU.
    • Treats unavailable storage, memory, or CPU signals as unknown compatibility warnings, still requiring explicit user consent before model download.
    • Bounds WebGPU adapter and storage estimate probes so browser/driver hangs do not leave the AI tab stuck on the hardware-checking screen.
    • Runs independent WebGPU and storage probes in parallel, so even two hung probes exit within the single timeout window rather than waiting sequentially.
  2. Added a curated model catalog and recommender

    • Moves model metadata into a testable catalog with family, tier, context window, VRAM estimate, artifact host, and required WebGPU features.
    • Keeps the default on Llama-3.2-1B-Instruct-q4f16_1-MLC.
    • Exposes only model IDs verified against pinned @mlc-ai/web-llm@0.2.82.
    • Keeps Gemma 3, Qwen 3.5, and Phi 4 Mini as upgrade-gated candidates until dependency upgrade and runtime verification prove they are loadable.
  3. Added privacy-safe invoice prompt context

    • Builds bounded prompt context from local invoice state.
    • Uses deterministic aliases such as invoice-1 and merchant-1 instead of raw IDs or merchant references.
    • Excludes user identifiers, shared users, raw OCR, scans, scan URLs, additional/raw metadata, implementation metadata, and raw invoice identifiers.
    • Preserves deterministic analytics such as invoice count, largest invoices, date ranges, merchant breakdowns, and totals grouped by currency.
  4. Added the browser-only WebLLM adapter

    • Dynamically imports WebLLM only after explicit model-load action.
    • Runs model work through a dedicated worker.
    • Supports progress, streaming generation, interruption, disposal, unload, and cache deletion.
    • Enforces catalog model IDs at load/delete boundaries so arbitrary model IDs or URLs cannot be passed through the UI.
  5. Added the React hook and UI panel

    • Introduces useLocalInvoiceAssistant as the assistant lifecycle state machine.
    • Keeps chat history in session-only React state; it is not persisted.
    • Provides recovery paths for hardware failures, model-load errors, generation errors, cancellation, reset, and cache deletion failures.
    • Adds stop-generation, reset-chat, dismiss-error, retry-hardware, benchmark, and clear-cache actions.
    • Handles React dev StrictMode remounts by restoring mounted state and recreating owned adapters after cleanup.
  6. Optimized streaming and lifecycle safety

    • Buffers streamed deltas through streamingBuffer.ts so the UI does not re-render on every token.
    • Adds abortable model loading and stronger cleanup for navigation/reset/cache-clear cases.
    • Shares a generation-owner lock between chat and benchmark generation, preventing overlapping adapter calls and stale settlement races after interrupt/reset.
  7. Added instant local usefulness

    • Shows deterministic analytics preview before model download when invoices exist.
    • Adds localized suggested prompt chips after the model is loaded.
    • Keeps totals separated by currency and avoids cross-currency sums.
  8. Wired the assistant into invoice routes

    • Adds the assistant to the invoice list route using visible invoices.
    • Adds the assistant tab to the invoice detail route scoped to the active invoice.
    • Keeps the assistant experience client-side.
  9. Localized and documented the feature

    • Adds EN, RO, and FR translations for assistant UI, warnings, actions, analytics preview, suggested prompts, cache copy, and benchmark copy.
    • Adds RFC 1009 documenting the architecture, model policy, privacy model, hardware gate, benchmark semantics, cache behavior, third-party compatibility posture, testing strategy, and future paths.

How it works

Invoice list/detail route
  -> LocalInvoiceAssistantPanel
     -> useLocalInvoiceAssistant
        -> analyzeLocalAiHardwareEligibility
        -> createLocalInvoiceAssistantContext
        -> LocalInvoiceAssistantAdapter
           -> WebLLM worker
              -> @mlc-ai/web-llm
  1. The invoice route passes invoice objects into LocalInvoiceAssistantPanel.
  2. The hook checks hardware and storage hints before showing model download UI.
  3. If the device is eligible or compatibility is unknown, the user can explicitly start model download.
  4. The adapter dynamically imports WebLLM, creates the worker, and loads the selected catalog model.
  5. User questions and suggested prompt chips go through the same sendMessage boundary.
  6. The hook builds sanitized prompt messages from local invoice state and session chat history.
  7. The worker streams local model output back to the UI through buffered updates.
  8. The user can interrupt generation, reset the session, run a local benchmark, dismiss recoverable errors, or clear cached model artifacts.

Privacy and security notes

  • Local-only invoice prompts: Invoice prompt data is not sent to the app backend or to a remote AI service.
  • External model artifacts: Model weights and WebLLM runtime artifacts are downloaded from approved external hosts only after explicit user action.
  • Catalog-only models: Arbitrary model IDs and user-provided model URLs are rejected at adapter boundaries.
  • Prompt sanitization: Prompt context excludes raw invoice IDs, user IDs, shared users, raw OCR, scans, scan URLs, and raw metadata.
  • Session-only chat: Chat history and benchmark metrics stay in React state and are not persisted to Zustand, IndexedDB, localStorage, or the backend.
  • Cache separation: WebLLM model artifacts are browser-cache data managed by WebLLM; invoice objects remain separate in IndexedDB through Zustand persistence.
  • No CSP header for now: next.config.ts intentionally does not emit a Content Security Policy header. The previous strict allowlist blocked legitimate third-party tooling such as Clarity and Hotjar and was brittle as browser SDK hosts changed.
  • Runtime model safety remains: model loading is still catalog-only, and arbitrary model IDs or user-provided artifact URLs are rejected before WebLLM initialization.

Device support and model policy

  • Known missing WebGPU, missing WebGPU adapter, missing Worker support, low storage, low memory, or low CPU blocks the local assistant.
  • Unknown storage, memory, or CPU signals show a compatibility warning and still require explicit download consent.
  • The current balanced default is Llama-3.2-1B-Instruct-q4f16_1-MLC.
  • Low-footprint and fallback candidates remain available through the curated catalog where their WebLLM metadata is verified.
  • Gemma 3, Qwen 3.5, Phi 4 Mini, and any future "Gemma 4" path are future candidates only after exact WebLLM model IDs, artifact hosts, required features, licensing, and runtime load/benchmark behavior are verified.

Performance and benchmark notes

  • The UI shows deterministic analytics before model download so users get value immediately when invoice data exists.
  • Streaming output is buffered to frame cadence rather than rendering every token delta.
  • The in-app benchmark uses a privacy-safe benchmark prompt, not invoice content.
  • Benchmark results are local/session-only and depend on the exact device, browser, GPU driver, battery/thermal state, selected model, and background load.
  • The displayed tokens/sec value is an estimate based on streamed deltas/chunks unless a future WebLLM version exposes true token usage.
  • This PR does not claim a tokens/sec number for the user's current browser; reviewers should run the manual benchmark on target devices.

Manual review checklist

Run these manually on real target browsers/devices; do not automate them as CI/E2E/live tests because WebLLM can download multi-GB artifacts.

  1. Open the invoice list assistant on a WebGPU-capable desktop browser and confirm hardware analysis exits the checking state, then confirm model preparation UI, explicit download, chat, stop, reset, benchmark, and cache-clear flows.
  2. Open the feature on an unsupported or constrained device/browser and confirm the localized fallback or compatibility warning appears before any model download.
  3. Confirm no model download starts before clicking the download action.
  4. Run the benchmark after model load and record first-token latency, estimated tokens/sec, model ID, browser, device, GPU, and thermal state.
  5. Confirm suggested prompts are localized and disabled while generation is active.
  6. Confirm deterministic analytics preview separates totals by currency.
  7. Use DevTools to check that model artifacts are requested from documented WebLLM/HuggingFace hosts and that third-party tooling is not blocked by a site CSP header.
  8. Refresh the page and confirm chat history is gone while invoice data remains.

Future upgrade paths

  • Add a remote AI assistant fallback for devices that fail local hardware requirements.
  • Add a model picker after UX, performance, and model-quality tradeoffs are validated.
  • Upgrade WebLLM only after verifying new model IDs and artifact behavior in the installed package.
  • Add richer deterministic analytics before invoking generated text.
  • Add privacy-safe capability telemetry only after a separate privacy review and user consent path.
  • Improve cache-size/status UX so users understand how much local storage the model occupies.

Validation

  • npm run test:unit
    • Assertions passed across the reported unit suites.
    • The command exits non-zero because the existing website global branch coverage threshold is not met: branches 86.67% < 90%.
    • Website unit assertions passed: 90 test files, 1903 tests.

No lint, E2E/live/browser/Playwright, focused Vitest, TypeScript-only validation, or generate:i18n command was run for the final validation pass.

arolariu and others added 12 commits April 25, 2026 12:09
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread sites/arolariu.ro/package.json Outdated

This comment was marked as resolved.

@github-actions

This comment was marked as resolved.

arolariu and others added 2 commits April 25, 2026 21:52
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated 2 comments.

Comment on lines +346 to +364
async generate(messages, options = {}): Promise<string> {
if (!engine) {
throw new Error("Load the local invoice assistant model before generating a response.");
}

const generationOptions = {
...DEFAULT_LOCAL_INVOICE_ASSISTANT_GENERATION_OPTIONS,
...options.generationOptions,
};
const stream = await engine.chat.completions.create({
max_tokens: generationOptions.maxTokens,
messages: messages.map((message) => ({
content: message.content,
role: message.role,
})),
stream: true,
temperature: generationOptions.temperature,
top_p: generationOptions.topP,
});
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate() reads the shared engine variable multiple times. If another call (e.g. deleteCachedModel()/dispose()) runs concurrently and sets engine = null after the initial if (!engine) guard, this can throw at runtime (engine.chat... on null) and/or produce inconsistent lifecycle behavior. Capture engine into a local constant (and similarly the worker if needed) before awaiting, and use that stable reference throughout the call.

Copilot uses AI. Check for mistakes.
Comment on lines +336 to +341
return {
async deleteCachedModel(model = DEFAULT_LOCAL_INVOICE_ASSISTANT_MODEL): Promise<void> {
await unloadActiveModel({interrupt: true});
const webLlm = await importWebLlm();
await webLlm.deleteModelAllInfoInCache(model.id);
},
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteCachedModel() calls unloadActiveModel() but doesn't account for a model load already in-flight (loadingModel and worker can be set before engine exists). If deleteCachedModel() is invoked while load() is pending, the current logic can terminate the worker but still allow loadModelOnce() to later install an engine backed by a terminated worker. Consider adding an explicit “load cancelled” flag/token (similar to isDisposed) and/or awaiting/cancelling loadingModel so pending loads cannot resurrect the engine after cache deletion.

Copilot uses AI. Check for mistakes.
arolariu and others added 8 commits April 26, 2026 00:05
Add a curated catalog of 8 WebLLM models across 4 families (Llama, Gemma, Phi,
Qwen, SmolLM) and 4 tiers (fallback, balanced, quality, experimental). All
models use q4f16_1 quantization for browser feasibility.

Implement hardware-aware recommendation logic that filters models by:
- Hardware eligibility status
- Required GPU features (e.g., shader-f16)
- VRAM limits
- Available storage

The recommender prefers balanced tier models (defaulting to Llama 3.2 1B) for
eligible devices and falls back to the smallest compatible model for
constrained devices.

Upgrade-gated models (Qwen 3.5, Phi 4 Mini) are excluded from the catalog
until WebLLM dependency upgrade and verification.

BREAKING CHANGE: LocalInvoiceAssistantModelMetadata type now includes family,
tier, vramRequiredMB, and requiredFeatures fields.

Files:
- Create: modelCatalog.ts (catalog and recommender)
- Create: modelCatalog.test.ts (16 TDD tests, all passing)
- Modify: types.ts (extend model metadata)
- Modify: webLlmAdapter.ts (consume catalog instead of hardcoded model)
- Modify: RFC 1009 (document model matrix and recommender logic)

Tests: 28 passing (modelCatalog, webLlmAdapter, hardwareEligibility)
Coverage: modelCatalog.ts has 16 tests covering all requirements

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix 4 blocking issues identified in Task 2 spec review:

1. **Add shader-f16 required features:**
   - SmolLM2-360M-Instruct now requires shader-f16
   - Gemma-2-2b-it now requires shader-f16
   - Added test to verify models with shader-f16 requirements exist
   - Recommender now correctly filters by GPU feature availability

2. **Add Gemma 4 exclusion test:**
   - Explicit test ensuring no Gemma 4 models in selectable catalog

3. **Create UPGRADE_GATED_MODEL_CANDIDATES constant:**
   - Replaced comment-only upgrade-gated list with typed records
   - Exported as separate constant with 3 models (Qwen 3.5 0.8B/2B, Phi 4 Mini)
   - Added 4 tests verifying upgrade-gated candidates are typed and excluded
   - Updated RFC with upgrade-gated candidates table

4. **Add null recommendation test for over-constrained scenarios:**
   - Test when hardware is eligible but all constraints impossible to satisfy
   - Test when GPU features filter out shader-f16 models correctly
   - Recommender already returned null correctly; test confirms behavior

TDD evidence:
- Red phase: 5 failing tests (shader-f16 count, upgrade-gated undefined)
- Green phase: All 24 tests passing after implementation
- Full suite: 36/36 tests passing (modelCatalog + webLlmAdapter + hardwareEligibility)

Files changed:
- modelCatalog.ts: Added shader-f16 features, UPGRADE_GATED_MODEL_CANDIDATES
- modelCatalog.test.ts: Added 8 new tests (Gemma 4, shader-f16, upgrade-gated, null constraints)
- webLlmAdapter.ts: Re-export UPGRADE_GATED_MODEL_CANDIDATES
- RFC 1009: Updated model matrix with shader-f16 column and upgrade-gated table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix critical code-quality issues after controller verification of WebLLM 0.2.82:

1. Remove non-existent Gemma 3 1B from selectable catalog, replace with gemma-2b-it-q4f16_1-MLC (verified in WebLLM 0.2.82). Move Gemma 3 1B to upgrade-gated candidates.

2. Add WebLLM 0.2.82 allowlist verification test ensuring only verified IDs are selectable.

3. Make omitted gpuFeatures conservative (treat as empty set, filter out shader-f16 models).

4. Strengthen recommendation tests with exact assertions instead of optional if-blocks.

5. Replace non-null assertion on DEFAULT with explicit invariant error.

6. Achieve 93.75% branch coverage (exceeds 90% threshold), 30 tests passing.

TDD: Red phase 5 failures, green phase 30/30 passing, full suite 42/42 passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
**Context:**
Previous model catalog used estimated VRAM/context values that didn't match
the installed WebLLM 0.2.82 prebuiltAppConfig registry. This caused incorrect
hardware recommendations and broke metadata integrity.

**Changes:**
1. Added WEBLLM_0_2_82_REGISTRY_METADATA constant with exact values from
   installed package for all 8 selectable models
2. Added test 'matches WebLLM 0.2.82 registry metadata exactly' to enforce
   metadata correctness (prevents future drift)
3. Updated all model VRAM/context values in modelCatalog.ts to match registry:
   - SmolLM2: 376.06 MB (was 512), context 4096 (was 2048)
   - Qwen3-0.6B: 1403.34 MB (was 768)
   - Llama-3.2-1B: 879.04 MB (was 1536)
   - Gemma 2B: 1476.52 MB (was 1477)
   - Gemma 2 2B: 1895.3 MB (was 2048), context 4096 (was 8192)
   - Llama-3.2-3B: 2263.69 MB (was 3072)
   - Phi-3.5-mini: 3672.07 MB (was 4096), context 4096 (was 128000)
   - Phi-3.5-mini-1k: 2520.07 MB (was 4096)
4. Refactored recommender to sort balanced models by VRAM (ascending) instead
   of explicit Llama find + fallback, removing unreachable branch
5. Updated 3 tests affected by metadata changes:
   - Low-storage test: now expects Llama (879*1.5=1318 MB fits in 2 GiB)
   - VRAM limits test: Llama (879 MB) fits under 1000 MB limit
   - Balanced model filtering: SmolLM2 recommended when Llama filtered by 850 MB limit
6. Added test for omitted gpuLimits parameter (no VRAM filtering)
7. Updated RFC 1009 model matrix table with corrected metadata

**Impact:**
- Hardware recommendations now accurate (e.g., Llama 3.2 1B correctly fits under 1 GiB VRAM, not 1.5 GiB)
- Test coverage: 92.85% branches (exceeds 90%), 32 tests, full suite 44/44
- All VRAM/context values now verifiable against installed WebLLM package

**TDD Evidence:**
- Added metadata validation test (red → green)
- Updated 3 existing tests to match corrected metadata (red → green)
- All 44 local AI tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ta docs

- Add installed package bundle verification test using require.resolve + readFileSync
  - Validates all selectable models appear in @mlc-ai/web-llm bundle text
  - Validates upgrade-gated models are absent (catches accidental downgrades)
  - Complements existing hardcoded metadata validation

- Fix RFC 1009 model family count: 4 → 5 families (Llama, Gemma, Phi, Qwen, SmolLM)

- Fix modelCatalog.ts JSDoc balanced tier: 'Gemma 3 1B' → 'Gemma 2 2B'
  - Gemma 3 1B is upgrade-gated (not in WebLLM 0.2.82), not selectable
  - Gemma 2 2B is the correct balanced tier model

Test results:
- 33 modelCatalog tests passing (was 32, +1 bundle validation test)
- 45 total local AI tests passing (modelCatalog + hardwareEligibility + webLlmAdapter)
- 92.85% branch coverage in modelCatalog.ts (exceeds 90% threshold)

Resolves final spec re-review issues from commit 9e264a6.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nion compatibility

Create widened ReadonlyArray views of LOCAL_INVOICE_ASSISTANT_MODELS and
UPGRADE_GATED_MODEL_CANDIDATES in modelCatalog.test.ts to resolve TypeScript
errors when performing dynamic checks (.includes(), .has(), ID comparisons).

The catalog exports use 'as const satisfies ReadonlyArray<...>' to preserve
literal tuple types for type safety, but tests need widened array types for
runtime assertions that TypeScript literal unions reject as impossible.

Changes:
- Add selectableModels and upgradeGatedCandidates widened views with JSDoc
- Replace direct catalog array usage with widened views in all test assertions
- Preserve catalog integrity and type safety in production code

Fixes TypeScript errors:
- Line 109: .includes('shader-f16') on literal empty array
- Line 136: ID comparison with non-existent literal ID
- Line 184: Set.has() with upgrade-gated ID not in selectable union

All tests pass with 94.28% statement coverage, 92.85% branch coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the remaining literal-union escape cast in the WebLLM package guard with a widened string Set so dynamic test assertions stay type-safe without weakening catalog exports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extend HardwareEligibilityResult with safe GPU features/limits and device memory/cores
- Change storage-estimate-unavailable from ineligible to unknown (warning, not blocker)
- Collect GPU features (shader-f16) and limits (maxBufferSize) without vendor/device IDs
- Add device compatibility details UI component with GPU/CPU/memory info
- Wire enriched hardware into recommendLocalInvoiceAssistantModel
- Add explicit user confirmation for download when storage unavailable
- Add i18n keys for device compatibility details in EN/RO/FR
- Update model preparation card to show recommended model and storage warning
- All hardwareEligibility tests pass (11/11)

Privacy-preserving: No adapter name, vendor ID, or device ID collected.
Task 3: Hardware enrichment complete for real model selection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
arolariu and others added 15 commits April 26, 2026 14:08
… resolution

Task 6 spec review fix: Close mounted late-resolution gap

Issue:
Hook could mark itself ready if an aborted load resolved instead of rejected.
After await adapter.load(), code checked isMountedRef but not abortController.signal.aborted
before setting loadedRef=true and lifecycle=ready.

If deleteCachedModel/resetSession aborted active load while mounted, then adapter.load
later resolved (instead of rejecting), the original loadModel() async function would
still mark ready incorrectly.

Fix:
- Added abort signal check after await and before ready transition:
  if (!isMountedRef.current || abortController.signal.aborted) return;
- Ensures state remains recoverable (not-downloaded/compatibility-unknown) after mounted abort
- Hook never transitions to ready if abort happened, even if load promise resolves

Tests:
- Replaced weak unmount test with stronger mounted abort test
- New test: deleteCachedModel aborts load, adapter resolves late, hook stays not-downloaded
- New test: resetSession aborts load, adapter resolves late, hook stays not-downloaded
- Both tests verify adapter does NOT reject on abort, just resolves late
- Both tests assert lifecycle remains recoverable and canLoadModel stays true

All tests pass (23 useLocalInvoiceAssistant tests, 10 webLlmAdapter tests, 1826 total).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ntroller ownership

Task 6 quality review fix: Close lifecycle race in duplicate loadModel calls

Issue:
Duplicate loadModel() calls could leave the real pending load un-aborted, allowing
late resolution to mark hook ready after reset/cache deletion.

Concrete race:
1. First loadModel() starts pending adapter load with controller A
2. Second loadModel() runs while first is pending, creates controller B, overwrites ref
3. Adapter coalesces concurrent load by reusing existing loadingModel
4. Only first signal A is wired into loadModelOnce()
5. resetSession()/deleteCachedModel() aborts current ref B only
6. Controller A remains un-aborted
7. Original engine resolves late
8. Adapter sees original signal not aborted and can assign active engine
9. First hook call resumes and can mark lifecycle ready after reset/cache deletion

Fix:
- Replaced loadAbortControllerRef with pendingLoadRef that stores {controller, promise}
- First loadModel() creates controller, stores both controller and promise together
- Second loadModel() sees existing pendingLoadRef and returns the existing promise (deduplication)
- No controller overwrite - only one controller exists per in-flight load
- resetSession()/deleteCachedModel() abort the correct controller (the one actually used)
- Finally block cleans up pendingLoadRef only if it's still the current one
- Preserved single-load abort behavior and adapter late-engine cleanup

Tests:
- New: Duplicate loadModel then resetSession then late resolution stays not-downloaded
- New: Duplicate loadModel then deleteCachedModel then late resolution stays not-downloaded
- Both verify adapter called only once (coalesced) and first signal is aborted
- New adapter test: Pending load + cache deletion + late engine resolution leaves engine unusable
- All existing abort tests still pass

All tests pass (25 useLocalInvoiceAssistant tests, 11 webLlmAdapter tests, 1829 total).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hs and retry-after-abort

Task 6 quality re-review fix: Close two remaining pending-load lifecycle gaps

Issue 1: Retry after reset/cache-delete can bind to wrong adapter load
- resetSession/deleteCachedModel abort pending controller and clear pendingLoadRef immediately
- Adapter's loadingModel may still be unresolved
- User retries loadModel() before old one settles
- New hook call creates controller B, but adapter coalesces onto old loadingModel with controller A
- When old load rejects (abort), retry sees controller B not aborted and surfaces error

Fix 1: Keep aborted pending load entry until promise settles
- resetSession/deleteCachedModel abort the controller but do NOT clear pendingLoadRef
- loadModel checks if existing pendingLoad is aborted:
  - If aborted: wait for it to settle (with catch to ignore intentional abort errors), then start new load
  - If not aborted: reuse existing promise (prevent duplicate controllers)
- Only unmount clears pendingLoadRef immediately (component is gone)
- Prevents retry from binding to stale adapter load
- User stays in recoverable state; no scary errors from intentional aborts

Issue 2: Adapter deleteCachedModel does not invalidate pending loads
- adapter.load() without signal
- adapter.deleteCachedModel() while pending
- Late engine resolves
- Late engine may become active
- generate() may work even though cache was deleted

Fix 2: Add adapter-level load generation/epoch
- Added loadGeneration counter incremented by deleteCachedModel() and dispose()
- loadModelOnce() captures generation at start
- After engine creation resolves, check if generation changed
- If changed: unload late engine, terminate worker, return without assigning
- Prevents late engines from becoming active after invalidation
- Works without external abort signal

Tests:
- New adapter test: deleteCachedModel without manual abort invalidates pending load
- Verifies late engine is unloaded and adapter not ready for generation
- New hook test: retry after resetSession before old load settles, no scary error
- Verifies wait-for-settlement, no error from aborted load, retry succeeds
- New hook test: retry after deleteCachedModel before old load settles, no scary error
- Verifies wait-for-settlement, cache delete completes, retry succeeds
- Updated Deferred helper to support reject for realistic abort testing

All tests pass (27 useLocalInvoiceAssistant tests, 12 webLlmAdapter tests, 1832 total).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Task 7: Make external model artifact behavior explicit and safe in production.

**Changes:**

1. **Model catalog lookup helper (security)**:
   - Add getLocalInvoiceAssistantModelById() to validate model IDs
   - Reject arbitrary user input, upgrade-gated, or unsupported model IDs
   - Ensure artifact host URLs come from trusted catalog metadata
   - Prevent malicious model IDs from reaching WebLLM initialization

2. **CSP hardening**:
   - Add explicit webLlmArtifactHosts variable in next.config.ts
   - Allow https://huggingface.co for connect-src (model artifact downloads)
   - Document CSP directives for WebLLM (connect-src, worker-src, script-src)

3. **Cache behavior UI copy (localized EN/RO/FR)**:
   - Add model.size key explaining approximate cached model size
   - Add cache.behavior key explaining Cache API vs IndexedDB separation
   - Add cache.clearImpact key explaining cache clear does not delete invoices
   - Add cache.source key displaying trusted artifact host
   - Update ModelPreparationCard to display model size and cache info

4. **RFC 1009 documentation update**:
   - Document WebLLM model artifact hosts and CSP allowlist
   - Document Cache API for model artifacts vs IndexedDB for invoice data
   - Document cache clearing impact (artifacts removed, invoices preserved)
   - Document storage quota considerations and browser eviction behavior
   - Document security guarantees (catalog-only model IDs, CSP enforcement)

**Tests:**
- Add 7 new test cases for getLocalInvoiceAssistantModelById()
- Verify unsupported, upgrade-gated, and arbitrary model IDs return null
- Verify artifact host comes from catalog, not user input
- Verify all selectable models can be looked up by ID
- All existing tests pass (LocalInvoiceAssistantPanel: 9/9, modelCatalog: 40/40)

**Validation:**
- Ran npm run test:unit (website tests pass, exit 1 due to global coverage threshold < 90% as expected)
- Model catalog tests: 40 passed, 94.59% statement coverage
- Panel tests: 9 passed, 93.15% statement coverage for LocalInvoiceAssistantPanel.tsx

**Security posture:**
- Arbitrary model IDs rejected at catalog lookup boundary
- CSP enforces connection to trusted artifact hosts only
- User-facing UI explains cache behavior and data isolation
- RFC documents actual implementation and cache/CSP behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…UX, docs

Fix three spec review findings after Task 7 commit 23f0222:

**Issue 1: CSP not explicit/safe artifact-host allowlist**
- Create shared constant file modelArtifactHosts.ts for single source of truth
- Export WEBLLM_ARTIFACT_HOST_FULL and WEBLLM_ARTIFACT_HOST_CSP_ORIGIN
- Update model catalog to use WEBLLM_ARTIFACT_HOST_FULL constant
- Update next.config.ts to import and use WEBLLM_ARTIFACT_HOST_CSP_ORIGIN
- Remove broad https: from connect-src CSP directive
- Document CSP origin derivation (full URL vs origin for CSP)
- Keep default-src, script-src, style-src with https: for existing functionality
- Explicit allowlist: connect-src now only self + trustedDomains + HuggingFace

**Issue 2: Clear-cache UX missing model-specific info**
- Add CacheInfoSection component displaying model name, host, size, cache behavior
- Show cache info only after model loads (ready/generating/cancelled/error states)
- Display model-specific clear cache button with name and size
- Use locale keys cache.behavior, cache.clearImpact, cache.source
- Move clear cache button from MessageComposer to CacheInfoSection for visibility
- Add test verifying cache info appears after model load with model-specific details
- Test verifies model name, size (approx 1319 MB for Llama 1B), and cache behavior copy

**Issue 3: Adapter docs contradict cache storage behavior**
- Update webLlmAdapter.ts JSDoc to say Cache API not IndexedDB
- Correct deleteCachedModel comment: browser Cache API (WebLLM-managed)
- Correct architecture comment: Model artifacts cached in browser Cache API
- Consistent with RFC 1009 documentation

**Changes:**

1. **New file: modelArtifactHosts.ts**:
   - Single source of truth for WebLLM artifact host constants
   - WEBLLM_ARTIFACT_HOST_FULL: https://huggingface.co/mlc-ai
   - WEBLLM_ARTIFACT_HOST_CSP_ORIGIN: https://huggingface.co
   - Documents CSP origin derivation and WebLLM behavior

2. **modelCatalog.ts**:
   - Import WEBLLM_ARTIFACT_HOST_FULL constant
   - Replace all hard-coded https://huggingface.co/mlc-ai strings
   - Eliminates duplication and ensures catalog/CSP consistency

3. **next.config.ts**:
   - Import WEBLLM_ARTIFACT_HOST_CSP_ORIGIN from modelArtifactHosts
   - Remove broad https: from connect-src directive
   - Explicit connect-src: self + trustedDomains + webLlmArtifactHosts
   - Document derivation and security constraints in comments

4. **LocalInvoiceAssistantPanel.tsx**:
   - Add CacheInfoSection component with model-specific cache info
   - Update ChatShellProps to include activeModelDisplayName, activeModelHost, activeModelSizeMB
   - Render CacheInfoSection in ChatShell after model loads
   - Remove duplicate clear cache button from MessageComposer
   - Display cache behavior, source, impact, and model-specific clear button

5. **webLlmAdapter.ts**:
   - Update deleteCachedModel JSDoc: browser Cache API (WebLLM-managed)
   - Update architecture comment: Model artifacts cached in browser Cache API
   - Consistent with RFC 1009 Cache API vs IndexedDB separation

6. **LocalInvoiceAssistantPanel.test.tsx**:
   - Add cache locale keys to mock translations
   - Add test displays model-specific cache information after model loads
   - Verify cache info not visible before load
   - Verify cache behavior, impact, and source copy appear after load
   - Verify clear cache button shows model name (Llama 3.2 1B) and size (approx 1319 MB)

**Tests:**
- LocalInvoiceAssistantPanel: 10/10 passed (was 9/9)
- modelCatalog: 40/40 passed, 94.87% statement coverage
- New test verifies cache UX displays model-specific name, host, size
- All existing tests pass (no regressions)

**Validation:**
- Exit code 1 due to global website coverage < 90% threshold (expected)
- All test assertions passed
- LocalInvoiceAssistantPanel.tsx: 93.33% statement coverage
- modelArtifactHosts.ts: 100% statement coverage

**Security posture:**
- CSP connect-src now explicit allowlist (no broad https:)
- Single source of truth for artifact hosts prevents catalog/CSP drift
- User-visible cache UI explains Cache API vs IndexedDB separation
- Clear cache button shows model-specific impact (artifacts removed, invoices preserved)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…derive origins

Address spec re-review findings after commit e3375d7:

**Issue 1: Missing CSP allowlist entry for WebLLM WASM/model-library host**
- Add WEBLLM_BINARY_LIBRARY_HOST constant: https://raw.githubusercontent.com
- WebLLM 0.2.82 fetches prebuilt WASM libraries from this GitHub raw host
- Source: @mlc-ai/web-llm/lib/index.js modelLibURLPrefix (line ~920)
- Include in CSP connect-src alongside model artifact origins

**Issue 2: RFC CSP docs incomplete/incorrect for binary libraries**
- Update RFC 1009 section 6.2 with correct artifact host table
- HuggingFace: model weights (*.bin, *.safetensors), tokenizer, config
- GitHub raw: WebLLM prebuilt WASM model execution libraries (*.wasm)
- Document both hosts with distinct purposes and artifact types
- Update CSP enforcement section with 7 security guarantees

**Issue 3: Artifact-host allowlist not derived from catalog metadata**
- Add deriveUniqueArtifactOrigins() helper to extract CSP origins from URLs
- Update next.config.ts to derive model artifact origins from catalog
- Map LOCAL_INVOICE_ASSISTANT_MODELS artifact hosts to unique CSP origins
- Keep WebLLM binary library origin explicit (runtime dependency, not catalog)
- CSP now dynamically syncs with catalog changes

**Issue 4: Remove accidental duplicate RFC fragment**
- Delete dangling duplicate lines after section 6.3 (lines 412-414)
- Clean up horizontal rule placement

**Changes:**

1. **modelArtifactHosts.ts**:
   - Add WEBLLM_BINARY_LIBRARY_HOST constant
   - Add deriveUniqueArtifactOrigins() helper function
   - Document HuggingFace artifact types (weights, tokenizer, config)
   - Document GitHub raw artifact types (WASM libraries)
   - Comprehensive JSDoc with CSP usage and security constraints

2. **next.config.ts**:
   - Import deriveUniqueArtifactOrigins, WEBLLM_BINARY_LIBRARY_HOST
   - Import LOCAL_INVOICE_ASSISTANT_MODELS catalog
   - Derive modelArtifactOrigins from catalog artifact hosts
   - Combine derived origins + binary library origin for CSP
   - Update connect-src to use webLlmCspOrigins (derived + explicit)
   - Document CSP origin derivation and security model

3. **RFC 1009**:
   - Update section 6.2 with correct artifact host table
   - Separate HuggingFace (model artifacts) from GitHub raw (WASM libraries)
   - Add artifact type columns to clarify what each host provides
   - Update CSP enforcement section with derived origins explanation
   - Add security guarantee #6: derived origins update with catalog
   - Add security guarantee #7: binary library origin explicit/documented
   - Remove duplicate fragment after section 6.3

4. **modelArtifactHosts.test.ts (NEW)**:
   - Test WEBLLM_ARTIFACT_HOST_FULL constant
   - Test WEBLLM_ARTIFACT_HOST_CSP_ORIGIN constant
   - Test WEBLLM_BINARY_LIBRARY_HOST constant
   - Test CSP origin derivation from full host
   - Test deriveUniqueArtifactOrigins() with single/multiple hosts
   - Test deduplication of same origin
   - Test multiple distinct origins
   - Test sorted output
   - Test empty input, malformed URLs, custom ports
   - Test http vs https as distinct origins
   - Test path/query/fragment stripping
   - 15 tests covering all edge cases

5. **modelCatalog.test.ts**:
   - Import deriveUniqueArtifactOrigins and host constants
   - Add test: all models use same artifact host (WEBLLM_ARTIFACT_HOST_FULL)
   - Add test: derives single unique CSP origin from all selectable models
   - Verify catalog artifact origins match expected CSP origin
   - 2 new tests ensuring catalog/CSP origin consistency

**Tests:**
- modelArtifactHosts.test.ts: 15/15 passed (new test file)
- modelCatalog.test.ts: 42/42 passed (was 40/40)
- Full unit suite: 88 test files passed, 1858 tests passed
- Exit code 1 due to global website branch coverage 87.03% < 90% (expected)
- All test assertions passed

**Validation:**
Full unit test suite command:
Set-Location feat/local-ai-assistant
env:NX_DAEMON=false; env:CI=true
npm run test:unit

Results:
- Test Files: 88 passed (88)
- Tests: 1858 passed (1858)
- Exit code 1: global branch coverage 87.03% below 90% threshold (NOT assertion failures)

**CSP Before (Issue)**:
connect-src 'self' trustedDomains https://huggingface.co

**CSP After (Fixed)**:
connect-src 'self' trustedDomains https://huggingface.co https://raw.githubusercontent.com

**Security posture:**
- CSP origins derived from catalog artifact hosts (automatic sync)
- WebLLM binary library origin explicit and documented
- Two distinct hosts: HuggingFace (model artifacts) + GitHub (WASM libraries)
- No broad https: in connect-src
- RFC documentation matches actual implementation and WebLLM behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed derivation, catalog enforcement

Fix three important code-quality issues identified in spec review:

1. **Azure Blob Storage CSP allowlist**
   - Add https://*.blob.core.windows.net to connect-src for existing scan upload/download
   - Add Azurite dev emulator origins (localhost:10000) for development
   - Document in RFC 1009 Section 6.2 with upload/download flow references
   - Separate from local AI functionality (predates this RFC)

2. **Fail-closed HTTPS-only artifact origin derivation**
   - deriveUniqueArtifactOrigins() now throws for malformed URLs (not silent skip)
   - Throws for any non-HTTPS protocol (http, ftp, etc.)
   - Uses url.origin instead of manual string reconstruction
   - Updated tests: expect throws for malformed/http/ftp, removed fail-open tests
   - Security: prevents silent CSP bypass from invalid catalog entries

3. **Catalog enforcement at adapter load/delete boundary**
   - Adapter load() validates model through getLocalInvoiceAssistantModelById()
   - Adapter deleteCachedModel() validates model through catalog lookup
   - Throws for unsupported/upgrade-gated/arbitrary model IDs before WebLLM calls
   - Added 5 tests proving catalog enforcement at adapter boundary
   - Security: prevents loading arbitrary model IDs from user input

Minor fixes:
- Strengthen cache-clear UI test to assert concrete catalog host (huggingface.co)
- Update stale useLocalInvoiceAssistant.ts comment: Cache API not IndexedDB

All AI component tests pass (153/153), exit 1 due to global coverage threshold.

Related: #683 Task 7 (PR feat/local-ai-assistant)
…log enforcement validation

## Changes

### webLlmAdapter.test.ts
- Import LocalInvoiceAssistantModelMetadata and UPGRADE_GATED_MODEL_CANDIDATES
- Add createTestModelMetadata() helper for fully-typed test fixtures
- Replace partial/obsolete model shapes (estimatedVRAM) with complete metadata
- Use real upgrade-gated candidates from catalog (gemma3-1b, Qwen3.5-0.8B)
- Strengthen catalog enforcement tests with mock spies:
  - Prove importWebLlm never called before rejection (load tests)
  - Prove deleteModelAllInfoInCache never called before rejection (delete tests)
  - Assert WebLLM receives catalog-verified ID on successful load
- Use satisfies LocalInvoiceAssistantModelMetadata for compile-time safety

### webLlmAdapter.ts
- Fix activeModelId assignment: use \catalogModel.id\ after load (not \model.id\)
- Ensures active model ID always matches catalog-verified value

## Type Safety
All test fixtures now include required fields:
- \contextWindowTokens\, \amily\, \
equiredFeatures\, \	ier\, \�ramRequiredMB\
- No obsolete \�stimatedVRAM: {gb, bytes}\ field
- TypeScript strict mode enforced via \satisfies\ operator

## Security Validation
Tests now prove fail-closed catalog enforcement:
1. Reject before WebLLM import for unsupported IDs
2. Reject before WebLLM cache delete for upgrade-gated models
3. Accept and pass catalog-verified ID to WebLLM engine

## Test Results
- All 153 AI component tests pass
- No lint violations
- No type errors (Vitest transpilation verified)

Completes Task 7 type-safety cleanup before final review.

Related: #683 (Task 7 - Model Host CSP and Cache UX)
…AI assistant

Task 8: Improve assistant usefulness without adding remote dependencies

Features:
- Suggested prompt chips with localized questions (summarize spending, largest invoice, top merchant, spending by currency)
- Deterministic analytics preview showing invoice count, spending by currency, and top merchant
- Analytics preview never cross-sums currencies - displays each separately
- Prompt chips hidden when no invoices available
- All UI strings localized in EN/RO/FR

Implementation:
- Created suggestedPrompts.ts with shouldShowSuggestedPrompts() and getSuggestedPromptKeys()
- Added AnalyticsPreview component showing instant insights from context.analytics
- Added SuggestedPrompts component with clickable prompt chips
- Updated LocalInvoiceAssistantPanel to surface analytics and handle prompt clicks
- Enhanced invoiceContext.test.ts with currency separation tests

Tests:
- suggestedPrompts.test.ts: 6 tests covering prompt visibility and conditional currency prompts
- invoiceContext.test.ts: Added 2 tests for currency grouping and merchant breakdown
- All 11 tests pass (6 + 5)

Validation:
- npm run test:unit passes all assertions
- Coverage threshold error expected per constraints
- No remote dependencies added
- Analytics computed from existing sanitized context

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes three issues identified in Task 8 spec review:

1. Missing SCSS styles for new UI components:
   - Added .analyticsPreview, .suggestedPrompts, .promptChips styles
   - Added .cacheInfo, .benchmarkSection, .metricsBox, .metricsList, .metricsDetails
   - Consistent with existing panel spacing/typography patterns

2. Panel-level test coverage requirements:
   - Added test: analytics preview visible before model loads
   - Added test: analytics displays totals by currency separately (no cross-sum)
   - Added test: localized prompt chips render after model loads
   - Added test: prompt chips do not render when no invoices
   - Added test: clicking chip sends localized prompt text through adapter
   - Updated mock translations to include suggestedPrompts.* and analyticsPreview.* keys
   - All tests verify visible behavior through existing fake adapter seam

3. Analytics visibility timing improvement:
   - Moved AnalyticsPreview outside ChatShell to render before model loads
   - Now visible immediately after hardware check when invoices exist
   - Achieves 'feel fast even before LLM generation' design goal
   - Kept SuggestedPrompts inside ChatShell (only usable post-load)

Test results:
- All 1878 tests pass (15 panel tests, 6 suggestedPrompts tests, 5 invoiceContext tests)
- Panel tests verify localized chips, click-to-send, analytics currency separation, no chips when empty
- Coverage threshold error expected per constraints (branches 87.07% < 90%)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes remaining i18n violation in Task 8:

Replaced hardcoded 'invoices' text in top merchant invoice count with localized translation key.

Changes:
- Added 'analyticsPreview.topMerchantInvoiceCount' i18n key to en/ro/fr locales:
  - en: '{count} invoices'
  - ro: '{count} facturi'
  - fr: '{count} factures'
- Updated LocalInvoiceAssistantPanel.tsx line 508 to use t() with count parameter
- Updated test mock to include new translation key
- Added test assertion to verify localized count (not hardcoded 'invoices')

Implementation:
- Before: {topMerchantEntry[1].invoiceCount} invoices
- After: t('analyticsPreview.topMerchantInvoiceCount', {count: topMerchantEntry[1].invoiceCount})

Test verification:
- Panel test now asserts 'Top merchant: merchant-1 (2 invoices)' using localized key
- All 15 LocalInvoiceAssistantPanel tests pass
- No hardcoded English user-facing strings remain in Task 8 code

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…age tests

Fixes two Important spec review issues for Task 8:

Issue 1: Prompt chips now respect send-state guards
- Added canSendMessage and isGenerating props to SuggestedPrompts component
- Prompt buttons disabled when !canSendMessage || isGenerating (matching composer behavior)
- Added defensive guard in handlePromptClick to prevent programmatic/click race sends
- Behavior choice: DISABLED (not hidden) - prompts remain visible but non-interactive during generation
- Added 2 panel tests proving chips cannot trigger adapter.generate when blocked:
  * Test: chips disabled and blocks sending when model is generating
  * Test: chips not shown when chat is not ready (before model loads)

Issue 2: Added real locale coverage tests
- Created localInvoiceAssistantMessages.test.ts (7 tests)
- Imports actual en.json, ro.json, fr.json files
- Verifies all required Task 8 keys exist in IMS--LocalInvoiceAssistant namespace:
  * suggestedPrompts: title, summarizeSpending, largestInvoice, topMerchant, spendingByCurrency
  * analyticsPreview: title, totalSpendLabel, invoiceCountLabel, noData, currencyBreakdown, topMerchantLabel, topMerchantInvoiceCount
- Validates topMerchantInvoiceCount has {count} parameter in all locales
- No generate:i18n required - directly imports JSON

Implementation details:
- SuggestedPrompts computes canUsePrompts = canSendMessage && !isGenerating
- All prompt buttons use disabled={!canUsePrompts} attribute
- handlePromptClick early-returns if !canSendMessage || isGenerating
- Locale test verifies non-empty string values for all keys

Test results:
- All 1887 tests pass (up from 1878, +9 new tests)
- 17 LocalInvoiceAssistantPanel tests (15 original + 2 new guard tests)
- 7 localInvoiceAssistantMessages tests (new)
- Coverage threshold error expected per constraints (branches 87.05% < 90%)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…g adapter calls

Solves re-entrant generation bug where back-to-back sendMessage() calls
could invoke adapter.generate multiple times despite UI-level guards.

Changes:
- Added generationInFlightRef to track if adapter.generate is running
- Set flag immediately before setState to prevent same-render re-entry
- Clear flag in all completion/error/unmount paths after promise settles
- Added regression test proving two sends only invoke generate once
- Verified later sends accepted after first generation completes

Guard relies solely on in-flight ref, not stateRef.current.lifecycle,
because stateRef updates asynchronously via useEffect after setState.

All 1888 tests pass (+1 new regression test).
Coverage threshold error (87.07% < 90%) expected and acceptable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds canSendMessageRef to prevent sends from non-send-ready lifecycle
states (error, generating, downloading, etc.), closing the boundary
invariant gap after the in-flight guard (commit aedfe0c).

Changes:
- Added canSendMessageRef boolean ref updated synchronously at lifecycle transitions
- Updated loadModel success path to set canSendMessageRef = true
- Updated sendMessage to check canSendMessageRef before accepting call
- Set canSendMessageRef = false when entering generating state
- Set canSendMessageRef = true after generation success
- Set canSendMessageRef = false after generation error (error state is not send-ready)
- Updated dismissError to restore canSendMessageRef based on recovered lifecycle
- Updated interrupt to set canSendMessageRef = true (cancelled is send-ready)
- Updated resetSession to restore canSendMessageRef based on recovered lifecycle
- Added regression test: rejects sendMessage when lifecycle is error
- Added regression test: allows sendMessage after dismissError recovery

Rationale:
- In-flight guard alone insufficient: after error, loadedRef=true but lifecycle=error
- Cannot use stateRef.current.lifecycle: updated asynchronously via useEffect (race)
- Solution: dedicated ref updated synchronously at same moment as setState calls
- Preserves interrupt behavior: in-flight guard blocks overlapping, lifecycle guard
  ensures only send-ready states (ready/cancelled) accept new sends

Test results:
- All 178 AI component tests pass (31 hook tests, +2 new regression)
- Coverage threshold 85% < 90% expected (not a failure)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 26 changed files in this pull request and generated 1 comment.

Comment on lines +265 to +273
largestInvoices: invoices
.toSorted((left, right) => right.totalAmount - left.totalAmount)
.slice(0, 5)
.map((invoice) => ({
currencyCode: invoice.currencyCode,
invoiceAlias: invoice.invoiceAlias,
name: invoice.name,
totalAmount: invoice.totalAmount,
})),
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

largestInvoices is computed by sorting SanitizedInvoiceForAssistant records purely by totalAmount, which mixes different currencies in a single ordering. This contradicts the module’s stated approach of avoiding cross-currency arithmetic (and can produce misleading “largest invoice” results if the context includes multiple currencies). Consider grouping by currencyCode (e.g., top-N per currency) or otherwise ensuring the ranking is only computed within a single currency.

Copilot uses AI. Check for mistakes.
arolariu and others added 6 commits April 26, 2026 22:26
…ettles

Fixes edge case where stale/superseded generation promise rejection
incorrectly overwrites canSendMessageRef to false, desynchronizing it
from lifecycle state.

Problem scenario:
1. Start generation (canSendMessageRef=false, generationInFlightRef=true)
2. Call interrupt() (canSendMessageRef=true, lifecycle=cancelled)
3. Old promise rejects (stale catch runs)
4. Stale catch previously set canSendMessageRef=false
5. UI shows cancelled (send-ready) but hook silently rejects sends

Solution:
- In stale/superseded settlement paths (success and error), do NOT
  overwrite canSendMessageRef
- Only clear generationInFlightRef to signal completion
- Preserve whatever readiness was established by interrupt(),
  resetSession(), or current lifecycle state

Regression tests added:
- interrupt + late rejection preserves send-ready (cancelled)
- resetSession + late rejection preserves send-ready (ready)

Co-authored-by: Dependabot <dependabot@github.com>
…pers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 38 changed files in this pull request and generated 1 comment.

Comment thread sites/arolariu.ro/next.config.ts Outdated
Comment on lines +68 to +70
img-src 'self' blob: data: https: ${trustedDomains} ${localDevSources};
worker-src 'self' blob: data: https: ${trustedDomains};
worker-src 'self' blob: data: ${trustedDomains};
connect-src 'self' ${trustedDomains} ${azureBlobOrigins} ${webLlmCspOrigins};
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect-src is now restricted to trusted domains, Azure Blob, and WebLLM origins. The app still injects tracking scripts (Clarity/Hotjar) from src/app/tracking.tsx, which will typically attempt to send network beacons/XHR back to their own origins; with this CSP they’ll be blocked and will generate CSP violations (and likely break analytics).

Add the required analytics endpoints to connect-src (and consider gating them by environment/feature flag), or remove/disable the tracking scripts if the intent is to block them entirely.

Copilot uses AI. Check for mistakes.
arolariu and others added 3 commits April 27, 2026 15:28
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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.

2 participants