Skip to content

Upgrade to version 2 - #13

Draft
thnhmai06 wants to merge 356 commits into
mainfrom
develop
Draft

Upgrade to version 2#13
thnhmai06 wants to merge 356 commits into
mainfrom
develop

Conversation

@thnhmai06

Copy link
Copy Markdown
Owner

No description provided.

- Updated class names for clarity and consistency (improves code readability).
- Refactored DownloadManager to use new class names.
- Enhanced copilot instructions for better architectural guidance.
- Changed the deserializer and serializer to use YamlDotNet's types directly (improves clarity and reduces namespace usage).
…structure

- Changed ISlide and IImageShape to inherit from ISlideObject (ensures a unified interface structure).
- Updated namespaces in various files to reflect the new structure (improves clarity and organization).
- Renamed methods in ISpecializable to Flatten for better semantic meaning (enhances code readability).
…de, tasks

- Updated activity constructors to accept identifiers instead of raw strings (improves type safety).
- Refined error handling for invalid inputs in several activities (enhances robustness).
- Introduced a new FileSystem class for file operations (promotes single responsibility).
- Normalized file naming conventions across the application (ensures consistency).
- Moved various activity classes to the new Generation namespace for better organization and clarity (improves maintainability).
- Updated constructors in several activities to use required properties instead of nullable ones (enhances type safety).
- Updated CleanupResources activity to manage multi temporary resources during workflow execution (promotes resource management).
- Updated namespaces for clarity and consistency (e.g., SlideGenerator.Domain.Slide.Models to SlideGenerator.Domain.Slide.Models.Previews).
- Modified method signatures to remove default parameters for better readability (e.g., GetOrOpen method).
- Introduced new interfaces and classes for better abstraction (e.g., IImageReplacer, ITextReplacer).
- Changed IRegistry interface to an abstract class Registry for better resource management.
- Implemented reference counting for resources to prevent premature disposal.
- Introduced RegistryLease for managing resource leases.
- Updated GenerationWorkflow and ScanningService to use the new Registry system.
- Removed obsolete IPC-related files and endpoints.
- Added XML documentation for public APIs in the new registry classes.
…ion tasks

- Removed the RegistryLease class from Common and moved it to Resources.
- Introduced a new Lease base class for better resource management.
- Implemented a Registry class for shared, reference-counted resources.
- Added AcquireActivitySlot, AcquireEditingSlot, and AcquirePreparingSlot activities for managing concurrency.
- Created ActivityGate and ActivityLease classes to control and represent activity execution permits.
- Updated GenerationWorkflow to utilize new concurrency management features.
- Replaced usages of the old RegistryLease with the new implementation across various activities.
- Enhanced JobSetting to include separate max concurrent flows for preparing and editing.
- Renamed FaceDetectorModelKey to FaceDetectorModel for clarity.
- Updated RoiType enum values for better understanding (RuleOfThirds to Center and vice versa).
- Introduced new classes for CenterOption and RuleOfThirdsOption to encapsulate ROI strategies.
- Refactored FaceDetector and related classes to improve maintainability and readability.
…tingWorkflow

This commit introduces a major refactoring of the GeneratingWorkflow to improve concurrency control, modularity, and clarity. The previous implementation used rigid, monolithic activities for batch processing, which has been replaced with a more flexible, granular, and robust system.

Key Changes:

- **Introduced `IAsyncKeyedLocker<SlotType>`:** Replaced hardcoded concurrency limits with a generic, keyed locking mechanism. This allows defining flexible, process-wide concurrency gates (e.g., for downloads, image edits, slide edits) that are managed centrally.

- **Generic `AcquireSlot` and `ReleaseSlot` Activities:** The numerous specific slot activities (e.g., `AcquirePreparingSlot`, `AcquireEditingSlot`) have been removed and replaced by a single, reusable pair of `AcquireSlot` and `ReleaseSlot` activities. These now operate on the `IAsyncKeyedLocker` by specifying a `SlotType`.

- **Parallel-Per-Item Processing:** The monolithic `DownloadImages` and `EditImages` activities have been replaced with a `ParallelForEach` pattern. The workflow now iterates over a collection of items (URLs or paths), and each item is processed individually within its own `AcquireSlot`/`ReleaseSlot` block. This provides fine-grained control over the degree of parallelism.

- **Renamed Activities for Clarity:**
  - `SummarizeDownloadedImagePaths` is now `ReadDownloadedImagePathStore`.
  - `SummarizeEditedImagePaths` is now `ReadEditedImagePathStore`.
  This better reflects their function of reading results from the transient workflow store.

- **Code Cleanup:** Removed numerous obsolete activity files and updated dependencies across the workflow and related services to support the new architecture.
…gine

- Introduced a custom workflow DSL (`Activity`, `Sequence`, `ForEach`, `ParallelForEach`, `Inline`, `SlotGated`) and state management (`Variable`, `IExecutionContext`) in the Application layer.
- Added `IActivityFactory` to abstract workflow activity creation.
- Migrated all generation workflow activities to use the new Application-layer DSL, removing direct Elsa dependencies (e.g., `Input<T>`, `Output<T>`, `ActivityExecutionContext`).
- Refactored `GeneratingWorkflow` to construct the execution tree using the new `IActivityFactory` and strongly-typed context variables.
- Introduced `WorksheetContextRules` to standardize workflow variable keys for the generation process.
- Implemented Elsa-specific adapters (`ElsaSequence`, `ElsaInline`, `ElsaForEach`, `ElsaParallelForEach`, `ElsaSlotGated`, `ElsaVariable`, `ElsaExecutionContext`) and `ElsaActivityFactory` in the Infrastructure layer.
- Reorganized the project structure by moving generating and scanning components to the `Services` namespace.
- Updated `CLAUDE.md` to document the architectural constraint that Elsa dependencies must reside exclusively in the Infrastructure layer.
- Added extensive XML documentation to interfaces, classes, properties, and methods across the Domain, Application, and Infrastructure layers to improve maintainability.
- Relocated IClientService to the Cloud.Abstractions namespace for better logical organization.
- Enhanced resource management by adding GC.SuppressFinalize(this) to the Dispose methods of Mat, StreamTextFile, and YuNet.
- Improved thread safety in YuNet.DeInitAsync by ensuring the semaphore is released within a try-finally block.
- Removed the obsolete ImageReplacer class in favor of using PictureReplacer and BlipFillReplacer directly.
- Corrected an exception message typo in StreamTextFile.Write to accurately reflect read-only file states.
…ttening

- Introduced the `InstructionResolver` service to evaluate general instructions against row content and produce specialized instructions with actual values.
- Updated the `ISpecializable.Flatten` method signature to require row data, shifting value resolution logic to the execution phase.
- Modified `SpecializedInstruction` models for both images and texts to store resolved values (`Uri` and `string`) rather than column source definitions.
- Refactored the `SpecializeInstructions` activity to filter and store general instructions in the context, removing premature flattening.
- Adapted `ResolveImageUrls`, `DownloadImage`, `ResolveImagePathsFromDisk`, and `ReplaceSlideContents` activities to process the new instruction structure and per-row resolved values.
- Added a `Utilities.NormalizeUri` helper to standardize URI formatting during image resolution.
- Introduced a custom Workflow-as-Code DSL (e.g., `SequenceNode`, `ForEachNode`, `TryNode`, `SlotGatedNode`) in the Application layer.
- Implemented strongly-typed, stateless `Variable<T>` and `IActivityContext` for lexical scoping and isolated state management across concurrent branches.
- Integrated `WorkflowCore` in the Infrastructure layer to interpret and execute the custom DSL via a single `WcInterpreterStep`.
- Refactored `GeneratingWorkflow` and all related activities to implement `ILeafActivity<WorkflowTask>`, completely removing dependencies on Elsa.
- Added hierarchical state monitoring (`ExecutionState`, `WorkflowState`) and thread-safe, in-memory logging (`Logger`, `LogEntry`).
- Replaced `ScanningService` with modular `ScanWorkbook` and `ScanPresentation` leaf activities.
- Redesigned temporary output paths to use deterministic, hash-based folder structures for downloaded and edited images.
- Renamed replacer abstractions to composers (e.g., `ITextReplacer` to `ITextComposer`, `IImageReplacer` to `IImageComposer`).
- Extended the `IImage` interface and its `Mat` implementation with `Crop`, `Resize`, and `SaveAsync` capabilities.
- Updated `INSTRUCTIONS.MD` and `CLAUDE.md` to document the new DSL architecture, variable scoping rules, and strict dependency constraints.
- Relocated multiple root application directories (Cloud, Download, Images, Resources, Settings, Slides, Systems, Workflows) into a new `Modules` directory to improve project organization.
- Updated namespaces and using directives across the Application and Infrastructure layers to align with the new `Modules` path structure.
- Moved WorkflowCore-specific infrastructure implementations (e.g., `WcWorkflowService`, `WcInterpreterStep`, `WcExecutionContext`) into a dedicated `Adapters` subfolder within `SlideGenerator.Infrastructure/Workflows`.
- Deleted the obsolete `AssemblyInfo.cs` file from the Application project.
- Updated the Application `.csproj` file to replace the old `Cloud` folder inclusion with the new `Modules` directory.
…bust resource locking

- Replaced `ExecutionState` with `ExecutionSnapshot`, splitting transient context (`IExecutionContext`) from persisted variables (`IExecutionPayload`).
- Eliminated mutable state objects (e.g., `SheetTask`) in favor of strict, scope-based `Variable<T>` persistence for workflow execution.
- Refactored `Registry` implementations to support shared-read/exclusive-write locking using `FileLocker`.
- Introduced `GateLocker`, `GateNode`, and `GateType`, replacing `SlotGatedNode` and `SlotType` for granular concurrency throttling.
- Extracted custom DSL interpretation logic from `WcInterpreterStep` into a dedicated `WorkflowInterpreter` service.
- Updated generation and scanning activities to manage data, instructions, and open file leases exclusively through scope variables.
- Introduced `Lease<T>` and `ILock` abstractions for reliable, strongly-typed resource lifecycle management.
- Updated documentation (INSTRUCTIONS.MD, CLAUDE.md) to reflect new variable scope rules, registry patterns, and strict architectural constraints.
…e and sheet processing

- Replaced OpenXML and ClosedXML implementations with Syncfusion equivalents (e.g., SfPresentation, SfReadOnlyWorkbook, SfImageComposer, SfTextComposer).
- Moved sheet identifier models (e.g., WorkbookIdentifier, WorksheetIdentifier) to the Models.Identifiers namespace for better organization.
- Introduced GetPreview methods in IReadOnlyWorksheet, IReadOnlySlide, and IReadOnlyShape to generate lightweight visual and data previews.
- Simplified SlidePreview to only retain the index and image, promoting Id and Name metadata to SlideSummary.
- Updated ScanWorkbook and ScanPresentation activities to adopt the new preview rendering approaches.
- Removed the Scan method from IImageComposer as preview extraction is now handled directly by shapes.
- Cleaned up obsolete XML and ClosedXML adapters, services, and the GeneratingService.
- Added Syncfusion packages to the infrastructure project dependencies.
… wrapper

- Removed `WorkflowTask` class and updated generating activities and nodes to consume `GeneratingRequest` directly.
- Introduced `ScanningWorkflow` and `ScanningRequest` to encapsulate and separate scanning logic from generation.
- Created `ScanningVariables` to define specific variables for scanning, aliasing them within `VariablesDeclaration`.
- Updated `GeneratingWorkflow` to execute the new `ScanningWorkflow` during its initial parallel scanning phase.
- Modified `ScanWorkbook` and `ScanPresentation` activities to implement `ILeafActivity<object>`, improving reusability.
- Added `TryGetVariable` to `IActivityContext` and implemented it in `WcInterpreterContext` for safer variable resolution across nested scopes.
…ing activity DSL

- Removed the centralized `WorkflowInterpreter` and record-based DSL nodes (e.g., `SequenceNode`, `ForEachNode`, `TryNode`).
- Introduced an object-oriented `Activity<TData>` base class with self-executing implementations (`Sequence`, `Parallel`, `ForEach`, `Try`, `Condition`, `Inline`, `GateWrapper`).
- Renamed `Variable<T>` to `Handle<T>` to better represent workflow-scoped references.
- Renamed `IActivityContext` to `IExecutionContext`, renaming the `State` property to `Snapshot` and exposing an `IServiceProvider` for internal dependency resolution.
- Renamed `IWorkflowDefinition` to `IWorkflow` and updated implementations to construct activity trees using the new DSL.
- Refactored `GateLocker` to be a generic `GateLocker<TGate>` that accepts a resolver function for determining concurrency limits.
- Updated all leaf activities to implement `Activity<TData>` instead of `ILeafActivity<TData>`.
- Updated WorkflowCore infrastructure adapters (`WcWorkflowService`, `WcInterpreterStep`, `WcInterpreterContext`) to support the new interface types and execute the activity tree directly.
- Updated architectural documentation (`INSTRUCTIONS.MD`, `CLAUDE.md`) to reflect the new terminology and scope management guidelines.
thnhmai06 and others added 4 commits August 24, 2026 21:35
Turn RecipeEditorViewModel from a P4.1 skeleton into a real coordinator:
owns SlideCanvasViewModel/TextBindingsViewModel/WorksheetSourcesViewModel,
loads a mapping's presentation+workbook summaries via ISummaryCache, and
feeds all three panels the same flattened column list. MappingEditSession
gives the mapping navigator a stable identity per mapping (a record's own
identity changes on every edit) and owns the touched-placeholder/shape
sets so switching mappings and back no longer reverts a confirmed
Normalized binding to Suggested.

Fixes two data-loss gaps found while wiring the coordinator:
TextBindingsViewModel used to reset its touched set on every Load; and
SlideCanvasViewModel had no way to project edits back to ImageInstruction,
which would have silently dropped existing RoiOptions/FallbackImagePath.

RecipesViewModel now resolves a real RecipeEditorViewModel per edit/new
session and RecipeEditorView replaces the P3 placeholder overlay.
Mapping add/remove/reorder and the ROI/fallback inspector still need a
template-slide picker that doesn't exist yet - left for the next P4.4 pass.
…ping remove/reorder

Advisor review of the P4.4 coordinator (2914d60) found a real bug: when
a mapping's template slide no longer exists, LoadMappingAsync bails early
and the three panels keep showing the previous session's content while
SelectedSession already points at the new one - the next switch would
project stale content onto the wrong mapping. Fixed with a _loadedSession
guard that only lets ProjectCurrentSessionEdits run against the session
actually reflected in the panels.

Also drop the unconditional IsDirty = true in ProjectCurrentSessionEdits -
it fired on pure navigation (just looking at a different mapping), not
just on edits. Real dirty tracking belongs to P4.6; leaving it alone here
is more correct than a half-measure.

Add ToRecipe() (needed for P4.6's save path - nothing previously pulled
edits back into the Recipe field) and mapping remove/reorder commands,
which turned out not to need the template picker the previous commit's
deferral note claimed - only adding a new mapping does.
…P4.4)

ShapeOverlayViewModel.RoiOptions is now a live ObservableCollection
instead of a read-only snapshot, with EditInstruction recomputed from it
on read - move/remove commands live on the overlay itself since they need
no external dependency. FallbackImagePath becomes an [ObservableProperty]
so the inspector can edit it directly.

RecipeEditorViewModel keeps the IFilePicker it already received (only
handed to Sources before) and adds PickFallbackImageCommand, since a file
picker is the one thing these edits need that ShapeOverlayViewModel
shouldn't own itself.

Deliberately does not support creating a new RoiOption (Anchor vs
Interest configuration is its own UI surface) - task 32 only asked for
reordering the existing chain.
RecipeEditorViewModel.AddMappingCommand opens TemplatePickerView (presentation
+ slide picker with real thumbnails) via IDialogService.ShowTemplatePickerAsync,
appending a new Mapping and selecting it.

fix(document): configure font fallback on presentation open

SfSlide.GetPreview() threw NullReferenceException in Syncfusion's
FontSettings.GetFont during text layout for any real presentation, since
SfPresentationOpener never configured font substitution/fallback nor set
IPresentation.PresentationRenderer. Found via windows-mcp smoke test of the
new template picker against a real .pptx fixture (previous smoke tests only
used synthetic ViewModel data). Also affects the existing P4.2 canvas preview.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVAEGbVw6p9tfS271RDBGz

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Several reviewed changes introduce concrete correctness/maintenance risks (e.g., backoff overflow behavior, event handler subscription leak, invalid concurrency values, analyzer version split) that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 253/716 changed files
  • Comments generated: 7
  • Review effort level: Lite

Comment on lines +27 to +33
public static TimeSpan ComputeBackoffDelay(int attempt, TimeSpan maxDelay)
{
var jitterMs = Random.Shared.Next(0, 1001);
var backoff = TimeSpan.FromSeconds(System.Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(jitterMs);
return backoff < maxDelay ? backoff : maxDelay;
}
}
Comment on lines +17 to +19
/// <summary>Pure math helpers for retry/backoff calculations.</summary>
public static class Math
{
Comment on lines +22 to +34
public static class Registration
{
/// <summary>
/// Adds summarization services to the service collection.
/// </summary>
/// <param name="services">The service collection to add services to.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddSummarizationServices(this IServiceCollection services)
{
services.AddSingleton<ISummarizationService, SummarizationService>();
return services;
}
}
Comment on lines +20 to +32
/// <summary>View for <see cref="RunDialogViewModel" />. Closes itself when the ViewModel requests it.</summary>
public sealed partial class RunDialogView : Window
{
/// <summary>Constructs the view and loads its XAML.</summary>
public RunDialogView()
{
InitializeComponent();
DataContextChanged += (_, _) =>
{
if (DataContext is RunDialogViewModel vm) vm.RequestClose += started => Close(started);
};
}
}
Comment on lines +21 to +25
internal sealed class SettingConcurrencyProvider(ISettingProvider settingProvider) : IJobConcurrencyProvider
{
/// <inheritdoc />
public int MaxConcurrentJobs => (int)settingProvider.Current.Performance.MaxConcurrentJobs;
}
Comment thread .env.example
@@ -0,0 +1 @@
SYNCFUSION_LICENSE_KEY= # Syncfustion License Key No newline at end of file
Comment thread Directory.Build.props
Comment on lines +6 to +8
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all"/>
</ItemGroup>
thnhmai06 and others added 4 commits August 25, 2026 08:22
…alidation

Canvas: double-click an image-shape overlay opens a quick column-assign
dropdown (Flyout), completing the P4.4 remaining item deferred since P4.2.

Editor: RecipeEditorViewModel gains Id/Name, a SaveCommand (blocked while
clean, while a binding still needs Ambiguous resolution, or while Name is
blank) wired to IRecipeRepository Add/Update, and real dirty tracking via
explicit Changed events from the three child panels (not from Load, so
switching mappings never marks dirty). RecipesView's editor header gets an
editable Name textbox and a Lưu button; closing the editor while dirty now
confirms via the existing ConfirmDialog before discarding edits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVAEGbVw6p9tfS271RDBGz
…ting

RecipeEditorViewModel gains IsGuided/GuidedStep (Template/Data/Binding/
Review) driving which panels RecipeEditorView shows — one ViewModel, one
Recipe, the flag only swaps the displayed template per plan §5.2.a. New
recipes start Guided; recipes opened from the list start Advanced. All four
Guided steps reuse the existing SlideCanvasView/TextBindingsView/
WorksheetSourcesView panels, no new controls.

Also corrects a P4.6 regression found while reading the Guided-mode design
more carefully: the plan blocks "Lưu và chạy" on unresolved Ambiguous
bindings, not plain "Lưu" (which the previous commit incorrectly gated on
both). SaveCommand now only requires dirty + a name; the new
SaveAndRunCommand (Guided step ④) carries the Ambiguous-block and opens the
existing run dialog after saving.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVAEGbVw6p9tfS271RDBGz
…bout)

New SettingsViewModel/SettingsView: Theme + Language + reduced-motion
radios/checkbox, MaxConcurrentJobs slider with a scoped "restore default"
that resets only Performance, proxy/retry/download-size network fields, and
an About block (version/license/repo + a working "check for updates" button
wired to UpdateChecker, whose CheckForUpdatesAsync now returns a result
instead of only logging). Every field persists immediately through
ISettingManager, matching ThemeService's existing no-debounce convention.

Also fixes a pre-existing gap this page's Language switch surfaced: neither
UpdateChecker's TODO ("hook into a real notification once the main UI
exists") nor live language refresh had ever been exercised by a real UI
control before. Live language refresh itself turned out not to work in this
app's compiled-binding pipeline — reverted to the known-safe (restart-to-
apply) form and documented the gap in TrExtension rather than risk further
regressions chasing it; Theme's live refresh is unaffected (different,
working mechanism).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVAEGbVw6p9tfS271RDBGz
Indexer binding (Binding("[key]")) against LocalizationService's
INotifyPropertyChanged instance never re-fired in this app's compiled
XAML pipeline, even though every other named-property binding worked.
Add ILocalizationService.Revision (a plain int counter bumped on each
SetLanguage call) and bind TrExtension to that instead, routing the
actual resource lookup through a new LocalizedTextConverter parameterized
by the resource key. Confirmed live via windows-mcp: switching the
Settings language radio now updates the whole UI immediately.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Ctrl+S saves the open recipe, Ctrl+F focuses the recipe search box,
Delete removes the selected recipe/run (list-scoped via ListBox.KeyBindings
so it doesn't collide with per-row delete buttons), Escape closes
ConfirmDialog/RunDialogView/TemplatePickerView via Button.HotKey.
…ngs (P5 task 37/38 audit)

ReducedMotion persisted correctly but had no consumer — Tokens.axaml and
SplashView.axaml comments described an App.axaml.cs ApplyReducedMotion
that was never written. ThemeService.ApplyFromSettings now also zeroes
(or restores) the MotionUi/MotionBrand duration resources; Settings applies
it live on toggle, same pattern as Theme. Known remaining gap: ShellView's
CrossFade page transition sets Duration as a literal (not bindable) so it
still animates regardless of this setting.

Also fixes 4 em-dash violations found during the pre-flight audit
(plan §10 bans them in display strings): SettingsMaxConcurrentJobsHint
in both resx files, and two update-check status messages.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…design-system gate (P1)

Two pre-existing bugs blocked all further frontend work and had to be fixed
before any visual polish could be verified:

- ThemeService.ApplyReducedMotion read MotionUi/MotionBrand via the
  Resources indexer, which does not walk merged dictionaries (where
  Tokens.axaml actually lives) — returned null, and casting null to the
  non-nullable TimeSpan threw a NullReferenceException on every startup.
  Confirmed independent of any other change by reverting to a clean stash
  and reproducing identically. Fixed via Application.TryGetResource, which
  does walk merged dictionaries.

- ThemeService.SetThemeAsync awaited settingManager.Update(...) with
  ConfigureAwait(false); since SettingManager.Update() also uses
  ConfigureAwait(false) internally, execution resumed on a thread-pool
  thread, where ApplyFromSettings() then set Application.RequestedThemeVariant
  (an Avalonia styled property) off the UI thread — silently swallowed, since
  SettingsViewModel.OnThemeChanged calls SetThemeAsync fire-and-forget with no
  continuation to observe a fault. Runtime theme switching therefore never
  worked. Fixed by using ConfigureAwait(true), matching the UI-thread-affinity
  convention already documented in App.axaml.cs's StartupAsync. Verified live:
  toggling Light/Dark in Settings now re-themes the running app immediately.

Also, foundation work for the Avalonia frontend overhaul (blueprint P1):

- DesignSystemTests.cs: a machine-checkable design-system gate (contrast,
  raw-color/duration/radius, spacing grid, placeholder markers, divider
  discipline) that parses the token XAML directly, no Avalonia runtime
  needed. Found 5 real WCAG AA contrast failures on first run; fixed by
  darkening/lightening NeutralLight500/NeutralDark400 and switching dark
  mode's TextOnAccentBrush off white (3.24:1 on BrandDodger).
- Extended tokens: elevation (BoxShadows), RadiusFull, Thickness1-6,
  focus-ring shadow, accent hover/pressed brushes (unused pending a
  follow-up to brand Semi's native Primary/Danger button colors).
- Removed custom .primary/.danger/.ghost/.icon button-variant styles after
  confirming via Avalonia DevTools that Semi.Avalonia's own Button
  ControlTheme implements :pointerover/:pressed at StyleTrigger priority,
  which always outranks a plain Style-priority override — idle-state colors
  applied fine, hover/pressed silently fell back to Semi's own grey.
  RecipesView's New recipe/Import buttons now use Semi's native
  Classes="Primary"/"Tertiary" instead.
- MainWindow/ShellView page transitions moved from a literal XAML
  <CrossFade Duration="0:0:0.15"/> (ignored Appearance.ReducedMotion, and
  is a raw-duration violation of the new gate) to code-built CrossFade
  reading the same Motion* resource.

102/102 Desktop tests pass (95 existing + 7 new). No ViewModel touched.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

thnhmai06 and others added 5 commits August 26, 2026 20:40
…ination (P2a)

Application shell rebuild per the Avalonia frontend overhaul blueprint, phase
P2a (shell chrome). Splits the old 200px sidebar out in favor of a persistent
top toolbar: brand mark, a centered Recipes/Runs nav pill, theme-toggle/
About/Settings icon buttons, and self-drawn caption buttons — all in one 48px
row (TitleToolbar.axaml), replacing the system titlebar via
ExtendClientAreaToDecorationsHint + WindowDecorations="BorderOnly" (the one
configuration, of the three tried during the P0 spike, where maximize
actually fills the work area and Win11 Snap Layouts still triggers on
hovering the maximize button).

ShellDestination gains a fourth value, About, landing on the existing
(previously unreachable) PlaceholderPageViewModel default branch until P6
builds the real page. ShellViewModel gains ToggleThemeCommand (cycles
System -> Light -> Dark) so the toolbar's theme button doesn't need its own
DI wiring — it binds to the same ShellViewModel instance ShellView's page
host already uses, inherited via DataContext, no plumbing added.

TitleToolbar deliberately lives in ShellView, not MainWindow: the nav pill's
active-state binding needs ShellViewModel, which isn't associated with any
content while Splash is showing. Splash therefore has no toolbar at all
(accepted trade-off: no mouse-driven move/minimize/close during the brief,
non-interactive splash window; taskbar/Alt+F4 still work).

Found by hands-on testing, not covered by any prior research: every
interactive control placed inside a Border carrying
WindowDecorationProperties.ElementRole="TitleBar" silently stops receiving
clicks unless it also declares its own ElementRole. The three caption
buttons worked because MinimizeButton/MaximizeButton/CloseButton are
themselves recognized roles; the nav pill and the three icon buttons had
none and were swallowed by the ambient drag surface exactly like clicking
blank space on a real titlebar. Fixed by adding ElementRole="User" (documented
as "an interactive element set by user code that should receive input even
when overlapping chrome areas") to all five. Verified live: Recipes/Runs/
About now switch correctly.

Theme reveal animation, page-transition upgrade, window-title progress, and
BrandLockup extraction are deferred to P2b to keep this diff reviewable.

102/102 Desktop tests pass. No ViewModel other than ShellViewModel touched
(it owns 0 tests and is explicitly in scope for this phase per the plan).
… 4-stage BrandLockup (P2b)

Motion polish pass on top of P2a's shell chrome, per the frontend overhaul
blueprint:

- ThemeService.SetThemeAnimatedAsync(mode, origin): the toolbar's theme
  button now switches with a circle expanding outward from the clicked
  button instead of an instant repaint. Renders the old theme into a
  RenderTargetBitmap, overlays it via Avalonia's built-in OverlayLayer (no
  need to hand-roll an adorner Panel in MainWindow.axaml), yields one frame
  so the overlay is actually composited, swaps the theme underneath it, then
  animates a growing exclude-clip (CombinedGeometry + EllipseGeometry) via
  TopLevel.RequestAnimationFrame. Falls back to the instant SetThemeAsync
  under ReducedMotion, no main window, or a zero-size window.
  TitleToolbar's code-behind computes the origin (the button's own
  translated position) since that's a view-layer concern, not ShellViewModel
  state; ShellViewModel.ToggleThemeAsync(Point? origin) picks animated vs.
  instant based on whether one was supplied.

- BrandLockup extracted from SplashView into Components/, upgraded from one
  simultaneous 400ms beat to four real sequential stages (hold on the icon
  alone, animate, land on the full lockup, hold) matching the product
  brief's "logo -> animation -> full -> hold". Both holds collapse to zero
  under ReducedMotion. App.axaml.cs's splash-duration floor is now computed
  from BrandLockup.GetTotalDuration(motionBrand) instead of a fixed 420ms,
  so the shell can no longer swap in before the (now ~900ms) animation
  finishes.

- ThemeService.BuildPageTransition composes CrossFade + PageSlide via
  CompositePageTransition, replacing P1's plain CrossFade for both
  MainWindow and ShellView's page hosts.

- MainWindowViewModel.WindowTitle shows how many jobs are active
  ("SlideGenerator - N job running"), tracking IProgressHub.Jobs. Simplified
  from the v1-style "5/727 slide" the product brief references: JobSnapshot
  has no row-total field yet (that's P5 scope), so this counts active jobs
  rather than fabricating a percentage from data that doesn't exist.

Verified live, not just by code review: built and ran the app, used Win32
SetForegroundWindow/MoveWindow (via a PowerShell Add-Type shim) to target
the exact window handle — this session's own Rider IDE window also titles
itself "SlideGenerator", which was causing window-switching tools to land on
the wrong window — then clicked the theme toggle twice in a row. Both
switches completed with the correct end-state theme, no exception in the
log, and no leftover overlay artifact.

102/102 Desktop tests pass, full solution builds clean. No ViewModel other
than ShellViewModel/MainWindowViewModel touched (both explicitly in scope
for this phase; neither carries a test fence).
…ning hardcode (P5 P3)

Renames all 70 resx keys from PascalCase to dot-key (nav.recipes,
settings.theme.dark, ...), adds the 16 missing vi.resx Recipes* keys, and
extracts remaining hardcoded strings in Shell/Recipes/Runs/Settings (confirm
dialogs, file-picker titles, update-status messages) behind the same keys.

Adds EnumLocalizedTextConverter (MultiBinding + Revision, mirrors TrExtension's
live-refresh trick) and wires it onto the JobStatus/JobPhase bindings in
RunsView/RecipesView so those no longer show raw English enum names under vi.

Fixes two bugs found while migrating: LocalizationService.Instance threw NRE
in ViewModel unit tests that construct the VM outside DI (now lazily
self-initializes instead of staying null), and DesignSystemTests' line-pinned
KnownViolations entry for RecipesView.axaml had drifted after an insertion
above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178xLpgqmda2BotNXUetREm
…adge (P4a)

Recipe selection now fetches the full RecipeEntry (mirrors the existing
recent-runs fetch) to compute mapping/source/text/image counts, plus a
records estimate summed from each worksheet source's ISummaryCache row
count — failures fall back to an em dash rather than blocking the view.

Renders as five reused pill chips with a skeleton placeholder while
counting, and adds a Saved/Unsaved pill next to the editor's Save button
using two loc:Tr-bound TextBlocks toggled by IsDirty (no new converter).

Also swaps the recent-runs loading "..." for the existing skeleton style,
shrinking DesignSystemTests' KnownViolations by one tracked entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178xLpgqmda2BotNXUetREm
…P4b)

Gives the two dead suggested/needs-selection classes on the column
ComboBoxes a real style (amber/red border) instead of no matching selector
at all, adds :pointerover feedback to canvas shape overlays, and wraps the
column-label chip in a proper padded/radius pill instead of a bare
margined TextBlock.

Replaces the Guided mode's numbered title ("① Mẫu slide") with a real
4-dot stepper plus a plain localized title, and swaps the ROI list's raw
Anchor/Interest mode text for a new RoiDescriptionConverter that explains
each option's concrete AnchorType/InterestType in plain language.

Extracts the remaining hardcoded Vietnamese across RecipeEditorView,
TextBindingsView, WorksheetSourcesView, BindingSummaryConverter, and
GuidedStepTitle behind new recipeEditor.*/enums.roiMode.*/enums.anchorType.*/
enums.interestType.* keys (40 new entries, both locales).

Swaps the editor's "..." loading overlay for Semi's ProgressBar
IsIndeterminate, clearing DesignSystemTests' KnownViolations to empty —
every entry tracked since P1 is now fixed rather than re-pinned.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178xLpgqmda2BotNXUetREm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

thnhmai06 and others added 6 commits August 27, 2026 11:43
…ger variant (P4c)

TemplatePicker's slide grid becomes a real ListBox bound to SelectedSlide
instead of a plain ItemsControl with a manual pointer-pressed handler —
gets a free :selected pseudo-class, a check badge, and an accent border on
the picked thumbnail with no new binding machinery. Same style selectors
also sharpen the existing selected-row look on Recipes/Runs' own lists,
which reuse the same hairline-row class.

Fixes RunDialog's "Sẽ tạo3file" (no spaces around the count) by folding
the spacing into the resx values instead of bare XAML literals, and splits
its single generic conflict badge into the two real ConflictKind values
via EnumLocalizedTextConverter.

Adds an opt-in danger variant to ConfirmDialog (IDialogService.ConfirmAsync
gains a defaulted `danger` parameter — no existing call site needs
changes): a warning icon plus a red confirm button, wired to the two
actual delete confirmations (recipe, request) but left off the
leave-unsaved-editor prompt, which isn't destructive in the same sense.

Extracts remaining hardcoded strings across both dialogs and their
ViewModels behind new recipeEditor.templatePicker.*/runDialog.*/
enums.conflictKind.* resx keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178xLpgqmda2BotNXUetREm
…ns live UI (P5)

JobSnapshot/JobSummary gain an optional TotalRows (known from the moment
SlideGenerationWorkload starts, threaded through every phase-transition and
per-row report). Persisting it needs a new migration —
002_add-total-rows-to-jobs.sql (ALTER TABLE) — since 001_2.0.0.sql already
ran on any existing Data.db and CREATE TABLE IF NOT EXISTS alone would
silently no-op there; verified by relaunching against this session's own
pre-existing Data.db, not just a fresh test database.

Runs' ProgressBar becomes determinate (Value=CurrentIndex/Maximum=TotalRows)
with a fallback to indeterminate when TotalRows is null, correct for every
job status rather than only "indeterminate while Running". Wires up
IProgressHub.Rows (previously collected but never consumed) into a live
per-job activity line (stage + note), following the same
CollectionChanged-subscribe pattern already used for Jobs.

Also gives the log pane a header (using the previously-dead runs.logsHeader
key), a copy-to-clipboard button, level-based coloring (new
LogLevelBrushConverter), and scroll-to-end on new lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178xLpgqmda2BotNXUetREm
…s (P6)

Adds Features/About: an AboutViewModel/View showing the BrandLockup replay,
bilingual tagline, an update-check card (moved from Settings' old closing
"Giới thiệu" block), live Developers (GitHub REST contributors API) and
Supporters (sponsors.json published by a new CI workflow) lists, and
repository/sponsor links. Both network calls are disk-cached (24h) and fail
soft to an empty list — a network problem never surfaces as an error on
this page.

Developers/Supporters show only real, fetched data — no fabricated
fallback roster, since no official contributor role map exists yet
(plan §8-Q2). Avatars are an initials-circle placeholder rather than
downloaded images, a deliberate scope cut noted in code.

Settings: the theme picker becomes three card-style ToggleButtons (icon +
label, reusing the .chip technique already proven for Runs' filter row
rather than restyling RadioButton's own template untested), and the four
Retry/Network TextBoxes become NumericUpDown. The old "Giới thiệu" block
is gone, superseded by the About page.

Adds .github/workflows/sponsors.yml: reads FUNDING.yml's github: profiles,
queries GraphQL sponsorshipsAsMaintainer, and publishes sponsors.json to a
data branch the app reads from — degrades to an empty file until the
SPONSORS_PAT secret is added (plan §8-Q5). Validated with actionlint per
the authoring-github-workflows skill (exit 0, no regressions in existing
workflows).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178xLpgqmda2BotNXUetREm
…aceholderPageView (P7 part 1)

- Add 4 HeadlessUnitTestSession-driven smoke tests (Recipes/Runs/Settings/About
  views construct + resolve resources without throwing). Bypasses
  Avalonia.Headless.XUnit's [AvaloniaFact] entirely: its AvaloniaFactDiscoverer
  reflects into a Xunit.v3.TestIntrospectionHelper overload that no longer
  matches this repo's xunit.v3 4.0.0, throwing MissingMethodException at test
  discovery. HeadlessUnitTestSession itself has no xunit coupling, so driving
  it from a plain [Fact] sidesteps the incompatible discoverer while keeping
  the real App resource pipeline.
- Swap PackageReference from Avalonia.Headless.XUnit to plain Avalonia.Headless
  (only the runtime package is needed now).
- Remove PlaceholderPageViewModel/PlaceholderPageView: all 4 ShellDestination
  values now resolve to a real page, so the fallback branch was dead. Replace
  it with a throw (UnreachableException) so a future missing case fails loudly
  instead of silently showing a placeholder. Drop the now-unused
  shell.placeholder.message resx key and the matching DesignSystemTests
  allowlist entry.
- Fix CLAUDE.md drift found while investigating: actual migration scripts are
  001_2.0.0.sql + 002_add-total-rows-to-jobs.sql (not the old
  0001/0002/0003-named scripts); DatabaseMigrator.Migrate is now called from
  SlideGenerator.Desktop/Program.cs (SlideGenerator.Stdio no longer exists);
  NameAndPaths.cs lives under Settings/Immutable/, not Settings/Rules/; test
  package versions (xunit.v3, xunit.runner.visualstudio, NSubstitute) were
  documented as older majors than what every test project actually pins.

Full solution: 536/537 tests green (1 pre-existing Syncfusion-license skip),
Desktop test project 106/106 (102 existing + 4 new).

Known gap (flagged, not fixed here): CLAUDE.md's "IPC Layer
(SlideGenerator.Stdio)" section and related tables still describe the
removed JSON-RPC sidecar architecture wholesale — a much larger rewrite than
this pass's scope, needs explicit go-ahead before touching.
…, reduced motion (P7 part 2)

- RecipesViewModel: stats aggregation across mappings/sources (record count
  sums per-worksheet counts from ISummaryCache) and the failure fallback
  (a summary-cache error nulls RecordCount but leaves the other counts intact).
- JobRunViewModel: TotalRows round-trips through both entry points (JobSummary
  constructor, ApplyLiveUpdate from JobSnapshot); ApplyLiveRowUpdate overwrites
  rather than accumulates.
- AboutViewModel.LoadAsync against a mocked IAboutDataService: populates both
  lists, empty results don't throw, second call doesn't re-fetch.
- ThemeService.ApplyFromSettings reduced-motion branch: toggling zeroes and
  restores the three Motion* tokens.

Add HeadlessTestSession as the one shared HeadlessUnitTestSession for the
whole test assembly. Each test class independently calling
HeadlessUnitTestSession.StartNew raced to initialize the process-wide
Application.Current when xunit ran the classes in parallel, causing
intermittent failures in ViewConstructionTests/ThemeServiceTests only when
run together (not alone) — confirmed by running the full suite 4x after the
fix with no flakes.

Full solution: 548 tests, 547 green, 1 pre-existing Syncfusion-license skip.
…vatar contrast (P7 part 3)

Extended DesignSystemTests.ContrastPairs to cover brush pairs added since P1
that weren't yet gated: TextOnAccentBrush on Success/Warning/DangerBrush
(Border.pill.success/.warning/.danger), and AccentBrush on AccentMutedBrush
(Border.pill.info + Button.nav-item.active + Border.avatar-circle).

The new AccentBrush/AccentMutedBrush pair failed immediately: dark theme was
2.57:1, well under WCAG AA. This pair is live in two places already shipped:
the toolbar's active nav-pill tab text, and the initials text on About
page's developer/supporter avatar circles — both currently render under the
readability threshold in dark mode.

Fix: darken dark-theme AccentMutedBrush from #2A4E85 to #152742 (halved RGB,
same hue/ratio, just darker) — 4.63:1, clears AA. Light theme's value was
already fine, left unchanged. Only 5 usages of this brush exist app-wide,
all "muted background + accent text/border", so darkening only helps
contrast in every one of them.

Full solution: 548 tests, 547 green, 1 pre-existing skip.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

thnhmai06 and others added 2 commits September 1, 2026 11:17
Complete domain reference derived from the V2 codebase (not CLAUDE.md, which
still describes the removed IPC sidecar / Tauri frontend). 21 sections: core
domain, every concept and entity, relationships, user workflows, state machines,
use cases, Recipe/Template/Runs deep-dives, settings, file-system model,
persistence, events, validation/errors, architecture, glossary, V1-vs-V2
differences, domain diagram, and design constraints. Intended for an external
UI/UX design agent.

Also records code/doc contradictions found while writing: the inert
AllowLocalPaths request flag, the missing request-level Error aggregate status,
and four declared-but-never-emitted enum values (JobPhase.Queued,
RowStatus.Waiting, RowStatus.Error, RowStage.CroppingImage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qa7AsUQCAXew9twMU6A5h
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.

5 participants