diff --git a/.github/workflows/docker-gallery.yml b/.github/workflows/docker-gallery.yml index 84cc5b0b..5c15edc6 100644 --- a/.github/workflows/docker-gallery.yml +++ b/.github/workflows/docker-gallery.yml @@ -24,7 +24,7 @@ jobs: image: name: Build and push the Gallery image runs-on: ubuntu-latest - # The publish compiles the .NET code ahead of time to WebAssembly (about 12 minutes on an M5, more on a runner). + # The publish compiles the .NET code ahead of time to WebAssembly (about 11 minutes on an M5, more on a runner). timeout-minutes: 90 permissions: contents: read diff --git a/Directory.Packages.props b/Directory.Packages.props index 3bf5f409..50f01de6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,10 +19,10 @@ - - + + - + diff --git a/Dockerfile b/Dockerfile index 7bc1641e..07ee9628 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Gallery WebAssembly host: publish in a container, pre-compress the output, serve it with nginx (brotli + gzip). # Built by .github/workflows/docker-gallery.yml, not locally. The project is not in TKCode.Arc.slnx -# because WasmBuildNative needs the wasm-tools-net10 workload. +# because WasmBuildNative needs the wasm-tools workload. ARG BUILDER_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0 ARG RUNTIME_BASE=alpine:3.21 @@ -24,9 +24,11 @@ ENV DOTNET_ROOT=/opt/dotnet \ PATH=/opt/dotnet:$PATH \ DOTNET_NOLOGO=1 \ DOTNET_CLI_TELEMETRY_OPTOUT=1 -RUN dotnet --info && dotnet workload install wasm-tools-net10 +# The host targets net11.0-browser: the wasm-tools workload (wasm-tools-net10 is for net10.0-browser). +RUN dotnet --info && dotnet workload install wasm-tools COPY . . -# Trimmed (compiling ahead of time to WebAssembly is off: -p:ArcAot=true, see the Browser project file). +# Trimmed and compiled ahead of time to WebAssembly, except the assemblies the Browser project file keeps interpreted (-p:ArcAot=false +# publishes interpreted, much faster). RUN dotnet publish gallery/TKCode.Arc.Gallery.Browser/TKCode.Arc.Gallery.Browser.csproj \ -c Release -o /app \ ${STREAMMARK_VERSION:+-p:StreamMarkVersion=${STREAMMARK_VERSION}} diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 16768d66..43c1c6a4 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -147,7 +147,9 @@ and paints the result in its own palette. TextMateSharp runs its regular expressions on the native Oniguruma library through Onigwrap (native libraries per runtime identifier). Onigwrap: MIT License, Copyright (c) 2024 Aikawa Yataro, derived from fluentCODE/onigwrap -(MIT License, Copyright (c) 2015 Fluent Solutions). Oniguruma: +(MIT License, Copyright (c) 2015 Fluent Solutions). The Gallery's WebAssembly host links +`gallery/TKCode.Arc.Gallery.Browser/native/onigwrap//st/libonigwrap.a`, compiled from Onigwrap 1.0.11 and +Oniguruma 6.9.10 by `tools/onigwrap/build-wasm.sh` (the package has no build for the Emscripten of .NET 11). Oniguruma: ``` Copyright (c) 2002-2021 K.Kosako diff --git a/gallery/TKCode.Arc.Gallery.Browser/TKCode.Arc.Gallery.Browser.csproj b/gallery/TKCode.Arc.Gallery.Browser/TKCode.Arc.Gallery.Browser.csproj index e6f045e7..08195493 100644 --- a/gallery/TKCode.Arc.Gallery.Browser/TKCode.Arc.Gallery.Browser.csproj +++ b/gallery/TKCode.Arc.Gallery.Browser/TKCode.Arc.Gallery.Browser.csproj @@ -1,26 +1,27 @@ - net10.0-browser + net11.0-browser TKCode.Arc.Gallery.Browser false $(NoWarn);CS0436 true - + true + + $(MSBuildThisFileDirectory)native/onigwrap true - - - true + + true $(_IsPublishing) @@ -35,10 +36,31 @@ + + + + + + + + + + <_AOT_InternalForceInterpretAssemblies Include="TKCode.Arc.Blocks.dll;TKCode.Arc.Special.dll;TKCode.Arc.Gallery.dll" /> + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/FromYourAgent.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/FromYourAgent.axaml.cs new file mode 100644 index 00000000..aeb0faaa --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/FromYourAgent.axaml.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Edits fetched from a (simulated) agent through Run: Stop cancels the request, a failure offers Retry. +public partial class AgentCursorFromYourAgentDemo : UserControl +{ + public AgentCursorFromYourAgentDemo() + { + InitializeComponent(); + Card.Blocks = + [ + new AgentCursorBlock("title", AgentCursorBlockKind.Title, "Support macro: refunds"), + new AgentCursorBlock("intro", AgentCursorBlockKind.Paragraph, + "Thank you so much for reaching out to us about your refund, we really do appreciate you taking the time to write to our team today."), + new AgentCursorBlock("steps", AgentCursorBlockKind.Paragraph, "Refunds go back to the original payment method within five buisness days."), + ]; + Card.Agent = AgentCursorDemoData.Agent; + Card.Collaborator = AgentCursorDemoData.Collaborator; + Card.Run = FetchEditsAsync; + } + + /// Stands in for a call to your agent: waits a moment, then returns its edits (or fails when asked to). + private async Task> FetchEditsAsync(CancellationToken cancellation) + { + var fail = FailNext.IsChecked == true; + await Task.Delay(1100, cancellation); + if (fail) + { + FailNext.IsChecked = false; + throw new InvalidOperationException("The agent did not answer in time."); + } + return + [ + AgentCursorEdit.Rewrite("intro", "Thanks for writing in about your refund.", "Shortening the greeting"), + AgentCursorEdit.Fix("steps", "buisness", "business"), + ]; + } + + private void OnPhaseChanged(object? sender, AgentCursorPhaseChangedEventArgs e) => Log.Text = $"Phase: {e.Phase}"; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/Narrow.axaml new file mode 100644 index 00000000..10cee7ff --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/Narrow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/Preview.axaml new file mode 100644 index 00000000..0964b8cb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/Preview.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/YourDocument.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/YourDocument.axaml new file mode 100644 index 00000000..3d36e31a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/YourDocument.axaml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/YourDocument.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/YourDocument.axaml.cs new file mode 100644 index 00000000..fa9163c4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentCursor/YourDocument.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app's own document, edits, agent and teammate; the accepted document is stored and passed back. +public partial class AgentCursorYourDocumentDemo : UserControl +{ + public AgentCursorYourDocumentDemo() + { + InitializeComponent(); + Card.Blocks = + [ + new AgentCursorBlock("title", AgentCursorBlockKind.Title, "Weekly update"), + new AgentCursorBlock("shipped", AgentCursorBlockKind.Paragraph, + "This week the team shipped the new onboarding flow and also fixed two crashes that happened on start for some people."), + new AgentCursorBlock("next", AgentCursorBlockKind.Paragraph, "Next week we plan to recieve feedback from five customers."), + ]; + Card.Edits = + [ + AgentCursorEdit.Rewrite("shipped", "We shipped the new onboarding flow and fixed two crashes on start.", "Making it shorter"), + AgentCursorEdit.Insert("shipped", AgentCursorBlockKind.Heading, "Next week", "next-heading"), + AgentCursorEdit.Fix("next", "recieve", "collect", "Choosing a better word"), + ]; + // An agent of your own: its name, a 24 × 24 stroked mark and the color of its cursor, selection and tint. + Card.Agent = new AgentModel(AgentCursorDemoData.YourAgentName, ArcIcons.Brain, new ImmutableSolidColorBrush(Color.FromRgb(0x3B, 0x82, 0xD6))); + Card.Collaborator = new AgentCursorCollaborator("Leo Park"); + } + + private void OnAccepted(object? sender, AgentCursorDocumentEventArgs e) + { + // Store the document and pass it back: an equal document does not reset the card. + Card.Blocks = e.Blocks.ToList(); + Log.Text = $"Accepted: {e.Blocks.Count} blocks saved."; + } + + private void OnUndone(object? sender, AgentCursorDocumentEventArgs e) => Log.Text = $"Undone: back to {e.Blocks.Count} blocks."; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/AgentHandoffDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/AgentHandoffDemoData.cs new file mode 100644 index 00000000..1af6c693 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/AgentHandoffDemoData.cs @@ -0,0 +1,44 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented support chat the agent handoff demos play: a store, an order and a person, all made up. The assistant's +/// model is a real one from . +/// +internal static class AgentHandoffDemoData +{ + public const string Title = "Order NW-48213"; + + public const string Subtitle = "Northwind Home support"; + + public const string ReplyEstimate = "2 min"; + + /// The model behind the demo's assistant (Google's Gemini 3.8 Flash; checked on ). + public static readonly string AssistantModel = AiModelCatalog.Gemini38Flash.Name; + + /// The chat the simulation plays. + public static readonly AgentHandoffScript Script = new() + { + CustomerOpening = "Hi, my standing desk arrived today and one of the legs is cracked.", + AssistantAnswer = + "I'm sorry about that. I found order NW-48213, the Oslo standing desk delivered this morning. " + + "I can ship a replacement leg, which usually arrives in two days.", + CustomerRequest = "I'd rather have a full refund. Can I talk to a person?", + AssistantHandoff = "Of course. I'll pass this to our support team with a summary, so you won't have to explain it again.", + Summary = new AgentHandoffSummary( + "Standing desk arrived with a cracked leg. The customer wants a full refund instead of a replacement part.", + new AgentHandoffField("Order", "NW-48213"), + new AgentHandoffField("Item", "Oslo standing desk"), + new AgentHandoffField("Asks for", "Full refund")), + CustomerNote = "I have photos of the damage if they help.", + Agent = new AgentHandoffAgent("Jordan Reyes", "Support specialist", "14:05"), + AgentGreeting = "Hi, I'm Jordan. I've read the summary, so no need to repeat anything. Sorry the desk arrived damaged.", + AgentResolution = + "I've issued a full refund of $649 to your card. You'll see it in 3 to 5 business days, and we'll collect the desk for free.", + ResolutionNotice = "Refund issued.", + CustomerThanks = "That was quick, thank you!", + AgentClosing = "Happy to help. I'll mark this as resolved; reply here any time if something comes up.", + AssistantCancelled = "No problem, I've cancelled the transfer. I'm still here if you need anything.", + AssistantReply = "Thanks for the details. If you'd like, I can bring in a person from our team.", + AgentReply = "Thanks, I've added that to your case.", + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Narrow.axaml new file mode 100644 index 00000000..a12dad9e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Preview.axaml new file mode 100644 index 00000000..9a3e74be --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Preview.axaml.cs new file mode 100644 index 00000000..fbd6edbb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/Preview.axaml.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated chat, handed off or with nobody available, and the empty card. +public partial class AgentHandoffPreviewDemo : UserControl +{ + private readonly AgentHandoffSimulation _simulation; + private readonly bool _ready; + + public AgentHandoffPreviewDemo() + { + InitializeComponent(); + _simulation = Chat.Simulation!; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (!_ready) + { + return; + } + var picked = Scenario.SelectedValue as string; + // A new scenario starts the story over; Empty takes the simulation away. + _simulation.Scenario = picked == "unavailable" ? AgentHandoffScenario.Unavailable : AgentHandoffScenario.Handoff; + Chat.Simulation = picked == "empty" ? null : _simulation; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/YourBackend.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/YourBackend.axaml new file mode 100644 index 00000000..4d43b10a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/YourBackend.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/YourBackend.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/YourBackend.axaml.cs new file mode 100644 index 00000000..dac37070 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentHandoff/YourBackend.axaml.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The card driven by the app: stages, messages, summary and person set from code, events answered. +public partial class AgentHandoffYourBackendDemo : UserControl +{ + private readonly bool _ready; + + public AgentHandoffYourBackendDemo() + { + InitializeComponent(); + Chat.Summary = new AgentHandoffSummary( + "Desk arrived with a cracked leg. The customer wants a full refund.", + new AgentHandoffField("Order", "NW-48213"), + new AgentHandoffField("Asks for", "Full refund")); + Chat.Agent = new AgentHandoffAgent("Jordan Reyes", "Support specialist", "14:05"); + _ready = true; + Show(AgentHandoffStage.Assistant); + } + + private void OnStageChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready && Enum.TryParse(StagePicker.SelectedValue as string, out var stage)) + { + Show(stage); + } + } + + /// Rebuilds the conversation as it would be at . + private void Show(AgentHandoffStage stage) + { + Chat.Typing = AgentHandoffTyping.None; + Chat.Messages.Clear(); + Chat.AgentMessages.Clear(); + Chat.Messages.Add(new AgentHandoffMessage(AgentHandoffSender.Customer, "My desk arrived with a cracked leg.", "14:02")); + Chat.Messages.Add(new AgentHandoffMessage(AgentHandoffSender.Assistant, + "Sorry about that. I can ship a replacement leg, which usually arrives in two days.", "14:02")); + if (stage != AgentHandoffStage.Assistant) + { + Chat.Messages.Add(new AgentHandoffMessage(AgentHandoffSender.Customer, "Talk to a person", "14:03")); + Chat.AgentMessages.Add(new AgentHandoffMessage(AgentHandoffSender.Customer, "I'd like a full refund, please.", "14:03")); + } + if (stage is AgentHandoffStage.Connected or AgentHandoffStage.Resolved) + { + Chat.AgentMessages.Add(new AgentHandoffMessage(AgentHandoffSender.Agent, + "Hi, I'm Jordan. I've read the summary and issued a full refund of $649.", "14:05")); + } + if (stage == AgentHandoffStage.Connected) + { + Chat.Typing = AgentHandoffTyping.Agent; + } + Chat.Stage = stage; + Chat.Notice = stage == AgentHandoffStage.Resolved ? "Refund issued." : null; + } + + private void Pick(AgentHandoffStage stage) => StagePicker.SelectedValue = stage.ToString(); + + private void OnHumanRequested(object? sender, RoutedEventArgs e) => Pick(AgentHandoffStage.Transferring); + + private void OnCancel(object? sender, RoutedEventArgs e) => Pick(AgentHandoffStage.Assistant); + + private void OnRetry(object? sender, RoutedEventArgs e) => Pick(AgentHandoffStage.Connected); + + private void OnSend(object? sender, AgentHandoffSendEventArgs e) + { + var message = new AgentHandoffMessage(AgentHandoffSender.Customer, e.Text); + if (Chat.Stage == AgentHandoffStage.Assistant) + { + Chat.Messages.Add(message); + } + else + { + Chat.AgentMessages.Add(message); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/AgentPlanDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/AgentPlanDemoData.cs new file mode 100644 index 00000000..987e5ff2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/AgentPlanDemoData.cs @@ -0,0 +1,69 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The plan the agent plan demos simulate: a coding agent adding full-text search to a help center, written by a real +/// model from . Six drafted steps, three suggestions for Keep planning, a step the agent inserts +/// while running, a test step that fails once, and a summary. The task, files and numbers are invented. Demo content only; +/// the mark is a Lucide sparkle, not the vendor's logo. +/// +internal static class AgentPlanDemoData +{ + /// The goal. + public const string Goal = "Add full-text search to the help center"; + + /// The summary shown after "Done in …". + public const string Summary = "Search now covers all 31 article fields, with a disk fallback and 14 passing tests."; + + private static readonly IBrush s_markBrush = new ImmutableSolidColorBrush(Color.FromRgb(0xE0, 0x7A, 0x45)); + + /// The model that writes the preview's plan: GPT-6.1 Sol (catalog name) with a sparkle mark in a warm color. + public static AgentModel Model { get; } = new(AiModelCatalog.Gpt61Sol.Name, ArcIcons.Sparkles, s_markBrush); + + /// The model of the smaller plans in the other demos: Mistral Medium 3.5 (catalog name). + public static AgentModel SmallPlansModel { get; } = new(AiModelCatalog.MistralMedium35.Name, ArcIcons.Sparkles); + + /// The six drafted steps with their scripts, in order. + public static IReadOnlyList Steps { get; } = + [ + new("inventory", "Inventory the article fields the help center stores", + new(["Read 5 files", "Searched for articleSchema and draftSchema", "Found 31 article fields"], TimeSpan.FromSeconds(23))), + new("index", "Build a search index from every published article", + new(["Read jobs/publish.ts", "Created search/build-index.ts", "Edited 3 files"], TimeSpan.FromSeconds(46))), + new("cache", "Cache the index in object storage for ten minutes", + new(["Read lib/storage.ts", "Edited search/build-index.ts", "Ran 7 tests"], TimeSpan.FromSeconds(35)) + { + Inserts = new("fallback", "Rebuild the index from disk when the cache is empty", + new(["Edited lib/storage.ts", "Ran 5 tests"], TimeSpan.FromSeconds(31))), + InsertAfterActivity = 1 + }), + new("rank", "Rank results by title match, then by freshness", + new(["Edited search/rank.ts", "Ran 9 tests"], TimeSpan.FromSeconds(26))), + new("tests", "Write tests for typos, stop words and empty queries", + new(["Created search.test.ts", "Ran 14 tests"], TimeSpan.FromSeconds(67)) + { + FailureReason = "3 of 14 tests failed: typo matching skips two-letter words", + RetryActivities = ["Edited search/rank.ts", "Ran 14 tests, all passed"], + RetryDuration = TimeSpan.FromSeconds(67) + }), + new("docs", "Describe the search syntax in the help center guide", + new(["Read guide/overview.md", "Edited guide/search.md"], TimeSpan.FromSeconds(18))) + ]; + + /// The three steps Keep planning suggests, in order; each goes in before the last step. + public static IReadOnlyList Suggestions { get; } = + [ + new("highlight", "Highlight matched words in the result snippets", + new(["Read search/snippet.ts", "Edited search/snippet.ts", "Ran 4 tests"], TimeSpan.FromSeconds(33))), + new("zero-results", "Log searches that return no results", + new(["Read lib/telemetry.ts", "Edited search/handler.ts"], TimeSpan.FromSeconds(21))), + new("benchmark", "Benchmark search at 500 queries per second", + new(["Ran a load script against staging for 60 seconds", "Measured 38 ms p99 at 500 queries per second"], TimeSpan.FromSeconds(69))) + ]; + + /// How the simulation runs a step it has no script for (one you added or renamed into a new id). + public static AgentPlanStepScript GenericScript { get; } = + new(["Read 2 files", "Edited 3 files", "Ran 5 tests"], TimeSpan.FromSeconds(30)); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Controlled.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Controlled.axaml.cs index 1c3efc36..5ef409e4 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Controlled.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Controlled.axaml.cs @@ -45,7 +45,7 @@ private void Reset() { Plan.Steps.Add(new AgentPlanStep(null, title)); } - Plan.Model = new AgentModel("Ember 1", ArcIcons.Sparkles); + Plan.Model = AgentPlanDemoData.SmallPlansModel; Plan.CanKeepPlanning = true; Plan.IsPlanning = false; } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Labels.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Labels.axaml.cs index c1cc654d..29c0fe73 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Labels.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Labels.axaml.cs @@ -19,7 +19,7 @@ public AgentPlanLabelsDemo() Suggested = "Idea", DoneIn = total => $"Finished in {total}." }; - Plan.Model = new AgentModel("Ember 1", ArcIcons.Sparkles); + Plan.Model = AgentPlanDemoData.SmallPlansModel; Plan.Simulation = new AgentPlanSimulation { Steps = diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/MidRun.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/MidRun.axaml.cs index 94f65f5c..10b5b00a 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/MidRun.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/MidRun.axaml.cs @@ -8,7 +8,7 @@ public partial class AgentPlanMidRunDemo : UserControl public AgentPlanMidRunDemo() { InitializeComponent(); - Plan.Model = new AgentModel("Ember 1", ArcIcons.Sparkles); + Plan.Model = AgentPlanDemoData.SmallPlansModel; Plan.Steps.Add(Finished("Find the slowest ten tasks", 18, "Ran the build with timing", "Found 4 tasks over a minute")); Plan.Steps.Add(Finished("Cache the generated sources", 41, "Edited build/cache.json", "Ran 12 tests")); var added = Finished("Skip the cache when the lockfile changes", 25, "Edited build/cache.json"); diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Narrow.axaml index a65d7030..f8351aef 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Narrow.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Narrow.axaml @@ -1,7 +1,17 @@ - + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Preview.axaml index d34520a9..4d790f9f 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Preview.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentPlan/Preview.axaml @@ -1,7 +1,18 @@ - - + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Actions.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Actions.axaml index 4556b81f..45a038e4 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Actions.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Actions.axaml @@ -1,17 +1,18 @@ + Agent="{x:Static demos:AgentStatusDemoData.CodingAgent}" + Activity="{x:Static demos:AgentStatusDemoData.ApprovalRequest}" /> + Agent="{x:Static demos:AgentStatusDemoData.ResearchAgent}" + Activity="{x:Static demos:AgentStatusDemoData.Reading}" /> diff --git a/src/TKCode.Arc.Special/Ai/AgentStatus/AgentStatusSamples.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/AgentStatusDemoData.cs similarity index 54% rename from src/TKCode.Arc.Special/Ai/AgentStatus/AgentStatusSamples.cs rename to gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/AgentStatusDemoData.cs index bf627c78..4ef51633 100644 --- a/src/TKCode.Arc.Special/Ai/AgentStatus/AgentStatusSamples.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/AgentStatusDemoData.cs @@ -1,23 +1,54 @@ using Avalonia.Media; using Avalonia.Media.Immutable; -namespace TKCode.Arc; +namespace TKCode.Arc.Gallery.Demos; /// -/// Two invented runs for (the agents, file names and texts are made up). They keep the -/// reference's rhythm: the coding run takes six steps of 1.9, 1.5, 2.5, 1.9 and 3.3 seconds (the fifth counting 12 specs -/// in place) and then waits for an approval for as long as it takes; the research run takes five steps, and its third -/// fails once after five of nine pages so Retry can be seen. +/// The runs the agent status demos play: a coding agent and a research agent, named after real models from +/// . The tasks, file names, counts and texts are invented. They keep the reference's rhythm: +/// the coding run takes six steps of 1.9, 1.5, 2.5, 1.9 and 3.3 seconds (the fifth counting 12 specs in place) and then +/// waits for an approval for as long as it takes; the research run takes five steps, and its third fails once after five +/// of nine pages so Retry can be seen. Demo content only; the marks are Lucide glyphs, not the vendors' logos. /// -public static class AgentStatusSamples +internal static class AgentStatusDemoData { - /// The coding agent: "Juniper Code 3" with a hexagon mark in deep orange. + // Lucide glyphs (ISC) the runs use that Core ArcIcons does not ship. + private static readonly Geometry s_fileSearch = ArcIcons.Parse( + "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z " + + "M14 2v5a1 1 0 0 0 1 1h5 M9 14.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0z M13.3 16.3 15 18"); + + private static readonly Geometry s_filePen = ArcIcons.Parse( + "M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34 " + + "M14 2v5a1 1 0 0 0 1 1h5 M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z"); + + private static readonly Geometry s_files = ArcIcons.Parse( + "M15 2h-4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8 M16.706 2.706A2.4 2.4 0 0 0 15 2v5a1 1 0 0 0 1 1h5a2.4 2.4 0 0 0-.706-1.706z " + + "M5 7a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 1.732-1"); + + private static readonly Geometry s_flaskConical = ArcIcons.Parse( + "M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2 M6.453 15h11.094 M8.5 2h7"); + + private static readonly Geometry s_scale = ArcIcons.Parse( + "M12 3v18 M19 8l3 8a5 5 0 0 1-6 0zV7 M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1 M5 8l3 8a5 5 0 0 1-6 0zV7 M7 21h10"); + + private static readonly Geometry s_notebookPen = ArcIcons.Parse( + "M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4 M2 6h4 M2 10h4 M2 14h4 M2 18h4 " + + "M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"); + + private static readonly Geometry s_hexagon = ArcIcons.Parse( + "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"); + + private static readonly Geometry s_orbit = ArcIcons.Parse( + "M20.341 6.484A10 10 0 0 1 10.266 21.85 M3.659 17.516A10 10 0 0 1 13.74 2.152 M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0z " + + "M17 5a2 2 0 1 0 4 0a2 2 0 1 0 -4 0z M3 19a2 2 0 1 0 4 0a2 2 0 1 0 -4 0z"); + + /// The coding agent: Claude Sonnet 5.5 (catalog name) with a hexagon mark in deep orange. public static AgentModel CodingAgent { get; } = - new("Juniper Code 3", AgentStatusIcons.Hexagon, new ImmutableSolidColorBrush(Color.FromRgb(0xC2, 0x41, 0x0C))); + new(AiModelCatalog.ClaudeSonnet55.Name, s_hexagon, new ImmutableSolidColorBrush(Color.FromRgb(0xC2, 0x41, 0x0C))); - /// The research agent: "Atlas Scout 2" with an orbit mark in blue. + /// The research agent: Gemini 4 Argon (catalog name) with an orbit mark in blue. public static AgentModel ResearchAgent { get; } = - new("Atlas Scout 2", AgentStatusIcons.Orbit, new ImmutableSolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB))); + new(AiModelCatalog.Gemini4Argon.Name, s_orbit, new ImmutableSolidColorBrush(Color.FromRgb(0x25, 0x63, 0xEB))); /// /// A coding agent fixes a date parsing bug: it scans, looks up a function, patches a file, adjusts tests, checks 12 @@ -36,7 +67,7 @@ public static class AgentStatusSamples new() { Id = "scan", ActiveLabel = "Scanning 9 modules", DoneLabel = "Scanned 9 modules", - Icon = AgentStatusIcons.FileSearch, Duration = TimeSpan.FromMilliseconds(1917) + Icon = s_fileSearch, Duration = TimeSpan.FromMilliseconds(1917) }, new() { @@ -46,17 +77,17 @@ public static class AgentStatusSamples new() { Id = "patch", ActiveLabel = "Patching lib/dates/parse-due.ts", DoneLabel = "Patched lib/dates/parse-due.ts", - Icon = AgentStatusIcons.FilePen, Duration = TimeSpan.FromMilliseconds(2533) + Icon = s_filePen, Duration = TimeSpan.FromMilliseconds(2533) }, new() { Id = "tests", ActiveLabel = "Adjusting 4 test suites", DoneLabel = "Adjusted 4 test suites", - Icon = AgentStatusIcons.Files, Duration = TimeSpan.FromMilliseconds(1917) + Icon = s_files, Duration = TimeSpan.FromMilliseconds(1917) }, new() { Id = "specs", ActiveLabel = "Checking specs", DoneLabel = "12 of 12 specs green", - Icon = AgentStatusIcons.FlaskConical, Duration = TimeSpan.FromMilliseconds(3316), + Icon = s_flaskConical, Duration = TimeSpan.FromMilliseconds(3316), ProgressTotal = 12, ProgressInterval = TimeSpan.FromMilliseconds(279) }, new() @@ -102,12 +133,12 @@ public static class AgentStatusSamples new() { Id = "compare", ActiveLabel = "Weighing warranty terms across 4 shops", DoneLabel = "Weighed 4 shops", - Icon = AgentStatusIcons.Scale, Duration = TimeSpan.FromMilliseconds(2217) + Icon = s_scale, Duration = TimeSpan.FromMilliseconds(2217) }, new() { Id = "write", ActiveLabel = "Drafting the summary", DoneLabel = "Wrote a 520 word summary", - Icon = AgentStatusIcons.NotebookPen, Duration = TimeSpan.FromMilliseconds(2350) + Icon = s_notebookPen, Duration = TimeSpan.FromMilliseconds(2350) } ] }; @@ -117,7 +148,4 @@ public static class AgentStatusSamples /// A reading activity ("Reading 12 files"), for pills that are working. public static AgentStatusActivity Reading { get; } = new("Reading 12 files", ArcIcons.Search); - - /// Both runs: coding, then research. - public static IReadOnlyList All { get; } = [Coding, Research]; } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml index fed60630..2476fba8 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml @@ -1,9 +1,10 @@ + grows its panel from the start edge, the right one from the end edge. Each pill plays its own simulated run. --> - + + + + + - + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml.cs deleted file mode 100644 index b0dc590b..00000000 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HeaderBar.axaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Controls; - -namespace TKCode.Arc.Gallery.Demos; - -public partial class AgentStatusHeaderBarDemo : UserControl -{ - private readonly AgentRunSimulation _coding = new(AgentStatusSamples.Coding); - private readonly AgentRunSimulation _research = new(AgentStatusSamples.Research); - - public AgentStatusHeaderBarDemo() - { - InitializeComponent(); - Left.Simulation = _coding; - Right.Simulation = _research; - // The demo owns the runs: stop their timers when the page goes away. - DetachedFromVisualTree += (_, _) => - { - _coding.Dispose(); - _research.Dispose(); - }; - } -} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HostDriven.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HostDriven.axaml index f85bcdaa..0276a3ce 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HostDriven.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/HostDriven.axaml @@ -1,6 +1,7 @@ @@ -8,7 +9,7 @@ - + - + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Preview.axaml.cs index 95b2a340..e7c6938c 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Preview.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/Preview.axaml.cs @@ -5,22 +5,22 @@ namespace TKCode.Arc.Gallery.Demos; public partial class AgentStatusPreviewDemo : UserControl { - private readonly AgentRunSimulation _simulation = new(); - public AgentStatusPreviewDemo() { InitializeComponent(); - Pill.Simulation = _simulation; // The demo owns the run: stop its timer when the page goes away. - DetachedFromVisualTree += (_, _) => _simulation.Dispose(); + DetachedFromVisualTree += (_, _) => Pill.Simulation?.Dispose(); } private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) { // The event also fires while the XAML loads, before the named field exists: read the sender. var research = sender is SegmentedControl { SelectedValue: "research" }; - _simulation.Scenario = research ? AgentStatusSamples.Research : AgentStatusSamples.Coding; + if (Pill?.Simulation is { } simulation) + { + simulation.Scenario = research ? AgentStatusDemoData.Research : AgentStatusDemoData.Coding; + } } - private void OnReplay(object? sender, RoutedEventArgs e) => _simulation.Replay(); + private void OnReplay(object? sender, RoutedEventArgs e) => Pill.Simulation?.Replay(); } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/ReducedMotion.axaml index 833ea58e..2afa45ca 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/ReducedMotion.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/ReducedMotion.axaml @@ -1,10 +1,15 @@ - + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/States.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/States.axaml.cs index d026dff5..471d87e1 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/States.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentStatus/States.axaml.cs @@ -8,15 +8,15 @@ public partial class AgentStatusStatesDemo : UserControl public AgentStatusStatesDemo() { InitializeComponent(); - var agent = AgentStatusSamples.CodingAgent; + var agent = AgentStatusDemoData.CodingAgent; var started = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(74); Add(AgentStatusRunState.Idle, null, agent, started); Add(AgentStatusRunState.Working, new AgentStatusActivity("Checking specs", ArcIcons.Search, 8, 12), agent, started); Add(AgentStatusRunState.NeedsInput, new AgentStatusActivity("Needs your approval to deploy", ArcIcons.Search), agent, started); Add(AgentStatusRunState.Paused, new AgentStatusActivity("Checking specs", ArcIcons.Search, 8, 12), agent, started); Add(AgentStatusRunState.Done, new AgentStatusActivity("5 files touched"), agent, started, DateTimeOffset.UtcNow - TimeSpan.FromSeconds(14)); - Add(AgentStatusRunState.Failed, new AgentStatusActivity("Fetch failed · 2 pages did not load"), AgentStatusSamples.ResearchAgent, started); - Add(AgentStatusRunState.Stopped, null, AgentStatusSamples.ResearchAgent, started, DateTimeOffset.UtcNow - TimeSpan.FromSeconds(7)); + Add(AgentStatusRunState.Failed, new AgentStatusActivity("Fetch failed · 2 pages did not load"), AgentStatusDemoData.ResearchAgent, started); + Add(AgentStatusRunState.Stopped, null, AgentStatusDemoData.ResearchAgent, started, DateTimeOffset.UtcNow - TimeSpan.FromSeconds(7)); } private void Add(AgentStatusRunState state, AgentStatusActivity? activity, AgentModel agent, DateTimeOffset started, DateTimeOffset? ended = null) diff --git a/src/TKCode.Arc.Special/Ai/AgentTimeline/AgentTimelineSamples.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/AgentTimelineDemoData.cs similarity index 69% rename from src/TKCode.Arc.Special/Ai/AgentTimeline/AgentTimelineSamples.cs rename to gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/AgentTimelineDemoData.cs index 585000e4..5e2567ea 100644 --- a/src/TKCode.Arc.Special/Ai/AgentTimeline/AgentTimelineSamples.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/AgentTimelineDemoData.cs @@ -1,14 +1,15 @@ using Avalonia.Media; -namespace TKCode.Arc; +namespace TKCode.Arc.Gallery.Demos; /// -/// Invented runs for : the incident investigation the timeline replays when it is given no -/// run, and a small finished run. Every name, model, number and sentence is made up; model marks and tool marks are -/// Lucide glyphs. Each call returns new objects, so a run can be changed (or fed to a live driver) without touching -/// another timeline's copy. +/// The runs the agent timeline demos show: an incident investigation and a small refund summary. The agents run on real +/// models from , and each agent's price is a blend of that model's list prices (three input +/// tokens to one output token, USD per 1M tokens, as checked on ), so the cost the +/// timeline shows is the invented token counts at real list prices. The tasks, agents' names, timings, token counts and +/// sentences are invented; model marks and tool marks are Lucide glyphs, not the vendors' logos. /// -public static class AgentTimelineSamples +internal static class AgentTimelineDemoData { private const string Lead = "lead"; private const string Status = "status"; @@ -16,32 +17,65 @@ public static class AgentTimelineSamples private const string Logs = "logs"; private const string Scribe = "scribe"; + /// The coordinator (and the refund planner): Qwen3.8-Max. + public static AgentModel CoordinatorModel { get; } = new(AiModelCatalog.Qwen38Max.Name, ArcIcons.Sparkles); + + /// The coordinator's blended price. + public static double CoordinatorPrice { get; } = Blended(AiModelCatalog.Qwen38Max); + + /// The status watcher: Grok 4.3. + public static AgentModel WatcherModel { get; } = new(AiModelCatalog.Grok43.Name, ArcIcons.Zap); + + /// The status watcher's blended price. + public static double WatcherPrice { get; } = Blended(AiModelCatalog.Grok43); + + /// The config auditor: Qwen3.7-Plus. + public static AgentModel AuditorModel { get; } = new(AiModelCatalog.Qwen37Plus.Name, ArcIcons.Sparkles); + + /// The config auditor's blended price. + public static double AuditorPrice { get; } = Blended(AiModelCatalog.Qwen37Plus); + + /// The log reader (and the refund data clerk): Qwen3.8-Flash. + public static AgentModel ReaderModel { get; } = new(AiModelCatalog.Qwen38Flash.Name, ArcIcons.Sparkles); + + /// The log reader's blended price. + public static double ReaderPrice { get; } = Blended(AiModelCatalog.Qwen38Flash); + + /// The scribe: Mistral Small 4. + public static AgentModel ScribeModel { get; } = new(AiModelCatalog.MistralSmall4.Name, ArcIcons.Star); + + /// The scribe's blended price. + public static double ScribePrice { get; } = Blended(AiModelCatalog.MistralSmall4); + + /// The incident run the replaying demos share (they only read it; the live demo plays its own copy). + public static AgentTimelineRun IncidentRun { get; } = CreateIncidentRun(); + /// - /// The default run, "Signup emails bouncing since Mar 3": a coordinator plans three parallel checks (provider status, + /// The preview's run, "Signup emails bouncing since Mar 3": a coordinator plans three parallel checks (provider status, /// mail config, mail logs), waits, combines their reports, has a scribe write the incident note and files it. Five - /// agents, 30 spans (a failed log scan and its retry among them), eight handoffs; 26.8 s, 49,920 tokens, $0.15, - /// 1.5x parallel speedup. + /// agents, 30 spans (a failed log scan and its retry among them), eight handoffs; 26.8 s, 49,920 tokens, 1.5x parallel + /// speedup. A new run each call. /// public static AgentTimelineRun CreateIncidentRun() { var run = new AgentTimelineRun { Title = "Signup emails bouncing since Mar 3" }; - run.Agents.Add(new AgentTimelineAgent(Lead, "Coordinator", new AgentModel("Nimbus Pro 3", ArcIcons.Sparkles), 8) + run.Agents.Add(new AgentTimelineAgent(Lead, "Coordinator", CoordinatorModel, CoordinatorPrice) { Role = "Plans the checks and writes the answer" }); - run.Agents.Add(new AgentTimelineAgent(Status, "Status watcher", new AgentModel("Vela 4 Swift", ArcIcons.Zap), 2.6) + run.Agents.Add(new AgentTimelineAgent(Status, "Status watcher", WatcherModel, WatcherPrice) { Role = "Checks outside services" }); - run.Agents.Add(new AgentTimelineAgent(Config, "Config auditor", new AgentModel("Nimbus Core 3", ArcIcons.Sparkles), 2.5) + run.Agents.Add(new AgentTimelineAgent(Config, "Config auditor", AuditorModel, AuditorPrice) { Role = "Reads configuration history" }); - run.Agents.Add(new AgentTimelineAgent(Logs, "Log reader", new AgentModel("Nimbus Lite 3", ArcIcons.Sparkles), 0.8) + run.Agents.Add(new AgentTimelineAgent(Logs, "Log reader", ReaderModel, ReaderPrice) { Role = "Queries the mail logs" }); - run.Agents.Add(new AgentTimelineAgent(Scribe, "Scribe", new AgentModel("Orion Flash 2", ArcIcons.Star), 0.6) + run.Agents.Add(new AgentTimelineAgent(Scribe, "Scribe", ScribeModel, ScribePrice) { Role = "Writes the incident note" }); @@ -104,13 +138,13 @@ public static AgentTimelineRun CreateIncidentRun() /// /// A small finished run, "Monthly refund summary": a planner asks a data clerk which refund reasons grew and waits for - /// the answer. Two agents, four spans, two handoffs, 5.8 s. + /// the answer. Two agents, four spans, two handoffs, 5.8 s. A new run each call. /// public static AgentTimelineRun CreateRefundRun() { var run = new AgentTimelineRun { Title = "Monthly refund summary" }; - run.Agents.Add(new AgentTimelineAgent("planner", "Planner", new AgentModel("Nimbus Pro 3", ArcIcons.Sparkles), 8)); - run.Agents.Add(new AgentTimelineAgent("clerk", "Data clerk", new AgentModel("Nimbus Lite 3", ArcIcons.Sparkles), 0.2)); + run.Agents.Add(new AgentTimelineAgent("planner", "Planner", CoordinatorModel, CoordinatorPrice)); + run.Agents.Add(new AgentTimelineAgent("clerk", "Data clerk", ReaderModel, ReaderPrice)); Think(run, "planner", 0, 1.2, "Outlined the summary", 1100); Wait(run, "planner", 1.2, 5.8, "Waiting on the data clerk"); Tool(run, "clerk", 1.5, 3.9, "Exported last month's refunds", "Ledger", ArcIcons.Database, 2200); @@ -135,4 +169,7 @@ private static AgentTimelineSpan Add(AgentTimelineRun run, AgentTimelineSpan spa run.Spans.Add(span); return span; } + + /// A blended price: three parts input to one part output of the catalog's list prices (USD per 1M tokens). + private static double Blended(AiModelInfo model) => (3 * model.InputPerMTok + model.OutputPerMTok) / 4; } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Live.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Live.axaml.cs index 12d5a5f3..6ca3a8b1 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Live.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Live.axaml.cs @@ -6,8 +6,8 @@ namespace TKCode.Arc.Gallery.Demos; public partial class AgentTimelineLiveDemo : UserControl { - // Replays the sample run as if its events were arriving from a backend right now. - private readonly AgentTimelineLiveDriver _driver = new(AgentTimelineSamples.CreateIncidentRun()); + // Replays the demo's run as if its events were arriving from a backend right now (the driver gets its own copy). + private readonly AgentTimelineLiveDriver _driver = new(AgentTimelineDemoData.CreateIncidentRun()); public AgentTimelineLiveDemo() { diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Narrow.axaml index 40ed532b..2cb76ebf 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Narrow.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Narrow.axaml @@ -1,7 +1,9 @@ - + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/OwnRun.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/OwnRun.axaml index 05128609..e731511f 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/OwnRun.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/OwnRun.axaml @@ -1,6 +1,7 @@ @@ -8,16 +9,13 @@ - - - - - - - - - - + + + - - + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Speeds.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Speeds.axaml index 8151dc59..c02b0b94 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Speeds.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AgentTimeline/Speeds.axaml @@ -1,7 +1,9 @@ - + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/AiCommandPaletteDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/AiCommandPaletteDemoData.cs new file mode 100644 index 00000000..d5f7607a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/AiCommandPaletteDemoData.cs @@ -0,0 +1,125 @@ +using Avalonia.Media; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented workspace the AI command palette demos search and ask about: pages and actions, three suggested questions +/// and the canned answers of the simulated model, with sources and actions that pretend to run. The people, customers and +/// numbers are made up; the model name is a real one from . The marks are neutral Lucide glyphs. +/// +internal static class AiCommandPaletteDemoData +{ + private const string Pages = "Pages"; + private const string Actions = "Actions"; + private const string Recent = "Recent"; + + /// The model credited with the answers: xAI's Grok 4.7 (checked on ). + public static readonly string ModelName = AiModelCatalog.Grok47.Name; + + /// What the walkthrough searches for. + public const string SearchQuery = "inv"; + + /// The pages and actions. + public static readonly IReadOnlyList Items = + [ + Item("inbox", "Inbox", Pages, ArcIcons.Inbox, "12 unread", "G I", "messages notifications", suggested: true), + Item("my-issues", "My issues", Pages, ArcIcons.CircleDot, "8 open", "G M", "assigned tasks", suggested: true), + Item("projects", "Projects", Pages, ArcIcons.Layers, null, "G P", "roadmap"), + Item("billing", "Billing", Pages, ArcIcons.CreditCard, "Settings", null, "invoice payment card plan subscription"), + Item("invoices", "Invoices", Pages, ArcIcons.FileSpreadsheet, "Billing", null, "payment receipts"), + Item("members", "Team members", Pages, ArcIcons.Users, "Settings", null, "people invite roles"), + Item("settings", "Settings", Pages, ArcIcons.Settings, null, "⌘ ,", "preferences account"), + Item("roadmap", "Q3 roadmap", Recent, ArcIcons.FileText, "Doc · Product", null, "plan quarter", suggested: true), + Item("incident", "billing-api 502s", Recent, ArcIcons.TriangleAlert, "Incident · Resolved 2d ago", null, "outage error deploy"), + Item("new-issue", "Create issue", Actions, ArcIcons.Plus, null, "C", "new bug task ticket", suggested: true), + Item("invite", "Invite teammate", Actions, ArcIcons.Mail, null, null, "add member people"), + Item("theme", "Switch theme", Actions, ArcIcons.SunMoon, "Light · Dark", "⌘ ⇧ L", "appearance dark mode", suggested: true), + Item("copy-link", "Copy link to page", Actions, ArcIcons.Link, null, "⌘ L", "share url"), + Item("log-out", "Log out", Actions, ArcIcons.LogOut, null, null, "sign out"), + ]; + + /// The questions offered while Ask AI is open and empty. + public static readonly IReadOnlyList Questions = + [ + "Why did the March invoice for Acme fail?", + "What shipped in the last release?", + "Who owns the onboarding flow?", + ]; + + /// The canned answers; the last one, without keywords, answers anything else. + public static readonly IReadOnlyList Answers = + [ + new() + { + Keywords = "invoice acme march fail payment billing", + Sources = + [ + new("card", "Acme · payment history", "Acme’s payment history: the card on file expired on March 1.", ArcIcons.CreditCard), + new("pr", "billing-api #1893", "Pull request billing-api #1893: roll back the retry worker after 502s during the deploy.", ArcIcons.GitBranch), + new("ticket", "Support ticket 4417", "Support ticket 4417: card expiry notice sent to Acme’s billing contact.", ArcIcons.MessageSquare), + ], + Text = "The March invoice for Acme failed because the card on file expired on March 1 [1]. The automatic retry on March 4 then hit " + + "a 502 from billing-api during a deploy that was rolled back the same day [2].\n\nSupport already emailed Acme’s billing " + + "contact about the expired card, and the account is in its 14 day grace period [3]. Once the card is updated, retrying " + + "the charge should settle the invoice. The fix that keeps retries from running during a deploy is still open.", + Actions = + [ + new("issue", "Create issue in Linear", "ENG · Billing", ArcIcons.CircleDot, "Created ENG-2481"), + new("reply", "Draft reply to Acme", "Support", ArcIcons.Mail, "Draft saved to Support"), + new("runbook", "Open billing runbook in Notion", "Docs", ArcIcons.BookOpen, "Opened Billing runbook"), + ], + }, + new() + { + Keywords = "shipped ship release released changelog new", + Sources = + [ + new("notes", "Release notes 4.12", "Release notes for version 4.12, published on Tuesday.", ArcIcons.FileText), + new("channel", "#eng-releases", "The #eng-releases channel: the release thread for 4.12.", ArcIcons.Hash), + ], + Text = "Version 4.12 went out on Tuesday [1]. It adds saved views to the issue list, bulk editing for labels and a faster search " + + "index that cuts typical queries to under 80 ms [1].\n\nTwo fixes rode along: invoices no longer retry during a deploy, " + + "and exports keep their column order [2]. Nothing was rolled back.", + Actions = + [ + new("post", "Post summary to Slack", "#eng-releases", ArcIcons.MessageSquare, "Posted to #eng-releases"), + new("notes", "Open release notes", "Docs", ArcIcons.FileText, "Opened Release notes 4.12"), + ], + }, + new() + { + Keywords = "owns owner onboarding who flow", + Sources = + [ + new("directory", "Team directory", "The team directory: Growth team members and their areas.", ArcIcons.Users), + new("roadmap", "Q3 roadmap", "The Q3 roadmap: onboarding checklist work and its owner.", ArcIcons.FileText), + ], + Text = "Priya Shah on the Growth team owns the onboarding flow [1]. She took it over in July and is leading the new checklist " + + "work on the Q3 roadmap [2].\n\nFor design questions, Marco Diaz is the designer on the project [1].", + Actions = + [ + new("message", "Message Priya Shah", "Direct message", ArcIcons.MessageCircle, "Message sent to Priya"), + new("roadmap", "Open Q3 roadmap", "Docs", ArcIcons.FileText, "Opened Q3 roadmap"), + ], + }, + new() + { + Sources = + [ + new("search", "Workspace search", "Pages and docs in this workspace that mention the question.", ArcIcons.Search), + new("docs", "Team handbook", "The team handbook, the closest page to the question.", ArcIcons.BookOpen), + ], + Text = "I looked through the workspace for “{question}”. The closest matches are a few pages in the workspace search [1] " + + "and the team handbook [2], but none of them answers it directly.\n\nTry naming a project, a customer or a person, " + + "or open the handbook to read on.", + Actions = + [ + new("handbook", "Open team handbook", "Docs", ArcIcons.BookOpen, "Opened Team handbook"), + ], + }, + ]; + + private static AiCommandPaletteItem Item(string id, string label, string group, Geometry icon, string? meta, string? shortcut, + string keywords, bool suggested = false) => + new(id, label, group, icon, meta) { Shortcut = shortcut, Keywords = keywords, IsSuggested = suggested }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Dialog.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Dialog.axaml new file mode 100644 index 00000000..28f4ad56 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Dialog.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Narrow.axaml new file mode 100644 index 00000000..ba4e8ef5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Narrow.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Preview.axaml new file mode 100644 index 00000000..d70f9c10 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Preview.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Wired.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Wired.axaml new file mode 100644 index 00000000..07400ef2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Wired.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Wired.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Wired.axaml.cs new file mode 100644 index 00000000..7188c139 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiCommandPalette/Wired.axaml.cs @@ -0,0 +1,62 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A palette wired to its own items and a responder that streams a canned answer with sources and actions. +public partial class AiCommandPaletteWiredDemo : UserControl +{ + private const string Answer = + "The last deploy failed at the build step because the environment variable DATABASE_URL is missing in Preview [1]. " + + "It was removed yesterday when the staging database moved [2].\n\nAdd it back to the Preview environment and redeploy."; + + public AiCommandPaletteWiredDemo() + { + InitializeComponent(); + Palette.SuggestedQuestions = ["Why did the last deploy fail?", "What is using the most bandwidth?"]; + Palette.Responder = RespondAsync; + Palette.ItemSelected += (_, e) => Log.Text = "Opened " + e.Item.Label + " (" + e.Item.Id + ")."; + Palette.ActionCompleted += (_, e) => Log.Text = "Action finished: " + e.Result + "."; + Palette.SourceSelected += (_, e) => Log.Text = "Source chosen: " + e.Item.Title + "."; + } + + private static async Task RespondAsync(AiCommandPaletteAskContext context) + { + var token = context.CancellationToken; + await Task.Delay(600, token); + // Sources first, so the chips land before the text that cites them. + context.SetSources( + [ + new AiCommandPaletteSource("build", "Build log #482", "Build log of deployment #482: DATABASE_URL is not defined.", ArcIcons.Terminal), + new AiCommandPaletteSource("audit", "Audit log", "Audit log: DATABASE_URL removed from Preview yesterday.", ArcIcons.ListChecks), + ]); + foreach (var piece in Pieces(Answer)) + { + await Task.Delay(40, token); + context.Append(piece); + } + return new AiCommandPaletteAnswer + { + Actions = + [ + new AiCommandPaletteAction("env", "Add DATABASE_URL to Preview", "Settings", ArcIcons.Key, async t => + { + await Task.Delay(900, t); + return "Added to Preview"; + }) { DoneMeta = "Simulated, nothing was sent" }, + new AiCommandPaletteAction("redeploy", "Redeploy", "Preview", ArcIcons.RotateCw, async t => + { + await Task.Delay(1400, t); + return "Deployment #483 queued"; + }) { DoneMeta = "Simulated, nothing was sent" }, + ], + }; + } + + private static IEnumerable Pieces(string text) + { + for (var i = 0; i < text.Length; i += 12) + { + yield return text.Substring(i, Math.Min(12, text.Length - i)); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/EmptyState.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/EmptyState.axaml new file mode 100644 index 00000000..4cdea8a1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/EmptyState.axaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Preview.axaml new file mode 100644 index 00000000..6d153d55 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Preview.axaml.cs new file mode 100644 index 00000000..483ffa51 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Preview.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class AiMascotPreviewDemo : UserControl +{ + private readonly bool _ready; + + public AiMascotPreviewDemo() + { + InitializeComponent(); + // The caption follows the state on screen, whether the tour or the picker set it. + Mascot.PropertyChanged += (_, e) => + { + if (e.Property == AiMascot.AccessibleLabelProperty) + { + Caption.Text = Mascot.AccessibleLabel; + } + }; + _ready = true; + } + + private void OnStatusChosen(object? sender, SelectionChangedEventArgs e) + { + // SelectionChanged also fires while the XAML loads, before the named fields exist. + if (!_ready || sender is not SegmentedControl { SelectedValue: string value }) + { + return; + } + Mascot.Status = Enum.TryParse(value, out var status) ? status : null; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReducedMotion.axaml new file mode 100644 index 00000000..793eb156 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReducedMotion.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReplyHeader.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReplyHeader.axaml new file mode 100644 index 00000000..404fea26 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReplyHeader.axaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReplyHeader.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReplyHeader.axaml.cs new file mode 100644 index 00000000..2bf53d49 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/ReplyHeader.axaml.cs @@ -0,0 +1,62 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class AiMascotReplyHeaderDemo : UserControl +{ + private const string Answer = + "The nightly sync times out because the export query scans the whole events table. Add an index on created_at and page the export in batches of 5,000 rows."; + + private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(40) }; + private int _ticks; + private int _shown; + + public AiMascotReplyHeaderDemo() + { + InitializeComponent(); + _timer.Tick += (_, _) => OnTick(); + AttachedToVisualTree += (_, _) => Start(); + DetachedFromVisualTree += (_, _) => _timer.Stop(); + } + + private void OnAsk(object? sender, RoutedEventArgs e) => Start(); + + private void Start() + { + _ticks = 0; + _shown = 0; + Reply.Text = string.Empty; + SetPhase(AiMascotStatus.Thinking, "Thinking"); + _timer.Start(); + } + + // Thinking for 1.6s, then the words stream while the mascot reads along, then done. + private void OnTick() + { + _ticks++; + if (_ticks < 40) + { + return; + } + if (_shown < Answer.Length) + { + if (_shown == 0) + { + SetPhase(AiMascotStatus.Writing, "Writing"); + } + _shown = Math.Min(Answer.Length, _shown + 3); + Reply.Text = Answer[.._shown]; + return; + } + SetPhase(AiMascotStatus.Done, "Done"); + _timer.Stop(); + } + + private void SetPhase(AiMascotStatus status, string text) + { + Mascot.Status = status; + Phase.Text = text; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Sizes.axaml new file mode 100644 index 00000000..8f4ffb4b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Sizes.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/States.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/States.axaml new file mode 100644 index 00000000..665f6978 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/States.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Tones.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Tones.axaml new file mode 100644 index 00000000..0f79dc9c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiMascot/Tones.axaml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiModelCatalog.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiModelCatalog.cs new file mode 100644 index 00000000..e013f474 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiModelCatalog.cs @@ -0,0 +1,277 @@ +// Generated by .work/ai-port/real-models/gen_catalog.py from models.json; regenerate instead of editing by hand. +namespace TKCode.Arc.Gallery.Demos; + +/// An AI provider as its own pages describe it (gallery sample data). +internal sealed record AiProviderInfo(string Name, string Key, string PricingPageUrl, string? StatusPageUrl, + IReadOnlyList StatusComponents, string? ApiKeyPrefix); + +/// A benchmark score the vendor publishes, with the benchmark version and the page it is on. +internal sealed record AiBenchmarkScore(double Value, string Version, string Source); + +/// +/// A real model with its list prices (USD per 1M tokens, standard tier) and limits, read from the vendor's official +/// pages on . Null means the vendor does not state it. +/// +internal sealed record AiModelInfo( + AiProviderInfo Provider, string Name, string ApiId, + double InputPerMTok, double OutputPerMTok, double? CachedInputPerMTok, string? PriceTierNote, + int? ContextWindow, int? MaxOutputTokens, string? KnowledgeCutoff, string? ReleaseDate, + IReadOnlyList InputModalities, bool SupportsTools, bool SupportsReasoning, + AiBenchmarkScore? SweBenchVerified, AiBenchmarkScore? GpqaDiamond, AiBenchmarkScore? TerminalBench, + IReadOnlyList Sources); + +/// +/// Real AI models and providers for the gallery's AI demos: names, ids, list prices and limits from the vendors' +/// official pages, checked on 2026-10-29. Prices change; regenerate from models.json when they do. +/// Benchmark versions differ between vendors (for example Terminal-Bench 4.0 and 2.1): compare scores of one version. +/// +internal static class AiModelCatalog +{ + /// The day every value here was read from the vendors' pages. + public const string CheckedOn = "2026-10-29"; + + public static readonly AiProviderInfo Anthropic = new("Anthropic", "anthropic", "https://claude.com/pricing#api", "https://status.claude.com", ["claude.ai", "Claude Console (platform.claude.com)", "Claude API (api.anthropic.com)", "Claude Code", "Claude Cowork", "Claude for Government"], "sk-ant-api03-"); + public static readonly AiProviderInfo OpenAI = new("OpenAI", "openai", "https://developers.openai.com/api/docs/pricing", "https://status.openai.com/", ["APIs", "ChatGPT", "Codex", "FedRAMP", "Ads Platform"], "sk-"); + public static readonly AiProviderInfo Google = new("Google", "google", "https://ai.google.dev/gemini-api/docs/pricing", "https://aistudio.google.com/status", ["API", "Multimodal Live API", "Google AI Studio"], null); + public static readonly AiProviderInfo XAi = new("xAI", "xai", "https://docs.x.ai/developers/pricing", "https://status.x.ai", [], "xai-"); + public static readonly AiProviderInfo MistralAI = new("Mistral AI", "mistral", "https://docs.mistral.ai/inference/pricing", "https://status.mistral.ai", ["Completion API", "Embedding API", "OCR API", "Agents API", "Conversations API", "Fine-Tuning API", "Audio API", "Integrations API", "Files API", "Batch API", "Workflows API", "Console", "Prompts API", "Responses API", "Skills API", "Vibe"], null); + public static readonly AiProviderInfo DeepSeek = new("DeepSeek", "deepseek", "https://api-docs.deepseek.com/quick_start/pricing", "https://status.deepseek.com/", ["DeepSeek V4 Pro API服务(API Service)", "DeepSeek V4.1 Flash API服务(API Service)", "搜索API服务(Search API Service)", "抓取API服务(Fetch API Service)", "对话服务(Chat Service)", "对话服务(Chatservice)", "上传文件服务(File Upload Service)", "搜索服务(Search Service)"], "sk-"); + public static readonly AiProviderInfo AlibabaCloud = new("Alibaba Cloud", "alibaba", "https://www.alibabacloud.com/help/en/model-studio/billing-for-model-studio", "https://status.alibabacloud.com/", [], "sk-"); + + public static readonly AiModelInfo ClaudeFable51 = new( + Anthropic, "Claude Fable 5.1", "claude-fable-5-1", + 10.0, 50.0, 0.25, "Cache read price; 5-minute cache write is $12.50/MTok. No prompt-length tier.", + 1000000, 128000, "2026-06", "2026-09-01", + ["text", "image", "pdf"], true, true, + null, + null, + new(55.8, "Terminal-Bench 4.0", "https://www.anthropic.com/claude-fable-and-mythos-5-1"), + ["https://claude.com/pricing#api", "https://www.anthropic.com/claude-fable-and-mythos-5-1", "https://www.anthropic.com/claude/fable", "https://www.anthropic.com/claude-fable-5-1-mythos-5-1-system-card", "https://platform.claude.com/docs/en/models/fable-5-1/overview"]); + + public static readonly AiModelInfo ClaudeOpus55 = new( + Anthropic, "Claude Opus 5.5", "claude-opus-5-5", + 4.0, 20.0, 0.2, "Cache read price; 5-minute cache write is $5/MTok. No prompt-length tier. Fast mode (up to 2.5x faster) is 2x standard pricing ($8 / $40).", + 1000000, 128000, "2026-06", "2026-09-22", + ["text", "image", "pdf"], true, true, + null, + null, + new(66.4, "Terminal-Bench 4.0", "https://www.anthropic.com/claude-opus-5-5"), + ["https://claude.com/pricing#api", "https://www.anthropic.com/claude-opus-5-5", "https://www.anthropic.com/claude/opus", "https://www.anthropic.com/claude-opus-5-5-system-card", "https://platform.claude.com/docs/en/models/opus-5-5/overview"]); + + public static readonly AiModelInfo ClaudeSonnet55 = new( + Anthropic, "Claude Sonnet 5.5", "claude-sonnet-5-5", + 2.0, 10.0, 0.1, "Cache read price (updated on 2026-10-07 per the Haiku 5.5 system card); 5-minute cache write is $2.50/MTok. No prompt-length tier.", + 1000000, 128000, "2026-06", "2026-09-28", + ["text", "image", "pdf"], true, true, + null, + null, + new(70.6, "Terminal-Bench 4.0", "https://www.anthropic.com/claude-sonnet-5-5"), + ["https://claude.com/pricing#api", "https://www.anthropic.com/claude-sonnet-5-5", "https://www.anthropic.com/claude/sonnet", "https://www.anthropic.com/claude-sonnet-5-5-system-card", "https://platform.claude.com/docs/en/models/sonnet-5-5/overview"]); + + public static readonly AiModelInfo ClaudeHaiku55 = new( + Anthropic, "Claude Haiku 5.5", "claude-haiku-5-5", + 0.1, 0.5, 0.01, "Prompts <= 100K tokens. Prompts > 100K tokens: input $0.50, output $2.50, cache read $0.05, cache write $0.625; the whole request is charged at the rate for its prompt length. Base-tier cache write $0.125.", + 1000000, 128000, "2026-06", "2026-10-07", + ["text", "image", "pdf"], true, true, + null, + null, + new(39.2, "Terminal-Bench 4.0", "https://www.anthropic.com/claude-haiku-5-5"), + ["https://claude.com/pricing#api", "https://www.anthropic.com/claude-haiku-5-5", "https://www.anthropic.com/claude/haiku", "https://www.anthropic.com/claude-haiku-5-5-system-card", "https://platform.claude.com/docs/en/models/haiku-5-5/overview"]); + + public static readonly AiModelInfo Gpt6Astra = new( + OpenAI, "GPT-6 Astra", "gpt-6-astra", + 10.0, 50.0, 1.0, "Short context (<= 272K input tokens). Prompts with more than 272K input tokens are priced at 2x input and cache rates and 1.5x output for the full request. Cache writes are billed separately: $12.50/MTok (30-minute retention), $25.00 (12-hour). Batch/Flex 50% off, Fast 2x; regional processing +10%.", + 1050000, 128000, "2026-04-30", "2026-09-03", + ["text", "image", "pdf"], true, true, + null, + null, + new(57.9, "Terminal-Bench 4.0", "https://openai.com/index/gpt-6-astra-next-generation-work/"), + ["https://developers.openai.com/api/docs/models/gpt-6-astra", "https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/changelog", "https://developers.openai.com/api/docs/guides/latest-model", "https://developers.openai.com/api/docs/guides/file-inputs", "https://deploymentsafety.openai.com/gpt-6-astra", "https://openai.com/index/gpt-6-astra-next-generation-work/"]); + + public static readonly AiModelInfo Gpt61Sol = new( + OpenAI, "GPT-6.1 Sol", "gpt-6.1-sol", + 2.0, 10.0, 0.1, "Short context (<= 272K input tokens). Prompts with more than 272K input tokens are priced at 2x input and cache rates and 1.5x output for the full request. Cache writes are billed separately: $2.50/MTok (30-minute), $5.00 (12-hour). Batch/Flex 50% off, Fast 2x, Ultrafast 6x; regional processing +10%.", + 1050000, 128000, "2026-04-30", "2026-09-29", + ["text", "image", "pdf"], true, true, + null, + null, + null, + ["https://developers.openai.com/api/docs/models/gpt-6.1-sol", "https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/changelog", "https://developers.openai.com/api/docs/guides/latest-model", "https://developers.openai.com/api/docs/guides/file-inputs", "https://deploymentsafety.openai.com/gpt-6-1-sol"]); + + public static readonly AiModelInfo Gpt61Terra = new( + OpenAI, "GPT-6.1 Terra", "gpt-6.1-terra", + 1.0, 5.0, 0.05, "Short context (<= 272K input tokens). Prompts with more than 272K input tokens are priced at 2x input and cache rates and 1.5x output for the full request. Cache writes are billed separately: $1.25/MTok (30-minute), $2.50 (12-hour). Batch/Flex 50% off, Fast 2x; regional processing +10%.", + 1050000, 128000, "2026-06-01", "2026-10-26", + ["text", "image", "pdf"], true, true, + null, + null, + new(50.3, "Terminal-Bench 4.0", "https://openai.com/index/introducing-gpt-6-1-terra/"), + ["https://developers.openai.com/api/docs/models/gpt-6.1-terra", "https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/changelog", "https://developers.openai.com/api/docs/guides/latest-model", "https://developers.openai.com/api/docs/guides/file-inputs", "https://deploymentsafety.openai.com/gpt-6-1-terra", "https://openai.com/index/introducing-gpt-6-1-terra/"]); + + public static readonly AiModelInfo Gpt6Luna = new( + OpenAI, "GPT-6 Luna", "gpt-6-luna", + 0.1, 0.5, 0.01, "Short context (<= 272K input tokens). Prompts with more than 272K input tokens are priced at 2x input and cache rates and 1.5x output for the full request. Cache writes are billed separately: $0.125/MTok (30-minute), $0.25 (12-hour). Batch/Flex 50% off, Fast 2x; regional processing +10%.", + 1050000, 128000, "2026-05-18", "2026-09-22", + ["text", "image", "pdf"], true, true, + null, + null, + null, + ["https://developers.openai.com/api/docs/models/gpt-6-luna", "https://developers.openai.com/api/docs/pricing", "https://developers.openai.com/api/docs/changelog", "https://developers.openai.com/api/docs/guides/latest-model", "https://developers.openai.com/api/docs/guides/file-inputs"]); + + public static readonly AiModelInfo Gemini4Argon = new( + Google, "Gemini 4 Argon", "gemini-4-argon", + 2.0, 10.0, 0.1, "Introductory Paid Tier price through 2026-11-30; from 2026-12-01: $4.00 input / $20.00 output / $0.20 cached. Output includes thinking tokens. Implicit caching only (no explicit caching). No prompt-length tier. Not on the Free Tier.", + 1048576, 65536, "2026-03", "2026-10-13", + ["text", "image", "pdf", "audio", "video"], true, true, + null, + null, + new(57.4, "Terminal-Bench 4.0", "https://deepmind.google/models/model-cards/gemini-4-argon/"), + ["https://ai.google.dev/gemini-api/docs/models", "https://ai.google.dev/gemini-api/docs/models/gemini-4-argon", "https://ai.google.dev/gemini-api/docs/pricing", "https://ai.google.dev/gemini-api/docs/latest-model", "https://ai.google.dev/gemini-api/docs/changelog", "https://deepmind.google/models/model-cards/gemini-4-argon/", "https://storage.googleapis.com/deepmind-media/gemini/gemini_4_argon_model_evaluation.pdf", "https://deepmind.google/models/gemini/", "https://blog.google/innovation-and-ai/technology/developers-tools/build-with-gemini-4-argon/"]); + + public static readonly AiModelInfo Gemini38Flash = new( + Google, "Gemini 3.8 Flash", "gemini-3.8-flash", + 0.75, 3.75, 0.075, "Introductory Paid Tier price through 2026-12-31; from 2027-01-01: $1.50 input / $7.50 output / $0.15 cached. Output includes thinking tokens. Cache storage $0.50 per 1M tokens per hour ($1.00 from 2027-01-01). No prompt-length tier. Free Tier available.", + 1048576, 65536, "2026-03", "2026-09-02", + ["text", "image", "pdf", "audio", "video"], true, true, + null, + null, + new(19.1, "Terminal-Bench 4.0", "https://deepmind.google/models/model-cards/gemini-3-8-flash/"), + ["https://ai.google.dev/gemini-api/docs/models", "https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash", "https://ai.google.dev/gemini-api/docs/pricing", "https://ai.google.dev/gemini-api/docs/whats-new-gemini-3.8", "https://ai.google.dev/gemini-api/docs/changelog", "https://deepmind.google/models/model-cards/gemini-3-8-flash/", "https://storage.googleapis.com/deepmind-media/gemini/gemini_3-8_flash_model_evaluation.pdf", "https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/"]); + + public static readonly AiModelInfo Gemini35FlashLite = new( + Google, "Gemini 3.5 Flash-Lite", "gemini-3.5-flash-lite", + 0.3, 2.5, 0.03, "Paid Tier. One input price for text / image / video / audio. Output includes thinking tokens. Cache storage $1.00 per 1M tokens per hour. No prompt-length tier, no scheduled change. Free Tier available.", + 1048576, 65536, "2026-03", "2026-07-21", + ["text", "image", "pdf", "audio", "video"], true, true, + null, + null, + new(54.0, "Terminal-Bench 2.1", "https://deepmind.google/models/model-cards/gemini-3-5-flash-lite/"), + ["https://ai.google.dev/gemini-api/docs/models", "https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite", "https://ai.google.dev/gemini-api/docs/pricing", "https://ai.google.dev/gemini-api/docs/changelog", "https://deepmind.google/models/model-cards/gemini-3-5-flash-lite/", "https://storage.googleapis.com/deepmind-media/gemini/gemini_3-5_flash-lite_model_evaluation.pdf", "https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-6-flash-3-5-flash-lite-3-5-flash-cyber/"]); + + public static readonly AiModelInfo Grok47 = new( + XAi, "Grok 4.7", "grok-4.7", + 2.0, 6.0, 0.5, "Prompts < 200K tokens. A request whose prompt reaches 200K tokens is billed at the higher rate for all its tokens: $4.00 input / $1.00 cached / $12.00 output. Batch API 20% off; Priority Processing 2x; US regional endpoint 1.1x.", + 500000, null, "2026-05", "2026-09-21", + ["text", "image"], true, true, + null, + null, + new(37.6, "Terminal-Bench 4.0", "https://x.ai/news/grok-4-7"), + ["https://docs.x.ai/developers/models", "https://docs.x.ai/developers/models/grok-4.7", "https://docs.x.ai/developers/grok-4-7", "https://docs.x.ai/developers/pricing", "https://docs.x.ai/developers/release-notes", "https://x.ai/news/grok-4-7"]); + + public static readonly AiModelInfo Grok43 = new( + XAi, "Grok 4.3", "grok-4.3", + 1.25, 2.5, 0.2, "Prompts < 200K tokens. A request whose prompt reaches 200K tokens is billed at the higher rate for all its tokens: $2.50 input / $0.40 cached / $5.00 output. Batch API 20% off; Priority 2x. Alias grok-4.3-latest.", + 1000000, null, null, null, + ["text", "image"], true, true, + null, + null, + null, + ["https://docs.x.ai/developers/models", "https://docs.x.ai/developers/models/grok-4.3", "https://docs.x.ai/developers/pricing", "https://docs.x.ai/developers/migration/may-15-retirement"]); + + public static readonly AiModelInfo Grok420NonReasoning = new( + XAi, "Grok 4.20 (Non-Reasoning)", "grok-4.20-0309-non-reasoning", + 1.25, 2.5, 0.2, "Prompts < 200K tokens. A request whose prompt reaches 200K tokens is billed at the higher rate for all its tokens: $2.50 input / $0.40 cached / $5.00 output. Batch API 20% off; Priority 2x. Aliases grok-4.20-non-reasoning, grok-4.20-non-reasoning-latest.", + 1000000, null, null, null, + ["text", "image"], true, false, + null, + null, + null, + ["https://docs.x.ai/developers/models", "https://docs.x.ai/developers/models/grok-4.20-0309-non-reasoning", "https://docs.x.ai/developers/pricing", "https://docs.x.ai/developers/release-notes"]); + + public static readonly AiModelInfo MistralMedium35 = new( + MistralAI, "Mistral Medium 3.5", "mistral-medium-3-5", + 1.5, 7.5, 0.15, "USD, Standard tier (EUR also offered). Cached input appears in the rendered docs pricing table and matches the documented rule (cached tokens billed at 10% of input). Batch 50% off; Priority Tier 1.75x. Aliases mistral-medium-3, mistral-medium-latest.", + 256000, null, null, "2026-04-28", + ["text", "image", "pdf"], true, true, + new(77.6, "", "https://mistral.ai/news/vibe-remote-agents-mistral-medium-3-5"), + null, + null, + ["https://docs.mistral.ai/models/mistral-medium-3-5-26-04", "https://docs.mistral.ai/inference/pricing", "https://docs.mistral.ai/inference/model-selection-guide", "https://mistral.ai/news/vibe-remote-agents-mistral-medium-3-5", "https://docs.mistral.ai/studio/conversations/reasoning", "https://docs.mistral.ai/studio/conversations/advanced/prompt-caching", "https://legal.mistral.ai/ai-governance/models/mistral-medium-3"]); + + public static readonly AiModelInfo MistralLarge3 = new( + MistralAI, "Mistral Large 3", "mistral-large-2512", + 0.5, 1.5, 0.05, "USD, Standard tier; mistral.ai/pricing FAQ confirms $0.5 in / $1.5 out. Cached input from the rendered docs pricing table (10% rule). Batch 50% off; Priority Tier 1.75x. Alias mistral-large-latest.", + 256000, null, null, "2025-12-02", + ["text", "image", "pdf"], true, false, + null, + new(64.1, "", "https://mistral.ai/news/mistral-small-4/"), + null, + ["https://docs.mistral.ai/models/mistral-large-3-25-12", "https://docs.mistral.ai/inference/pricing", "https://mistral.ai/pricing", "https://mistral.ai/news/mistral-3", "https://mistral.ai/news/mistral-small-4/", "https://legal.mistral.ai/ai-governance/models/mistral-large-3"]); + + public static readonly AiModelInfo MistralSmall4 = new( + MistralAI, "Mistral Small 4", "mistral-small-2603", + 0.15, 0.6, 0.015, "USD, Standard tier; the launch post also gives $0.15 in / $0.6 out. Cached input from the rendered docs pricing table (10% rule). Batch 50% off; Priority Tier 1.75x. Alias mistral-small-latest.", + 256000, null, null, "2026-03-16", + ["text", "image", "pdf"], true, true, + null, + new(71.2, "", "https://mistral.ai/news/mistral-small-4/"), + null, + ["https://docs.mistral.ai/models/mistral-small-4-0-26-03", "https://docs.mistral.ai/inference/pricing", "https://mistral.ai/news/mistral-small-4/", "https://docs.mistral.ai/studio/conversations/reasoning", "https://legal.mistral.ai/ai-governance/models/mistral-small-4"]); + + public static readonly AiModelInfo Ministral38B = new( + MistralAI, "Ministral 3 8B", "ministral-8b-2512", + 0.15, 0.15, 0.015, "USD, Standard tier. Cached input from the rendered docs pricing table (10% rule). Batch 50% off; Priority Tier 1.75x. Alias ministral-8b-latest.", + 256000, null, null, "2025-12-02", + ["text", "image", "pdf"], true, false, + null, + null, + null, + ["https://docs.mistral.ai/models/ministral-3-8b-25-12", "https://docs.mistral.ai/inference/pricing", "https://mistral.ai/news/mistral-3", "https://legal.mistral.ai/ai-governance/models/ministral-3-8b"]); + + public static readonly AiModelInfo DeepSeekV4Pro = new( + DeepSeek, "DeepSeek-V4-Pro-0813", "deepseek-v4-pro", + 1.32, 3.96, 0.044, "Peak (full) rates; input = cache miss, cached = cache hit. Off-peak rates are half ($0.66 miss / $0.022 hit / $1.98 output). Peak hours: 01:00-04:00 and 06:00-10:00 UTC, Monday-Friday, excluding Chinese public holidays. Thinking and non-thinking modes cost the same. Default max_tokens when unset: 8K non-thinking, 64K thinking (128K at reasoning_effort=max).", + 1048576, 393216, null, "2026-08-13", + ["text"], true, true, + null, + new(92.4, "", "https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash"), + new(87.9, "Terminal-Bench 2.1", "https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813"), + ["https://api-docs.deepseek.com/quick_start/pricing", "https://api-docs.deepseek.com/api/create-chat-completion", "https://api-docs.deepseek.com/guides/thinking_mode", "https://api-docs.deepseek.com/guides/tool_calls", "https://api-docs.deepseek.com/updates", "https://api-docs.deepseek.com/news/news260813", "https://api-docs.deepseek.com/news/news260910", "https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813", "https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash"]); + + public static readonly AiModelInfo DeepSeekV41Flash = new( + DeepSeek, "DeepSeek-V4.1-Flash-1017", "deepseek-flash", + 0.3, 1.2, 0.006, "Peak (full) rates; input = cache miss, cached = cache hit. Off-peak rates are half ($0.15 miss / $0.003 hit / $0.60 output). Peak hours: 01:00-04:00 and 06:00-10:00 UTC, Monday-Friday, excluding Chinese public holidays. Thinking and non-thinking modes cost the same. Legacy ids deepseek-v4-flash and deepseek-v4-flash-vision-exp route to this model at this price. Default max_tokens when unset: 8K non-thinking, 64K thinking (128K at reasoning_effort=max).", + 1048576, 393216, null, "2026-10-17", + ["text", "image"], true, true, + null, + null, + new(42.3, "Terminal-Bench 4.0", "https://api-docs.deepseek.com/updates"), + ["https://api-docs.deepseek.com/quick_start/pricing", "https://api-docs.deepseek.com/api/create-chat-completion", "https://api-docs.deepseek.com/guides/vision", "https://api-docs.deepseek.com/guides/files_api", "https://api-docs.deepseek.com/guides/thinking_mode", "https://api-docs.deepseek.com/guides/tool_calls", "https://api-docs.deepseek.com/updates", "https://api-docs.deepseek.com/news/news261017", "https://api-docs.deepseek.com/news/news260910", "https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017"]); + + public static readonly AiModelInfo Qwen38Max = new( + AlibabaCloud, "Qwen3.8-Max", "qwen3.8-max", + 2.0, 6.0, 0.25, "Singapore (International) deployment, USD, one tier (0 < tokens <= 1M). Same price with thinking on or off. Cached = implicit-cache input; explicit cache creation $2.50, explicit cache read $0.17. Stable id now points to snapshot qwen3.8-max-0902 (since 2026-09-05, prices unchanged).", + 1000000, 131072, null, "2026-08-02", + ["text", "image", "video"], true, true, + null, + new(92.6, "", "https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B"), + new(86.6, "Terminal-Bench 2.1", "https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B"), + ["https://www.alibabacloud.com/help/en/model-studio/qwen3-8-max", "https://www.alibabacloud.com/help/en/model-studio/billing-for-model-studio", "https://www.alibabacloud.com/help/en/model-studio/models", "https://www.alibabacloud.com/help/en/model-studio/newly-released-models", "https://www.alibabacloud.com/en/notice/model_studio_update_notice_for_qwen38max_models_863", "https://www.alibabacloud.com/help/en/model-studio/context-cache", "https://huggingface.co/Qwen/Qwen3.8-2.4T-A95B"]); + + public static readonly AiModelInfo Qwen37Plus = new( + AlibabaCloud, "Qwen3.7-Plus", "qwen3.7-plus", + 0.4, 1.6, 0.08, "Singapore (International) deployment, USD list price, tiered by input tokens per request. <= 256K: $0.40 input / $1.60 output / $0.08 implicit-cache input (explicit cache creation $0.50, read $0.04). 256K-1M: $1.20 / $4.80 / $0.24 (explicit $1.50 / $0.12). Same price with thinking on or off. The pricing page marks the stable id 'Limited-time 20% off' without stating the discounted price or end date. Stable id is equivalent to snapshot qwen3.7-plus-2026-05-26.", + 1000000, 131072, null, "2026-06-01", + ["text", "image", "video"], true, true, + null, + new(90.3, "", "https://huggingface.co/Qwen/Qwen3.8-Flash-Next"), + null, + ["https://www.alibabacloud.com/help/en/model-studio/qwen3-7-plus", "https://www.alibabacloud.com/help/en/model-studio/billing-for-model-studio", "https://www.alibabacloud.com/help/en/model-studio/models", "https://www.alibabacloud.com/help/en/model-studio/newly-released-models", "https://www.alibabacloud.com/help/en/model-studio/context-cache", "https://www.alibabacloud.com/blog/qwen3-7-plus-multimodal-agent-intelligence_603206", "https://huggingface.co/Qwen/Qwen3.8-Flash-Next"]); + + public static readonly AiModelInfo Qwen38Flash = new( + AlibabaCloud, "Qwen3.8-Flash", "qwen3.8-flash", + 0.15, 0.47, 0.016, "Singapore (International) deployment, USD, one tier (0 < tokens <= 1M). Same price in every mode. Cached = implicit-cache input; explicit cache creation $0.20, explicit cache read $0.016.", + 1000000, 131072, null, "2026-08-26", + ["text", "image", "video"], true, true, + null, + null, + null, + ["https://www.alibabacloud.com/help/en/model-studio/qwen3-8-flash", "https://www.alibabacloud.com/help/en/model-studio/billing-for-model-studio", "https://www.alibabacloud.com/help/en/model-studio/models", "https://www.alibabacloud.com/help/en/model-studio/newly-released-models", "https://www.alibabacloud.com/help/en/model-studio/context-cache", "https://huggingface.co/Qwen/Qwen3.8-Flash-Next"]); + + /// Every provider, in the order of models.json. + public static IReadOnlyList Providers { get; } = [Anthropic, OpenAI, Google, XAi, MistralAI, DeepSeek, AlibabaCloud]; + + /// Every model, grouped by provider. + public static IReadOnlyList Models { get; } = + [ClaudeFable51, ClaudeOpus55, ClaudeSonnet55, ClaudeHaiku55, Gpt6Astra, Gpt61Sol, Gpt61Terra, Gpt6Luna, Gemini4Argon, Gemini38Flash, Gemini35FlashLite, Grok47, Grok43, Grok420NonReasoning, MistralMedium35, MistralLarge3, MistralSmall4, Ministral38B, DeepSeekV4Pro, DeepSeekV41Flash, Qwen38Max, Qwen37Plus, Qwen38Flash]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Avatar.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Avatar.axaml new file mode 100644 index 00000000..cff77f71 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Avatar.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Hero.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Hero.axaml new file mode 100644 index 00000000..70f4048f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Hero.axaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Inline.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Inline.axaml new file mode 100644 index 00000000..15682fbf --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Inline.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Pill.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Pill.axaml new file mode 100644 index 00000000..d691466f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Pill.axaml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Playback.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Playback.axaml new file mode 100644 index 00000000..99a080ea --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Playback.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Preview.axaml new file mode 100644 index 00000000..372eea2d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Preview.axaml.cs new file mode 100644 index 00000000..45b9730e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Preview.axaml.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class AiSparkPreviewDemo : UserControl +{ + private readonly bool _ready; + + public AiSparkPreviewDemo() + { + InitializeComponent(); + _ready = true; + Caption.Text = Hero.EffectiveStatus.ToString(); + Hero.PropertyChanged += (_, e) => + { + if (e.Property == AiSpark.EffectiveStatusProperty) + { + Caption.Text = Hero.EffectiveStatus.ToString(); + } + }; + } + + private void OnStatusChanged(object? sender, SelectionChangedEventArgs e) + { + // SelectionChanged also fires while the XAML loads, before the sparks exist. + if (!_ready || sender is not SegmentedControl control) + { + return; + } + AiSparkStatus? status = control.SelectedValue is string name && Enum.TryParse(name, out var parsed) + ? parsed + : null; + // In Auto the sparks run their own self-play; with a status they all show it. + Hero.Status = status; + Pill.Status = status; + Avatar.Status = status; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Sizes.axaml new file mode 100644 index 00000000..96b4e38d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Sizes.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Tones.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Tones.axaml new file mode 100644 index 00000000..e3e0fa25 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiSpark/Tones.axaml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/AiTranslateDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/AiTranslateDemoData.cs new file mode 100644 index 00000000..ddf6bfa0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/AiTranslateDemoData.cs @@ -0,0 +1,46 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented message the AI translate demos play: a short French release note from an invented product lead, with +/// translations into the other five languages made ahead of time. The product, people and numbers are invented; the model +/// credited is a real one from . Demo content only. +/// +internal static class AiTranslateDemoData +{ + public const string Text = + "La version 4.2 est disponible. Les exports PDF sont désormais deux fois plus rapides et les tableaux gardent leur mise en forme. " + + "Merci à toutes celles et ceux qui ont signalé des bugs cette semaine."; + + public const string Timestamp = "Oct 27, 09:14"; + + /// The model credited in the status line. + public static string ModelName => AiModelCatalog.Gemini38Flash.Name; + + public static AiTranslateAuthor Author { get; } = new() { Name = "Camille Laurent", Role = "Product lead" }; + + /// The self playing tour: English, Deutsch, 日本語 and Português, then back to the original. + public static IReadOnlyList Tour { get; } = + [ + AiTranslateLanguage.English, AiTranslateLanguage.German, AiTranslateLanguage.Japanese, AiTranslateLanguage.Portuguese, + ]; + + /// The release note in the other five languages. + public static IReadOnlyDictionary Translations { get; } = new Dictionary + { + [AiTranslateLanguage.English] = + "Version 4.2 is out. PDF exports are now twice as fast, and tables keep their formatting. " + + "Thanks to everyone who reported bugs this week.", + [AiTranslateLanguage.German] = + "Version 4.2 ist da. PDF-Exporte sind jetzt doppelt so schnell, und Tabellen behalten ihre Formatierung. " + + "Danke an alle, die diese Woche Fehler gemeldet haben.", + [AiTranslateLanguage.Spanish] = + "Ya está disponible la versión 4.2. Las exportaciones a PDF ahora son el doble de rápidas y las tablas conservan su formato. " + + "Gracias a todas las personas que informaron de errores esta semana.", + [AiTranslateLanguage.Japanese] = + "バージョン4.2を公開しました。PDFの書き出しが2倍速くなり、表の書式も保たれるようになりました。" + + "今週バグを報告してくださった皆さん、ありがとうございます。", + [AiTranslateLanguage.Portuguese] = + "A versão 4.2 já está disponível. As exportações em PDF agora são duas vezes mais rápidas e as tabelas mantêm a formatação. " + + "Obrigado a todos que relataram bugs esta semana.", + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Narrow.axaml new file mode 100644 index 00000000..45c2931b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Narrow.axaml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Preview.axaml new file mode 100644 index 00000000..7bb9e78a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Preview.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Ready.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Ready.axaml new file mode 100644 index 00000000..176d0dee --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Ready.axaml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Ready.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Ready.axaml.cs new file mode 100644 index 00000000..3cc73440 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Ready.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Ready translations streamed by the built-in simulation, with a missing language. +public partial class AiTranslateReadyDemo : UserControl +{ + public AiTranslateReadyDemo() + { + InitializeComponent(); + // Demo content: translations you stored with the post. + Card.Translations = new Dictionary + { + [AiTranslateLanguage.English] = "We have updated the prices of the Team plan. Current customers keep their rate until January.", + [AiTranslateLanguage.Portuguese] = "Atualizamos os preços do plano Equipe. Os clientes atuais mantêm sua tarifa até janeiro.", + }; + Card.TranslationFailed += (_, e) => Log.Text = $"Failed ({e.From} to {e.To}): {e.Message}"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Streaming.axaml new file mode 100644 index 00000000..6df91cb5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Streaming.axaml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Streaming.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Streaming.axaml.cs new file mode 100644 index 00000000..531f3ba6 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Streaming.axaml.cs @@ -0,0 +1,46 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A card wired to a translator that streams its reply in small deltas. +public partial class AiTranslateStreamingDemo : UserControl +{ + // Demo content: what the stand-in model "replies" for each language. + private static readonly Dictionary s_replies = new() + { + [AiTranslateLanguage.English] = + "Hi all, since the update the export has been stuck at 80 percent. Could you please take a look by Friday?", + [AiTranslateLanguage.French] = + "Bonjour à tous, depuis la mise à jour, l'export reste bloqué à 80 %. Pourriez-vous y jeter un œil d'ici vendredi ?", + [AiTranslateLanguage.Japanese] = + "皆さん、アップデート以降、エクスポートが80%で止まったままです。金曜日までに確認していただけますか?", + }; + + public AiTranslateStreamingDemo() + { + InitializeComponent(); + Card.Languages = [AiTranslateLanguage.English, AiTranslateLanguage.German, AiTranslateLanguage.French, AiTranslateLanguage.Japanese]; + Card.Author = new AiTranslateAuthor { Name = "Jonas Weber", Role = "Customer success" }; + Card.Translator = Translate; + Card.ValueChanged += (_, e) => Log.Text = e.IsOriginal ? "Showing the original" : $"Picked {e.Language}"; + Card.Translated += (_, e) => Log.Text = $"Translated {e.From} to {e.To}: {e.Text.Length} characters, now cached"; + Card.TranslationFailed += (_, e) => Log.Text = $"Failed: {e.Message}"; + } + + /// A stand-in for your model call: waits like a network request, then streams two words at a time. + private static async IAsyncEnumerable Translate(AiTranslateRequest request) + { + var token = request.CancellationToken; + await Task.Delay(400, token); + if (!s_replies.TryGetValue(request.To, out var reply)) + { + throw new InvalidOperationException("The translation service is unavailable."); + } + var words = reply.Split(' '); + for (var i = 0; i < words.Length; i += 2) + { + yield return string.Join(' ', words.Skip(i).Take(2)) + (i + 2 < words.Length ? " " : string.Empty); + await Task.Delay(60, token); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Thread.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Thread.axaml new file mode 100644 index 00000000..cea1bb25 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Thread.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Thread.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Thread.axaml.cs new file mode 100644 index 00000000..29c93991 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiTranslate/Thread.axaml.cs @@ -0,0 +1,47 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Two messages that follow one preferred language. +public partial class AiTranslateThreadDemo : UserControl +{ + private readonly bool _ready; + + public AiTranslateThreadDemo() + { + InitializeComponent(); + Second.Author = new AiTranslateAuthor { Name = "Haruto Sato", Role = "Design" }; + // Demo content: the second message's stored translations. + Second.Translations = new Dictionary + { + [AiTranslateLanguage.English] = "Tomorrow's weekly meeting now starts at 10:00.", + [AiTranslateLanguage.German] = "Das wöchentliche Meeting morgen beginnt jetzt um 10:00 Uhr.", + [AiTranslateLanguage.Spanish] = "La reunión semanal de mañana ahora empieza a las 10:00.", + }; + // A reader who picks a language in one card sets it for the whole thread. + First.ValueChanged += (_, e) => Follow(e); + Second.ValueChanged += (_, e) => Follow(e); + _ready = true; + } + + private void OnPreferredChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has run. + if (!_ready) + { + return; + } + AiTranslateLanguage? language = Enum.TryParse(Preferred.SelectedValue as string, out var picked) ? picked : null; + First.Value = language; + Second.Value = language; + } + + private void Follow(AiTranslateValueChangedEventArgs e) + { + var value = e.IsOriginal ? "original" : e.Language.ToString(); + if (!Equals(Preferred.SelectedValue, value)) + { + Preferred.SelectedValue = value; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/AiWorkflowDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/AiWorkflowDemoData.cs new file mode 100644 index 00000000..537d3372 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/AiWorkflowDemoData.cs @@ -0,0 +1,247 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The support flow the AI workflow demos share: a new email, a Claude Sonnet 5.5 step that labels it, a branch on the +/// label, then a chat message and an issue on the urgent path or a drafted reply and a ticket on the other one; and the +/// add step catalog. The models are real (); every person, company, address, key and number +/// is invented. Demo content only. +/// +internal static class AiWorkflowDemoData +{ + public const string Title = "Escalate urgent support email"; + + public const string Description = "Runs on every new email to support@example.com"; + + /// + /// The app of the model steps: Anthropic, whose Claude models label, draft and summarize here. The mark is a neutral + /// sparkle in the accent; no brand mark is drawn. + /// + public static AiWorkflowApp Claude { get; } = + new(AiModelCatalog.Anthropic.Key, AiModelCatalog.Anthropic.Name, ArcIcons.Sparkles, isAccent: true); + + private static string Classifier => AiModelCatalog.ClaudeSonnet55.Name; + + private static string Writer => AiModelCatalog.ClaudeHaiku55.Name; + + /// The sample flow, a fresh copy on every read (a demo may change its copy). + public static List Steps => + [ + new() + { + Id = "trigger", + Kind = AiWorkflowStepKind.Trigger, + App = AiWorkflowApp.Mail, + Title = "New email", + Detail = "support@example.com", + Activity = "Fetching the latest email", + Duration = TimeSpan.FromMilliseconds(420), + Output = new AiWorkflowEmailOutput + { + From = "Dana Whitfield", + Address = "dana@contoso.example", + Subject = "Checkout fails with error 502 since this morning", + Preview = "Hi team, since about 9:00 every checkout on our store ends in a 502 error. Customers are writing to us and we are losing orders. Can someone look at this today?" + } + }, + new() + { + Id = "classify", + Kind = AiWorkflowStepKind.Ai, + App = Claude, + Title = "Classify with " + Classifier, + Detail = "Label urgency and topic", + Activity = "Reading the email and choosing a label", + Duration = TimeSpan.FromMilliseconds(1240), + Output = new AiWorkflowLabelOutput + { + Label = "Urgent", + Confidence = 0.94, + Reason = "Checkout fails for every customer since this morning, which stops orders, and the sender asks for a fix today." + } + }, + new() + { + Id = "route", + Kind = AiWorkflowStepKind.Branch, + App = AiWorkflowApp.Branch, + Title = "Route by label", + Detail = "If the label is Urgent", + Activity = "Checking the label", + Duration = TimeSpan.FromMilliseconds(180), + Output = new AiWorkflowRouteOutput { PathId = "urgent", Note = "Label is Urgent" }, + Paths = + [ + new() + { + Id = "urgent", + Label = "Urgent", + Steps = + [ + new() + { + Id = "post", + Kind = AiWorkflowStepKind.Action, + App = AiWorkflowApp.Chat, + Title = "Post message", + Detail = "#support", + Activity = "Posting to #support", + Duration = TimeSpan.FromMilliseconds(560), + Output = new AiWorkflowMessageOutput + { + Channel = "#support", + Author = "Workflow bot", + Text = "Urgent: checkout fails with error 502 for every customer since 9:00. Dana Whitfield (Contoso) is waiting for an update." + } + }, + new() + { + Id = "issue", + Kind = AiWorkflowStepKind.Action, + App = AiWorkflowApp.Tracker, + Title = "Create issue", + Detail = "Support team", + Activity = "Creating the issue", + Duration = TimeSpan.FromMilliseconds(700), + Output = new AiWorkflowRecordOutput + { + Id = "SUP-1042", + Title = "Checkout fails with error 502", + Meta = "Urgent, assigned to On call" + } + } + ] + }, + new() + { + Id = "other", + Label = "Everything else", + Steps = + [ + new() + { + Id = "draft", + Kind = AiWorkflowStepKind.Ai, + App = Claude, + Title = "Draft a reply", + Detail = Writer, + Activity = "Writing a reply", + Duration = TimeSpan.FromMilliseconds(980), + Output = new AiWorkflowTextOutput + { + Text = "Thanks for writing in. We have your message and someone from the team will get back to you within one business day." + } + }, + new() + { + Id = "ticket", + Kind = AiWorkflowStepKind.Action, + App = AiWorkflowApp.Helpdesk, + Title = "Open ticket", + Detail = "Normal priority", + Activity = "Opening the ticket", + Duration = TimeSpan.FromMilliseconds(520), + Output = new AiWorkflowRecordOutput { Id = "#4821", Title = "Question from Dana Whitfield", Meta = "Normal priority" } + } + ] + } + ] + } + ]; + + /// The add step catalog (four triggers, two model steps, six actions and a branch), a fresh copy on every read. + public static List Catalog => + [ + new() + { + Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Mail, Title = "New email", Detail = "support@example.com", + Activity = "Fetching the latest email", Duration = TimeSpan.FromMilliseconds(420), + Output = new AiWorkflowEmailOutput + { + From = "Dana Whitfield", Address = "dana@contoso.example", Subject = "Checkout fails with error 502 since this morning", + Preview = "Hi team, since about 9:00 every checkout on our store ends in a 502 error." + } + }, + new() + { + Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Schedule, Title = "On a schedule", Detail = "Weekdays at 9:00", + Activity = "Waiting for 9:00", Duration = TimeSpan.FromMilliseconds(300), + Output = new AiWorkflowTextOutput { Text = "Started at 9:00 on Monday." } + }, + new() + { + Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Payments, Title = "Payment failed", Detail = "Any customer", + Activity = "Fetching the latest event", Duration = TimeSpan.FromMilliseconds(380), + Output = new AiWorkflowRecordOutput { Id = "evt_8f2k", Title = "Card declined for Fabrikam", Meta = "Sample amount 120.00" } + }, + new() + { + Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Webhook, Title = "Webhook received", Detail = "POST /hooks/support", + Activity = "Waiting for a request", Duration = TimeSpan.FromMilliseconds(260), + Output = new AiWorkflowTextOutput { Text = "{ \"type\": \"ticket.created\", \"id\": 4821 }" } + }, + new() + { + Kind = AiWorkflowStepKind.Ai, App = Claude, Title = "Summarize with " + Classifier, Detail = "Three bullet points", + Activity = "Summarizing", Duration = TimeSpan.FromMilliseconds(1100), + Output = new AiWorkflowTextOutput { Text = "Checkout fails with a 502 for every customer. It started around 9:00. The sender asks for a fix today." } + }, + new() + { + Kind = AiWorkflowStepKind.Ai, App = Claude, Title = "Extract fields with " + Writer, Detail = "Order number, company", + Activity = "Extracting fields", Duration = TimeSpan.FromMilliseconds(860), + Output = new AiWorkflowRecordOutput { Id = "fields", Title = "Company: Contoso, order: none given", Meta = "2 of 2 fields found" } + }, + new() + { + Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Chat, Title = "Post message", Detail = "#support", + Activity = "Posting to #support", Duration = TimeSpan.FromMilliseconds(560), + Output = new AiWorkflowMessageOutput { Channel = "#support", Author = "Workflow bot", Text = "New email from Dana Whitfield: checkout fails with error 502." } + }, + new() + { + Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Tracker, Title = "Create issue", Detail = "Support team", + Activity = "Creating the issue", Duration = TimeSpan.FromMilliseconds(700), + Output = new AiWorkflowRecordOutput { Id = "SUP-1043", Title = "Checkout fails with error 502", Meta = "Assigned to On call" } + }, + new() + { + Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Docs, Title = "Create page", Detail = "Incident notes", + Activity = "Creating the page", Duration = TimeSpan.FromMilliseconds(640), + Output = new AiWorkflowRecordOutput { Id = "Incident notes", Title = "Checkout 502, morning of the incident", Meta = "In Support / Incidents" } + }, + new() + { + Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Sheets, Title = "Add row", Detail = "Escalations log", + Activity = "Adding a row", Duration = TimeSpan.FromMilliseconds(480), + Output = new AiWorkflowRecordOutput { Id = "Row 128", Title = "Contoso, Urgent, checkout", Meta = "Escalations log" } + }, + new() + { + Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Crm, Title = "Update contact", Detail = "Add a note", + Activity = "Updating Dana Whitfield", Duration = TimeSpan.FromMilliseconds(520), + Output = new AiWorkflowRecordOutput { Id = "Dana Whitfield", Title = "Note added: checkout incident", Meta = "Contoso" } + }, + new() + { + Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Repo, Title = "Open issue", Detail = "web-store", + Activity = "Opening the issue", Duration = TimeSpan.FromMilliseconds(620), + Output = new AiWorkflowRecordOutput { Id = "#982", Title = "502 on checkout", Meta = "web-store" } + }, + new() + { + Kind = AiWorkflowStepKind.Branch, App = AiWorkflowApp.Branch, Title = "Branch", Detail = "Split by a condition", + Activity = "Checking the condition", Duration = TimeSpan.FromMilliseconds(180), + Output = new AiWorkflowRouteOutput { PathId = "yes", Note = "Condition met" }, + Paths = [new() { Id = "yes", Label = "Yes" }, new() { Id = "no", Label = "No" }] + } + ]; + + /// A simulation of the sample flow with its catalog (the code-behind demos use it; the XAML ones spell it out). + public static AiWorkflowSimulation Simulation() => new() + { + Title = Title, + Description = Description, + Steps = Steps, + Catalog = Catalog, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Narrow.axaml new file mode 100644 index 00000000..115a8fa9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/OwnRunner.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/OwnRunner.axaml new file mode 100644 index 00000000..ce53af38 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/OwnRunner.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/OwnRunner.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/OwnRunner.axaml.cs new file mode 100644 index 00000000..d61c08f7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/OwnRunner.axaml.cs @@ -0,0 +1,61 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A flow of its own with a runner that returns outputs after a delay and fails the chat step once. +public partial class AiWorkflowOwnRunnerDemo : UserControl +{ + public AiWorkflowOwnRunnerDemo() + { + InitializeComponent(); + Flow.Steps = + [ + new AiWorkflowStep + { + Id = "signup", Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Webhook, Title = "New signup", + Detail = "POST /hooks/signup", Activity = "Waiting for a signup" + }, + new AiWorkflowStep + { + Id = "extract", Kind = AiWorkflowStepKind.Ai, App = AiWorkflowDemoData.Claude, + Title = "Find the company with " + AiModelCatalog.ClaudeHaiku55.Name, + Detail = "From the email domain", Activity = "Looking up the domain" + }, + new AiWorkflowStep + { + Id = "crm", Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Crm, Title = "Update contact", + Detail = "Company and size", Activity = "Saving the contact" + }, + new AiWorkflowStep + { + Id = "notify", Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Chat, Title = "Post message", + Detail = "#sales", Activity = "Posting to #sales" + } + ]; + Flow.RunStep = RunAsync; + Flow.StepsChanged += (_, e) => Log.Text = $"{e.Kind}: {e.Step.Title}. The flow has {e.Steps.Count} top-level steps."; + Flow.RunCompleted += (_, e) => Log.Text = e.Status == AiWorkflowRunStatus.Passed + ? $"Passed in {e.Duration.TotalSeconds:0.0}s with {e.Outputs.Count} outputs." + : $"Failed at {e.FailedStepId}."; + } + + // Stands in for a call to your test endpoint. Pass the cancellation token on so Stop ends it at once. + private static async Task RunAsync(AiWorkflowStep step, AiWorkflowRunStepContext context) + { + await Task.Delay(step.Kind == AiWorkflowStepKind.Ai ? 1100 : 500, context.Cancellation); + return step.Id switch + { + "signup" => new AiWorkflowRecordOutput { Id = "usr_4821", Title = "maya@fabrikam.example", Meta = "Signed up from the pricing page" }, + "extract" => new AiWorkflowLabelOutput + { + Label = "Fabrikam", + Confidence = 0.88, + Reason = "The email domain matches the company website, and the sample record lists about 200 people." + }, + "crm" => new AiWorkflowRecordOutput { Id = "Maya Chen", Title = "Company set to Fabrikam", Meta = "Sample size: 200" }, + "notify" when context.Attempt == 1 => throw new InvalidOperationException("The chat app did not answer. Try again."), + "notify" => new AiWorkflowMessageOutput { Channel = "#sales", Author = "Workflow bot", Text = "New signup from Fabrikam: Maya Chen." }, + _ => new AiWorkflowTextOutput { Text = "Done." } + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Preview.axaml new file mode 100644 index 00000000..1ec720bd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Preview.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Preview.axaml.cs new file mode 100644 index 00000000..8718a556 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/Preview.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated sample flow playing, a failing step with Retry, the empty state and loading. +public partial class AiWorkflowPreviewDemo : UserControl +{ + private readonly bool _ready; + + public AiWorkflowPreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready) + { + Host.Child = Create(Scenario.SelectedValue as string ?? "sample"); + } + } + + private static AiWorkflow Create(string scenario) + { + switch (scenario) + { + case "failing": + { + // The sample flow, but creating the issue fails on the first try; Retry resumes the run from it. + var steps = AiWorkflowDemoData.Steps; + steps[2].Paths[0].Steps[1].Error = "The tracker token has expired. Reconnect it, then retry."; + var flow = new AiWorkflow + { + Title = AiWorkflowDemoData.Title, + Description = AiWorkflowDemoData.Description, + Steps = steps, + Catalog = AiWorkflowDemoData.Catalog, + AutoPlay = false + }; + flow.AttachedToVisualTree += (_, _) => flow.StartRun(); + return flow; + } + case "empty": + return new AiWorkflow + { + Title = "New workflow", Description = "Pick a trigger to start", Steps = [], Catalog = AiWorkflowDemoData.Catalog, + AutoPlay = false + }; + case "loading": + return new AiWorkflow { Title = AiWorkflowDemoData.Title, Description = "Loading the flow", IsLoading = true, AutoPlay = false }; + default: + return new AiWorkflow { Simulation = AiWorkflowDemoData.Simulation() }; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/ReadOnly.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/ReadOnly.axaml new file mode 100644 index 00000000..696f9d9a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AiWorkflow/ReadOnly.axaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/AskYourDataDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/AskYourDataDemoData.cs new file mode 100644 index 00000000..0401369c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/AskYourDataDemoData.cs @@ -0,0 +1,196 @@ +using System.Globalization; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The model and database the ask your data demos play: an invented product's signups, MRR and churn by plan on an +/// "analytics" PostgreSQL database. A question about churn, MRR (or revenue) or signups gets its dataset; any other +/// question runs a search that matches no rows. Every number is invented. Demo content only. +/// +internal static class AskYourDataDemoData +{ + private static readonly CultureInfo s_enUs = CultureInfo.GetCultureInfo("en-US"); + + /// The question the card starts with. + public const string Question = "Weekly signups by plan since August"; + + /// The suggestions offered in the empty state. + public static IReadOnlyList Suggestions { get; } = + [ + "Weekly signups by plan since August", + "MRR by plan over the last 6 months", + "Churn rate by plan per month", + ]; + + /// The database the answers come from. + public static AskYourDataSource Source { get; } = new() { Name = "analytics", Dialect = "PostgreSQL" }; + + /// The three answers, in the order they are matched: churn, then MRR or revenue, then signups. + public static IList Answers { get; } = + [ + new() + { + Keywords = ["churn"], + Sql = """ + WITH monthly AS ( + SELECT date_trunc('month', m.month)::date AS month, + p.name AS plan, + count(*) FILTER (WHERE m.churned) AS churned, + count(*) AS active + FROM account_months m + JOIN plans p ON p.id = m.plan_id + WHERE m.month >= date '2026-05-01' AND p.name <> 'Free' + GROUP BY 1, 2 + ) + SELECT month, plan, round(100.0 * churned / active, 1) AS churn_rate + FROM monthly + ORDER BY month, plan; + """, + RowsScanned = 9_630, + Result = Churn(), + }, + new() + { + Keywords = ["mrr", "revenue"], + Sql = """ + SELECT + date_trunc('month', s.period_start)::date AS month, + p.name AS plan, + sum(s.amount_cents) / 100.0 AS mrr + FROM subscriptions s + JOIN plans p ON p.id = s.plan_id + WHERE s.status = 'active' + AND s.period_start >= date '2026-05-01' + AND p.name <> 'Free' + GROUP BY month, plan + ORDER BY month, plan; + """, + RowsScanned = 4_812, + Result = Mrr(), + }, + new() + { + Keywords = ["signup", "sign up", "sign-up"], + Sql = """ + SELECT + date_trunc('week', u.created_at)::date AS week, + p.name AS plan, + count(*) AS signups + FROM users u + JOIN plans p ON p.id = u.plan_id + WHERE u.created_at >= date '2026-08-03' + GROUP BY week, plan + ORDER BY week, plan; + """, + RowsScanned = 18_342, + Result = Signups(), + }, + ]; + + /// Any other question: a search for its longest word that matches no rows. + public static AskYourDataSimulationAnswer Fallback { get; } = new() + { + Sql = "SELECT e.name, count(*) AS events\nFROM events e\nWHERE e.name ILIKE '%" + AskYourDataSimulationAnswer.KeywordToken + + "%'\nGROUP BY e.name\nORDER BY events DESC\nLIMIT 50;", + RowsScanned = 2_406, + Result = new AskYourDataResult { Title = string.Empty, RowCount = 0, DurationMs = 38 }, + }; + + /// A new simulation of the model and database (tests and code use it; the XAML demos spell it out). + public static AskYourDataSimulation Simulation() => new() + { + Question = Question, + Suggestions = Suggestions, + Source = Source, + Answers = Answers, + Fallback = Fallback, + }; + + /// Weekly signups by plan, 12 weeks from Aug 3: stacked bars with totals. + public static AskYourDataResult Signups() + { + string[] weeks = ["Aug 3", "Aug 10", "Aug 17", "Aug 24", "Aug 31", "Sep 7", "Sep 14", "Sep 21", "Sep 28", "Oct 5", "Oct 12", "Oct 19"]; + double[] free = [52, 61, 58, 66, 70, 64, 73, 79, 75, 84, 88, 93]; + double[] pro = [21, 26, 24, 27, 30, 29, 33, 35, 34, 38, 41, 44]; + double[] team = [8, 10, 9, 11, 12, 11, 13, 14, 15, 16, 17, 19]; + double[] enterprise = [1, 2, 1, 2, 3, 2, 2, 3, 3, 4, 3, 4]; + var total = free.Sum() + pro.Sum() + team.Sum() + enterprise.Sum(); + return new AskYourDataResult + { + Title = "Weekly signups by plan", + Subtitle = "Aug 3 to Oct 25 · " + total.ToString("N0", s_enUs) + " signups", + Categories = weeks, + Series = + [ + new AskYourDataSeries { Id = "free", Label = "Free", Values = free }, + new AskYourDataSeries { Id = "pro", Label = "Pro", Values = pro }, + new AskYourDataSeries { Id = "team", Label = "Team", Values = team }, + new AskYourDataSeries { Id = "enterprise", Label = "Enterprise", Values = enterprise }, + ], + CategoryLabel = "Week", + Insight = "Weekly signups nearly doubled, from 82 to 160. Free drives the volume, and Pro more than doubled, from 21 to 44 a week.", + RowCount = 48, + DurationMs = 214, + }; + } + + /// MRR by paid plan, May to October: lines, the legend shows the latest month. + public static AskYourDataResult Mrr() + { + string[] months = ["May", "Jun", "Jul", "Aug", "Sep", "Oct"]; + double[] pro = [31_200, 33_000, 34_900, 36_100, 38_400, 40_200]; + double[] team = [18_400, 19_900, 21_500, 23_800, 25_100, 27_600]; + double[] enterprise = [42_000, 42_000, 48_500, 48_500, 55_000, 61_300]; + return new AskYourDataResult + { + Title = "MRR by plan", + Subtitle = "May to Oct · paid plans", + Categories = months, + Series = + [ + new AskYourDataSeries { Id = "pro", Label = "Pro", Values = pro }, + new AskYourDataSeries { Id = "team", Label = "Team", Values = team }, + new AskYourDataSeries { Id = "enterprise", Label = "Enterprise", Values = enterprise }, + ], + LegendValue = AskYourDataLegendValue.Last, + Chart = AskYourDataChartKind.Line, + CategoryLabel = "Month", + FormatValue = Thousands, + Insight = "MRR reached $129.1k in October, up from $91.6k in May. Enterprise added the most, $19.3k, as two annual contracts started.", + RowCount = 18, + DurationMs = 1240, + }; + } + + /// Monthly churn rate by paid plan: rates, so bars group side by side and nothing is totalled. + public static AskYourDataResult Churn() + { + string[] months = ["May", "Jun", "Jul", "Aug", "Sep", "Oct"]; + double[] pro = [4.1, 3.9, 4.4, 3.6, 3.4, 3.1]; + double[] team = [2.8, 2.6, 2.9, 2.4, 2.2, 2.0]; + double[] enterprise = [0.9, 1.1, 0.8, 0.7, 0.9, 0.6]; + return new AskYourDataResult + { + Title = "Monthly churn rate by plan", + Subtitle = "May to Oct · paid plans", + Categories = months, + Series = + [ + new AskYourDataSeries { Id = "pro", Label = "Pro", Values = pro }, + new AskYourDataSeries { Id = "team", Label = "Team", Values = team }, + new AskYourDataSeries { Id = "enterprise", Label = "Enterprise", Values = enterprise }, + ], + Stacked = false, + CategoryLabel = "Month", + FormatValue = Percent, + Insight = "Churn fell on every paid plan since May. Pro is still the highest at 3.1%, down from 4.1%.", + RowCount = 18, + DurationMs = 87, + }; + } + + private static string Thousands(double value) => + value >= 1000 ? "$" + (value / 1000).ToString("0.#", s_enUs) + "k" : "$" + value.ToString("0", s_enUs); + + private static string Percent(double value) => value.ToString("0.0", s_enUs) + "%"; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Errors.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Errors.axaml new file mode 100644 index 00000000..a4e414eb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Errors.axaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Errors.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Errors.axaml.cs new file mode 100644 index 00000000..0708c785 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Errors.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A query that fails once with a database error, then answers (simulated). +public partial class AskYourDataErrorsDemo : UserControl +{ + private int _attempts; + + public AskYourDataErrorsDemo() + { + InitializeComponent(); + Card.Source = AskYourDataDemoData.Source; + Card.Suggestions = []; + Card.GenerateSql = _ => Task.FromResult(""" + SELECT date_trunc('week', o.refunded_at)::date AS week, + count(*) AS refunds + FROM orders o + WHERE o.refunded_at >= date_trunc('quarter', now()) + GROUP BY week + ORDER BY week; + """); + Card.RunQuery = RunAsync; + } + + private async Task RunAsync(AskYourDataQueryRequest request) + { + request.ReportProgress(0, 8_420); + await Task.Delay(700, request.CancellationToken); + if (++_attempts % 2 == 1) + { + throw new InvalidOperationException( + "ERROR: column o.refunded_at does not exist\nHINT: Perhaps you meant to reference the column \"o.refund_at\"."); + } + request.ReportProgress(8_420, 8_420); + return new AskYourDataResult + { + Title = "Refunds by week", + Subtitle = "This quarter · sample data", + Categories = ["Oct 5", "Oct 12", "Oct 19", "Oct 26"], + Series = [new AskYourDataSeries { Id = "refunds", Label = "Refunds", Values = [14, 9, 12, 7] }], + CategoryLabel = "Week", + Insight = "Refunds fell to 7 last week, the lowest this quarter.", + RowCount = 4, + DurationMs = 702, + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Narrow.axaml new file mode 100644 index 00000000..d232ecf1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Narrow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Preview.axaml new file mode 100644 index 00000000..750dc95d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Preview.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Streaming.axaml new file mode 100644 index 00000000..d34eb971 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Streaming.axaml @@ -0,0 +1,13 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Streaming.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Streaming.axaml.cs new file mode 100644 index 00000000..2a1ad577 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/AskYourData/Streaming.axaml.cs @@ -0,0 +1,71 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A card wired to a streaming SQL writer and a query runner that reports progress (both simulated). +public partial class AskYourDataStreamingDemo : UserControl +{ + private const string Query = """ + SELECT + 'Q' || extract(quarter FROM o.placed_at) AS quarter, + r.name AS region, + round(avg(o.total_cents) / 100.0, 2) AS avg_order_value + FROM orders o + JOIN regions r ON r.id = o.region_id + WHERE o.placed_at >= date '2026-01-01' + GROUP BY quarter, region + ORDER BY quarter, region; + """; + + public AskYourDataStreamingDemo() + { + InitializeComponent(); + Card.Source = new AskYourDataSource { Name = "shop", Dialect = "PostgreSQL" }; + Card.Suggestions = []; + Card.StreamSql = request => StreamAsync(request.CancellationToken); + Card.RunQuery = RunAsync; + } + + /// The model's SQL in small chunks, as a streaming API delivers it. + private static async IAsyncEnumerable StreamAsync([EnumeratorCancellation] CancellationToken token) + { + await Task.Delay(300, token); + for (var i = 0; i < Query.Length; i += 9) + { + await Task.Delay(45, token); + yield return Query.Substring(i, Math.Min(9, Query.Length - i)); + } + } + + /// The query: about a second of scanning with progress, then values by region (not stacked: bars group). + private static async Task RunAsync(AskYourDataQueryRequest request) + { + const long total = 61_204; + for (var step = 1; step <= 8; step++) + { + await Task.Delay(120, request.CancellationToken); + request.ReportProgress(total * step / 8, total); + } + var money = CultureInfo.GetCultureInfo("en-US"); + return new AskYourDataResult + { + Title = "Average order value by region", + Subtitle = "2026 by quarter · sample data", + Categories = ["Q1", "Q2", "Q3", "Q4"], + Series = + [ + new AskYourDataSeries { Id = "north", Label = "North", Values = [84.2, 86.9, 91.4, 95.0] }, + new AskYourDataSeries { Id = "south", Label = "South", Values = [72.5, 74.1, 73.8, 79.6] }, + new AskYourDataSeries { Id = "west", Label = "West", Values = [91.0, 89.3, 94.7, 101.2] }, + ], + Stacked = false, + CategoryLabel = "Quarter", + FormatValue = value => value.ToString("$0.00", money), + Insight = "West leads every quarter and passed $100 in Q4. South trails by about $20 but grew 10% since Q1.", + RowCount = 12, + DurationMs = 960, + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/HostDriven.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/HostDriven.axaml new file mode 100644 index 00000000..8a1c8b55 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/HostDriven.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/HostDriven.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/HostDriven.axaml.cs new file mode 100644 index 00000000..8260c8f2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/HostDriven.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app drives the stage from a pretend request (a timer stands in for the network). +public partial class BuildingBlocksHostDrivenDemo : UserControl +{ + private readonly DispatcherTimer _request = new() { Interval = TimeSpan.FromSeconds(7) }; + private readonly bool _ready; + + public BuildingBlocksHostDrivenDemo() + { + InitializeComponent(); + Blocks.Labels = new BuildingBlocksLabels { Ready = "Your CRM is ready" }; + _request.Tick += (_, _) => Finish(); + DetachedFromVisualTree += (_, _) => _request.Stop(); + _ready = true; + } + + private void OnGenerate(object? sender, RoutedEventArgs e) => Start(); + + private void OnReplayRequested(object? sender, RoutedEventArgs e) => Start(); + + private void Start() + { + _request.Stop(); + Blocks.Status = BuildingBlocksStatus.Building; + Generate.IsEnabled = false; + _request.Start(); + } + + private void Finish() + { + _request.Stop(); + Blocks.Status = BuildingBlocksStatus.Done; + Generate.IsEnabled = true; + } + + private void OnPhaseChanged(object? sender, BuildingBlocksPhaseChangedEventArgs e) + { + // The first phase is set while the XAML loads, before the log exists. + if (_ready) + { + Log.Text = $"Phase: {e.Phase}"; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/NoLabel.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/NoLabel.axaml new file mode 100644 index 00000000..b5ed622b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/NoLabel.axaml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/PlayOnce.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/PlayOnce.axaml new file mode 100644 index 00000000..8b8e4470 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/PlayOnce.axaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Preview.axaml new file mode 100644 index 00000000..9bb3e9ec --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Preview.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Preview.axaml.cs new file mode 100644 index 00000000..9872c27d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Preview.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated build, and the three statuses an app sets. +public partial class BuildingBlocksPreviewDemo : UserControl +{ + private readonly bool _ready; + + public BuildingBlocksPreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (!_ready) + { + return; + } + Blocks.Status = (Scenario.SelectedValue as string) switch + { + "idle" => BuildingBlocksStatus.Idle, + "building" => BuildingBlocksStatus.Building, + "done" => BuildingBlocksStatus.Done, + _ => null + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/ReducedMotion.axaml new file mode 100644 index 00000000..bd763d3a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/ReducedMotion.axaml @@ -0,0 +1,10 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Sizes.axaml new file mode 100644 index 00000000..63dede2f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Sizes.axaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Tones.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Tones.axaml new file mode 100644 index 00000000..10890546 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/BuildingBlocks/Tones.axaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/CodeSuggestionDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/CodeSuggestionDemoData.cs new file mode 100644 index 00000000..0f6ebcd9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/CodeSuggestionDemoData.cs @@ -0,0 +1,126 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented suggestions the code suggestion demos share: a search hook that fires a request on every keystroke with +/// two hunks that skip blank queries and debounce and cancel stale requests, and the simulated one of the Preview, a +/// billing helper that gains retries with backoff. Demo content only; the model is a real one from +/// . +/// +internal static class CodeSuggestionDemoData +{ + public const string FileName = "use-search.ts"; + + public const string FilePath = "src/hooks"; + + public const string Instruction = "Debounce the search"; + + public const string Code = + "import { useEffect, useState } from \"react\";\n" + + "\n" + + "export function useSearch(query: string) {\n" + + " const [results, setResults] = useState([]);\n" + + " useEffect(() => {\n" + + " fetch(`/api/search?q=${query}`)\n" + + " .then((res) => res.json())\n" + + " .then(setResults);\n" + + " }, [query]);\n" + + " return results;\n" + + "}"; + + /// The two hunks, complete (a fresh copy each call). + public static CodeSuggestionHunk[] Hunks() => + [ + new("guard", 4, 1, + " useEffect(() => {", + " if (!query.trim()) {", + " setResults([]);", + " return;", + " }") { Summary = "Skip blank queries" }, + new("debounce", 5, 3, + " const controller = new AbortController();", + " const timer = setTimeout(() => {", + " fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })", + " .then((res) => res.json())", + " .then(setResults)", + " .catch(() => {});", + " }, 250);", + " return () => {", + " clearTimeout(timer);", + " controller.abort();", + " };") { Summary = "Debounce and cancel stale requests" }, + ]; + + /// + /// The model credited in the demos: OpenAI's GPT-6.1 Sol (checked on ), with a + /// neutral mark; no brand mark is drawn. + /// + public static AgentModel Model { get; } = new(AiModelCatalog.Gpt61Sol.Name, ArcIcons.Sparkles); + + // The simulated suggestion of the Preview -------------------------------------------------------------------------- + + public const string SimulatedFileName = "fetch-invoice.ts"; + + public const string SimulatedFilePath = "src/billing"; + + public const string SimulatedInstruction = "Add retry with backoff"; + + public const string SimulatedCode = + "import { db } from \"./db\";\n" + + "import type { Invoice } from \"./types\";\n" + + "\n" + + "export async function fetchInvoice(id: string): Promise {\n" + + " const url = `${process.env.BILLING_API}/invoices/${id}`;\n" + + " const res = await fetch(url);\n" + + " if (!res.ok) {\n" + + " throw new Error(`Invoice ${id} failed: ${res.status}`);\n" + + " }\n" + + " const invoice = (await res.json()) as Invoice;\n" + + " await db.invoices.upsert(invoice);\n" + + " return invoice;\n" + + "}"; + + /// The three hunks the simulation streams (it copies them, so the list is shared). + public static readonly IReadOnlyList SimulatedHunks = + [ + new("limits", 2, 0, + "", + "const MAX_ATTEMPTS = 4;", + "const BASE_DELAY_MS = 250;") { Summary = "Add retry limits" }, + new("backoff", 5, 4, + " let res = await fetch(url);", + " for (let attempt = 1; attempt < MAX_ATTEMPTS; attempt++) {", + " if (res.ok || (res.status !== 429 && res.status < 500)) break;", + " await sleep(BASE_DELAY_MS * 2 ** (attempt - 1));", + " res = await fetch(url);", + " }", + " if (!res.ok) {", + " throw new Error(`Invoice ${id} failed after ${MAX_ATTEMPTS} tries: ${res.status}`);", + " }") { Summary = "Retry 429 and 5xx with backoff" }, + new("sleep", 13, 0, + "", + "function sleep(ms: number) {", + " return new Promise((resolve) => setTimeout(resolve, ms));", + "}") { Summary = "Add a sleep helper" }, + ]; + + /// Points the card at the simulated suggestion's file (the simulation itself holds the code and the hunks). + public static void ShowSimulated(CodeSuggestion card) + { + card.FileName = SimulatedFileName; + card.FilePath = SimulatedFilePath; + card.Instruction = SimulatedInstruction; + card.Model = Model; + } + + /// Points the card at the demo file. + public static void Show(CodeSuggestion card) + { + card.FileName = FileName; + card.FilePath = FilePath; + card.Instruction = Instruction; + card.Model = Model; + card.Hunks.Clear(); + card.Hunks.AddRange(Hunks()); + card.Code = Code; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/LongFile.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/LongFile.axaml new file mode 100644 index 00000000..36cf38e3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/LongFile.axaml @@ -0,0 +1,13 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/LongFile.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/LongFile.axaml.cs new file mode 100644 index 00000000..ffa9387a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/LongFile.axaml.cs @@ -0,0 +1,44 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A longer file in a capped code area. +public partial class CodeSuggestionLongFileDemo : UserControl +{ + public CodeSuggestionLongFileDemo() + { + InitializeComponent(); + Card.FileName = "settings.py"; + Card.FilePath = "app/core"; + Card.Instruction = "Read the timeouts from the environment"; + Card.Code = string.Join('\n', + [ + "\"\"\"Runtime settings for the worker.\"\"\"", + "", + "import os", + "", + "QUEUE_URL = os.environ[\"QUEUE_URL\"]", + "BATCH_SIZE = 50", + "", + "# Seconds before a job is retried.", + "RETRY_AFTER = 30", + "", + "# Seconds before an HTTP call gives up.", + "HTTP_TIMEOUT = 10", + "", + "LOG_LEVEL = \"info\"", + "SENTRY_DSN = None", + "", + "def describe() -> str:", + " return f\"batch={BATCH_SIZE} retry={RETRY_AFTER}s timeout={HTTP_TIMEOUT}s\"", + ]); + Card.Hunks.Add(new CodeSuggestionHunk("retry", 8, 1, "RETRY_AFTER = int(os.environ.get(\"RETRY_AFTER\", \"30\"))") + { + Summary = "Read the retry delay", + }); + Card.Hunks.Add(new CodeSuggestionHunk("timeout", 11, 1, "HTTP_TIMEOUT = float(os.environ.get(\"HTTP_TIMEOUT\", \"10\"))") + { + Summary = "Read the HTTP timeout", + }); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Narrow.axaml new file mode 100644 index 00000000..14428805 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Narrow.axaml @@ -0,0 +1,11 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Narrow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Narrow.axaml.cs new file mode 100644 index 00000000..59627d68 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Narrow.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The demo suggestion in a narrow column. +public partial class CodeSuggestionNarrowDemo : UserControl +{ + public CodeSuggestionNarrowDemo() + { + InitializeComponent(); + CodeSuggestionDemoData.Show(Card); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Preview.axaml new file mode 100644 index 00000000..2245fb22 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Preview.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Preview.axaml.cs new file mode 100644 index 00000000..928732e0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Preview.axaml.cs @@ -0,0 +1,76 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: a simulated suggestion, one to review, a failed one and the empty card. +public partial class CodeSuggestionPreviewDemo : UserControl +{ + private readonly CodeSuggestionSimulation _simulation; + private readonly bool _ready; + + public CodeSuggestionPreviewDemo() + { + InitializeComponent(); + _simulation = Card.Simulation!; + Card.RegenerateRequested += OnRegenerate; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "live"); + } + } + + private void Show(string scenario) + { + switch (scenario) + { + case "review": + CodeSuggestionDemoData.Show(Card); + Card.AllowsRegenerate = true; + Card.Status = CodeSuggestionStatus.Ready; + break; + case "failed": + CodeSuggestionDemoData.Show(Card); + Card.AllowsRegenerate = true; + Card.Status = CodeSuggestionStatus.Error; + break; + case "empty": + // No code, no hunks and no simulation: an empty file. + Clear(); + Card.ClearValue(CodeSuggestion.FileNameProperty); + Card.ClearValue(CodeSuggestion.FilePathProperty); + Card.ClearValue(CodeSuggestion.InstructionProperty); + Card.ClearValue(CodeSuggestion.ModelProperty); + Card.Simulation = null; + break; + default: + // No code and no hunks: the card plays its simulation from the top. + CodeSuggestionDemoData.ShowSimulated(Card); + Clear(); + Card.Simulation = _simulation; + break; + } + } + + private void Clear() + { + Card.Hunks.Clear(); + Card.ClearValue(CodeSuggestion.CodeProperty); + Card.ClearValue(CodeSuggestion.StatusProperty); + Card.ClearValue(CodeSuggestion.AllowsRegenerateProperty); + } + + private void OnRegenerate(object? sender, RoutedEventArgs e) + { + // A real app asks its model again; the demo shows the same suggestion afresh (thinking clears the decisions). + Card.Status = CodeSuggestionStatus.Thinking; + Card.Status = CodeSuggestionStatus.Ready; + Scenario.SelectedValue = "review"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Streaming.axaml new file mode 100644 index 00000000..58184cdb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Streaming.axaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Streaming.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Streaming.axaml.cs new file mode 100644 index 00000000..0002ae1a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CodeSuggestion/Streaming.axaml.cs @@ -0,0 +1,99 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// Streams the demo suggestion through the public API the way an app would while tokens arrive: Thinking, then each hunk +/// appended with no lines and its lines appended one by one, then Ready. +/// +public partial class CodeSuggestionStreamingDemo : UserControl +{ + private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(60) }; + private CodeSuggestionHunk[] _script = []; + private int _hunk; + private int _line; + private int _wait; + + public CodeSuggestionStreamingDemo() + { + InitializeComponent(); + _timer.Tick += OnTick; + AttachedToVisualTree += (_, _) => Run(); + DetachedFromVisualTree += (_, _) => _timer.Stop(); + } + + private void Run() + { + _timer.Stop(); + _script = CodeSuggestionDemoData.Hunks(); + Card.FileName = CodeSuggestionDemoData.FileName; + Card.FilePath = CodeSuggestionDemoData.FilePath; + Card.Instruction = CodeSuggestionDemoData.Instruction; + Card.Model = CodeSuggestionDemoData.Model; + Card.Hunks.Clear(); + Card.Code = CodeSuggestionDemoData.Code; + Card.Status = CodeSuggestionStatus.Thinking; + Log.Text = "Waiting for the model…"; + _hunk = 0; + _line = 0; + _wait = 12; + _timer.Start(); + } + + private void OnTick(object? sender, EventArgs e) + { + if (_wait-- > 0) + { + return; + } + if (Card.Status == CodeSuggestionStatus.Thinking) + { + Card.Status = CodeSuggestionStatus.Streaming; + } + if (_hunk >= _script.Length) + { + _timer.Stop(); + Card.Status = CodeSuggestionStatus.Ready; + return; + } + var source = _script[_hunk]; + if (_line == 0 && (Card.Hunks.Count == 0 || Card.Hunks[^1].Id != source.Id)) + { + // A new hunk arrives with no lines yet; its removed lines tint while the first line is on its way. + Card.Hunks.Add(new CodeSuggestionHunk(source.Id, source.Start, source.Remove) { Summary = source.Summary }); + _wait = 3; + return; + } + if (_line < source.Insert.Count) + { + Card.Hunks[^1].AppendLine(source.Insert[_line++]); + return; + } + _hunk++; + _line = 0; + } + + private void OnStop(object? sender, RoutedEventArgs e) + { + // Abort the request, drop a hunk that had not finished, and let the person review the rest. + _timer.Stop(); + if (Card.Hunks.Count > 0 && _hunk < _script.Length && Card.Hunks[^1].Insert.Count < _script[_hunk].Insert.Count) + { + Card.Hunks.RemoveAt(Card.Hunks.Count - 1); + } + Card.Status = CodeSuggestionStatus.Ready; + } + + private void OnRegenerate(object? sender, RoutedEventArgs e) => Run(); + + private void OnDecision(object? sender, CodeSuggestionDecisionEventArgs e) + { + var verb = e.Decision == CodeSuggestionDecision.Accepted ? "Accepted" : "Rejected"; + Log.Text = $"{verb} \"{e.Hunk.Summary}\". The file now has {e.Code.Split('\n').Length} lines."; + } + + private void OnResolved(object? sender, CodeSuggestionResolvedEventArgs e) => + Log.Text = $"Done: {e.Accepted.Count} accepted, {e.Rejected.Count} rejected. A real app writes {CodeSuggestionDemoData.FileName} here."; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/ConnectorSetupDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/ConnectorSetupDemoData.cs new file mode 100644 index 00000000..ee18aa7f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/ConnectorSetupDemoData.cs @@ -0,0 +1,109 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The apps the connector setup demos list: eight generic kinds of app (no real products) with an invented company's +/// accounts and counts, two of them connected, and the scripted run (Chat connects, then a CRM attempt needs admin +/// approval and is retried). The assistant is an invented one. Demo content only. +/// +internal static class ConnectorSetupDemoData +{ + /// The assistant named in the texts (an invented one). + public const string AssistantName = "Atlas"; + + /// The ids connected when the card first shows. + public static IReadOnlyList Connected { get; } = ["code", "drive"]; + + /// The scripted run: connect Chat; then a CRM attempt that needs admin approval; then its retry. + public static IList Steps { get; } = + [ + new("chat", TimeSpan.FromMilliseconds(900)), + new("crm", TimeSpan.FromMilliseconds(1200), "Needs admin approval"), + new("crm", TimeSpan.FromMilliseconds(1800)), + ]; + + /// The eight apps. + public static IReadOnlyList Apps { get; } = + [ + new("chat", "Chat", "Channels, threads and direct messages", ConnectorMarkKind.Chat) + { + Host = "chat.example.com", + Account = "tailspin.chat", + SyncCount = 1240, + SyncNoun = "messages", + Scopes = ["Read messages in channels you belong to", "Search message history", "Post replies as Atlas"], + Keywords = ["messages", "team", "channels"], + }, + new("code", "Code", "Repositories, pull requests and reviews", ConnectorMarkKind.Code) + { + Host = "code.example.com", + Account = "tailspin-toys", + SyncCount = 86, + SyncNoun = "repositories", + Scopes = ["Read code and pull requests", "Comment on pull requests", "Open draft pull requests"], + Keywords = ["git", "repos", "pull requests"], + }, + new("docs", "Docs", "Pages, wikis and meeting notes", ConnectorMarkKind.Docs) + { + Host = "docs.example.com", + Account = "Tailspin wiki", + SyncCount = 412, + SyncNoun = "pages", + Scopes = ["Read pages shared with you", "Create pages in your private space"], + Keywords = ["wiki", "notes", "pages"], + }, + new("drive", "Drive", "Files and folders shared with you", ConnectorMarkKind.Drive) + { + Host = "drive.example.com", + Account = "maya@tailspin.example", + SyncCount = 2480, + SyncNoun = "files", + Scopes = ["Read files you can open", "Search file contents"], + Keywords = ["files", "storage", "folders"], + }, + new("issues", "Issues", "Issues, projects and cycles", ConnectorMarkKind.Issues) + { + Host = "issues.example.com", + Account = "Tailspin engineering", + SyncCount = 930, + SyncNoun = "issues", + Scopes = ["Read issues and projects", "Create and update issues", "Comment as Atlas"], + Keywords = ["tickets", "bugs", "tracker"], + }, + new("calendar", "Calendar", "Events, invites and availability", ConnectorMarkKind.Calendar) + { + Host = "calendar.example.com", + Account = "maya@tailspin.example", + SyncCount = 318, + SyncNoun = "events", + Scopes = ["See your events and free time", "Draft invites for you to send"], + Keywords = ["meetings", "schedule", "events"], + }, + new("mail", "Mail", "Inbox, threads and attachments", ConnectorMarkKind.Mail) + { + Host = "mail.example.com", + Account = "maya@tailspin.example", + SyncCount = 5600, + SyncNoun = "emails", + Scopes = ["Read and search your email", "Draft replies for you to send"], + Keywords = ["email", "inbox"], + }, + new("crm", "CRM", "Accounts, contacts and deals", ConnectorMarkKind.Crm) + { + Host = "crm.example.com", + Account = "Tailspin sales", + SyncCount = 1875, + SyncNoun = "records", + Scopes = ["Read accounts, contacts and deals", "Log activity on records"], + Keywords = ["sales", "customers", "deals"], + }, + ]; + + /// A new simulation of the apps (tests and code use it; the XAML demos spell it out). + public static ConnectorSetupSimulation Simulation() => new() + { + AssistantName = AssistantName, + Apps = Apps, + Connected = Connected, + Steps = Steps, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Marks.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Marks.axaml new file mode 100644 index 00000000..5603f81b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Marks.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Onboarding.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Onboarding.axaml new file mode 100644 index 00000000..ac451171 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Onboarding.axaml @@ -0,0 +1,12 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Onboarding.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Onboarding.axaml.cs new file mode 100644 index 00000000..13ce22ac --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Onboarding.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Three apps for a first-run checklist; with no handlers the card simulates the flow. +public partial class ConnectorSetupOnboardingDemo : UserControl +{ + public ConnectorSetupOnboardingDemo() + { + InitializeComponent(); + Setup.Connectors = + [ + new ConnectorApp("chat", "Chat", "Channels and direct messages", ConnectorMarkKind.Chat) + { + SyncCount = 860, + SyncNoun = "messages", + Scopes = ["Read messages in channels you belong to", "Post replies as Atlas"], + }, + new ConnectorApp("docs", "Docs", "Pages, wikis and meeting notes", ConnectorMarkKind.Docs) + { + SyncCount = 145, + SyncNoun = "pages", + Scopes = ["Read pages shared with you"], + }, + new ConnectorApp("drive", "Drive", "Files and folders shared with you", ConnectorMarkKind.Drive) + { + SyncCount = 1320, + SyncNoun = "files", + Scopes = ["Read files you can open", "Search file contents"], + }, + ]; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Preview.axaml new file mode 100644 index 00000000..c046eb72 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Preview.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Preview.axaml.cs new file mode 100644 index 00000000..a8cc7d1a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/Preview.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The simulated card; Replay puts a fresh card with a new simulation in place so the scripted run plays again. +public partial class ConnectorSetupPreviewDemo : UserControl +{ + public ConnectorSetupPreviewDemo() + { + InitializeComponent(); + } + + private void OnReplay(object? sender, RoutedEventArgs e) + { + Host.Child = new ConnectorSetup { Simulation = ConnectorSetupDemoData.Simulation() }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/YourFlow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/YourFlow.axaml new file mode 100644 index 00000000..70c03842 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/YourFlow.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/YourFlow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/YourFlow.axaml.cs new file mode 100644 index 00000000..ef621503 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ConnectorSetup/YourFlow.axaml.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// A card with its own apps and a connect handler that stands in for an OAuth popup and a first sync (the delays are +/// invented; a real handler would await your popup and sync calls with the same token). +/// +public partial class ConnectorSetupYourFlowDemo : UserControl +{ + private bool _issuesFailed; + + public ConnectorSetupYourFlowDemo() + { + InitializeComponent(); + Setup.Connectors = + [ + new ConnectorApp("mail", "Mail", "Inbox, threads and attachments", ConnectorMarkKind.Mail) + { + Host = "mail.example.com", + SyncCount = 3200, + SyncNoun = "emails", + Scopes = ["Read and search your email", "Draft replies for you to send"], + }, + new ConnectorApp("calendar", "Calendar", "Events, invites and availability", ConnectorMarkKind.Calendar) + { + SyncCount = 240, + SyncNoun = "events", + Scopes = ["See your events and free time"], + }, + new ConnectorApp("issues", "Issues", "Issues, projects and cycles", ConnectorMarkKind.Issues) + { + SyncCount = 610, + SyncNoun = "issues", + Scopes = ["Read issues and projects", "Comment as Atlas"], + }, + ]; + Setup.ConnectedIds = ["calendar"]; + Setup.ConnectHandler = ConnectAsync; + Setup.DisconnectHandler = (_, token) => Task.Delay(500, token); + Setup.ConnectedIdsChanged += (_, e) => Show(e.Ids); + Show(Setup.ConnectedIds); + } + + private async Task ConnectAsync(ConnectorApp app, ConnectorConnectContext context) + { + var token = context.CancellationToken; + await Task.Delay(800, token); // the OAuth popup opens + context.ReportAuthorizing(); + await Task.Delay(1000, token); // the person approves + if (app.Id == "issues" && !_issuesFailed) + { + _issuesFailed = true; + throw new InvalidOperationException("Needs admin approval"); + } + var total = app.SyncCount ?? 100; + for (var synced = 0L; synced < total; synced += Math.Max(1, total / 30)) + { + context.ReportSyncing(synced, total); + await Task.Delay(50, token); + } + return new ConnectorConnectResult { Account = "maya@tailspin.example", Synced = total }; + } + + private void Show(IReadOnlyList? ids) => + Log.Text = "Connected: " + (ids is { Count: > 0 } ? string.Join(", ", ids) : "none"); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ContextWindowDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ContextWindowDemoData.cs index cbca3e23..505b18be 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ContextWindowDemoData.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ContextWindowDemoData.cs @@ -1,20 +1,81 @@ -using Avalonia.Media; -using Avalonia.Media.Immutable; +using Avalonia.Collections; namespace TKCode.Arc.Gallery.Demos; -/// An invented session of a documentation assistant, for the ContextWindow demos (made-up models, files and numbers). +/// +/// Sessions for the ContextWindow demos. The models are real, with the context window from ; +/// the output reserve is what this sample app asks for in one reply (, at most the model's +/// published output limit). The items, turns, files and token counts are invented. Each property returns new lists, so +/// every card gets its own. +/// internal static class ContextWindowDemoData { - private static readonly IBrush s_heronMark = new ImmutableSolidColorBrush(Color.Parse("#009E84")); + /// The longest reply the sample app requests, held back as the output reserve. + public const long ReplyReserve = 32_000; - public static IEnumerable Models() => + /// + /// The coding session's models: Claude Sonnet 5.5 (1M window), Grok 4.7 (500K) and Mistral Medium 3.5 (256K); the + /// session overfills the two smaller windows, so switching to them compacts. + /// + public static AvaloniaList SessionModels => + [ModelOf(AiModelCatalog.ClaudeSonnet55), ModelOf(AiModelCatalog.Grok47), ModelOf(AiModelCatalog.MistralMedium35)]; + + /// A session of 436K tokens: a 3.8K system prompt, 30.2K of tools, 98K of files and 24 turns (304K). + public static AvaloniaList SessionItems + { + get + { + var items = new AvaloniaList + { + new(ContextWindowCategory.System, "System prompt", 3_800, "system"), + new(ContextWindowCategory.Tools, "search_docs", 12_400, "tool-search-docs"), + new(ContextWindowCategory.Tools, "run_tests", 9_100, "tool-run-tests"), + new(ContextWindowCategory.Tools, "query_db", 8_700, "tool-query-db"), + new(ContextWindowCategory.Files, "AGENTS.md", 6_200, "file-agents"), + new(ContextWindowCategory.Files, "memory/project-notes.md", 21_800, "file-notes"), + new(ContextWindowCategory.Files, "schema.sql", 20_500, "file-schema"), + new(ContextWindowCategory.Files, "src/ingest/queue.go", 49_500, "file-queue") + }; + for (var i = 0; i < s_messages.Length; i++) + { + var (label, tokens) = s_messages[i]; + items.Add(new ContextWindowItem(ContextWindowCategory.Messages, label, tokens, $"message-{i + 1}")); + } + return items; + } + } + + /// Five turns for Send message: 30.6K, 27.4K, 35.2K, 24.8K and 18.6K. + public static AvaloniaList SessionTurns => [ - new ContextWindowModel("heron-2", "Heron 2", 256_000, 16_000, ArcIcons.BookOpen, s_heronMark), - new ContextWindowModel("heron-2-mini", "Heron 2 Mini", 128_000, 8_000, ArcIcons.BookOpen) + new("Add a dry-run flag to the import job", 30_600), + new("Write tests for the dedupe step", 27_400), + new("Why does the nightly sync time out?", 35_200), + new("Rename the queue metrics to snake_case", 24_800), + new("Draft release notes for 2.4", 18_600) ]; - public static IEnumerable Items() => + /// Three files for Attach file: 62K, 21K and 18.5K. + public static AvaloniaList SessionFiles => + [ + new("spec/ingest-api.yaml", 62_000), + new("docs/oncall-playbook.md", 21_000), + new("src/ingest/dedupe.go", 18_500) + ]; + + /// The documentation session's models: Mistral Medium 3.5 and Mistral Small 4 (256K windows). + public static IEnumerable Models() => + [ModelOf(AiModelCatalog.MistralMedium35), ModelOf(AiModelCatalog.MistralSmall4)]; + + /// A catalog model with its context window and the sample app's reply reserve. + public static ContextWindowModel ModelOf(AiModelInfo model) => + new(model.ApiId, model.Name, model.ContextWindow ?? 0, Math.Min(ReplyReserve, model.MaxOutputTokens ?? ReplyReserve)); + + /// A documentation session; multiplies every token count. + public static IEnumerable Items(int scale = 1) => + DocsItems().Select(i => new ContextWindowItem(i.Category, i.Label, i.Tokens * scale, i.Id)); + + private static IEnumerable DocsItems() => [ new ContextWindowItem(ContextWindowCategory.System, "Style guide and instructions", 2_400, "system"), new ContextWindowItem(ContextWindowCategory.Tools, "search_handbook", 3_100, "tool-search"), @@ -29,6 +90,7 @@ public static IEnumerable Items() => new ContextWindowItem(ContextWindowCategory.Messages, "Shorten the introduction", 7_600, "m6") ]; + /// Four turns for Send message in the documentation session. public static IEnumerable Turns() => [ new ContextWindowTurn("Write the cancellation section", 14_800), @@ -37,6 +99,7 @@ public static IEnumerable Turns() => new ContextWindowTurn("Proofread the whole page", 21_500) ]; + /// Two files for Attach file in the documentation session. public static IEnumerable Files() => [ new ContextWindowTurn("handbook/invoices.md", 18_300), @@ -51,4 +114,33 @@ public static readonly (string Label, long Tokens)[] Replies = ("Draft a reply for the top issue", 7_200), ("Compare with last month", 16_500) ]; + + // 22 older turns (279K, between 6K and 25K) and the newest two (11.6K and 13.4K). + private static readonly (string Label, long Tokens)[] s_messages = + [ + ("Plan the importer rewrite", 9_400), + ("Fix the flaky retry test", 14_200), + ("Map the CSV columns to the schema", 7_800), + ("Explain the backoff settings", 18_600), + ("Add logging around the batch writer", 11_300), + ("Split the parser into two stages", 6_400), + ("Review the queue consumer", 22_100), + ("Handle empty rows in the import", 13_700), + ("Bump the client library", 8_900), + ("Trace the duplicate events", 16_200), + ("Write a migration for the status column", 10_500), + ("Profile the nightly job", 24_300), + ("Tidy the config loader", 7_100), + ("Add a health check endpoint", 12_800), + ("Investigate the memory spike", 15_600), + ("Cache the schema lookups", 9_700), + ("Document the import flags", 19_400), + ("Rename the staging tables", 6_900), + ("Check the timezone handling", 11_800), + ("Speed up the dedupe query", 13_100), + ("Add retries to the uploader", 8_200), + ("Summarize the open questions", 11_000), + ("Draft the rollout checklist", 11_600), + ("Review the failing import run", 13_400) + ]; } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/HostDriven.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/HostDriven.axaml.cs index 3d773f10..961f3172 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/HostDriven.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/HostDriven.axaml.cs @@ -12,7 +12,8 @@ public ContextWindowHostDrivenDemo() { InitializeComponent(); Card.Models.Add(ContextWindowDemoData.Models().Last()); - Card.Items.AddRange(ContextWindowDemoData.Items()); + // Twice the documentation session, so a few replies take the 256K window past 80%. + Card.Items.AddRange(ContextWindowDemoData.Items(scale: 2)); // Your summarizer knows the real size; this one says 5% of what it folds, at least 2,000 tokens. Card.Summarizer = folded => Math.Max(2_000, folded.Sum(item => item.Tokens) / 20); } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Narrow.axaml index 3701b435..9adc50c5 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Narrow.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Narrow.axaml @@ -1,8 +1,15 @@ - + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Preview.axaml index 002259d2..098a22ae 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Preview.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Preview.axaml @@ -1,7 +1,13 @@ - - + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ReducedMotion.axaml index 48450401..f3222030 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ReducedMotion.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/ReducedMotion.axaml @@ -1,8 +1,14 @@ - + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Threshold.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Threshold.axaml index 4f56fcdd..593f3909 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Threshold.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ContextWindow/Threshold.axaml @@ -1,7 +1,14 @@ - + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Colors.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Colors.axaml new file mode 100644 index 00000000..ea8a7940 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Colors.axaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/EmptyState.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/EmptyState.axaml new file mode 100644 index 00000000..684256de --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/EmptyState.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Preview.axaml new file mode 100644 index 00000000..4477dd27 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Preview.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Preview.axaml.cs new file mode 100644 index 00000000..ce2a158e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Preview.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class CubeMascotPreviewDemo : UserControl +{ + private readonly bool _ready; + + public CubeMascotPreviewDemo() + { + InitializeComponent(); + _ready = true; + Caption.Text = Hero.EffectiveStatus.ToString(); + Hero.PropertyChanged += (_, e) => + { + if (e.Property == CubeMascot.EffectiveStatusProperty) + { + Caption.Text = Hero.EffectiveStatus.ToString(); + } + }; + } + + private void OnStatusChanged(object? sender, SelectionChangedEventArgs e) + { + // SelectionChanged also fires while the XAML loads, before the cube exists. + if (!_ready || sender is not SegmentedControl control) + { + return; + } + CubeMascotStatus? status = control.SelectedValue is string name && Enum.TryParse(name, out var parsed) + ? parsed + : null; + Hero.Status = status; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/ReplyHeader.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/ReplyHeader.axaml new file mode 100644 index 00000000..b89e4c68 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/ReplyHeader.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/ReplyHeader.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/ReplyHeader.axaml.cs new file mode 100644 index 00000000..7964b92c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/ReplyHeader.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class CubeMascotReplyHeaderDemo : UserControl +{ + private readonly bool _ready; + + public CubeMascotReplyHeaderDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnPhaseChanged(object? sender, SelectionChangedEventArgs e) + { + // SelectionChanged also fires while the XAML loads, before the cube exists. + if (!_ready || sender is not SegmentedControl control) + { + return; + } + var (status, text) = (control.SelectedValue as string) switch + { + "streaming" => (CubeMascotStatus.Writing, "Writing"), + "complete" => (CubeMascotStatus.Done, "Done"), + "failed" => (CubeMascotStatus.Error, "Couldn't finish"), + _ => (CubeMascotStatus.Thinking, "Thinking") + }; + Mascot.Status = status; + Phase.Text = text; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Sizes.axaml new file mode 100644 index 00000000..7e4d9274 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/CubeMascot/Sizes.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DesignToCodeDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DesignToCodeDemoData.cs new file mode 100644 index 00000000..94f27f77 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DesignToCodeDemoData.cs @@ -0,0 +1,227 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The design the design to code demos play: a small pricing card "screenshot" drawn in code in the card's theme colors, +/// its five layout regions, the React and Tailwind-style component a model could write for it part by part, and a working +/// preview of that card. The model credited is real (, one that reads images); the plan, +/// prices and copy are invented. Demo content only. +/// +internal static class DesignToCodeDemoData +{ + /// The drawn card's natural size (it scales to fit the design pane). + public const double Width = 240; + + public const double Height = 300; + + /// The card's words, shared by the drawing and the live preview. + public const string Badge = "Pro"; + + public const string Title = "Pro plan"; + + public const string Subtitle = "For growing teams"; + + public const string Monthly = "Monthly"; + + public const string Yearly = "Yearly"; + + public const int MonthlyPrice = 24; + + public const int YearlyPrice = 19; + + public const string PerMonth = "/month"; + + public const string Cta = "Start free trial"; + + public static readonly string[] Features = ["Unlimited projects", "Shared workspaces", "Priority support"]; + + /// The design: the drawn pricing card. + public static DesignToCodeSource Source { get; } = new("pricing-card.png", null, DesignToCodeSourceKind.Sample) + { + Sketch = Sketch, + SketchSize = new Size(Width, Height), + }; + + /// The model credited in the code header: a real model that reads images (a neutral mark; no brand mark is drawn). + public static AgentModel Model { get; } = new(AiModelCatalog.Gemini4Argon.Name, ArcIcons.Sparkles); + + /// The file the code is written to. + public const string FileName = "PricingCard.tsx"; + + /// The frame Import design file brings in. + public const string FrameName = "Pricing / Pro card"; + + /// The five regions, in percent of the drawing (240 × 300 units). + public static IList Regions { get; } = + [ + new("header", "Header", 7.5, 6, 85, 21.5), + new("billing", "Billing toggle", 7.5, 30.5, 85, 9.5), + new("price", "Price", 7.5, 42.5, 52, 14.5), + new("features", "Features", 7.5, 59, 85, 22), + new("cta", "Button", 7.5, 83, 85, 12), + ]; + + /// The code, part by part, each tagged with the region it implements (null for the component's frame). + public static IList Parts { get; } = + [ + new(""" + import { useState } from "react"; + import { Check } from "./icons"; + + const features = ["Unlimited projects", "Shared workspaces", "Priority support"]; + + export function PricingCard() { + const [yearly, setYearly] = useState(false); + const price = yearly ? 19 : 24; + + return ( +
+ + """, null), + new(""" +
+ + Pro + +

Pro plan

+

For growing teams

+
+ + """, "header"), + new(""" +
+ {["Monthly", "Yearly"].map((label, i) => ( + + ))} +
+ + """, "billing"), + new(""" +

+ ${price} + /month +

+ + """, "price"), + new(""" +
    + {features.map(feature => ( +
  • + + {feature} +
  • + ))} +
+ + """, "features"), + new(""" + + + """, "cta"), + new(""" +
+ ); + } + + """, null), + ]; + + /// Renders the finished code's preview: the pricing card as working controls. + public static Func Preview { get; } = (_, _) => new DesignToCodeDemoPricingCard(); + + /// What the loop does on the preview: it flips the billing switch to Yearly, so the preview visibly works. + public static Action FlipBilling { get; } = control => + { + if (control is DesignToCodeDemoPricingCard card) + { + card.IsYearly = true; + } + }; + + /// A new simulation of the design (tests and code use it; the XAML demos spell it out). + public static DesignToCodeSimulation Simulation() => new() + { + Source = Source, + Regions = Regions, + Parts = Parts, + Model = Model, + FileName = FileName, + FrameName = FrameName, + PreviewFactory = Preview, + PreviewAction = FlipBilling, + }; + + /// Draws the pricing card on a 240 × 300 grid scaled to the frame, in the card's theme colors. + public static void Sketch(DrawingContext context, Rect bounds, DesignToCodeSketchPalette palette) + { + var s = bounds.Width / Width; + var radius = palette.CornerRadius; + Rect R(double x, double y, double w, double h) => new(bounds.X + x * s, bounds.Y + y * s, w * s, h * s); + + var line = palette.Line is { } lineBrush ? new Pen(lineBrush) : null; + context.DrawRectangle(palette.Paper, null, new RoundedRect(bounds, radius)); + if (line is not null) + { + context.DrawRectangle(null, line, new RoundedRect(bounds.Deflate(0.5), radius)); + } + + // Header: badge, title and subtitle. + var badge = R(20, 20, 34, 16); + context.DrawRectangle(palette.AccentSubtle, null, new RoundedRect(badge, badge.Height / 2)); + DrawText(context, palette.FontFamily, s, Badge, 9, FontWeight.SemiBold, palette.Accent, badge, center: true); + DrawText(context, palette.FontFamily, s, Title, 17, FontWeight.SemiBold, palette.Ink, R(20, 42, 200, 22)); + DrawText(context, palette.FontFamily, s, Subtitle, 10.5, FontWeight.Normal, palette.MutedInk, R(20, 66, 200, 14)); + + // Billing toggle: a well with a raised thumb on Monthly. + var well = R(20, 94, 200, 24); + context.DrawRectangle(palette.Well, null, new RoundedRect(well, well.Height / 2)); + var thumb = R(22, 96, 98, 20); + context.DrawRectangle(palette.Paper, line, new RoundedRect(thumb, thumb.Height / 2)); + DrawText(context, palette.FontFamily, s, Monthly, 10, FontWeight.Medium, palette.Ink, thumb, center: true); + DrawText(context, palette.FontFamily, s, Yearly, 10, FontWeight.Medium, palette.MutedInk, R(120, 96, 98, 20), center: true); + + // Price. + var price = DrawText(context, palette.FontFamily, s, "$" + MonthlyPrice, 30, FontWeight.SemiBold, palette.Ink, R(20, 128, 120, 38)); + DrawText(context, palette.FontFamily, s, PerMonth, 10.5, FontWeight.Normal, palette.MutedInk, + new Rect(bounds.X + 20 * s + price + 4 * s, bounds.Y + 147 * s, 80 * s, 14 * s)); + + // Features: a check dot and a line of text each. + for (var i = 0; i < Features.Length; i++) + { + var y = 184 + i * 20; + var dot = R(20, y + 2, 10, 10); + context.DrawEllipse(palette.AccentSubtle, null, dot); + context.DrawEllipse(palette.Accent, null, dot.Center, 2.2 * s, 2.2 * s); + DrawText(context, palette.FontFamily, s, Features[i], 10.5, FontWeight.Normal, palette.Ink, R(38, y, 180, 14)); + } + + // Call to action. + var cta = R(20, 252, 200, 30); + context.DrawRectangle(palette.Accent, null, new RoundedRect(cta, 9 * s)); + DrawText(context, palette.FontFamily, s, Cta, 11, FontWeight.SemiBold, palette.AccentForeground, cta, center: true); + } + + /// Draws one line of text in (vertically centered) and returns its width. + private static double DrawText(DrawingContext context, FontFamily font, double s, string text, double size, FontWeight weight, IBrush? brush, Rect box, bool center = false) + { + using var layout = new TextLayout(text, new Typeface(font, FontStyle.Normal, weight), Math.Max(1, size * s), brush ?? Brushes.Black); + var x = center ? box.X + (box.Width - layout.Width) / 2 : box.X; + var y = box.Y + (box.Height - layout.Height) / 2; + layout.Draw(context, new Point(x, y)); + return layout.Width; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DesignToCodeDemoPricingCard.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DesignToCodeDemoPricingCard.cs new file mode 100644 index 00000000..8c1239f5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DesignToCodeDemoPricingCard.cs @@ -0,0 +1,132 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using TKCode.Arc.DesignTokens; +using TKCode.Arc.Primitives; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The design to code demos' preview: the drawn pricing card as working controls. Monthly and Yearly switch the price, +/// which swaps with a short crossfade on tabular numerals; the features and the call to action are plain controls. Demo +/// content only. +/// +internal sealed class DesignToCodeDemoPricingCard : UserControl +{ + private static readonly FontFeatureCollection s_tabular = new() { new FontFeature { Tag = "tnum", Value = 1 } }; + + private readonly SegmentedControl _billing; + private readonly ArcContentSwap _price; + private bool _isYearly; + + /// Builds the card on Monthly. + public DesignToCodeDemoPricingCard() + { + Focusable = false; + var badgeText = new TextBlock { Text = DesignToCodeDemoData.Badge, FontWeight = FontWeight.SemiBold }; + ArcTokens.Bind(badgeText, TextBlock.FontSizeProperty, ArcDesignTokenKind.TypeXS); + ArcTokens.Bind(badgeText, TextBlock.ForegroundProperty, ArcDesignTokenKind.Accent); + var badge = new Border { HorizontalAlignment = HorizontalAlignment.Left, Padding = new Thickness(8, 2), Child = badgeText }; + ArcTokens.Bind(badge, Border.BackgroundProperty, ArcDesignTokenKind.AccentSubtle); + ArcTokens.Bind(badge, Border.CornerRadiusProperty, ArcDesignTokenKind.RadiusPill); + + var title = new TextBlock { Text = DesignToCodeDemoData.Title, Margin = new Thickness(0, 10, 0, 0), FontWeight = FontWeight.SemiBold }; + ArcTokens.Bind(title, TextBlock.FontSizeProperty, ArcDesignTokenKind.TypeLG); + var subtitle = Muted(DesignToCodeDemoData.Subtitle); + subtitle.Margin = new Thickness(0, 2, 0, 0); + + _billing = new SegmentedControl + { + Margin = new Thickness(0, 16, 0, 0), + HorizontalAlignment = HorizontalAlignment.Stretch, + Items = + { + new SegmentedOption("monthly", DesignToCodeDemoData.Monthly), + new SegmentedOption("yearly", DesignToCodeDemoData.Yearly), + }, + SelectedIndex = 0, + }; + _billing.SelectionChanged += (_, _) => IsYearly = _billing.SelectedIndex == 1; + + _price = new ArcContentSwap + { + Mode = ContentSwapMode.Label, + EnterOffset = 2, + HorizontalContentAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Bottom, + }; + var perMonth = Muted(DesignToCodeDemoData.PerMonth); + perMonth.Margin = new Thickness(0, 0, 0, 7); + perMonth.VerticalAlignment = VerticalAlignment.Bottom; + var price = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 18, 0, 0), Spacing = 4, Children = { _price, perMonth } }; + + var features = new StackPanel { Margin = new Thickness(0, 18, 0, 0), Spacing = 8 }; + foreach (var feature in DesignToCodeDemoData.Features) + { + var check = new ArcGlyph { Data = ArcIcons.Check, Size = 16, VerticalAlignment = VerticalAlignment.Center }; + ArcTokens.Bind(check, ArcGlyph.ForegroundProperty, ArcDesignTokenKind.Accent); + var text = new TextBlock { Text = feature, VerticalAlignment = VerticalAlignment.Center }; + ArcTokens.Bind(text, TextBlock.FontSizeProperty, ArcDesignTokenKind.TypeSM); + features.Children.Add(new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { check, text } }); + } + + var cta = new Button + { + Content = DesignToCodeDemoData.Cta, + Margin = new Thickness(0, 22, 0, 0), + HorizontalAlignment = HorizontalAlignment.Stretch, + Variant = ButtonVariant.Primary, + }; + + var card = new Border + { + MaxWidth = 300, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top, + Padding = new Thickness(22), + Child = new StackPanel { Children = { badge, title, subtitle, _billing, price, features, cta } }, + }; + ArcTokens.Bind(card, Border.BackgroundProperty, ArcDesignTokenKind.SurfaceRaised); + ArcTokens.Bind(card, Border.BorderBrushProperty, ArcDesignTokenKind.Border); + ArcTokens.Bind(card, Border.BorderThicknessProperty, ArcDesignTokenKind.EdgeThickness); + ArcTokens.Bind(card, Border.CornerRadiusProperty, ArcDesignTokenKind.RadiusStep18); + ArcTokens.Bind(card, Border.BoxShadowProperty, ArcDesignTokenKind.ShadowResting); + ArcTokens.Bind(this, ForegroundProperty, ArcDesignTokenKind.Foreground); + ArcTokens.Bind(this, FontFamilyProperty, ArcDesignTokenKind.BodyFontFamily); + Content = card; + ShowPrice(); + } + + /// Whether the yearly price shows (the billing switch follows). + public bool IsYearly + { + get => _isYearly; + set + { + if (_isYearly == value) + { + return; + } + _isYearly = value; + _billing.SelectedIndex = value ? 1 : 0; + ShowPrice(); + } + } + + private void ShowPrice() + { + var price = _isYearly ? DesignToCodeDemoData.YearlyPrice : DesignToCodeDemoData.MonthlyPrice; + var text = new TextBlock { Text = "$" + price, FontFeatures = s_tabular, FontWeight = FontWeight.SemiBold }; + ArcTokens.Bind(text, TextBlock.FontSizeProperty, ArcDesignTokenKind.Type3XL); + _price.Content = text; + } + + private static TextBlock Muted(string text) + { + var block = new TextBlock { Text = text }; + ArcTokens.Bind(block, TextBlock.FontSizeProperty, ArcDesignTokenKind.TypeSM); + ArcTokens.Bind(block, TextBlock.ForegroundProperty, ArcDesignTokenKind.TextMuted); + return block; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DropZone.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DropZone.axaml new file mode 100644 index 00000000..176548d5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DropZone.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DropZone.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DropZone.axaml.cs new file mode 100644 index 00000000..149da709 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/DropZone.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class DesignToCodeDropZoneDemo : UserControl +{ + private static readonly FilePickerFileType s_images = new("Screenshots") + { + Patterns = ["*.png", "*.jpg", "*.jpeg", "*.webp"], + MimeTypes = ["image/png", "image/jpeg", "image/webp"], + }; + + public DesignToCodeDropZoneDemo() + { + InitializeComponent(); + } + + private async void OnChooseFile(object? sender, RoutedEventArgs e) + { + // Handled: the card does not fall back to its simulation's design. + e.Handled = true; + if (TopLevel.GetTopLevel(this)?.StorageProvider is not { CanOpen: true } storage) + { + Card.UseSample(); + return; + } + var files = await storage.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = "Choose a UI screenshot", + AllowMultiple = false, + FileTypeFilter = [s_images], + }); + if (files.Count > 0) + { + await Card.LoadFileAsync(files[0]); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/Narrow.axaml new file mode 100644 index 00000000..bbfdaafb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/Narrow.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/Preview.axaml new file mode 100644 index 00000000..34863ec9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/Preview.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/ReducedMotion.axaml new file mode 100644 index 00000000..e409276c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/ReducedMotion.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/YourModel.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/YourModel.axaml new file mode 100644 index 00000000..03f7ad5f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/YourModel.axaml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/YourModel.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/YourModel.axaml.cs new file mode 100644 index 00000000..ca01890b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DesignToCode/YourModel.axaml.cs @@ -0,0 +1,70 @@ +using Avalonia.Controls; +using Avalonia.Layout; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class DesignToCodeYourModelDemo : UserControl +{ + // What a backend might send for the sample pricing card: the regions first, then code tagged with its region. + private static readonly DesignToCodeRegion[] s_regions = + [ + new("title", "Title", 7.5, 6, 85, 21.5), + new("plans", "Plan switch", 7.5, 30.5, 85, 9.5), + new("amount", "Amount", 7.5, 42.5, 52, 14.5), + new("perks", "Perks", 7.5, 59, 85, 22), + new("action", "Action", 7.5, 83, 85, 12), + ]; + + private static readonly (string Code, string? Region)[] s_code = + [ + ("export function ProCard({ yearly, onChange }: Props) {\n return (\n \n", null), + (" Pro plan\n For growing teams\n", "title"), + (" \n", "plans"), + (" \n", "amount"), + (" \n", "perks"), + (" \n", "action"), + (" \n );\n}\n", null), + ]; + + public DesignToCodeYourModelDemo() + { + InitializeComponent(); + Card.Model = new AgentModel(AiModelCatalog.Gemini38Flash.Name, ArcIcons.Eye); + Card.Generator = GenerateAsync; + // Your renderer goes here, ideally a sandboxed web view. Never run generated code inside your app. + Card.PreviewFactory = (code, _) => new TextBlock + { + Text = $"Your sandbox renders {Card.FileName} here ({code.Split('\n').Length - 1} lines).", + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + HorizontalAlignment = HorizontalAlignment.Center, + Margin = new Avalonia.Thickness(0, 40, 0, 0), + }; + } + + private static async Task GenerateAsync(DesignToCodeSource source, DesignToCodeContext context) + { + var token = context.CancellationToken; + foreach (var region in s_regions) + { + await Task.Delay(140, token); + context.AddRegion(region); + } + var random = new Random(7); + foreach (var (code, region) in s_code) + { + // A model streams uneven pieces; the card paces them into whole words. + for (var i = 0; i < code.Length;) + { + var length = Math.Min(code.Length - i, random.Next(6, 24)); + await Task.Delay(random.Next(20, 60), token); + context.Write(code.Substring(i, length), region); + i += length; + } + } + } + + private void OnCompleted(object? sender, DesignToCodeCompletedEventArgs e) + { + Log.Text = $"Completed: {e.LineCount} lines from {e.Design.Name} in {e.Elapsed.TotalSeconds:0.0}s."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Blocked.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Blocked.axaml new file mode 100644 index 00000000..85c78663 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Blocked.axaml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ControlOnly.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ControlOnly.axaml new file mode 100644 index 00000000..640f170d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ControlOnly.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ControlOnly.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ControlOnly.axaml.cs new file mode 100644 index 00000000..ef857208 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ControlOnly.axaml.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Interactivity; +using TKCode.Arc.DesignTokens; + +namespace TKCode.Arc.Gallery.Demos; + +/// DictationControl in a composer of its own: settled words in the text color, the guess muted. +public partial class DictationButtonControlOnlyDemo : UserControl +{ + private readonly Run _text = new(); + private readonly Run _final = new(); + private readonly Run _interim = new(); + private string _note = string.Empty; + + public DictationButtonControlOnlyDemo() + { + InitializeComponent(); + ArcTokens.Bind(_interim, Run.ForegroundProperty, ArcDesignTokenKind.TextMuted); + Note.Inlines = new InlineCollection { _text, _final, _interim }; + Mic.TranscriptChanged += OnTranscript; + Mic.Committed += OnCommitted; + Mic.Cancelled += OnCancelled; + Show(string.Empty, string.Empty); + } + + private void OnTranscript(object? sender, DictationTranscriptEventArgs e) => Show(e.Final, e.Interim); + + private void OnCommitted(object? sender, DictationCommittedEventArgs e) + { + _note = _note.Length == 0 ? e.Text : _note + " " + e.Text; + Show(string.Empty, string.Empty); + } + + private void OnCancelled(object? sender, RoutedEventArgs e) => Show(string.Empty, string.Empty); + + private void Show(string final, string interim) + { + if (_note.Length == 0 && final.Length == 0 && interim.Length == 0) + { + // An empty note reads as a muted prompt. + _text.Text = string.Empty; + _final.Text = string.Empty; + _interim.Text = "Press the mic and speak a note."; + return; + } + _text.Text = _note; + _final.Text = final.Length > 0 ? Lead(_note) + final : string.Empty; + _interim.Text = (interim.Length > 0 ? Lead(_note + final) : string.Empty) + interim; + } + + private static string Lead(string before) => before.Length == 0 || before.EndsWith(' ') ? string.Empty : " "; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/DictationButtonDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/DictationButtonDemoData.cs new file mode 100644 index 00000000..5912b748 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/DictationButtonDemoData.cs @@ -0,0 +1,33 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// What the DictationButton demos dictate and show: three short invented prompts, each with a misheard word the simulated +/// recognizer corrects, and the composer's model label, a real model from (the model the +/// message goes to, not a speech model). Demo content only: nothing is recorded or sent. +/// +internal static class DictationButtonDemoData +{ + /// The field's placeholder. + public const string Placeholder = "Message Claude"; + + /// The model label: the chat model the composer sends to. + public static DictationModel Model { get; } = new(AiModelCatalog.ClaudeSonnet55.Name); + + /// The scripts the simulated voice speaks, one per dictation, in turn. + public static IReadOnlyList> Scripts { get; } = + [ + [ + new("Draft a short reply to the design team", "Draft a short reply to the desk team"), + new("thanking them for the new onboarding screens,"), + new("and ask if the empty states can ship on Friday.", "and ask if the empty states can ship on fry day"), + ], + [ + new("Summarize the last three support tickets about billing", "Summarize the last free support tickets about billing"), + new("and list anything that still needs a refund."), + ], + [ + new("Plan a two day offsite for eight people in March,", "Plan a two day offsite for eight people in much"), + new("with one evening free and a budget under four thousand."), + ], + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Narrow.axaml new file mode 100644 index 00000000..211f41aa --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Preview.axaml new file mode 100644 index 00000000..fb26bf59 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/Preview.axaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ReducedMotion.axaml new file mode 100644 index 00000000..c13c8ead --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/ReducedMotion.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/YourRecognizer.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/YourRecognizer.axaml new file mode 100644 index 00000000..c981d742 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/YourRecognizer.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/YourRecognizer.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/YourRecognizer.axaml.cs new file mode 100644 index 00000000..2e02d945 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/DictationButton/YourRecognizer.axaml.cs @@ -0,0 +1,88 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// DictationButton with an app recognizer, an app model label, a real send task and an event log. +public partial class DictationButtonYourRecognizerDemo : UserControl +{ + private readonly List _log = new(); + + public DictationButtonYourRecognizerDemo() + { + InitializeComponent(); + Composer.Recognizer = new StandInRecognizer(); + Composer.Model = new DictationModel(AiModelCatalog.ClaudeHaiku55.Name, ArcIcons.Brain); + Composer.StatusChanged += (_, e) => Write(e.Error is { } error ? $"Status: {e.Status} ({error})" : $"Status: {e.Status}"); + Composer.Committed += (_, e) => Write($"Inserted {e.WordCount} words"); + Composer.SendRequested += (_, e) => + { + // Your request goes here; the send button spins until it completes and the field clears after. + e.Completion = Task.Delay(1200); + Write($"Sending \"{e.Text}\""); + }; + Write("Press the mic, or tab to it and press Enter."); + } + + private void Write(string line) + { + _log.Add(line); + if (_log.Count > 5) + { + _log.RemoveAt(0); + } + Log.Text = string.Join("\n", _log); + } + + /// + /// A stand-in for a platform recognizer or a speech service: it reports levels and a growing guess, then settles. + /// A real adapter opens the microphone in StartAsync and releases it when the token is cancelled or StopAsync runs. + /// + private sealed class StandInRecognizer : IDictationRecognizer + { + private static readonly string[] s_words = "Can you check whether the refund for this order went out".Split(' '); + + public async Task StartAsync(DictationHandlers handlers, CancellationToken cancellationToken) + { + await Task.Delay(300, cancellationToken); + var session = new Session(handlers, cancellationToken); + _ = session.RunAsync(); + return session; + } + + private sealed class Session(DictationHandlers handlers, CancellationToken token) : IDictationSession + { + private int _heard; + private bool _stopped; + + public async Task RunAsync() + { + var random = new Random(7); + try + { + while (_heard < s_words.Length && !_stopped) + { + for (var i = 0; i < 6; i++) + { + handlers.OnLevel(0.25 + random.NextDouble() * 0.6); + await Task.Delay(40, token); + } + _heard++; + handlers.OnTranscript(string.Empty, string.Join(' ', s_words, 0, _heard)); + } + handlers.OnLevel(0.03); + handlers.OnSpeechEnd(); + } + catch (OperationCanceledException) + { + } + } + + public async Task StopAsync() + { + _stopped = true; + await Task.Delay(400); + return string.Join(' ', s_words, 0, _heard); + } + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/EnrichColumnDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/EnrichColumnDemoData.cs new file mode 100644 index 00000000..039751d9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/EnrichColumnDemoData.cs @@ -0,0 +1,62 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The table the enrich column demos share: seven made-up companies on .example domains, the headcount and funding +/// column, and what each row settles into after "searching" and "reading" for a short while. Quarry Labs has no source +/// on its first attempt, so the failed state and Retry show; the retry finds it. The model is real +/// (); every company, number and address is invented. Demo content only. +/// +internal static class EnrichColumnDemoData +{ + /// The table's name in the top bar. + public const string Title = "Target accounts"; + + /// The row whose first attempt fails. + public const string FailingId = "quarry"; + + /// The AI column. + public static EnrichColumnDefinition Column { get; } = + new("Headcount and funding", "Find the headcount and latest funding round"); + + /// The footer's model: a real model that can call a web search tool. + public static EnrichColumnModel Model { get; } = new(AiModelCatalog.Gpt61Terra.Name, "web search"); + + private static readonly (EnrichColumnRow Row, string Value, string Source, int Millis)[] s_data = + [ + (new EnrichColumnRow("lumen", "Lumen Freight", "lumenfreight.example"), "240 people · Series B, $38M", "lumenfreight.example/about", 1500), + (new EnrichColumnRow("harbor", "Harbor Analytics", "harboranalytics.example"), "85 people · Seed, $6M", "harboranalytics.example/news", 1200), + (new EnrichColumnRow("kestrel", "Kestrel Robotics", "kestrel.example"), "410 people · Series C, $95M", "kestrel.example/press", 1800), + (new EnrichColumnRow(FailingId, "Quarry Labs", "quarrylabs.example"), "32 people · Pre-seed, $1.5M", "quarrylabs.example/team", 1300), + (new EnrichColumnRow("pinecrest", "Pinecrest Health", "pinecrest.example"), "1,200 people · Series D, $140M", "pinecrest.example/company", 1600), + (new EnrichColumnRow("bluefin", "Bluefin Payments", "bluefin.example"), "560 people · Series C, $72M", "bluefin.example/blog", 1400), + (new EnrichColumnRow("tidewater", "Tidewater Energy", "tidewater.example"), "150 people · Series A, $18M", "tidewater.example/investors", 1700), + ]; + + /// The seven rows. + public static IList Rows { get; } = s_data.Select(d => d.Row).ToArray(); + + /// + /// What each row settles into: "Searching for …" for 45% of its time, "Reading …" for the rest, then the value and + /// its source. Quarry Labs fails its first attempt with "No source found". + /// + public static IList Answers { get; } = s_data.Select(d => new EnrichColumnSimulationAnswer( + d.Row.Id, d.Value, new EnrichColumnSource(d.Source, "https://" + d.Source)) + { + Steps = + [ + new EnrichColumnSimulationStep("Searching for " + d.Row.Name, TimeSpan.FromMilliseconds(d.Millis * 0.45)), + new EnrichColumnSimulationStep("Reading " + d.Row.Domain, TimeSpan.FromMilliseconds(d.Millis * 0.55)), + ], + Error = d.Row.Id == FailingId ? "No source found" : null, + }).ToArray(); + + /// A new simulation of the table (tests and code use it; the XAML demos spell it out). + public static EnrichColumnSimulation Simulation() => new() + { + Title = Title, + Column = Column, + Model = Model, + Rows = Rows, + Answers = Answers, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/Narrow.axaml new file mode 100644 index 00000000..3d615558 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/Narrow.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/Preview.axaml new file mode 100644 index 00000000..5355da58 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/Preview.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/SavedValues.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/SavedValues.axaml new file mode 100644 index 00000000..82dae2aa --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/SavedValues.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/SavedValues.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/SavedValues.axaml.cs new file mode 100644 index 00000000..b7d8d8af --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/SavedValues.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// An enrich column restored from saved cells: two values, one failure, the rest not run. +public partial class EnrichColumnSavedValuesDemo : UserControl +{ + public EnrichColumnSavedValuesDemo() + { + InitializeComponent(); + Table.InitialCells = new Dictionary + { + ["lumen"] = EnrichColumnCell.Done("240 people · Series B, $38M", + new EnrichColumnSource("lumenfreight.example/about", "https://lumenfreight.example/about")), + ["harbor"] = EnrichColumnCell.Done("85 people · Seed, $6M", + new EnrichColumnSource("harboranalytics.example/news", "https://harboranalytics.example/news")), + ["kestrel"] = EnrichColumnCell.Failed("No source found"), + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/YourData.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/YourData.axaml new file mode 100644 index 00000000..897cb5ce --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/YourData.axaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/YourData.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/YourData.axaml.cs new file mode 100644 index 00000000..443de82b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/EnrichColumn/YourData.axaml.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// An enrich column on the app's own rows with its own delegate; settled cells are logged as an app would save them. +public partial class EnrichColumnYourDataDemo : UserControl +{ + private readonly List _entries = new(); + + public EnrichColumnYourDataDemo() + { + InitializeComponent(); + Table.Column = new EnrichColumnDefinition("Industry", "Classify the company's industry in two or three words"); + Table.Rows = + [ + new EnrichColumnRow("1", "Maple Street Bakery", "maplestreet.example"), + new EnrichColumnRow("2", "Orbit Freightworks", "orbitfreight.example"), + new EnrichColumnRow("3", "Willow Dental Group", "willowdental.example"), + new EnrichColumnRow("4", "Copperline Studio", "copperline.example"), + new EnrichColumnRow("5", "Northgate Robotics", "northgate.example"), + ]; + Table.Model = new EnrichColumnModel(AiModelCatalog.Gpt6Luna.Name, "classifier"); + Table.Enrich = EnrichAsync; + Table.CellChanged += (_, e) => + { + if (e.Cell.Status is EnrichColumnCellStatus.Done or EnrichColumnCellStatus.Failed) + { + Add($"{e.Row.Name}: {(e.Cell.HasValue ? e.Cell.Value : "failed, " + e.Cell.Error)}"); + } + }; + Table.RunCompleted += (_, e) => Add(e.WasStopped ? "Run stopped" : $"Run finished: {e.Cells.Count} cells"); + } + + // Stands in for your service: honor the token, say what you are doing, throw to fail the cell. + private static async Task EnrichAsync(EnrichColumnRow row, EnrichColumnContext context) + { + context.SetActivity("Reading " + row.Domain); + await Task.Delay(900 + row.Name.Length * 40, context.CancellationToken); + if (row.Id == "4" && context.Attempt == 1) + { + throw new InvalidOperationException("The site did not respond"); + } + var industry = row.Id switch + { + "1" => "Food and beverage", + "2" => "Logistics", + "3" => "Healthcare", + "4" => "Design services", + _ => "Industrial automation", + }; + return new EnrichColumnResult(industry, new EnrichColumnSource(row.Domain!, "https://" + row.Domain)); + } + + private void Add(string entry) + { + _entries.Add(entry); + if (_entries.Count > 4) + { + _entries.RemoveAt(0); + } + Log.Text = string.Join("\n", _entries); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Breakdown.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Breakdown.axaml new file mode 100644 index 00000000..f7028c1f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Breakdown.axaml @@ -0,0 +1,10 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Breakdown.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Breakdown.axaml.cs new file mode 100644 index 00000000..0d855081 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Breakdown.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A finished response with its breakdown open (a real model at its list prices; the usage is invented). +public partial class GenerationStatsBreakdownDemo : UserControl +{ + public GenerationStatsBreakdownDemo() + { + InitializeComponent(); + Stats.Model = GenerationStatsDemoData.Sonnet; + Stats.Pricing = GenerationStatsDemoData.SonnetPricing; + Stats.Note = GenerationStatsDemoData.ListPriceNote; + Stats.StartedAt = DateTimeOffset.Now.AddSeconds(-12); + Stats.Usage = new GenerationUsage + { + InputTokens = 12_480, + CachedTokens = 9_216, + OutputTokens = 612, + ReasoningTokens = 1_024, + Ttft = TimeSpan.FromMilliseconds(1_140), + Duration = TimeSpan.FromMilliseconds(8_920), + }; + Stats.SpeedSamples = [42, 61, 74, 79, 83, 81, 84, 86, 85, 83, 84, 82]; + Stats.Status = GenerationStatus.Done; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/GenerationStatsDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/GenerationStatsDemoData.cs new file mode 100644 index 00000000..92b6ecc8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/GenerationStatsDemoData.cs @@ -0,0 +1,56 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The responses the GenerationStats demos play: real models with their vendors' list prices from +/// (checked on ); the token counts, timings and the +/// failure are invented. +/// +internal static class GenerationStatsDemoData +{ + /// The footnote under the breakdown of a simulated strip. + public static string SimulatedNote { get; } = + $"Simulated runs: the tokens and timings are invented. Rates are each vendor's list price, checked {AiModelCatalog.CheckedOn}."; + + /// The footnote of the demos fed by hand. + public static string ListPriceNote { get; } = + $"Rates are the vendor's list prices, checked {AiModelCatalog.CheckedOn}."; + + /// The model of the demos fed by hand. + public static GenerationModel Sonnet { get; } = ModelOf(AiModelCatalog.ClaudeSonnet55); + + /// Its list prices. + public static GenerationPricing SonnetPricing { get; } = PricingOf(AiModelCatalog.ClaudeSonnet55); + + /// Four runs: three settle, the last one fails once and then succeeds when retried. + public static IReadOnlyList Runs { get; } = + [ + Run(AiModelCatalog.ClaudeSonnet55, input: 3_682, cached: 2_816, output: 284, reasoning: 0, ttftMs: 412, peak: 96), + Run(AiModelCatalog.Gpt61Sol, input: 5_120, cached: 4_096, output: 412, reasoning: 768, ttftMs: 1_180, peak: 132), + Run(AiModelCatalog.Gemini38Flash, input: 2_048, cached: 0, output: 356, reasoning: 0, ttftMs: 286, peak: 214), + Run(AiModelCatalog.MistralMedium35, input: 1_890, cached: 0, output: 318, reasoning: 0, ttftMs: 520, peak: 72) with + { + FailAtTokens = 120, + ErrorText = "The connection dropped before the response finished.", + }, + ]; + + /// The strip's model from a catalog entry: its name, the provider key and the API id. + public static GenerationModel ModelOf(AiModelInfo model) => new(model.Name, model.Provider.Key, model.ApiId); + + /// The catalog's list prices (US dollars per million tokens). + public static GenerationPricing PricingOf(AiModelInfo model) => + new(model.InputPerMTok, model.OutputPerMTok, model.CachedInputPerMTok); + + private static GenerationStatsRun Run(AiModelInfo model, long input, long cached, long output, long reasoning, double ttftMs, + double peak) => new() + { + Model = ModelOf(model), + Pricing = PricingOf(model), + InputTokens = input, + CachedTokens = cached, + OutputTokens = output, + ReasoningTokens = reasoning, + TimeToFirstToken = TimeSpan.FromMilliseconds(ttftMs), + PeakTokensPerSecond = peak, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Inline.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Inline.axaml new file mode 100644 index 00000000..07d1e5fa --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Inline.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Preview.axaml new file mode 100644 index 00000000..0caf9629 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Preview.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Streaming.axaml new file mode 100644 index 00000000..e46ee3d7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Streaming.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Streaming.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Streaming.axaml.cs new file mode 100644 index 00000000..3e3fdc0b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GenerationStats/Streaming.axaml.cs @@ -0,0 +1,96 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// GenerationStats fed the way a streaming handler feeds it. A timer stands in for the model: it answers after 450ms, then +/// sends chunks of a few tokens at about 90 tokens per second. +/// +public partial class GenerationStatsStreamingDemo : UserControl +{ + private const long Target = 320; + private readonly DispatcherTimer _timer; + private readonly Random _random = new(3); + private DateTimeOffset _startedAt; + private double _tokens; + private bool _fails; + + public GenerationStatsStreamingDemo() + { + InitializeComponent(); + Stats.Model = GenerationStatsDemoData.Sonnet; + Stats.Pricing = GenerationStatsDemoData.SonnetPricing; + _timer = new DispatcherTimer(TimeSpan.FromMilliseconds(50), DispatcherPriority.Normal, OnTick); + DetachedFromVisualTree += (_, _) => _timer.Stop(); + } + + private void OnSend(object? sender, RoutedEventArgs e) => Send(fails: false); + + private void OnSendAndFail(object? sender, RoutedEventArgs e) => Send(fails: true); + + private void OnRetry(object? sender, RoutedEventArgs e) => Send(fails: false); + + private void Send(bool fails) + { + // When the request leaves: the start time, the prompt's usage, then Waiting. + _fails = fails; + _tokens = 0; + _startedAt = DateTimeOffset.Now; + Stats.StartedAt = _startedAt; + Stats.ErrorText = null; + Stats.Usage = new GenerationUsage { InputTokens = 2_460, CachedTokens = 1_792 }; + Stats.Status = GenerationStatus.Waiting; + _timer.Start(); + } + + private void OnStop(object? sender, RoutedEventArgs e) + { + if (Stats.Status is GenerationStatus.Waiting or GenerationStatus.Streaming) + { + _timer.Stop(); + Stats.Usage = Stats.Usage! with { Duration = DateTimeOffset.Now - _startedAt }; + Stats.Status = GenerationStatus.Stopped; + } + } + + private void OnTick(object? sender, EventArgs e) + { + var elapsed = DateTimeOffset.Now - _startedAt; + var usage = Stats.Usage!; + if (Stats.Status == GenerationStatus.Waiting) + { + if (elapsed.TotalMilliseconds >= 450) + { + // The first token: set the time to it, then stream. + Stats.Usage = usage with { Ttft = elapsed, OutputTokens = 0 }; + Stats.Status = GenerationStatus.Streaming; + } + return; + } + _tokens += 90 * 0.05 * (0.9 + 0.2 * _random.NextDouble()); + var output = Math.Min(Target, (long)_tokens); + if (output - (usage.OutputTokens ?? 0) < 4 && output < Target) + { + return; + } + if (_fails && output >= Target / 2) + { + _timer.Stop(); + Stats.Usage = usage with { OutputTokens = output, Duration = elapsed }; + Stats.ErrorText = "The connection dropped before the model finished."; + Stats.Status = GenerationStatus.Error; + return; + } + if (output >= Target) + { + // The last token: the duration, then Done. + _timer.Stop(); + Stats.Usage = usage with { OutputTokens = Target, Duration = elapsed }; + Stats.Status = GenerationStatus.Done; + return; + } + Stats.Usage = usage with { OutputTokens = output }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Canvas.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Canvas.axaml new file mode 100644 index 00000000..6df09b57 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Canvas.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Canvas.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Canvas.axaml.cs new file mode 100644 index 00000000..a8622131 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Canvas.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class GhostMascotCanvasDemo : UserControl +{ + public GhostMascotCanvasDemo() + { + InitializeComponent(); + // Subscribed after InitializeComponent: the switch's initial IsChecked would otherwise fire before the ghost exists. + GeneratingSwitch.IsCheckedChanged += (_, _) => + Ghost.Status = GeneratingSwitch.IsChecked == true ? GhostMascotStatus.Thinking : GhostMascotStatus.Done; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/EmptyState.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/EmptyState.axaml new file mode 100644 index 00000000..400073e9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/EmptyState.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Preview.axaml new file mode 100644 index 00000000..ae1d8715 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Preview.axaml.cs new file mode 100644 index 00000000..ebda9717 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Preview.axaml.cs @@ -0,0 +1,43 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class GhostMascotPreviewDemo : UserControl +{ + private readonly bool _ready; + + public GhostMascotPreviewDemo() + { + InitializeComponent(); + _ready = true; + Caption.Text = Describe(Hero.EffectiveStatus); + // Raised by the self-playing ghost each time it moves to the next state. + Hero.StatusChanged += (_, e) => Caption.Text = Describe(e.Status); + } + + private static string Describe(GhostMascotStatus status) => status switch + { + GhostMascotStatus.Listening => "Listening to you", + GhostMascotStatus.Thinking => "Thinking", + GhostMascotStatus.Writing => "Writing a reply", + GhostMascotStatus.Done => "Done", + GhostMascotStatus.Error => "Couldn't finish", + _ => "Ready" + }; + + private void OnStatusChanged(object? sender, SelectionChangedEventArgs e) + { + // SelectionChanged also fires while the XAML loads, before the ghosts exist. + if (!_ready || sender is not SegmentedControl control) + { + return; + } + GhostMascotStatus? status = control.SelectedValue is string name && Enum.TryParse(name, out var parsed) + ? parsed + : null; + // In Auto the ghosts play on their own; with a status they both show it. + Hero.Status = status; + Inline.Status = status; + Caption.Text = Describe(Hero.EffectiveStatus); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/ReplyHeader.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/ReplyHeader.axaml new file mode 100644 index 00000000..3d1aaa17 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/ReplyHeader.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Sizes.axaml new file mode 100644 index 00000000..5a30b8f0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Sizes.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/States.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/States.axaml new file mode 100644 index 00000000..bd8c10b0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/States.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Tones.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Tones.axaml new file mode 100644 index 00000000..e87a989c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostMascot/Tones.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Events.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Events.axaml new file mode 100644 index 00000000..131331ad --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Events.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Events.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Events.axaml.cs new file mode 100644 index 00000000..271722b4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Events.axaml.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// Logs every accept, dismiss and status change, and drives the field through its methods. +public partial class GhostTextEventsDemo : UserControl +{ + private int _shown; + private int _accepted; + + public GhostTextEventsDemo() + { + InitializeComponent(); + Field.Suggest = GhostTextDemoModel.Create(); + Field.StatusChanged += (_, e) => + { + if (e.NewStatus == GhostTextStatus.Ready) + { + _shown++; + } + Write($"Status: {e.NewStatus}"); + }; + Field.Accepted += (_, e) => + { + if (e.Mode == GhostTextAcceptMode.All) + { + _accepted++; + } + Write($"Accepted ({e.Mode}): \"{e.AcceptedText.Trim()}\""); + }; + Field.Dismissed += (_, e) => Write($"Dismissed: \"{e.Suggestion.Trim()}\""); + Write("Press Ask now, or click the field and type."); + } + + private void Write(string line) + { + var rate = _shown == 0 ? 0 : _accepted * 100 / _shown; + Log.Text = $"{line}\nShown {_shown}, accepted {_accepted} ({rate}%)"; + } + + private void OnAsk(object? sender, RoutedEventArgs e) + { + Field.FocusField(); + Field.Request(); + } + + private void OnWord(object? sender, RoutedEventArgs e) => Field.AcceptWord(); + + private void OnAll(object? sender, RoutedEventArgs e) => Field.AcceptAll(); + + private void OnDismiss(object? sender, RoutedEventArgs e) => Field.Dismiss(); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/GhostTextDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/GhostTextDemoData.cs new file mode 100644 index 00000000..83b31ab1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/GhostTextDemoData.cs @@ -0,0 +1,28 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented pull request description the ghost text demos write: the lines the simulation types with the +/// suggestions that continue them, the field's label, placeholder and footer, and the model credited in the chip (a real +/// one from , with a neutral mark). +/// +internal static class GhostTextDemoData +{ + public const string LabelText = "Description"; + + public const string Placeholder = "Describe what changed and why"; + + public const string FooterText = "Markdown is supported"; + + /// The model in the chip: Mistral AI's Mistral Small 4 (checked on ). + public static AgentModel Model { get; } = new(AiModelCatalog.MistralSmall4.Name, ArcIcons.Sparkles); + + /// The texts the simulation types and the suggestions streamed for them. + public static readonly IReadOnlyList Script = + [ + new("Fixes the flaky checkout test", " by waiting for the payment webhook before it asserts."), + new("The retry helper now", " backs off for up to two seconds instead of sleeping."), + ]; + + /// The rule based stand-in for a model that answers whatever the person types (see ). + public static readonly GhostTextSuggest Suggest = GhostTextDemoModel.Create(); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/GhostTextDemoModel.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/GhostTextDemoModel.cs new file mode 100644 index 00000000..dde2e8ef --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/GhostTextDemoModel.cs @@ -0,0 +1,198 @@ +using System.Runtime.CompilerServices; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The rule based stand-in for a model that the ghost text demos use. It streams pull request writing continuations in 3 +/// to 6 word chunks after a short think, answers the demo's scripted lines, completes a partial word from a small +/// vocabulary, and fails with a timeout when the text contains the word "timeout" so the error state can be seen. Nothing +/// is sent anywhere; the phrases are demo content. +/// +internal static class GhostTextDemoModel +{ + /// How long the demo "thinks" before its first chunk. + public static TimeSpan ThinkTime { get; set; } = TimeSpan.FromMilliseconds(520); + + /// The pause between two streamed chunks. + public static TimeSpan ChunkGap { get; set; } = TimeSpan.FromMilliseconds(90); + + /// The reply that makes the demo throw (the text contained "timeout"). + public const string TimeoutMarker = "\u0000timeout"; + + /// Creates the demo function. + public static GhostTextSuggest Create() => (text, cancellationToken) => + { + var continuation = Continue(text); + if (continuation is null) + { + return null; + } + return GhostTextResponse.FromStream(Stream(continuation, cancellationToken)); + }; + + /// + /// The continuation the demo gives for : the scripted one, a completed partial word followed by a + /// phrase, or a phrase picked from the last words. A text that mentions "timeout" returns (the + /// stream then throws a ); null means no suggestion. + /// + public static string? Continue(string text) + { + foreach (var line in GhostTextDemoData.Script) + { + if (text.EndsWith(line.Typed, StringComparison.Ordinal)) + { + return line.Reply; + } + } + if (text.Contains("timeout", StringComparison.OrdinalIgnoreCase)) + { + return TimeoutMarker; + } + var trimmed = text.TrimEnd(); + if (trimmed.Length == 0) + { + return null; + } + var endsWithSpace = text.Length > 0 && char.IsWhiteSpace(text[^1]); + var lastWord = LastWord(trimmed); + if (!endsWithSpace && lastWord.Length >= 2) + { + foreach (var word in Vocabulary) + { + if (word.Length > lastWord.Length && word.StartsWith(lastWord, StringComparison.OrdinalIgnoreCase)) + { + return word[lastWord.Length..] + " " + Phrase(trimmed).TrimStart(); + } + } + } + var phrase = Phrase(trimmed); + if (trimmed[^1] is '.' or '!' or '?') + { + phrase = " " + Sentences[Math.Abs(Hash(trimmed)) % Sentences.Length]; + } + return endsWithSpace ? phrase.TrimStart() : phrase; + } + + /// Splits into chunks of 3 to 6 words (each keeps its leading space). + public static List Chunks(string text) + { + var chunks = new List(); + var start = 0; + var words = 0; + var target = 3; + var i = 0; + while (i < text.Length) + { + while (i < text.Length && char.IsWhiteSpace(text[i])) + { + i++; + } + while (i < text.Length && !char.IsWhiteSpace(text[i])) + { + i++; + } + words++; + if (words >= target) + { + chunks.Add(text[start..i]); + start = i; + words = 0; + target = 3 + chunks.Count * 2 % 4; + } + } + if (start < text.Length) + { + chunks.Add(text[start..]); + } + return chunks; + } + + private static async IAsyncEnumerable Stream(string continuation, [EnumeratorCancellation] CancellationToken token) + { + await Task.Delay(ThinkTime, token); + if (continuation == TimeoutMarker) + { + throw new TimeoutException("Autocomplete timed out"); + } + var first = true; + foreach (var chunk in Chunks(continuation)) + { + if (!first) + { + await Task.Delay(ChunkGap, token); + } + first = false; + yield return chunk; + } + } + + private static string LastWord(string text) + { + var i = text.Length; + while (i > 0 && char.IsLetter(text[i - 1])) + { + i--; + } + return text[i..]; + } + + private static string Phrase(string text) + { + var lower = text.ToLowerInvariant(); + foreach (var (keyword, phrase) in Phrases) + { + if (lower.EndsWith(keyword, StringComparison.Ordinal) || lower.Contains(" " + keyword + " ", StringComparison.Ordinal)) + { + return phrase; + } + } + return Fallbacks[Math.Abs(Hash(text)) % Fallbacks.Length]; + } + + private static int Hash(string text) + { + unchecked + { + var hash = 17; + foreach (var c in text) + { + hash = hash * 31 + c; + } + return hash == int.MinValue ? 0 : hash; + } + } + + private static readonly string[] Vocabulary = + [ + "checkout", "webhook", "retries", "payment", "migration", "regression", "flaky", "endpoint", "dashboard", + "description", "deployment", "release", "rollback", "feature", "refactor", "behavior", "because", "before", + "should", "screenshot", "reviewers", "tests", "timeouts", "customers", + ]; + + private static readonly (string Keyword, string Phrase)[] Phrases = + [ + ("fixes", " the flaky checkout test that failed on slow runners."), + ("this pr", " moves the retry logic into one helper so every caller behaves the same."), + ("because", " the old code retried before the webhook had arrived."), + ("test", " now waits for the payment webhook before it asserts."), + ("tests", " cover the empty cart and the expired card cases."), + ("to test", ", run the checkout suite twice with the network throttled."), + ("adds", " a short backoff between retries so we stop hammering the API."), + ("removes", " the sleep that hid the race in the payment step."), + ("the", " checkout flow now waits for the webhook before it continues."), + ]; + + private static readonly string[] Fallbacks = + [ + " so the checkout flow stays reliable on slow networks.", + " and keeps the existing behavior for every other caller.", + " without changing the public API.", + ]; + + private static readonly string[] Sentences = + [ + "No migration is needed and the change is behind the existing flag.", + "Reviewers can focus on the retry helper; the rest is renames.", + "I ran the suite fifty times locally without a failure.", + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Plain.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Plain.axaml new file mode 100644 index 00000000..090967a8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Plain.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Plain.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Plain.axaml.cs new file mode 100644 index 00000000..ed8fa6f0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Plain.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A reply box inside a card; the demo model completes the sentence. +public partial class GhostTextPlainDemo : UserControl +{ + public GhostTextPlainDemo() + { + InitializeComponent(); + Field.Suggest = GhostTextDemoModel.Create(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Preview.axaml new file mode 100644 index 00000000..bc049770 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Preview.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/SingleLine.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/SingleLine.axaml new file mode 100644 index 00000000..39a3a5bb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/SingleLine.axaml @@ -0,0 +1,12 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/SingleLine.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/SingleLine.axaml.cs new file mode 100644 index 00000000..14c1b746 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/SingleLine.axaml.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Whole suggestions from a task, as from a completion endpoint that does not stream. +public partial class GhostTextSingleLineDemo : UserControl +{ + public GhostTextSingleLineDemo() + { + InitializeComponent(); + Field.Suggest = (text, cancellationToken) => Complete(text, cancellationToken); + } + + private static async Task Complete(string text, CancellationToken cancellationToken) + { + await Task.Delay(400, cancellationToken); + foreach (var message in Messages) + { + if (message.Length > text.Length && message.StartsWith(text, StringComparison.OrdinalIgnoreCase)) + { + return message[text.Length..]; + } + } + return text.EndsWith(' ') ? "and update the tests" : " and update the tests"; + } + + private static readonly string[] Messages = + [ + "Fix flaky checkout test by waiting for the webhook", + "Add backoff to the payment retry helper", + "Remove the sleep that hid the payment race", + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Streaming.axaml new file mode 100644 index 00000000..e7f60581 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Streaming.axaml @@ -0,0 +1,13 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Streaming.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Streaming.axaml.cs new file mode 100644 index 00000000..2a96a400 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/GhostText/Streaming.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Streams continuations from the built-in demo model (nothing is sent anywhere). +public partial class GhostTextStreamingDemo : UserControl +{ + public GhostTextStreamingDemo() + { + InitializeComponent(); + Field.Suggest = GhostTextDemoModel.Create(); + Field.Model = GhostTextDemoData.Model; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Events.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Events.axaml new file mode 100644 index 00000000..79f99fca --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Events.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Events.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Events.axaml.cs new file mode 100644 index 00000000..dd10b442 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Events.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Shows what the card raises: the picked image and the downloads the app saves. +public partial class ImageGenerationEventsDemo : UserControl +{ + public ImageGenerationEventsDemo() + { + InitializeComponent(); + Card.SelectionChanged += (_, e) => Log.Text = e.Image is { } image + ? $"Picked image {e.Index + 1} ({image.Width} × {image.Height})" + : "Selection cleared"; + // A real app would save e.Image.Source here, for example through the window's storage provider. + Card.DownloadRequested += (_, e) => Log.Text = $"Download requested: {e.FileName}"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/ImageGenerationDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/ImageGenerationDemoData.cs new file mode 100644 index 00000000..9d4d0c09 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/ImageGenerationDemoData.cs @@ -0,0 +1,42 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// What the image generation demos offer: a golden hour prompt, three suggestions, three image models and a painter that +/// paints pictures in code to match the prompt (a lake at golden hour, a city street after rain, a cup on a sunlit sill or +/// a pastel room). The model catalog the other AI demos use lists no image generation models, so these models keep +/// invented names with neutral marks. Demo content only. +/// +internal static class ImageGenerationDemoData +{ + /// The prompt the card starts with. + public const string Prompt = "A quiet mountain lake at golden hour, mist over the water, pines on the far shore, soft film grain"; + + /// The image models on offer (invented names, neutral Lucide marks). + public static IReadOnlyList Models { get; } = + [ + new() { Id = "atlas-vision", Name = "Atlas Vision", Mark = ArcIcons.Sparkles }, + new() { Id = "atlas-3-pro", Name = "Atlas 3 Pro", Mark = ArcIcons.Palette }, + new() { Id = "atlas-mini", Name = "Atlas Mini", Mark = ArcIcons.Zap }, + ]; + + /// The suggestions in the empty grid. + public static IReadOnlyList Suggestions { get; } = + [ + "A ceramic coffee cup on a sunlit windowsill, soft morning shadows", + "A neon city street after rain at night, reflections on the asphalt", + "A cosy pastel reading nook with an arched window and a plant", + ]; + + /// Paints the picture for a prompt: the scene its words name, varied by the seed. + public static ImageGenerationPainter Painter { get; } = + (prompt, seed, width, height) => ImageGenerationDemoScenes.Paint(ImageGenerationDemoScenes.Pick(prompt), seed, width, height); + + /// A new simulation (code uses it; the XAML demos spell it out). + public static ImageGenerationSimulation Simulation() => new() + { + Prompt = Prompt, + Models = Models, + Suggestions = Suggestions, + Painter = Painter, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/ImageGenerationDemoScenes.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/ImageGenerationDemoScenes.cs new file mode 100644 index 00000000..3d7ffa39 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/ImageGenerationDemoScenes.cs @@ -0,0 +1,357 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// The kinds of picture the demos paint. +internal enum ImageGenerationDemoScene +{ + /// A lake under mountains at golden hour, with mist and the sun's reflection. + Landscape, + + /// A city street at night after rain: lit windows and their reflections. + City, + + /// A ceramic cup on a sunlit sill. + StillLife, + + /// A pastel room: an arched window, soft shapes and a plant. + Pastel +} + +/// +/// Paints the demos' pictures in code (layered gradients and simple shapes), so the card never downloads +/// anything. Each picture is a function of its scene, a seed and its size; the result is opaque BGRA, row by row. +/// +internal static class ImageGenerationDemoScenes +{ + /// Picks the scene whose words appear in the prompt, or one from the prompt's hash. + public static ImageGenerationDemoScene Pick(string? prompt) + { + var raw = (prompt ?? string.Empty).ToLowerInvariant(); + // Whole words only ("grain" is not "rain"): letters kept, everything else a space, padded at both ends. + var letters = new char[raw.Length + 2]; + letters[0] = letters[^1] = ' '; + for (var i = 0; i < raw.Length; i++) + { + letters[i + 1] = char.IsLetter(raw[i]) ? raw[i] : ' '; + } + var text = new string(letters); + if (Has(text, "city", "street", "neon", "night", "rain", "urban", "tokyo", "skyline")) + { + return ImageGenerationDemoScene.City; + } + if (Has(text, "cup", "coffee", "mug", "ceramic", "still life", "windowsill", "table", "product")) + { + return ImageGenerationDemoScene.StillLife; + } + if (Has(text, "room", "nook", "interior", "pastel", "plant", "cozy", "cosy", "isometric")) + { + return ImageGenerationDemoScene.Pastel; + } + if (Has(text, "lake", "mountain", "landscape", "sunset", "golden hour", "sunrise", "valley", "forest", "sea")) + { + return ImageGenerationDemoScene.Landscape; + } + return (ImageGenerationDemoScene)(int)(Hash(raw) % 4); + } + + /// A stable hash of a string (FNV-1a), so the same prompt always paints the same pictures. + public static uint Hash(string text) + { + var hash = 2166136261u; + foreach (var c in text) + { + hash = (hash ^ c) * 16777619u; + } + return hash; + } + + /// Paints a × picture as opaque BGRA bytes. + public static byte[] Paint(ImageGenerationDemoScene scene, uint seed, int width, int height) + { + width = Math.Max(1, width); + height = Math.Max(1, height); + var pixels = new byte[width * height * 4]; + var painter = new Painter(seed); + for (var y = 0; y < height; y++) + { + var v = (y + 0.5) / height; + for (var x = 0; x < width; x++) + { + var u = (x + 0.5) / width; + var aspect = (double)width / height; + var c = scene switch + { + ImageGenerationDemoScene.City => painter.City(u, v, aspect), + ImageGenerationDemoScene.StillLife => painter.StillLife(u, v, aspect), + ImageGenerationDemoScene.Pastel => painter.Pastel(u, v, aspect), + _ => painter.Landscape(u, v, aspect), + }; + // A little grain, like a photo. + var grain = (Noise.Hash(x, y, seed) - 0.5) * 0.035; + var i = (y * width + x) * 4; + pixels[i] = ToByte(c.B + grain); + pixels[i + 1] = ToByte(c.G + grain); + pixels[i + 2] = ToByte(c.R + grain); + pixels[i + 3] = 255; + } + } + return pixels; + } + + private static bool Has(string text, params string[] words) + { + foreach (var word in words) + { + if (text.Contains(" " + word + " ", StringComparison.Ordinal)) + { + return true; + } + } + return false; + } + + private static byte ToByte(double value) => (byte)Math.Clamp((int)Math.Round(value * 255), 0, 255); + + /// A color with channels from 0 to 1. + private readonly record struct Rgb(double R, double G, double B) + { + public static Rgb Hex(int hex) => new(((hex >> 16) & 0xFF) / 255.0, ((hex >> 8) & 0xFF) / 255.0, (hex & 0xFF) / 255.0); + + public static Rgb Mix(Rgb a, Rgb b, double t) + { + t = Math.Clamp(t, 0, 1); + return new(a.R + (b.R - a.R) * t, a.G + (b.G - a.G) * t, a.B + (b.B - a.B) * t); + } + + public Rgb Add(Rgb other, double amount) => new(R + other.R * amount, G + other.G * amount, B + other.B * amount); + + public Rgb Scale(double amount) => new(R * amount, G * amount, B * amount); + } + + private static class Noise + { + public static double Hash(int x, int y, uint seed) + { + unchecked + { + var h = (uint)x * 374761393u + (uint)y * 668265263u + seed * 2246822519u; + h = (h ^ (h >> 13)) * 1274126177u; + h ^= h >> 16; + return (h & 0xFFFFFF) / (double)0x1000000; + } + } + + /// Smooth value noise in [0, 1). + public static double Value(double x, double y, uint seed) + { + var ix = (int)Math.Floor(x); + var iy = (int)Math.Floor(y); + var fx = x - ix; + var fy = y - iy; + fx = fx * fx * (3 - 2 * fx); + fy = fy * fy * (3 - 2 * fy); + var a = Hash(ix, iy, seed); + var b = Hash(ix + 1, iy, seed); + var c = Hash(ix, iy + 1, seed); + var d = Hash(ix + 1, iy + 1, seed); + return a + (b - a) * fx + (c - a) * fy + (a - b - c + d) * fx * fy; + } + } + + private static double Smooth(double edge0, double edge1, double x) + { + var t = Math.Clamp((x - edge0) / (edge1 - edge0), 0, 1); + return t * t * (3 - 2 * t); + } + + /// The seeded choices of one picture. + private sealed class Painter + { + private readonly uint _seed; + private readonly double _a; + private readonly double _b; + private readonly double _c; + private readonly double _d; + + public Painter(uint seed) + { + _seed = seed; + _a = Noise.Hash(1, 7, seed); + _b = Noise.Hash(3, 11, seed); + _c = Noise.Hash(5, 13, seed); + _d = Noise.Hash(9, 17, seed); + } + + private double Ridge(double x, double baseline, double amplitude, double frequency, uint salt) => + baseline - amplitude * (0.55 * Math.Sin(x * frequency + _a * 6.28 + salt) + + 0.3 * Math.Sin(x * frequency * 2.3 + _b * 6.28 + salt * 1.7) + + 0.35 * (Noise.Value(x * frequency * 1.6, salt, _seed + salt) - 0.5)); + + public Rgb Landscape(double u, double v, double aspect) + { + var horizon = 0.58 + (_c - 0.5) * 0.06; + var sunX = 0.3 + _a * 0.4; + var sunY = horizon - 0.12 - _b * 0.12; + var warm = 0.7 + _d * 0.3; + var top = Rgb.Mix(Rgb.Hex(0x3E4F86), Rgb.Hex(0x5A6FA8), _b); + var mid = Rgb.Mix(Rgb.Hex(0xE88A6A), Rgb.Hex(0xF2A066), warm); + var low = Rgb.Hex(0xFFE0A8); + var sv = v < horizon ? v : horizon - (v - horizon) * 0.9; + var t = Math.Clamp(sv / horizon, 0, 1); + var sky = t < 0.6 ? Rgb.Mix(top, mid, t / 0.6) : Rgb.Mix(mid, low, (t - 0.6) / 0.4); + var dx = (u - sunX) * aspect; + var dy = sv - sunY; + var d2 = dx * dx + dy * dy; + sky = sky.Add(Rgb.Hex(0xFFD49A), 0.55 * Math.Exp(-d2 / 0.02)).Add(Rgb.Hex(0xFFF4DC), Math.Exp(-d2 / 0.0012)); + if (v < horizon) + { + var far = Ridge(u, horizon - 0.1, 0.08, 7, 1); + var near = Ridge(u, horizon - 0.02, 0.1, 11, 2); + var color = sky; + if (v > far) + { + color = Rgb.Mix(Rgb.Hex(0x8C7AA8), sky, 0.35 + 0.4 * (1 - (v - far) / 0.2)); + } + if (v > near) + { + // Pines on the near ridge: a jagged tree line, darker toward the shore. + color = Rgb.Mix(Rgb.Hex(0x3A3F55), Rgb.Hex(0x22283A), Smooth(near, horizon, v)); + } + // Mist over the water's edge. + var mist = Math.Exp(-Math.Pow((v - horizon + 0.015) / 0.035, 2)) * (0.55 + 0.3 * Noise.Value(u * 6, 3, _seed)); + return Rgb.Mix(color, Rgb.Hex(0xF6E2C8), mist); + } + // The lake mirrors the sky, darker and bluer, with ripples and a streak under the sun. + var ripple = 0.5 + 0.5 * Math.Sin((v - horizon) * 260 + Noise.Value(u * 8, v * 30, _seed) * 4); + var water = Rgb.Mix(sky.Scale(0.72), Rgb.Hex(0x2C3A5E), Smooth(horizon, 1, v) * 0.75); + var streak = Math.Exp(-Math.Pow((u - sunX) * aspect / (0.02 + (v - horizon) * 0.25), 2)) * ripple; + water = water.Add(Rgb.Hex(0xFFD9A0), 0.45 * streak); + var edge = Math.Exp(-Math.Pow((v - horizon) / 0.02, 2)) * 0.5; + return Rgb.Mix(water, Rgb.Hex(0xF6E2C8), edge); + } + + public Rgb City(double u, double v, double aspect) + { + var street = 0.68 + (_c - 0.5) * 0.06; + var sv = v < street ? v : street - (v - street) * 1.4; + var glow = Rgb.Mix(Rgb.Hex(0xC2559A), Rgb.Hex(0x4E7FD8), _a); + var sky = Rgb.Mix(Rgb.Hex(0x0E1230), Rgb.Hex(0x2A1E4A), Smooth(0, street, sv)); + sky = sky.Add(glow, 0.35 * Math.Exp(-Math.Pow((sv - street) / 0.18, 2))); + // Buildings: columns of seeded width and height, with windows on a grid. + var columns = 7 + (int)(_b * 4); + var col = (int)Math.Floor(u * columns); + var local = u * columns - col; + var height = 0.18 + Noise.Hash(col, 3, _seed) * 0.42; + var color = sky; + var roof = street - height; + if (sv > roof && sv < street) + { + var shade = 0.12 + Noise.Hash(col, 5, _seed) * 0.08; + color = new Rgb(shade * 0.8, shade * 0.85, shade * 1.25); + var wx = (int)Math.Floor(local * 5); + var wy = (int)Math.Floor((sv - roof) * 60); + var inX = local * 5 - wx; + var inY = (sv - roof) * 60 - wy; + if (local > 0.08 && local < 0.92 && inX > 0.25 && inX < 0.75 && inY > 0.3 && inY < 0.75 + && Noise.Hash(col * 31 + wx, wy, _seed) > 0.55) + { + var warm = Noise.Hash(col, wy, _seed + 1) > 0.3; + color = warm ? Rgb.Hex(0xFFD27A) : Rgb.Hex(0x8FE3FF); + } + } + if (v < street) + { + return color; + } + // Wet street: the scene above, broken by ripples, with a neon streak. + var wave = (Noise.Value(u * 40, v * 6, _seed) - 0.5) * 0.25; + var reflected = color.Scale(0.55 + wave); + var neon = Math.Exp(-Math.Pow((u - 0.25 - _d * 0.5) * aspect / 0.03, 2)) * Smooth(street, 1, v); + return reflected.Add(glow, 0.5 * neon).Add(Rgb.Hex(0x101426), 0.4); + } + + public Rgb StillLife(double u, double v, double aspect) + { + var sill = 0.64; + var wall = Rgb.Mix(Rgb.Hex(0xEBDDC8), Rgb.Hex(0xC9B6A0), Smooth(0, sill, v)); + // A slanted patch of window light on the wall. + var slant = u + v * 0.45 - 0.25 - _a * 0.2; + var light = Smooth(0.0, 0.03, slant) * (1 - Smooth(0.36, 0.4, slant)) * (1 - Smooth(sill - 0.05, sill, v)); + var color = wall.Add(Rgb.Hex(0xFFE9C2), 0.28 * light); + if (v >= sill) + { + var grain = Noise.Value(u * 3, v * 60, _seed) * 0.08; + color = Rgb.Mix(Rgb.Hex(0xB98A5E), Rgb.Hex(0x8C6340), Smooth(sill, 1, v) + grain); + } + var cupX = 0.4 + _b * 0.2; + var cupW = 0.11; + var cupTop = 0.42; + var cupBottom = sill + 0.04; + var glaze = _c switch + { + < 0.25 => Rgb.Hex(0xC8664A), + < 0.5 => Rgb.Hex(0x8FA88A), + < 0.75 => Rgb.Hex(0xEDE6D8), + _ => Rgb.Hex(0x5C7FA6), + }; + var dx = (u - cupX) * aspect; + // Its shadow falls away from the light. + var sdx = dx - (v - cupBottom) * 0.9; + if (v > cupBottom - 0.02 && v < cupBottom + 0.06 && Math.Abs(sdx - 0.06) < cupW * 1.1) + { + color = color.Scale(0.78 + 0.22 * Smooth(0, cupW * 1.1, Math.Abs(sdx - 0.06))); + } + var rim = (v - cupTop) / 0.022; + var inBody = Math.Abs(dx) < cupW * (1 - 0.12 * Smooth(cupTop, cupBottom, v)) && v > cupTop && v < cupBottom; + var handle = Math.Abs(Math.Sqrt(Math.Pow(dx - cupW - 0.02, 2) + Math.Pow((v - (cupTop + cupBottom) / 2) * 1.2, 2)) - 0.045) < 0.012 + && dx > cupW * 0.9; + if (inBody || handle) + { + var side = Math.Clamp(0.5 - dx / (cupW * 2), 0, 1); + color = Rgb.Mix(glaze.Scale(0.62), glaze.Add(Rgb.Hex(0xFFFFFF), 0.18), side * (0.4 + 0.6 * light + 0.3)); + } + if (Math.Abs(dx) < cupW && rim > -1 && rim < 1) + { + color = Rgb.Mix(glaze.Scale(0.45), Rgb.Hex(0x3B2418), Smooth(-1, 0.5, -Math.Abs(rim) + 0.5)); + } + return color; + } + + public Rgb Pastel(double u, double v, double aspect) + { + var floor = 0.72; + var tint = Rgb.Mix(Rgb.Hex(0xF3D9E2), Rgb.Hex(0xDCE6F5), _a); + var color = Rgb.Mix(tint, Rgb.Hex(0xFFF3E6), Smooth(0, floor, v) * 0.6); + // An arched window letting in a soft light. + var wx = (u - (0.3 + _b * 0.4)) * aspect; + var archTop = 0.22; + var inArch = Math.Abs(wx) < 0.14 && v > archTop + 0.14 && v < floor - 0.08 + || (v <= archTop + 0.14 && v > archTop && wx * wx + Math.Pow(v - archTop - 0.14, 2) < 0.14 * 0.14); + if (inArch) + { + color = Rgb.Mix(Rgb.Hex(0xFFFDF6), Rgb.Hex(0xBFDDF2), Smooth(archTop, floor, v)); + } + if (v >= floor) + { + color = Rgb.Mix(Rgb.Hex(0xE8C9A8), Rgb.Hex(0xD3AF8E), Smooth(floor, 1, v)); + } + // Soft round shapes: a chair and a lamp's glow. + var cx = (u - 0.2 - _c * 0.2) * aspect; + var chair = Math.Exp(-(cx * cx + Math.Pow((v - floor + 0.05) * 2.2, 2)) / 0.012); + color = Rgb.Mix(color, Rgb.Mix(Rgb.Hex(0xE79A8A), Rgb.Hex(0x9BC4B0), _d), Smooth(0.45, 0.6, chair)); + var lx = (u - 0.78 + _d * 0.1) * aspect; + color = color.Add(Rgb.Hex(0xFFE4B0), 0.35 * Math.Exp(-(lx * lx + Math.Pow(v - 0.35, 2)) / 0.01)); + // A plant: a pot and a few leaves. + var px = (u - 0.85 + _b * 0.08) * aspect; + if (Math.Abs(px) < 0.05 && v > floor - 0.1 && v < floor + 0.02) + { + color = Rgb.Hex(0xC97B5A); + } + var leaf = Noise.Value(px * 18, v * 18, _seed + 9); + if (Math.Abs(px) < 0.11 * (1 - Smooth(floor - 0.32, floor - 0.1, v) * 0.3) && v > floor - 0.32 && v < floor - 0.1 && leaf > 0.38) + { + color = Rgb.Mix(Rgb.Hex(0x4F8A5B), Rgb.Hex(0x7DB37A), leaf); + } + return color; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Narrow.axaml new file mode 100644 index 00000000..31ba9be5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Narrow.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Portrait.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Portrait.axaml new file mode 100644 index 00000000..829e8a0c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Portrait.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Preview.axaml new file mode 100644 index 00000000..d3aa738b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/Preview.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/YourGenerator.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/YourGenerator.axaml new file mode 100644 index 00000000..dc1ccbf2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/YourGenerator.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/YourGenerator.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/YourGenerator.axaml.cs new file mode 100644 index 00000000..2df6a3d2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageGeneration/YourGenerator.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Wires a generator: the card's simulation stands in for an image API, and one tile fails once. +public partial class ImageGenerationYourGeneratorDemo : UserControl +{ + private bool _failedOnce; + + public ImageGenerationYourGeneratorDemo() + { + InitializeComponent(); + Card.Generator = async (request, context) => + { + // Call your image API here: send request.Prompt, request.Model.Id and request.Aspect, report progress with + // context.ReportProgress and pass context.CancellationToken on so Stop cancels the call. + if (request.Index == 2 && !_failedOnce) + { + _failedOnce = true; + await Task.Delay(1200, context.CancellationToken); + throw new InvalidOperationException("The sample service timed out."); + } + return await Card.Simulation!.Generate(request, context); + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Estimated.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Estimated.axaml new file mode 100644 index 00000000..d973b878 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Estimated.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Estimated.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Estimated.axaml.cs new file mode 100644 index 00000000..f6d25a04 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Estimated.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// A controlled job with an estimated duration: the image arrives when the person says so. +public partial class ImageRevealEstimatedDemo : UserControl +{ + public ImageRevealEstimatedDemo() + { + InitializeComponent(); + } + + private void OnArrived(object? sender, RoutedEventArgs e) + { + Reveal.Source = ImageRevealDemoPicture.Get(); + Reveal.Status = ImageRevealStatus.Done; + } + + private void OnStartOver(object? sender, RoutedEventArgs e) + { + Reveal.Status = ImageRevealStatus.Queued; + Reveal.Source = null; + Reveal.Status = ImageRevealStatus.Generating; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Generator.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Generator.axaml new file mode 100644 index 00000000..9f9f539f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Generator.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Generator.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Generator.axaml.cs new file mode 100644 index 00000000..3c32086d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Generator.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace TKCode.Arc.Gallery.Demos; + +/// An image job behind a : 30 steps of 140ms, or a failure half way. +public partial class ImageRevealGeneratorDemo : UserControl +{ + private bool _failNext; + + public ImageRevealGeneratorDemo() + { + InitializeComponent(); + Reveal.Generator = GenerateAsync; + } + + private async Task GenerateAsync(IProgress progress, CancellationToken cancellationToken) + { + var fails = _failNext; + _failNext = false; + var picture = ImageRevealDemoPicture.Get(); + // A real service queues first; the canvas shows the queued look until the first report. + await Task.Delay(700, cancellationToken); + for (var step = 1; step <= 30; step++) + { + await Task.Delay(140, cancellationToken); + if (fails && step == 14) + { + throw new InvalidOperationException("The sample service failed."); + } + progress.Report(new ImageRevealProgress(step / 30.0, picture)); + } + return picture; + } + + private void OnGenerate(object? sender, RoutedEventArgs e) => Reveal.Restart(); + + private void OnFailNext(object? sender, RoutedEventArgs e) => _failNext = true; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Grid.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Grid.axaml new file mode 100644 index 00000000..d68b61e9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Grid.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Grid.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Grid.axaml.cs new file mode 100644 index 00000000..3caac215 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Grid.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// A staggered grid of variations with a Generate again button. +public partial class ImageRevealGridDemo : UserControl +{ + private int _run; + + public ImageRevealGridDemo() + { + InitializeComponent(); + } + + private void OnGenerateAgain(object? sender, RoutedEventArgs e) + { + _run++; + Tile1.RunKey = _run; + Tile2.RunKey = _run; + Tile3.RunKey = _run; + Tile4.RunKey = _run; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/ImageRevealDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/ImageRevealDemoData.cs new file mode 100644 index 00000000..c24e1e40 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/ImageRevealDemoData.cs @@ -0,0 +1,191 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The pictures the ImageReveal demos' simulations reveal: three invented painted scenes (an alpine lake at golden hour, +/// desert dunes at dusk, a lighthouse on sea cliffs at dawn) made of gradients and simple shapes, rendered once into a +/// 1024 × 1024 bitmap each the first time a demo asks for them. Nothing is downloaded. A real app reveals the images its +/// model returns. +/// +internal static class ImageRevealDemoData +{ + /// The bitmaps' side in pixels. + private const int Side = 1024; + + private static IReadOnlyList? s_pictures; + + /// The three scenes with their alt text, painted on first use (a scene stays null where the platform cannot render offscreen). + public static IReadOnlyList Pictures => s_pictures ??= + [ + new ImageRevealSimulationPicture { Image = Render(0), AltText = "A calm alpine lake at golden hour" }, + new ImageRevealSimulationPicture { Image = Render(1), AltText = "Desert dunes under a rising moon at dusk" }, + new ImageRevealSimulationPicture { Image = Render(2), AltText = "A lighthouse on sea cliffs at dawn" }, + ]; + + private static IImage? Render(int index) + { + try + { + var target = new RenderTargetBitmap(new PixelSize(Side, Side), new Vector(96, 96)); + using (var context = target.CreateDrawingContext()) + { + Draw(context, index, new Rect(0, 0, Side, Side)); + } + return target; + } + catch (Exception) + { + return null; + } + } + + /// Paints scene into (shapes are authored on a 100 × 100 grid). + private static void Draw(DrawingContext context, int index, Rect bounds) + { + using (context.PushClip(bounds)) + using (context.PushTransform(Matrix.CreateScale(bounds.Width / 100, bounds.Height / 100) * Matrix.CreateTranslation(bounds.X, bounds.Y))) + { + switch (index) + { + case 0: + DrawLake(context); + break; + case 1: + DrawDunes(context); + break; + default: + DrawLighthouse(context); + break; + } + } + } + + private static readonly Rect s_full = new(0, 0, 100, 100); + + private static IBrush Vertical(params (double Offset, string Color)[] stops) => Linear(0.5, 0, 0.5, 1, stops); + + private static IBrush Linear(double x1, double y1, double x2, double y2, params (double Offset, string Color)[] stops) + { + var brush = new LinearGradientBrush + { + StartPoint = new RelativePoint(x1, y1, RelativeUnit.Relative), + EndPoint = new RelativePoint(x2, y2, RelativeUnit.Relative), + }; + foreach (var (offset, color) in stops) + { + brush.GradientStops.Add(new GradientStop(Color.Parse(color), offset)); + } + return brush; + } + + private static IBrush Glow(string color, double alpha) + { + var c = Color.Parse(color); + var brush = new RadialGradientBrush(); + brush.GradientStops.Add(new GradientStop(Color.FromArgb((byte)(alpha * 255), c.R, c.G, c.B), 0)); + brush.GradientStops.Add(new GradientStop(Color.FromArgb(0, c.R, c.G, c.B), 1)); + return brush; + } + + private static void Fill(DrawingContext context, IBrush brush, string path) => + context.DrawGeometry(brush, null, Geometry.Parse(path)); + + private static void Disc(DrawingContext context, IBrush brush, double x, double y, double rx, double ry) => + context.DrawEllipse(brush, null, new Point(x, y), rx, ry); + + /// An alpine lake at golden hour: a warm sky, a low sun, two ranges of peaks with snow, pines and the reflection. + private static void DrawLake(DrawingContext c) + { + c.FillRectangle(Vertical((0, "#27345F"), (0.38, "#B86A73"), (0.56, "#F3A35C"), (0.64, "#F8D48E")), s_full); + Disc(c, Glow("#FFE2A6", 0.85), 63, 52, 34, 26); + Disc(c, new SolidColorBrush(Color.Parse("#FFF1CF")), 63, 54, 4.2, 4.2); + Disc(c, Glow("#FFFFFF", 0.25), 22, 20, 26, 6); + Disc(c, Glow("#FFE7C4", 0.22), 80, 28, 22, 4); + Fill(c, Vertical((0, "#6B5E8E"), (1, "#8A6F92")), "M0,52 L9,41 L16,46 L27,31 L36,42 L44,36 L55,48 L66,38 L75,45 L86,33 L100,45 L100,64 L0,64 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#E8DDE6"), 0.75), "M27,31 L31,35.5 L29,35 L27.5,37 L25,34.5 L23.5,35 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#EBDCE2"), 0.7), "M86,33 L90,37.8 L88,37.2 L86.3,39 L84,36.6 L82.6,37.4 Z"); + Fill(c, Vertical((0, "#3B3A62"), (1, "#2A2C4C")), "M0,58 L12,47 L21,53 L33,43 L45,55 L58,46 L70,56 L83,49 L100,57 L100,65 L0,65 Z"); + Fill(c, Vertical((0, "#F2B06E"), (0.25, "#B87079"), (0.7, "#3E4675"), (1, "#1E2645")), "M0,64 L100,64 L100,100 L0,100 Z"); + Disc(c, Glow("#FFE0A0", 0.8), 63, 72, 3.2, 16); + Disc(c, Glow("#FFD18A", 0.35), 63, 70, 12, 3); + var ripple = new SolidColorBrush(Color.Parse("#FFE6BD"), 0.35); + for (var i = 0; i < 9; i++) + { + var y = 67 + i * 3.4; + var w = 6 + i * 2.2; + c.FillRectangle(ripple, new Rect(63 - w / 2 + (i % 3 - 1) * 2, y, w, 0.35)); + } + var pines = new SolidColorBrush(Color.Parse("#141A30")); + for (var i = 0; i < 7; i++) + { + var x = i * 4.2 - 1; + var h = 10 + (i * 37 % 7); + Fill(c, pines, FormattableString.Invariant($"M{x},{66} L{x + 2.4},{66 - h} L{x + 4.8},{66} Z")); + } + for (var i = 0; i < 5; i++) + { + var x = 82 + i * 4; + var h = 8 + (i * 53 % 6); + Fill(c, pines, FormattableString.Invariant($"M{x},{66} L{x + 2.2},{66 - h} L{x + 4.4},{66} Z")); + } + c.FillRectangle(new SolidColorBrush(Color.Parse("#141A30")), new Rect(0, 65.5, 100, 1.2)); + Fill(c, Vertical((0, "#1A2038"), (1, "#0E1222")), "M0,86 C18,82 34,92 52,90 C70,88 84,84 100,88 L100,100 L0,100 Z"); + } + + /// Desert dunes at dusk: a dusky sky, a pale moon, layered dunes with lit ridges. + private static void DrawDunes(DrawingContext c) + { + c.FillRectangle(Vertical((0, "#1C2448"), (0.35, "#5B3F6E"), (0.55, "#C46A6A"), (0.7, "#F4B07C")), s_full); + Disc(c, Glow("#FFE9D2", 0.55), 30, 30, 16, 16); + Disc(c, new SolidColorBrush(Color.Parse("#FFF4E6")), 30, 30, 5.5, 5.5); + var star = new SolidColorBrush(Color.Parse("#FFFFFF"), 0.75); + for (var i = 0; i < 18; i++) + { + var x = (i * 61 % 100) + 0.5; + var y = (i * 37 % 34) + 2; + Disc(c, star, x, y, 0.22, 0.22); + } + Fill(c, Vertical((0, "#E08E62"), (1, "#B9604C")), "M0,62 C16,54 30,56 46,60 C60,63 74,53 100,57 L100,100 L0,100 Z"); + Fill(c, Linear(0, 0.2, 1, 0.8, (0, "#F2A774"), (0.55, "#C2674C"), (1, "#8C4440")), "M0,72 C14,64 28,63 40,68 C54,74 70,62 100,66 L100,100 L0,100 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#FFD0A0"), 0.55), "M0,72 C14,64 28,63 40,68 C30,65.8 16,66.6 0,73.4 Z"); + Fill(c, Linear(0, 0.1, 1, 0.9, (0, "#A4523F"), (0.6, "#6E3236"), (1, "#3E1F2C")), "M0,84 C20,74 38,76 54,82 C70,88 84,78 100,79 L100,100 L0,100 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#E88A60"), 0.6), "M54,82 C70,88 84,78 100,79 L100,80.2 C86,79.6 72,89.6 54,82 Z"); + Fill(c, Vertical((0, "#3A1C2A"), (1, "#24121D")), "M0,94 C24,88 52,96 76,92 C88,90 96,91 100,92 L100,100 L0,100 Z"); + } + + /// A lighthouse on sea cliffs at dawn: a pale sky with soft clouds, the sea, green cliffs and a striped tower. + private static void DrawLighthouse(DrawingContext c) + { + c.FillRectangle(Vertical((0, "#7FA8D4"), (0.42, "#C7D6EA"), (0.6, "#F6D8C6")), s_full); + Disc(c, Glow("#FFF3E2", 0.8), 78, 58, 30, 14); + var cloud = Glow("#FFFFFF", 0.7); + Disc(c, cloud, 22, 22, 18, 4); + Disc(c, cloud, 34, 19, 12, 3.4); + Disc(c, cloud, 70, 30, 20, 3.6); + Disc(c, Glow("#FFE4D2", 0.6), 58, 40, 16, 2.6); + Fill(c, Vertical((0, "#5C8FB2"), (0.4, "#2F6687"), (1, "#173F5A")), "M0,60 L100,60 L100,100 L0,100 Z"); + Disc(c, Glow("#FFEBD6", 0.7), 78, 62, 20, 2.2); + var glint = new SolidColorBrush(Color.Parse("#E9F2FA"), 0.4); + for (var i = 0; i < 12; i++) + { + var y = 63 + i * 3; + var x = (i * 29 % 70) + 30; + c.FillRectangle(glint, new Rect(x, y, 4 + i % 4, 0.3)); + } + Fill(c, Linear(0, 0, 1, 1, (0, "#5E7B54"), (0.5, "#3E5A3E"), (1, "#24382A")), "M0,46 L14,44 L30,47 L40,52 L44,62 L40,74 L46,100 L0,100 Z"); + Fill(c, Linear(0, 0, 1, 1, (0, "#8A7A66"), (1, "#4A4038")), "M40,52 L44,62 L40,74 L46,100 L34,100 L31,80 L35,62 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#A7C08A"), 0.6), "M0,46 L14,44 L30,47 L40,52 L28,49.5 L14,46.6 L0,48.4 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#FFF6D8"), 0.28), "M21.5,25.5 L58,18 L58,30 Z"); + c.FillRectangle(new SolidColorBrush(Color.Parse("#F4F1EA")), new Rect(18.6, 27, 5.8, 19)); + var band = new SolidColorBrush(Color.Parse("#C2453D")); + c.FillRectangle(band, new Rect(18.6, 31, 5.8, 3)); + c.FillRectangle(band, new Rect(18.6, 38, 5.8, 3)); + c.FillRectangle(new SolidColorBrush(Color.Parse("#2B2F36")), new Rect(18, 24.6, 7, 2.6)); + Disc(c, new SolidColorBrush(Color.Parse("#FFE9A8")), 21.5, 25.8, 1.6, 1.1); + Fill(c, new SolidColorBrush(Color.Parse("#2B2F36")), "M18,24.6 L21.5,21.6 L25,24.6 Z"); + Fill(c, new SolidColorBrush(Color.Parse("#E8EEF2"), 0.55), "M40,74 C44,75 47,73 52,74.5 C48,76 44,76.5 40.6,76 Z"); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/ImageRevealDemoPicture.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/ImageRevealDemoPicture.cs new file mode 100644 index 00000000..5341a7b9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/ImageRevealDemoPicture.cs @@ -0,0 +1,65 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The "generated" picture the demos hand to : a painted greenhouse at noon (gradients and simple +/// shapes) rendered once into a bitmap. A real app passes the decoded image its model returned. +/// +internal static class ImageRevealDemoPicture +{ + private static Bitmap? s_picture; + + /// The picture, painted on first use. + public static Bitmap Get() + { + if (s_picture is not null) + { + return s_picture; + } + var target = new RenderTargetBitmap(new PixelSize(960, 1200), new Vector(96, 96)); + using (var context = target.CreateDrawingContext()) + { + Paint(context, new Rect(0, 0, 960, 1200)); + } + s_picture = target; + return target; + } + + private static void Paint(DrawingContext context, Rect bounds) + { + var sky = new LinearGradientBrush + { + StartPoint = new RelativePoint(0.5, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(0.5, 1, RelativeUnit.Relative), + GradientStops = { new GradientStop(Color.Parse("#CFE8D6"), 0), new GradientStop(Color.Parse("#F3EBD2"), 0.55), new GradientStop(Color.Parse("#7EA36F"), 1) }, + }; + context.FillRectangle(sky, bounds); + var w = bounds.Width; + var h = bounds.Height; + var glass = new SolidColorBrush(Color.Parse("#FFFFFF"), 0.35); + var frame = new Pen(new SolidColorBrush(Color.Parse("#3F5A48")), 6); + var house = Geometry.Parse($"M{w * 0.12},{h * 0.86} L{w * 0.12},{h * 0.42} L{w * 0.5},{h * 0.18} L{w * 0.88},{h * 0.42} L{w * 0.88},{h * 0.86} Z"); + context.DrawGeometry(glass, frame, house); + for (var i = 1; i < 6; i++) + { + var x = w * (0.12 + 0.76 * i / 6); + context.DrawLine(frame, new Point(x, h * 0.86), new Point(x, h * (0.42 - 0.24 * (1 - Math.Abs(i - 3) / 3.0)))); + } + context.DrawLine(frame, new Point(w * 0.12, h * 0.6), new Point(w * 0.88, h * 0.6)); + var leaf = new SolidColorBrush(Color.Parse("#4E8A4F")); + var deep = new SolidColorBrush(Color.Parse("#2F6B3F")); + var fruit = new SolidColorBrush(Color.Parse("#F2A93B")); + for (var i = 0; i < 9; i++) + { + var cx = w * (0.18 + i * 0.08); + var cy = h * (0.74 - (i % 3) * 0.05); + context.DrawEllipse(i % 2 == 0 ? leaf : deep, null, new Point(cx, cy), w * 0.07, h * 0.06); + context.DrawEllipse(fruit, null, new Point(cx + w * 0.02, cy - h * 0.01), w * 0.012, w * 0.012); + } + context.FillRectangle(new SolidColorBrush(Color.Parse("#8A6A4A")), new Rect(0, h * 0.86, w, h * 0.14)); + context.DrawEllipse(new SolidColorBrush(Color.Parse("#FFF6DA"), 0.8), null, new Point(w * 0.78, h * 0.12), w * 0.06, w * 0.06); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Preview.axaml new file mode 100644 index 00000000..6066064b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Preview.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Preview.axaml.cs new file mode 100644 index 00000000..29c1fcdf --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ImageReveal/Preview.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulation, a queued job, a failed one and the empty canvas. +public partial class ImageRevealPreviewDemo : UserControl +{ + private readonly ImageRevealSimulation? _simulation; + private readonly bool _ready; + + public ImageRevealPreviewDemo() + { + InitializeComponent(); + _simulation = Reveal.Simulation; + Reveal.RetryRequested += OnRetry; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has run. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "simulated"); + } + } + + private void Show(string scenario) + { + switch (scenario) + { + case "queued": + Reveal.QueueLabel = "Queued, 2 ahead"; + Reveal.Status = ImageRevealStatus.Queued; + break; + case "failed": + Reveal.ErrorMessage = "The image could not be generated. Try again in a moment."; + Reveal.Status = ImageRevealStatus.Error; + break; + case "empty": + // No status, no image and no simulation: the plain surface. + Reveal.QueueLabel = null; + Reveal.Simulation = null; + Reveal.Status = null; + break; + default: + // No status: the simulation runs again. + Reveal.QueueLabel = null; + Reveal.Simulation = _simulation; + Reveal.Status = null; + break; + } + } + + private void OnRetry(object? sender, RoutedEventArgs e) => Scenario.SelectedValue = "simulated"; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/InstallSnippetDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/InstallSnippetDemoData.cs new file mode 100644 index 00000000..2270f6ad --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/InstallSnippetDemoData.cs @@ -0,0 +1,66 @@ +using Avalonia.Media; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// What the InstallSnippet demos show: the real install steps of this library's AI components (the +/// TKCode.Arc.Special package at the version in build/Versions.props; keep the two in step on release), in three +/// ways to add a package. Step 2 stores the key of the reader's own model provider, which their app uses for its model +/// calls; Arc's components never see it. Generate key only invents demo keys: nothing is created or sent. +/// +internal static class InstallSnippetDemoData +{ + /// The package version the snippets install (build/Versions.props). + public const string ArcVersion = "0.3.2-preview.1"; + + /// The version label at the top right. + public const string Version = "v" + ArcVersion; + + /// The line under the title. + public const string Description = "Three steps to your first AI screen."; + + /// The prefix of an invented key: obviously not a real key. + public const string KeyPrefix = "demo_"; + + /// A key the signed-in reader already has (invented). + public const string SignedInKey = "demo_4f7k2q9xm1p8w3z6t0b5"; + + private const string Program = + "AppBuilder.Configure()\n" + + " .UsePlatformDetect()\n" + + " .UseArc(arc => arc\n" + + " .UseArcControls()\n" + + " .UseArcSpecial());"; + + private const string ProgramDescription = "Register Arc and its AI components in Program.cs."; + + /// Three framework tabs, fresh for every card: the .NET CLI, a PackageReference and central package management. + public static IReadOnlyList Frameworks => + [ + Framework("dotnet", ".NET CLI", ArcIcons.Terminal, + new InstallSnippetFile("Terminal", "dotnet add package TKCode.Arc.Special --version " + ArcVersion, + "Add the package to your Avalonia app."), + new InstallSnippetFile("Terminal", "dotnet user-secrets set \"Chat:ApiKey\" \"{{key}}\"", + "Keep your model provider's key in user secrets, out of the repository.")), + Framework("csproj", "PackageReference", ArcIcons.FileCode, + new InstallSnippetFile("MyApp.csproj", "", + "Reference the package in your project file.", "xml"), + new InstallSnippetFile("appsettings.Development.json", "{\n \"Chat\": { \"ApiKey\": \"{{key}}\" }\n}", + "Keep the key in development settings. Never commit it.", "json")), + Framework("cpm", "Central packages", ArcIcons.Package, + new InstallSnippetFile("Directory.Packages.props", + "", + "Pin the version once for the whole solution.", "xml"), + new InstallSnippetFile(".env", "Chat__ApiKey={{key}}", "Or pass the key as an environment variable.")), + ]; + + private static InstallSnippetFramework Framework(string id, string label, Geometry mark, InstallSnippetFile install, + InstallSnippetFile key) => + new(id, label) + { + Mark = mark, + Install = install, + Key = key, + Code = new InstallSnippetFile("Program.cs", Program, ProgramDescription), + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Narrow.axaml new file mode 100644 index 00000000..f107e11c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Preview.axaml new file mode 100644 index 00000000..d64edaea --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Preview.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Preview.axaml.cs new file mode 100644 index 00000000..43eee1eb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/Preview.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: a new reader, a signed-in reader, a key generator that fails and the empty card. +public partial class InstallSnippetPreviewDemo : UserControl +{ + private readonly InstallSnippetSimulation? _simulation; + private readonly bool _ready; + + public InstallSnippetPreviewDemo() + { + InitializeComponent(); + _simulation = Snippet.Simulation; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (!_ready) + { + return; + } + Snippet.ResetProgress(); + var scenario = Scenario.SelectedValue as string; + // Empty: no frameworks and no simulation, so the steps show their titles and lines over empty snippets. + Snippet.Simulation = scenario == "empty" ? null : _simulation; + switch (scenario) + { + case "signed-in": + Snippet.KeyGenerator = null; + Snippet.ApiKey = InstallSnippetDemoData.SignedInKey; + break; + case "fails": + Snippet.ApiKey = null; + Snippet.KeyGenerator = async token => + { + await Task.Delay(700, token); + throw new InvalidOperationException("Could not reach the key service. Check your connection."); + }; + break; + default: + Snippet.ApiKey = null; + Snippet.KeyGenerator = null; + break; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourFrameworks.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourFrameworks.axaml new file mode 100644 index 00000000..239c3fe8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourFrameworks.axaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourKeyService.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourKeyService.axaml new file mode 100644 index 00000000..68e62078 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourKeyService.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourKeyService.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourKeyService.axaml.cs new file mode 100644 index 00000000..5342215f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/InstallSnippet/YourKeyService.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class InstallSnippetYourKeyServiceDemo : UserControl +{ + public InstallSnippetYourKeyServiceDemo() + { + InitializeComponent(); + // Your endpoint creates the key; the token is cancelled when the card leaves or a new key is asked for. + Snippet.KeyGenerator = async token => + { + await Task.Delay(800, token); + return InstallSnippetDemoData.KeyPrefix + Guid.NewGuid().ToString("N")[..20]; + }; + } + + private void OnFrameworkChanged(object? sender, InstallSnippetFrameworkChangedEventArgs e) => + Log.Text = $"onboarding_framework {{ id: \"{e.FrameworkId}\" }}"; + + private void OnPackageManagerChanged(object? sender, InstallSnippetPackageManagerChangedEventArgs e) => + Log.Text = $"onboarding_package_manager {{ id: \"{e.PackageManager}\" }}"; + + private void OnKeyGenerated(object? sender, InstallSnippetKeyGeneratedEventArgs e) => + Log.Text = $"onboarding_key {{ simulated: {(e.IsSimulated ? "true" : "false")} }}"; + + private void OnCopied(object? sender, InstallSnippetCopiedEventArgs e) => + Log.Text = $"onboarding_copy {{ step: \"{e.Step}\", length: {e.Value.Length} }}"; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/LiveVisitorsDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/LiveVisitorsDemoData.cs new file mode 100644 index 00000000..b2a2bea8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/LiveVisitorsDemoData.cs @@ -0,0 +1,36 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented site the LiveVisitors demos simulate: a SaaS marketing site with about 128 people on it, visitors from +/// 21 cities, eight pages and three kinds of conversion. The traffic is invented; nothing is fetched or tracked. +/// +internal static class LiveVisitorsDemoData +{ + /// About how many people are on the site. + public const int TypicalCount = 128; + + /// The cities people come from, weighted. + public static IReadOnlyList Cities { get; } = + [ + new("San Francisco", "US", 37.77, -122.42, 5), new("New York", "US", 40.71, -74.01, 5), new("Austin", "US", 30.27, -97.74, 3), + new("Toronto", "CA", 43.65, -79.38, 3), new("Mexico City", "MX", 19.43, -99.13, 2), new("São Paulo", "BR", -23.55, -46.63, 3), + new("London", "GB", 51.51, -0.13, 5), new("Dublin", "IE", 53.35, -6.26, 2), new("Paris", "FR", 48.86, 2.35, 3), + new("Amsterdam", "NL", 52.37, 4.90, 3), new("Berlin", "DE", 52.52, 13.40, 4), new("Zurich", "CH", 47.38, 8.54, 2), + new("Stockholm", "SE", 59.33, 18.07, 2), new("Madrid", "ES", 40.42, -3.70, 2), new("Lagos", "NG", 6.52, 3.38, 2), + new("Nairobi", "KE", -1.29, 36.82, 1), new("Bengaluru", "IN", 12.97, 77.59, 4), new("Singapore", "SG", 1.35, 103.82, 3), + new("Seoul", "KR", 37.57, 126.98, 2), new("Tokyo", "JP", 35.68, 139.69, 4), new("Sydney", "AU", -33.87, 151.21, 3), + ]; + + /// The site's pages, weighted by their share of the people on it. + public static IReadOnlyList SitePages { get; } = + [ + new("/", 34), new("/pricing", 22), new("/docs/quickstart", 17), new("/blog/launch-week", 14), new("/changelog", 9), + new("/signup", 8), new("/customers", 6), new("/docs/api/webhooks", 5), + ]; + + /// The conversions the ticker shows, weighted by how often they happen. + public static IReadOnlyList Conversions { get; } = + [ + new("Signed up", 62), new("Upgraded to Pro", 28), new("Upgraded to Team", 10), + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Narrow.axaml new file mode 100644 index 00000000..6b66cc91 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Preview.axaml new file mode 100644 index 00000000..270f0212 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Preview.axaml.cs new file mode 100644 index 00000000..f854acee --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/Preview.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated stream, live, connecting and reconnecting, and the empty card. +public partial class LiveVisitorsPreviewDemo : UserControl +{ + private readonly LiveVisitorsSimulation? _simulation; + private readonly bool _ready; + + public LiveVisitorsPreviewDemo() + { + InitializeComponent(); + _simulation = Card.Simulation; + _ready = true; + } + + private void OnStateChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the card exists. + if (!_ready) + { + return; + } + var state = State.SelectedValue as string; + // Empty: no data and no simulation, so the card holds its placeholders and says there is no data. + Card.Simulation = state == "empty" ? null : _simulation; + Card.Connection = state switch + { + "connecting" => LiveVisitorsConnection.Connecting, + "reconnecting" => LiveVisitorsConnection.Reconnecting, + _ => LiveVisitorsConnection.Live, + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/YourData.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/YourData.axaml new file mode 100644 index 00000000..3399ad85 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/YourData.axaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/YourData.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/YourData.axaml.cs new file mode 100644 index 00000000..3deaf694 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LiveVisitors/YourData.axaml.cs @@ -0,0 +1,95 @@ +using Avalonia.Controls; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// The card fed by the app: a timer plays the part of a realtime socket. +public partial class LiveVisitorsYourDataDemo : UserControl +{ + private static readonly (string City, string Country, double Lat, double Lon)[] s_places = + [ + ("Portland", "US", 45.52, -122.68), ("Chicago", "US", 41.88, -87.63), ("Lisbon", "PT", 38.72, -9.14), + ("Oslo", "NO", 59.91, 10.75), ("Warsaw", "PL", 52.23, 21.01), ("Cape Town", "ZA", -33.92, 18.42), + ("Mumbai", "IN", 19.08, 72.88), ("Osaka", "JP", 34.69, 135.50), ("Auckland", "NZ", -36.85, 174.76), + ]; + + private readonly DispatcherTimer _socket = new() { Interval = TimeSpan.FromMilliseconds(800) }; + private readonly Random _random = new(11); + private readonly Dictionary _pages = new() + { + ["/app/inbox"] = 21, ["/app/reports"] = 14, ["/app/settings/billing"] = 6, ["/app/onboarding"] = 9, + }; + private int _ticks; + + public LiveVisitorsYourDataDemo() + { + InitializeComponent(); + Card.Labels = new LiveVisitorsLabels { People = n => n == 1 ? "person in the app now" : "people in the app now" }; + Card.PausedChanged += (_, e) => + { + // Pause your own subscription with the card. + if (e.IsPaused) + { + _socket.Stop(); + Log.Text = "Feed paused."; + } + else + { + _socket.Start(); + Log.Text = "Feed resumed."; + } + }; + _socket.Tick += (_, _) => Receive(); + AttachedToVisualTree += (_, _) => + { + _ticks = 0; + Card.Connection = LiveVisitorsConnection.Connecting; + _socket.Start(); + }; + DetachedFromVisualTree += (_, _) => _socket.Stop(); + } + + private void Receive() + { + _ticks++; + if (_ticks == 2) + { + Card.Connection = LiveVisitorsConnection.Live; + Log.Text = "Connected."; + } + if (_ticks < 2) + { + return; + } + // People move between pages. + var keys = _pages.Keys.ToArray(); + var from = keys[_random.Next(keys.Length)]; + var to = keys[_random.Next(keys.Length)]; + if (_pages[from] > 1) + { + _pages[from]--; + _pages[to]++; + } + if (_random.NextDouble() < 0.4) + { + _pages[to]++; + } + Card.Pages = _pages.Select(p => new LiveVisitorsPage(p.Key, p.Value)).ToArray(); + Card.Count = _pages.Values.Sum(); + + var place = s_places[_random.Next(s_places.Length)]; + Card.PushVisitor(place.Lat + _random.NextDouble() * 2 - 1, place.Lon + _random.NextDouble() * 2 - 1); + if (_ticks % 4 == 0) + { + Card.PushEvent(new LiveVisitorsEvent + { + Label = _ticks % 12 == 0 ? "Upgraded to Pro" : "Started a trial", + City = place.City, + Country = place.Country, + At = DateTimeOffset.UtcNow, + Latitude = place.Lat, + Longitude = place.Lon, + }); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/LlmCostCalculatorDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/LlmCostCalculatorDemoData.cs new file mode 100644 index 00000000..58186e68 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/LlmCostCalculatorDemoData.cs @@ -0,0 +1,79 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// Real models for the LlmCostCalculator demos with their vendors' list prices from (standard +/// tier, US dollars per 1M tokens, checked on ), and invented workload presets. Marks +/// are left out, so each row shows its provider's initial. +/// +internal static class LlmCostCalculatorDemoData +{ + private static readonly AiModelInfo[] s_compared = + [ + AiModelCatalog.ClaudeHaiku55, AiModelCatalog.ClaudeSonnet55, AiModelCatalog.Gpt61Terra, AiModelCatalog.Gpt6Astra, + AiModelCatalog.Gemini38Flash, AiModelCatalog.Grok47, AiModelCatalog.MistralMedium35, AiModelCatalog.DeepSeekV4Pro, + ]; + + private static readonly AiModelInfo[] s_approved = + [ + AiModelCatalog.Gpt61Sol, AiModelCatalog.ClaudeSonnet55, AiModelCatalog.Qwen38Max, AiModelCatalog.MistralLarge3, + ]; + + /// Eight models from seven vendors. + public static IList Models { get; } = [.. s_compared.Select(PriceOf)]; + + /// A team's short list of approved models, for the demo with its own presets. + public static IList ApprovedModels { get; } = [.. s_approved.Select(PriceOf)]; + + /// The line under the heading. + public static string Description { get; } = $"List prices per 1M tokens, checked {AiModelCatalog.CheckedOn}"; + + /// The fine print: the prices are list prices on the checked day, not a quote. + public static string Note { get; } = NoteFor(s_compared, null); + + /// The fine print of the business-days demo. + public static string BusinessDaysNote { get; } = NoteFor(s_compared, "22 billable days a month"); + + /// The fine print of the approved-models demo. + public static string ApprovedNote { get; } = NoteFor(s_approved, null); + + /// Chat, RAG, Agent and Batch: invented workloads. + public static IList Presets { get; } = + [ + new("chat", "Chat", new LlmUsage(2_000, 500, 10_000, false)), + new("rag", "RAG", new LlmUsage(16_000, 500, 5_000, true)), + new("agent", "Agent", new LlmUsage(32_000, 1_000, 2_500, true)), + new("batch", "Batch", new LlmUsage(4_000, 1_000, 100_000, false)), + ]; + + /// A team's own workloads. + public static IList TeamPresets { get; } = + [ + new("support", "Support", new LlmUsage(6_000, 400, 20_000, true)), + new("search", "Search", new LlmUsage(16_000, 250, 50_000, true)), + new("reports", "Reports", new LlmUsage(64_000, 4_000, 500, false)), + ]; + + /// A catalog model as a row: its API id, name, provider and list prices. + public static LlmModelPrice PriceOf(AiModelInfo model) => + new(model.ApiId, model.Name, model.Provider.Name, model.InputPerMTok, model.OutputPerMTok, model.CachedInputPerMTok); + + /// + /// "Vendors' list prices (standard tier, short prompts), checked …, not a quote", naming the models whose price is + /// introductory (the catalog's price note says so). + /// + private static string NoteFor(IEnumerable models, string? extra) + { + var note = $"Vendors' list prices (standard tier, short prompts), checked {AiModelCatalog.CheckedOn}, not a quote."; + var introductory = models.Where(m => m.PriceTierNote?.StartsWith("Introductory", StringComparison.Ordinal) == true) + .Select(m => m.Name).ToList(); + if (introductory.Count > 0) + { + note += $" Introductory price: {string.Join(", ", introductory)}."; + } + if (extra is not null) + { + note += $" {extra}."; + } + return note + " Calculated on this device, nothing was sent."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Narrow.axaml new file mode 100644 index 00000000..529a10c7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Narrow.axaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/NoPresets.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/NoPresets.axaml new file mode 100644 index 00000000..04f580b3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/NoPresets.axaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Preview.axaml new file mode 100644 index 00000000..795faeb2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Preview.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Preview.axaml.cs new file mode 100644 index 00000000..0e73eea2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/Preview.axaml.cs @@ -0,0 +1,50 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: real list prices, then the loading, error and empty states. +public partial class LlmCostCalculatorPreviewDemo : UserControl +{ + private readonly DispatcherTimer _load = new() { Interval = TimeSpan.FromMilliseconds(1200) }; + private readonly bool _ready; + + public LlmCostCalculatorPreviewDemo() + { + InitializeComponent(); + _load.Tick += (_, _) => + { + _load.Stop(); + Calculator.IsLoading = false; + }; + Calculator.RetryRequested += OnRetryRequested; + DetachedFromVisualTree += (_, _) => _load.Stop(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The segmented control reports its first selection while the XAML loads, before the constructor has finished. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "prices"); + } + } + + private void Show(string scenario) + { + _load.Stop(); + Calculator.IsLoading = scenario == "loading"; + Calculator.ErrorText = scenario == "error" ? "The price service answered 503. Try again in a moment." : null; + Calculator.Models = scenario == "empty" ? null : LlmCostCalculatorDemoData.Models; + } + + private void OnRetryRequested(object? sender, RoutedEventArgs e) + { + // A pretend refetch: placeholder rows for a moment, then the prices. + Calculator.ErrorText = null; + Calculator.IsLoading = true; + _load.Start(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/YourPrices.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/YourPrices.axaml new file mode 100644 index 00000000..e730205a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/YourPrices.axaml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/YourPrices.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/YourPrices.axaml.cs new file mode 100644 index 00000000..2d8fb4b2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/LlmCostCalculator/YourPrices.axaml.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app's own models, presets and cache rate, with the workload and the pick reported back. +public partial class LlmCostCalculatorYourPricesDemo : UserControl +{ + public LlmCostCalculatorYourPricesDemo() + { + InitializeComponent(); + // The models and their list prices (US dollars per 1M tokens) come from your price list; here, the gallery's catalog. + Calculator.Models = LlmCostCalculatorDemoData.ApprovedModels; + Calculator.Presets = LlmCostCalculatorDemoData.TeamPresets; + Calculator.Description = $"Approved models, list prices per 1M tokens, checked {AiModelCatalog.CheckedOn}"; + Calculator.Note = LlmCostCalculatorDemoData.ApprovedNote; + Calculator.Usage = new LlmUsage(6_000, 400, 20_000, true); + // SelectedCost follows every change, whether it came from a slider, a preset, the switch or a picked row. + Calculator.PropertyChanged += (_, e) => + { + if (e.Property == LlmCostCalculator.SelectedCostProperty || e.Property == LlmCostCalculator.SelectedModelProperty) + { + Report(); + } + }; + Calculator.SelectedModelChanged += (_, e) => Picked.Text = $"Picked {e.Model.Name} at ${e.Cost.Total:N2} a month."; + } + + private void Report() + { + var usage = Calculator.Usage; + var model = Calculator.SelectedModel; + var cost = Calculator.SelectedCost; + Log.Text = model is null + ? string.Empty + : $"{model.Name}: {usage.InputTokens:N0} in, {usage.OutputTokens:N0} out, {usage.RequestsPerDay:N0} a day" + + $"{(usage.Cached ? ", cached" : string.Empty)}: ${cost.Total:N2} a month, ${cost.PerRequest:N4} a request."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/MeetingSummaryDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/MeetingSummaryDemoData.cs new file mode 100644 index 00000000..1c4fa4a3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/MeetingSummaryDemoData.cs @@ -0,0 +1,68 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented call the meeting summary demos play: a pricing page relaunch sync with five people, its summary, +/// decisions and action items, each with the transcript quote it came from. People, product and numbers are made up; +/// the model credited is a real one from . Demo content only. +/// +internal static class MeetingSummaryDemoData +{ + private static TimeSpan At(int minutes, int seconds) => new(0, minutes, seconds); + + /// The notes of the call (the card never changes them). + public static MeetingSummaryData Meeting { get; } = new() + { + Title = "Pricing page relaunch sync", + Platform = "Video call", + StartsAt = new DateTime(2026, 10, 28, 14, 0, 0), + DurationMinutes = 34, + TranscriptWords = 4812, + Attendees = + [ + new("maya", "Maya Lindqvist", "Design"), + new("daniel", "Daniel Okafor", "Engineering"), + new("priya", "Priya Raman", "Marketing"), + new("tom", "Tom Becker", "Product"), + new("lena", "Lena Moreau", "Support"), + ], + Summary = + [ + new("s1", "The team reviewed the new pricing page draft and agreed the three-tier layout reads clearly on desktop.", + new("maya", At(2, 14), "I think the three columns finally work. People see the middle plan first, which is what we want.")), + new("s2", "Support sees most confusion around seat limits, so the comparison table needs a clearer seats row.", + new("lena", At(9, 41), "Half of the tickets this month were about seats. Nobody can tell what happens when you add the sixth person.")), + new("s3", "Engineering can ship the page behind a flag next week once the checkout copy is final.", + new("daniel", At(17, 5), "The page itself is done. If copy lands by Monday I can put it behind the flag on Tuesday.")), + ], + Decisions = + [ + new("d1", "Launch with annual billing selected by default, with the monthly price shown beside it.", + new("tom", At(21, 30), "Let's default to annual and keep monthly right next to it, so nobody feels tricked.")), + new("d2", "Drop the enterprise calculator from this release and link to sales instead.", + new("priya", At(26, 52), "The calculator is a whole project. For now a Talk to sales link is enough.")), + ], + ActionItems = + [ + new("a1", "Redesign the seats row in the comparison table", "maya", new DateOnly(2026, 10, 31), + new("maya", At(11, 8), "I'll take the seats row. I'll have something to look at by Friday.")), + new("a2", "Finalize the checkout copy and send it for review", "priya", new DateOnly(2026, 11, 2), + new("priya", At(18, 20), "I'll write the checkout copy over the weekend and share it Monday morning.")), + new("a3", "Put the new page behind the pricing_v2 flag", "daniel", new DateOnly(2026, 11, 4), + new("daniel", At(19, 2), "Flag is pricing_v2. I'll wire it up as soon as the copy is in.")), + new("a4", "Draft a help article on how seat limits work", "lena", new DateOnly(2026, 11, 6), + new("lena", At(29, 47), "I can write the help article so we have something to link from the table.")), + ], + }; + + /// The model credited in the footer (a neutral mark; no brand mark is drawn). + public static AgentModel Model { get; } = new(AiModelCatalog.Qwen38Max.Name, ArcIcons.Sparkles); + + /// The references simulated sends hand out: "ISS-142" in Issues, "Notes 143" in Docs. + public static IReadOnlyDictionary ReferenceFormats { get; } = new Dictionary + { + [MeetingSummarySendTarget.Issues.Id] = "ISS-{0}", + [MeetingSummarySendTarget.Docs.Id] = "Notes {0}", + }; + + public const int FirstReferenceNumber = 142; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Narrow.axaml new file mode 100644 index 00000000..16d41ccd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Preview.axaml new file mode 100644 index 00000000..dc019fa6 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Preview.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Preview.axaml.cs new file mode 100644 index 00000000..aff22be9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Preview.axaml.cs @@ -0,0 +1,54 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulation live, a failed pass and a call with no transcript. +public partial class MeetingSummaryPreviewDemo : UserControl +{ + private readonly bool _ready; + + public MeetingSummaryPreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "live"); + } + } + + private void Show(string scenario) + { + switch (scenario) + { + case "error": + Card.Meeting = null; + Card.ErrorText = "The recording ended before the transcript was ready. Try again in a minute."; + Card.Status = MeetingSummaryStatus.Error; + break; + case "empty": + Card.Meeting = new MeetingSummaryData + { + Title = "Quick check-in", + StartsAt = new DateTime(2026, 10, 29, 9, 0, 0), + DurationMinutes = 1, + Attendees = [new("sam", "Sam Porter"), new("noor", "Noor Haddad")], + }; + Card.ErrorText = null; + Card.Status = MeetingSummaryStatus.Ready; + break; + default: + // Back to the card's own pass on the simulation. + Card.Meeting = null; + Card.ErrorText = null; + Card.Status = null; + Card.Regenerate(); + break; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Summarizer.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Summarizer.axaml new file mode 100644 index 00000000..c36f40fb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Summarizer.axaml @@ -0,0 +1,11 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Summarizer.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Summarizer.axaml.cs new file mode 100644 index 00000000..27241449 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/Summarizer.axaml.cs @@ -0,0 +1,69 @@ +using System.Runtime.CompilerServices; +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The notes from an async streaming delegate: snapshots that grow, and a failure on every other try. +public partial class MeetingSummarySummarizerDemo : UserControl +{ + private int _tries; + + public MeetingSummarySummarizerDemo() + { + InitializeComponent(); + // The call's header shows while the notes are summarized; each snapshot then takes its place. + Card.Meeting = Header(); + Card.Model = MeetingSummaryDemoData.Model; + Card.Summarizer = SummarizeAsync; + } + + /// A stand-in for your model call: each yield is the notes so far. + private async IAsyncEnumerable SummarizeAsync([EnumeratorCancellation] CancellationToken token) + { + var attempt = ++_tries; + await Task.Delay(1100, token); + if (attempt % 2 == 0) + { + throw new InvalidOperationException("The model timed out after 30 seconds."); + } + var notes = Copy(Header()); + notes.Summary.Add(new("s1", "Escalations doubled after the sync change, mostly from accounts with more than one workspace.", + new("ada", new TimeSpan(0, 1, 50), "It's almost all multi-workspace accounts. Single workspace is fine."))); + yield return notes; + + await Task.Delay(400, token); + notes = Copy(notes); + notes.Decisions.Add(new("d1", "Roll the sync change back for multi-workspace accounts until the fix ships.", + new("luis", new TimeSpan(0, 9, 12), "Let's flag it off for multi-workspace accounts today."))); + yield return notes; + + await Task.Delay(400, token); + notes = Copy(notes); + notes.ActionItems.Add(new("a1", "Turn the sync flag off for multi-workspace accounts", "luis", new DateOnly(2026, 10, 27), + new("luis", new TimeSpan(0, 10, 3), "I'll turn the flag off right after this."))); + notes.ActionItems.Add(new("a2", "Reply to the open escalations with the workaround", "ada", new DateOnly(2026, 10, 27), + new("ada", new TimeSpan(0, 15, 40), "I'll answer the open tickets with the workaround tonight."))); + yield return notes; + } + + private static MeetingSummaryData Header() => new() + { + Title = "Support escalation review", + StartsAt = new DateTime(2026, 10, 26, 16, 0, 0), + DurationMinutes = 22, + TranscriptWords = 2950, + Attendees = [new("ada", "Ada Brooks", "Support"), new("luis", "Luis Ortega", "Engineering")], + }; + + private static MeetingSummaryData Copy(MeetingSummaryData notes) => new() + { + Title = notes.Title, + StartsAt = notes.StartsAt, + DurationMinutes = notes.DurationMinutes, + TranscriptWords = notes.TranscriptWords, + Attendees = notes.Attendees, + Summary = [.. notes.Summary], + Decisions = [.. notes.Decisions], + ActionItems = [.. notes.ActionItems], + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/YourMeeting.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/YourMeeting.axaml new file mode 100644 index 00000000..2f79a84c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/YourMeeting.axaml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/YourMeeting.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/YourMeeting.axaml.cs new file mode 100644 index 00000000..fad9bfed --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/MeetingSummary/YourMeeting.axaml.cs @@ -0,0 +1,90 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// A meeting of your own, with the status, the sends and the checks handled by the app. +public partial class MeetingSummaryYourMeetingDemo : UserControl +{ + private readonly DispatcherTimer _request = new() { Interval = TimeSpan.FromMilliseconds(1200) }; + private int _issue = 30; + + public MeetingSummaryYourMeetingDemo() + { + InitializeComponent(); + Card.Meeting = Notes(); + Card.Model = MeetingSummaryDemoData.Model; + Card.Status = MeetingSummaryStatus.Summarizing; + _request.Tick += (_, _) => + { + // Your request resolved: the notes stream in. + _request.Stop(); + Card.Status = MeetingSummaryStatus.Ready; + }; + AttachedToVisualTree += (_, _) => _request.Start(); + DetachedFromVisualTree += (_, _) => _request.Stop(); + } + + private void OnSendRequested(object? sender, MeetingSummarySendEventArgs e) => + e.Completion = CreateAsync(e.Item, e.Target, e.CancellationToken); + + /// A stand-in for your tracker's API: pass the token on to your HTTP call. + private async Task CreateAsync(MeetingActionItem item, MeetingSummarySendTarget target, CancellationToken token) + { + await Task.Delay(700, token); + var reference = target == MeetingSummarySendTarget.Docs ? "Onboarding notes" : "ENG-" + _issue++; + return new MeetingSummarySendResult(reference); + } + + private void OnSent(object? sender, MeetingSummarySentEventArgs e) => + Log.Text = $"Sent \"{e.Item.Text}\" to {e.Target.Name} as {e.Result?.Reference}."; + + private void OnChanged(object? sender, MeetingActionItemChangedEventArgs e) => + Log.Text = $"\"{e.Item.Text}\" is {(e.IsDone ? "done" : "open again")}."; + + private void OnRegenerate(object? sender, RoutedEventArgs e) + { + Log.Text = "Regenerating…"; + Card.Status = MeetingSummaryStatus.Summarizing; + _request.Start(); + } + + private static TimeSpan At(int minutes, int seconds) => new(0, minutes, seconds); + + private static MeetingSummaryData Notes() => new() + { + Title = "Onboarding flow review", + Platform = "Video call", + StartsAt = new DateTime(2026, 10, 27, 10, 30, 0), + DurationMinutes = 48, + TranscriptWords = 6120, + Attendees = + [ + new("ines", "Ines Duarte", "Design"), + new("kofi", "Kofi Mensah", "Engineering"), + new("ruth", "Ruth Adler", "Research"), + ], + Summary = + [ + new("s1", "Research found that most new teams stall on the invite step, before they create a first project.", + new("ruth", At(4, 12), "Seven of the ten teams we watched got stuck on invites. They didn't know who to add yet.")), + new("s2", "The checklist on the home page helps, but people miss it below the fold on laptops.", + new("ines", At(15, 40), "On a 13 inch screen the checklist starts right at the bottom edge. Nobody scrolls to it.")), + ], + Decisions = + [ + new("d1", "Make inviting teammates optional and move it after the first project.", + new("kofi", At(27, 5), "Let's just let them skip invites. They can add people once there's something to share.")), + ], + ActionItems = + [ + new("a1", "Move the invite step after project creation", "kofi", new DateOnly(2026, 11, 3), + new("kofi", At(29, 18), "I can move the step this sprint, it's mostly routing.")), + new("a2", "Pin the checklist above the fold on small screens", "ines", new DateOnly(2026, 11, 5), + new("ines", At(33, 51), "I'll try a pinned version of the checklist for small screens.")), + new("a3", "Share the session recordings with the growth team", "ruth", new DateOnly(2026, 10, 30), + new("ruth", At(44, 2), "I'll send the recordings to growth after this call.")), + ], + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Compare.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Compare.axaml new file mode 100644 index 00000000..caab6c58 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Compare.axaml @@ -0,0 +1,18 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/ModelCardDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/ModelCardDemoData.cs new file mode 100644 index 00000000..5e612270 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/ModelCardDemoData.cs @@ -0,0 +1,131 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// Real models for the ModelCard demos, read from : names, ids, list prices, limits, input types +/// and the benchmark scores each vendor publishes, checked on . Code, Science and Shell +/// are SWE-bench Verified, GPQA Diamond and Terminal-Bench; each Terminal-Bench version is its own benchmark, so only +/// scores of one version are ranked against each other. A score a vendor does not publish is left out and reads as not +/// published. The summaries are neutral sentences made from the catalog, not the vendors' words. Marks are monograms. +/// +internal static class ModelCardDemoData +{ + private const string SweBenchId = "swe-bench-verified"; + private const string GpqaId = "gpqa-diamond"; + private const string TerminalBenchId = "terminal-bench-"; + + /// The footnote of the compare table. + public static string Note { get; } = + $"List prices (standard tier) and vendor-published scores, checked {AiModelCatalog.CheckedOn}."; + + /// A missing value reads as not published, never as zero. + public static ModelCardLabels Labels { get; } = new() { None = "Not published", NotListed = "Not published" }; + + /// Frontier models that all publish a Terminal-Bench 4.0 score (the Shell tab). + public static IReadOnlyList Frontier { get; } = + [.. new[] { AiModelCatalog.ClaudeOpus55, AiModelCatalog.Gpt6Astra, AiModelCatalog.Gemini4Argon, AiModelCatalog.Grok47 }.Select(ModelOf)]; + + /// The benchmarks of : SWE-bench Verified, GPQA Diamond and Terminal-Bench 4.0. + public static IReadOnlyList FrontierBenchmarks { get; } = + [SweBench(), Gpqa(), TerminalBench("Terminal-Bench 4.0")]; + + /// Models that publish GPQA Diamond (the Science tab) and, for two of them, Terminal-Bench 2.1. + public static IReadOnlyList OpenWeights { get; } = + [.. new[] { AiModelCatalog.DeepSeekV4Pro, AiModelCatalog.Qwen38Max, AiModelCatalog.Qwen37Plus, AiModelCatalog.MistralSmall4 }.Select(ModelOf)]; + + /// The benchmarks of : SWE-bench Verified, GPQA Diamond and Terminal-Bench 2.1. + public static IReadOnlyList OpenWeightsBenchmarks { get; } = + [SweBench(), Gpqa(), TerminalBench("Terminal-Bench 2.1")]; + + /// The model most demos open on. + public static ModelCardModel Opus { get; } = Frontier[0]; + + /// A card model from a catalog entry; only the scores the vendor publishes are filled in. + public static ModelCardModel ModelOf(AiModelInfo model) + { + var scores = new Dictionary(); + if (model.SweBenchVerified is { } swe) + { + scores[SweBenchId] = swe.Value; + } + if (model.GpqaDiamond is { } gpqa) + { + scores[GpqaId] = gpqa.Value; + } + if (model.TerminalBench is { } terminal) + { + scores[TerminalBenchId + VersionOf(terminal.Version)] = terminal.Value; + } + return new ModelCardModel + { + Id = model.ApiId, + Name = model.Name, + Provider = model.Provider.Name, + Summary = SummaryOf(model), + Modalities = ModalitiesOf(model), + ContextWindow = model.ContextWindow, + MaxOutput = model.MaxOutputTokens, + KnowledgeCutoff = model.KnowledgeCutoff, + Pricing = new ModelCardPricing + { + Input = model.InputPerMTok, + Output = model.OutputPerMTok, + CachedInput = model.CachedInputPerMTok, + }, + Scores = scores, + }; + } + + private static ModelCardBenchmark SweBench() => + new() { Id = SweBenchId, Name = "SWE-bench Verified", ShortName = "Code", Description = "real GitHub issues resolved" }; + + private static ModelCardBenchmark Gpqa() => + new() { Id = GpqaId, Name = "GPQA Diamond", ShortName = "Science", Description = "graduate level science questions" }; + + private static ModelCardBenchmark TerminalBench(string version) => + new() { Id = TerminalBenchId + VersionOf(version), Name = version, ShortName = "Shell", Description = "tasks done in a terminal" }; + + /// "Terminal-Bench 4.0" → "4.0". + private static string VersionOf(string version) => version[(version.LastIndexOf(' ') + 1)..]; + + private static ModelCardModalities ModalitiesOf(AiModelInfo model) + { + var flags = ModelCardModalities.None; + foreach (var input in model.InputModalities) + { + flags |= input switch + { + "text" => ModelCardModalities.Text, + "image" => ModelCardModalities.Image, + "pdf" => ModelCardModalities.Pdf, + "audio" => ModelCardModalities.Audio, + "video" => ModelCardModalities.Video, + _ => ModelCardModalities.None, + }; + } + if (model.SupportsTools) + { + flags |= ModelCardModalities.Tools; + } + if (model.SupportsReasoning) + { + flags |= ModelCardModalities.Reasoning; + } + return flags; + } + + /// A neutral sentence from the catalog: who released it and when, and what it takes in. + private static string SummaryOf(AiModelInfo model) + { + var who = model.ReleaseDate is { } date ? $"Released by {model.Provider.Name} on {date}" : $"A {model.Provider.Name} model"; + var inputs = model.InputModalities.Select(m => m switch + { + "pdf" => "PDFs", + "image" => "images", + "video" => "video", + "audio" => "audio", + _ => m, + }).ToList(); + var takes = inputs.Count > 1 ? string.Join(", ", inputs.Take(inputs.Count - 1)) + " and " + inputs[^1] : inputs.FirstOrDefault() ?? "text"; + return $"{who}; takes {takes}."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Narrow.axaml new file mode 100644 index 00000000..ddea0f63 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Narrow.axaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Picker.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Picker.axaml new file mode 100644 index 00000000..1bfb644b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Picker.axaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Picker.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Picker.axaml.cs new file mode 100644 index 00000000..3e66eb68 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Picker.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The card as the detail pane of a picker, with Use model. +public partial class ModelCardPickerDemo : UserControl +{ + private readonly bool _ready; + private string _selected = ModelCardDemoData.OpenWeights[1].Id; + + public ModelCardPickerDemo() + { + InitializeComponent(); + Picker.ItemsSource = ModelCardDemoData.OpenWeights.Select(m => new SegmentedOption(m.Id, m.Name)).ToList(); + Picker.SelectedIndex = 0; + Card.SelectRequested += (_, e) => + { + _selected = e.Model.Id; + Show(e.Model); + }; + Show(ModelCardDemoData.OpenWeights[0]); + _ready = true; + } + + private void OnPicked(object? sender, SelectionChangedEventArgs e) + { + // The segmented control may select while the XAML loads, before the card is set up. + if (_ready && Picker.SelectedValue is string id) + { + Show(ModelCardDemoData.OpenWeights.First(m => m.Id == id)); + } + } + + private void Show(ModelCardModel model) + { + Card.Model = model; + Card.IsSelected = model.Id == _selected; + Current.Text = "Default model: " + ModelCardDemoData.OpenWeights.First(m => m.Id == _selected).Name; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Preview.axaml new file mode 100644 index 00000000..3d6dc4de --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Preview.axaml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Preview.axaml.cs new file mode 100644 index 00000000..3bc9f354 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/Preview.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: one card for the highlighted model of a small catalog of real models. +public partial class ModelCardPreviewDemo : UserControl +{ + private readonly bool _ready; + + public ModelCardPreviewDemo() + { + InitializeComponent(); + Picker.ItemsSource = ModelCardDemoData.Frontier.Select(m => new SegmentedOption(m.Id, m.Name)).ToList(); + Picker.SelectedIndex = 0; + _ready = true; + } + + private void OnPicked(object? sender, SelectionChangedEventArgs e) + { + // The segmented control may select while the XAML loads, before the card is set up. + if (_ready && Picker.SelectedValue is string id) + { + Card.Model = ModelCardDemoData.Frontier.First(m => m.Id == id); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/States.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/States.axaml new file mode 100644 index 00000000..d9505075 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/States.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/States.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/States.axaml.cs new file mode 100644 index 00000000..970850a3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ModelCard/States.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The card ready, loading, failed and without a model. +public partial class ModelCardStatesDemo : UserControl +{ + private readonly bool _ready; + + public ModelCardStatesDemo() + { + InitializeComponent(); + Card.RetryRequested += OnRetry; + _ready = true; + } + + private void OnStateChanged(object? sender, SelectionChangedEventArgs e) + { + // The segmented control applies SelectedValue while the XAML loads, before the constructor has run. + if (_ready) + { + Show(State.SelectedValue as string ?? "ready"); + } + } + + private void Show(string state) + { + Card.IsLoading = state == "loading"; + Card.ErrorText = state == "error" ? "The model catalog did not answer. Check your connection and try again." : null; + Card.Model = state == "empty" ? null : ModelCardDemoData.Opus; + } + + private void OnRetry(object? sender, RoutedEventArgs e) => State.SelectedValue = "ready"; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Narrow.axaml new file mode 100644 index 00000000..5249364a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/OwnModel.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/OwnModel.axaml new file mode 100644 index 00000000..42ed428a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/OwnModel.axaml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/OwnModel.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/OwnModel.axaml.cs new file mode 100644 index 00000000..474c6029 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/OwnModel.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The card over an app's own model: names from the app, a stand-in model through Sender and an audit log. +public partial class PiiRedactionOwnModelDemo : UserControl +{ + public PiiRedactionOwnModelDemo() + { + InitializeComponent(); + Card.Message = "Hello, this is Mara Quill from the Lisbon office. Please update the booking for Theo Brandt " + + "and send the invoice to mara.quill@example.com. My direct line is +1 555 010 0188."; + // The signed-in person and their contacts; introductions are found on top of these. + Card.Names = ["Mara Quill", "Theo Brandt"]; + Card.Sender = async (prompt, token) => + { + // Your backend sends the safe prompt to the model; this stand-in answers after a short wait. + await Task.Delay(700, token); + return "Done, [NAME_1]. The booking for [NAME_2] is updated and the invoice is on its way to [EMAIL_1]. " + + "If anything changes I will call [PHONE_1]."; + }; + } + + private void OnRedacted(object? sender, PiiRedactedEventArgs e) + { + // Log the types server side, never the values next to the prompt. + var types = e.Entities.GroupBy(x => x.Type).Select(g => $"{g.Count()} {g.Key.ToString().ToLowerInvariant()}"); + Log.Text = "pii.redacted: " + string.Join(", ", types); + } + + private void OnReplyRestored(object? sender, PiiReplyRestoredEventArgs e) + { + Log.Text = $"Reply restored: {e.Raw.Length} characters from the model, {e.Restored.Length} shown to the person."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/PiiRedactionDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/PiiRedactionDemoData.cs new file mode 100644 index 00000000..86a74005 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/PiiRedactionDemoData.cs @@ -0,0 +1,34 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented support conversation the PII redaction demos play: a double charge request with a name, a card, an +/// email and a phone number, and the model's reply with placeholders. Every value is made up: the name is invented, the +/// email is on example.com, the phone number is in the fictional 555 range and the card number is a well-known test +/// number (it passes Luhn and belongs to nobody). The model named is a real one from . +/// Demo content only. +/// +internal static class PiiRedactionDemoData +{ + /// The name the detector always flags. + public const string Name = "Juniper Vale"; + + /// The names the detector always flags. + public static IReadOnlyList Names { get; } = [Name]; + + /// A double charge support request with a name, a test card, an email and a phone number. + public const string Message = + "Hi, I'm Juniper Vale. I was charged twice for my March order on my card 4242 4242 4242 4242. " + + "Please send the refund confirmation to juniper.vale@example.com or call me at (555) 555-0142."; + + /// The model's reply, placeholders included (the guard restores them). + public const string Reply = + "Sorry about the double charge, [NAME_1]. I can see two payments for the March order on [CARD_1], and I have " + + "refunded the second one. A confirmation is on its way to [EMAIL_1], and if anything looks off we will call you " + + "at [PHONE_1]. The refund reaches your card in 3 to 5 business days."; + + /// The model the safe prompt goes to. + public static string ModelName => AiModelCatalog.Grok47.Name; + + /// A long model name, to show it truncating in a narrow flow. + public static string LongModelName => AiModelCatalog.Grok420NonReasoning.Name; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Playground.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Playground.axaml new file mode 100644 index 00000000..8f10ba4d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Playground.axaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Preview.axaml new file mode 100644 index 00000000..cdfa5e08 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PiiRedaction/Preview.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Accent.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Accent.axaml new file mode 100644 index 00000000..2e6d3277 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Accent.axaml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/InStep.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/InStep.axaml new file mode 100644 index 00000000..7fb49dce --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/InStep.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Inline.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Inline.axaml new file mode 100644 index 00000000..323768e8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Inline.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Lifecycle.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Lifecycle.axaml new file mode 100644 index 00000000..fa731871 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Lifecycle.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Lifecycle.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Lifecycle.axaml.cs new file mode 100644 index 00000000..aeda3fec --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Lifecycle.axaml.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class PixelMascotLifecycleDemo : UserControl +{ + private readonly DispatcherTimer _timer = new(); + private readonly Queue<(TimeSpan After, PixelMascotStatus Status, string Caption)> _script = new(); + + public PixelMascotLifecycleDemo() + { + InitializeComponent(); + _timer.Tick += (_, _) => Next(); + DetachedFromVisualTree += (_, _) => _timer.Stop(); + } + + private void OnSend(object? sender, RoutedEventArgs e) => Play(fails: false); + + private void OnFail(object? sender, RoutedEventArgs e) => Play(fails: true); + + // A simulated request: 1.5s to the first token, 2.5s of streaming, then done (or an error) and idle 2s later. + private void Play(bool fails) + { + _timer.Stop(); + _script.Clear(); + _script.Enqueue((TimeSpan.Zero, PixelMascotStatus.Thinking, "Request sent")); + _script.Enqueue((TimeSpan.FromSeconds(1.5), PixelMascotStatus.Writing, "Streaming the answer")); + _script.Enqueue(fails + ? (TimeSpan.FromSeconds(1.2), PixelMascotStatus.Error, "The request failed") + : (TimeSpan.FromSeconds(2.5), PixelMascotStatus.Done, "Finished")); + _script.Enqueue((TimeSpan.FromSeconds(fails ? 3 : 2), PixelMascotStatus.Idle, "Ready")); + Next(); + } + + private void Next() + { + _timer.Stop(); + if (!_script.TryDequeue(out var step)) + { + return; + } + Mascot.Status = step.Status; + Caption.Text = step.Caption; + if (_script.TryPeek(out var next)) + { + _timer.Interval = next.After; + _timer.Start(); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Preview.axaml new file mode 100644 index 00000000..1b5d4798 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Preview.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/ReducedMotion.axaml new file mode 100644 index 00000000..fb432429 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/ReducedMotion.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Sizes.axaml new file mode 100644 index 00000000..2316812b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/PixelMascot/Sizes.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Empty.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Empty.axaml new file mode 100644 index 00000000..c7052582 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Empty.axaml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Loading.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Loading.axaml new file mode 100644 index 00000000..1589cfba --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Loading.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Narrow.axaml new file mode 100644 index 00000000..93c00ce8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Preview.axaml new file mode 100644 index 00000000..78399a11 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/Preview.axaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/ProviderKeysDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/ProviderKeysDemoData.cs new file mode 100644 index 00000000..c214831d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/ProviderKeysDemoData.cs @@ -0,0 +1,66 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// A sample workspace for the ProviderKeys demos: real providers from with the key prefixes +/// their docs show (Google documents none, so its keys skip the prefix check). The keys are placeholders no provider would +/// issue, and the rejected key and its error are invented. Marks are monograms. +/// +internal static class ProviderKeysDemoData +{ + /// Anthropic, OpenAI and Google. + public static IReadOnlyList Providers { get; } = + [ProviderOf(AiModelCatalog.Anthropic), ProviderOf(AiModelCatalog.OpenAI), ProviderOf(AiModelCatalog.Google)]; + + /// The line under the title of the sample workspace. + public const string Description = + "Sample workspace with placeholder keys: every test is simulated and nothing is sent."; + + /// + /// A connected Anthropic key used two minutes ago, an OpenAI key that was rejected and no Google key, relative to the + /// moment it is read (a new list each time). + /// + public static IReadOnlyList Keys => KeysAt(DateTimeOffset.Now); + + /// What a server returns for an app's own workspace: only the last four characters of each key. + public static IReadOnlyList StoredKeys + { + get + { + var now = DateTimeOffset.Now; + return + [ + new ProviderKey { Provider = AiModelCatalog.Anthropic.Key, Status = ProviderKeyStatus.Connected, Last4 = "7d3e", LastUsedAt = now.AddMinutes(-14) }, + new ProviderKey { Provider = AiModelCatalog.OpenAI.Key, Status = ProviderKeyStatus.Connected, Last4 = "a0b1", LastUsedAt = now.AddDays(-1) }, + ]; + } + } + + /// A row for a catalog provider: its key, its name and the key prefix its docs show (none when not documented). + public static ProviderKeyProvider ProviderOf(AiProviderInfo provider) => new() + { + Id = provider.Key, + Name = provider.Name, + KeyPrefix = provider.ApiKeyPrefix ?? string.Empty, + }; + + private static List KeysAt(DateTimeOffset now) => + [ + new ProviderKey + { + Provider = AiModelCatalog.Anthropic.Key, + Status = ProviderKeyStatus.Connected, + Value = (AiModelCatalog.Anthropic.ApiKeyPrefix ?? string.Empty) + "test-xxxxxxxxxxxxxxxxxxxxxxxx4f2a", + LastUsedAt = now - TimeSpan.FromMinutes(2), + CheckedAt = now - TimeSpan.FromHours(1), + }, + new ProviderKey + { + Provider = AiModelCatalog.OpenAI.Key, + Status = ProviderKeyStatus.Invalid, + Value = (AiModelCatalog.OpenAI.ApiKeyPrefix ?? string.Empty) + "test-0000000000000000000000009c1d", + LastUsedAt = now - TimeSpan.FromDays(3), + CheckedAt = now - TimeSpan.FromMinutes(20), + Error = "401 Unauthorized", + }, + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/YourApi.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/YourApi.axaml new file mode 100644 index 00000000..bc978972 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/YourApi.axaml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/YourApi.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/YourApi.axaml.cs new file mode 100644 index 00000000..459c08f9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderKeys/YourApi.axaml.cs @@ -0,0 +1,46 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Provider keys wired to an app: a test delegate standing in for a server call, and an event log. +public partial class ProviderKeysYourApiDemo : UserControl +{ + private readonly List _entries = new(); + + public ProviderKeysYourApiDemo() + { + InitializeComponent(); + // What your server returns: only the last four characters, never the full key (reveal and copy are off). + Card.Keys = ProviderKeysDemoData.StoredKeys; + Card.TestKey = TestOnServerAsync; + } + + /// Stands in for a request to your server, which would call the provider (for example its models list). + private static async Task TestOnServerAsync(ProviderKeyTestRequest request) + { + await Task.Delay(900, request.CancellationToken); + if (request.Key is { } key && key.Contains("revoked", StringComparison.OrdinalIgnoreCase)) + { + return ProviderKeyTestResult.Fail("401 Unauthorized"); + } + return ProviderKeyTestResult.Ok("sample server, nothing was sent"); + } + + private void OnSaved(object? sender, ProviderKeySavedEventArgs e) => Add($"Saved ({e.Mode}): {e.Provider}, ends in {e.Key[^4..]}"); + + private void OnRemoved(object? sender, ProviderKeyEventArgs e) => Add($"Removed for good: {e.Provider}"); + + private void OnCopied(object? sender, ProviderKeyEventArgs e) => Add($"Copied: {e.Provider}"); + + private void OnKeysChanged(object? sender, ProviderKeysChangedEventArgs e) => Add($"{e.Change}: {e.Provider} ({e.Keys.Count} keys)"); + + private void Add(string entry) + { + _entries.Add(entry); + if (_entries.Count > 4) + { + _entries.RemoveAt(0); + } + Log.Text = string.Join("\n", _entries); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/Narrow.axaml new file mode 100644 index 00000000..e52843d5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/Narrow.axaml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/OwnData.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/OwnData.axaml new file mode 100644 index 00000000..615163ff --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/OwnData.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/OwnData.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/OwnData.axaml.cs new file mode 100644 index 00000000..d0bcc8f7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/OwnData.axaml.cs @@ -0,0 +1,89 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class ProviderStatusOwnDataDemo : UserControl +{ + private int _checks; + + public ProviderStatusOwnDataDemo() + { + InitializeComponent(); + Card.Providers = Providers(0); + Card.RefreshHandler = RefreshAsync; + Card.IncidentOpened += (_, e) => + { + // Open your status page here; the demo only logs it. + e.Handled = true; + Log.Text = $"Opened the status page of {e.Provider.Name}: {e.Incident.Title}"; + }; + Card.ExpandedChanged += (_, e) => Log.Text = e.ExpandedId is null ? "Closed the incident" : $"Opened {e.ExpandedId}"; + } + + private async Task?> RefreshAsync(CancellationToken token) + { + await Task.Delay(700, token); + _checks++; + if (_checks % 3 == 0) + { + throw new InvalidOperationException("Couldn’t reach the status feeds. Showing the last results."); + } + return Providers(_checks); + } + + private static IReadOnlyList Providers(int check) + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var now = DateTimeOffset.UtcNow; + return + [ + new ProviderStatusItem + { + Id = "primary", + Name = AiModelCatalog.Anthropic.Name, + Detail = ProviderStatusDemoData.Detail(AiModelCatalog.Anthropic, null, AiModelCatalog.ClaudeOpus55) + " · primary route", + Health = ProviderStatusHealth.Degraded, + LatencyMs = 840 + check % 4 * 35, + Days = ProviderStatusDemoData.History(today, 29, 99.62, "Sample: slower responses since this morning."), + Incident = new ProviderStatusIncident + { + Id = "slow-region", + Title = "Sample incident: slower responses in one region", + Updates = + [ + new ProviderStatusUpdate + { + Stage = ProviderStatusStage.Investigating, + At = now.AddMinutes(-26), + Message = "Responses from one region are slower than usual. Investigating." + }, + new ProviderStatusUpdate + { + Stage = ProviderStatusStage.Identified, + At = now.AddMinutes(-11), + Message = "The cause is identified; traffic is moving to other regions." + } + ] + } + }, + new ProviderStatusItem + { + Id = "fallback", + Name = AiModelCatalog.OpenAI.Name, + Detail = ProviderStatusDemoData.Detail(AiModelCatalog.OpenAI, null, AiModelCatalog.Gpt61Sol) + " · fallback", + Health = ProviderStatusHealth.Operational, + LatencyMs = 390 + check % 3 * 20, + Days = ProviderStatusDemoData.History(today, 12, 99.7, "Sample: rate limits were lower than usual.") + }, + new ProviderStatusItem + { + Id = "vision", + Name = AiModelCatalog.Google.Name, + Detail = ProviderStatusDemoData.Detail(AiModelCatalog.Google, null, AiModelCatalog.Gemini4Argon) + " · image input", + Health = ProviderStatusHealth.Operational, + LatencyMs = 1240 + check % 5 * 60, + Days = ProviderStatusDemoData.History(today, 20, 98.4, "Sample: image requests failed for 50 minutes.") + } + ]; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/Preview.axaml new file mode 100644 index 00000000..0c19f55b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/Preview.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/ProviderStatusDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/ProviderStatusDemoData.cs new file mode 100644 index 00000000..9a855035 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/ProviderStatusDemoData.cs @@ -0,0 +1,270 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// Sample status data for the ProviderStatus demos. The provider names, the models and the status page components come +/// from ; every incident, latency and uptime figure is invented and labeled as sample data, never +/// any provider's real history. Incident titles start with "Sample". Marks are monograms. +/// +internal static class ProviderStatusDemoData +{ + private const int WindowDays = 30; + + /// The footnote under every sample card. + public const string Footnote = + "Sample data: the providers and their status page components are real; every incident, latency and uptime figure is invented, not their history."; + + /// + /// The story the preview plays, one step per simulated check, built relative to the moment it is read: an outage of the + /// first provider and a slowdown of the second resolve, the third has a short incident, and the tenth check fails to + /// show the error state. Then it starts over. + /// + public static IReadOnlyList Story + { + get + { + var now = DateTimeOffset.UtcNow; + var steps = new List(StoryLength); + for (var step = 0; step < StoryLength; step++) + { + steps.Add(step == FailingStep + ? new ProviderStatusSimulationStep { Fails = true } + : new ProviderStatusSimulationStep { Providers = StoryAt(step, now) }); + } + return steps; + } + } + + /// The id of the row whose incident the preview opens. + public static string OutageId => AiModelCatalog.Anthropic.Key; + + /// Three healthy providers for the states demo. + public static IReadOnlyList Healthy() + { + var today = DateOnly.FromDateTime(DateTime.UtcNow); + return + [ + HealthyItem(today, AiModelCatalog.Anthropic, Detail(AiModelCatalog.Anthropic, 2, AiModelCatalog.ClaudeSonnet55), 402), + HealthyItem(today, AiModelCatalog.OpenAI, Detail(AiModelCatalog.OpenAI, 0, AiModelCatalog.Gpt61Terra), 236), + HealthyItem(today, AiModelCatalog.Google, Detail(AiModelCatalog.Google, 0, AiModelCatalog.Gemini38Flash), 1180), + ]; + } + + /// + /// The row's line under the name: the model you call and, where the catalog lists them, the status page component + /// the row follows. + /// + public static string Detail(AiProviderInfo provider, int? component, params AiModelInfo[] models) + { + var detail = string.Join(" · ", models.Select(m => m.Name)); + if (component is { } index && index < provider.StatusComponents.Count) + { + detail += " · " + provider.StatusComponents[index]; + } + return detail; + } + + /// Thirty days of invented history ending today, with one event on . + public static List History(DateOnly today, int eventDay, double eventUptime, string note) + { + var days = new List(); + for (var d = 0; d < WindowDays; d++) + { + days.Add(new ProviderStatusDay + { + Date = today.AddDays(d - (WindowDays - 1)), + Uptime = d == eventDay ? eventUptime : d % 7 == 3 ? 99.96 : 100, + Note = d == eventDay ? note : null, + }); + } + return days; + } + + private const int StoryLength = 11; + + private const int FailingStep = 9; + + private static ProviderStatusItem HealthyItem(DateOnly today, AiProviderInfo provider, string detail, double latency) => new() + { + Id = provider.Key, + Name = provider.Name, + Detail = detail, + LatencyMs = latency, + Days = Enumerable.Range(0, WindowDays) + .Select(d => new ProviderStatusDay { Date = today.AddDays(d - (WindowDays - 1)), Uptime = d % 9 == 4 ? 99.95 : 100 }) + .ToList(), + }; + + /// The six providers at of the story, with times relative to . + private static IReadOnlyList StoryAt(int step, DateTimeOffset now) + { + var today = DateOnly.FromDateTime(now.UtcDateTime); + // Incident times are whole minutes before now, so they read like a status page. + var minute = new DateTimeOffset(now.UtcDateTime.Year, now.UtcDateTime.Month, now.UtcDateTime.Day, now.UtcDateTime.Hour, + now.UtcDateTime.Minute, 0, TimeSpan.Zero); + + var first = new ProviderStatusItem + { + Id = AiModelCatalog.Anthropic.Key, + Name = AiModelCatalog.Anthropic.Name, + Detail = Detail(AiModelCatalog.Anthropic, 2, AiModelCatalog.ClaudeSonnet55), + Health = step switch + { + <= 1 => ProviderStatusHealth.Outage, + <= 3 => ProviderStatusHealth.Degraded, + _ => ProviderStatusHealth.Operational + }, + LatencyMs = Latency(412, step, 0, step <= 1 ? 4.6 : step <= 3 ? 1.6 : 1), + Days = StoryHistory(0, today, step <= 5 ? (98.62, "Sample: elevated error rates since this morning.") : (98.9, "Sample: elevated error rates for 38 minutes."), + [(6, 99.82, "Sample: slower responses in one region."), (17, 98.4, "Sample: API errors for 52 minutes.")]), + Incident = step > 5 ? null : Incident("sample-errors", "Sample incident: elevated error rates", minute.AddMinutes(-14), + [ + (ProviderStatusStage.Investigating, 0, "Elevated error rates are being investigated."), + (ProviderStatusStage.Identified, 6, "The cause is identified and a fix is rolling out."), + (ProviderStatusStage.Monitoring, 11, "Error rates are back to normal. Monitoring."), + (ProviderStatusStage.Resolved, 14, "Resolved. Requests made during the incident can be retried.") + ], step switch + { + 0 => 1, + 1 => 2, + <= 3 => 3, + _ => 4 + }) + }; + + var second = new ProviderStatusItem + { + Id = AiModelCatalog.OpenAI.Key, + Name = AiModelCatalog.OpenAI.Name, + Detail = Detail(AiModelCatalog.OpenAI, 0, AiModelCatalog.Gpt61Sol), + Health = step <= 1 ? ProviderStatusHealth.Degraded : ProviderStatusHealth.Operational, + LatencyMs = Latency(620, step, 1, step <= 1 ? 3.1 : step == 2 ? 1.3 : 1), + Days = StoryHistory(1, today, (99.74, "Sample: slower responses for about 40 minutes."), + [(11, 99.86, "Sample: image uploads slower than usual."), (23, 99.5, "Sample: some requests timed out.")]), + Incident = step > 3 ? null : Incident("sample-slow", "Sample incident: slower responses", minute.AddMinutes(-9), + [ + (ProviderStatusStage.Investigating, 0, "Responses are slower than usual. Investigating."), + (ProviderStatusStage.Identified, 4, "Capacity in one region is short; more is coming online."), + (ProviderStatusStage.Monitoring, 7, "Response times are recovering. Monitoring."), + (ProviderStatusStage.Resolved, 9, "Response times have been normal for 15 minutes. Resolved.") + ], step + 1) + }; + + var thirdOpen = step is >= 6 and <= 8; + var third = new ProviderStatusItem + { + Id = AiModelCatalog.Google.Key, + Name = AiModelCatalog.Google.Name, + Detail = Detail(AiModelCatalog.Google, 0, AiModelCatalog.Gemini38Flash), + Health = step == 6 ? ProviderStatusHealth.Degraded : ProviderStatusHealth.Operational, + LatencyMs = Latency(280, step, 2, step == 6 ? 2.2 : 1), + Days = StoryHistory(2, today, step >= 6 ? (99.88, "Sample: a short rise in errors.") : (100, null), + [(3, 99.93, null), (20, 98.7, "Sample: streaming responses stopped early for an hour.")]), + Incident = !thirdOpen ? null : Incident("sample-streams", "Sample incident: streams ending early", minute.AddMinutes(-5), + [ + (ProviderStatusStage.Investigating, 0, "Some streaming responses end early. Investigating."), + (ProviderStatusStage.Monitoring, 3, "A fix is out and streams complete again. Monitoring."), + (ProviderStatusStage.Resolved, 5, "Resolved.") + ], step - 5) + }; + + var fourth = new ProviderStatusItem + { + Id = AiModelCatalog.MistralAI.Key, + Name = AiModelCatalog.MistralAI.Name, + Detail = Detail(AiModelCatalog.MistralAI, 0, AiModelCatalog.MistralMedium35), + Health = ProviderStatusHealth.Operational, + LatencyMs = Latency(455, step, 3, 1), + Days = StoryHistory(3, today, (100, null), [(9, 99.81, "Sample: elevated latency after a network change.")]) + }; + + var fifth = new ProviderStatusItem + { + Id = AiModelCatalog.DeepSeek.Key, + Name = AiModelCatalog.DeepSeek.Name, + Detail = Detail(AiModelCatalog.DeepSeek, 0, AiModelCatalog.DeepSeekV4Pro), + Health = ProviderStatusHealth.Operational, + LatencyMs = Latency(1340, step, 4, 1), + // Monitoring started 24 days ago: the bar pads the older days as No data. + Days = StoryHistory(4, today, (100, null), [(14, 99.4, "Sample: requests queued longer than usual.")], startDay: 6) + }; + + var sixth = new ProviderStatusItem + { + Id = AiModelCatalog.XAi.Key, + Name = AiModelCatalog.XAi.Name, + Detail = Detail(AiModelCatalog.XAi, null, AiModelCatalog.Grok47), + Health = ProviderStatusHealth.Operational, + LatencyMs = Latency(190, step, 5, 1), + Days = StoryHistory(5, today, (100, null), [(19, null, "Sample: checks were paused for maintenance.")]) + }; + + return [first, second, third, fourth, fifth, sixth]; + } + + /// A plausible latency: the base, a ±8% wobble per check and a factor while slow. + private static double Latency(double baseMs, int check, int provider, double factor) + { + var wobble = (Noise(provider * 131 + check * 17) - 0.5) * 0.16; + return Math.Round(baseMs * factor * (1 + wobble)); + } + + /// A repeatable number in [0, 1). + private static double Noise(int seed) + { + unchecked + { + var x = (uint)seed * 2654435761u; + x ^= x >> 15; + x *= 2246822519u; + x ^= x >> 13; + return (x & 0xFFFFFF) / (double)0x1000000; + } + } + + /// + /// Thirty days ending today: mostly 100% with a few near misses, the listed (days counted from + /// the oldest) and today's value. Days before are left out. + /// + private static List StoryHistory(int provider, DateOnly today, (double? Uptime, string? Note) todayValue, + (int Day, double? Uptime, string? Note)[] events, int startDay = 0) + { + var days = new List(WindowDays); + var first = today.AddDays(-(WindowDays - 1)); + for (var d = startDay; d < WindowDays; d++) + { + double? uptime = 100; + string? note = null; + var noise = Noise(provider * 977 + d * 31); + if (noise < 0.22) + { + uptime = Math.Round(99.94 + noise * 0.25, 2); + } + foreach (var e in events) + { + if (e.Day == d) + { + uptime = e.Uptime; + note = e.Note; + } + } + if (d == WindowDays - 1) + { + uptime = todayValue.Uptime; + note = todayValue.Note; + } + days.Add(new ProviderStatusDay { Date = first.AddDays(d), Uptime = uptime, Note = note }); + } + return days; + } + + private static ProviderStatusIncident Incident(string id, string title, DateTimeOffset start, + (ProviderStatusStage Stage, int Minutes, string Message)[] script, int posted) + { + var updates = new List(); + for (var i = 0; i < Math.Clamp(posted, 0, script.Length); i++) + { + updates.Add(new ProviderStatusUpdate { Stage = script[i].Stage, At = start.AddMinutes(script[i].Minutes), Message = script[i].Message }); + } + return new ProviderStatusIncident { Id = id, Title = title, Updates = updates }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/States.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/States.axaml new file mode 100644 index 00000000..28a42883 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/States.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/States.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/States.axaml.cs new file mode 100644 index 00000000..83d270dd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ProviderStatus/States.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class ProviderStatusStatesDemo : UserControl +{ + private readonly bool _ready; + + public ProviderStatusStatesDemo() + { + InitializeComponent(); + Card.Providers = []; + Card.RefreshHandler = _ => Task.FromResult?>(null); + // SelectionChanged fires while the XAML loads; subscribe once the fields are set. + State.SelectionChanged += (_, _) => Show(); + _ready = true; + Show(); + } + + private void Show() + { + if (!_ready) + { + return; + } + var state = State.SelectedValue as string; + Card.IsLoading = state == "loading"; + Card.ErrorText = state == "failed" ? "Couldn’t load provider status. Check your connection and try again." : null; + Card.Providers = state == "healthy" ? ProviderStatusDemoData.Healthy() : []; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Plain.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Plain.axaml new file mode 100644 index 00000000..7793ad50 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Plain.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Plain.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Plain.axaml.cs new file mode 100644 index 00000000..6366da04 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Plain.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class ReasoningBlockPlainDemo : UserControl +{ + public ReasoningBlockPlainDemo() + { + InitializeComponent(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Preview.axaml new file mode 100644 index 00000000..7681ecb5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Preview.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Preview.axaml.cs new file mode 100644 index 00000000..9b750422 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Preview.axaml.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the block live (a simulation), finished and interrupted (the app's own data), and empty. +public partial class ReasoningBlockPreviewDemo : UserControl +{ + private readonly ReasoningBlockSimulation _simulation; + private readonly bool _ready; + + public ReasoningBlockPreviewDemo() + { + InitializeComponent(); + // Open the reasoning once the simulated model is done, as an app might when the answer starts. + _simulation = Block.Simulation!; + _simulation.Finished += (_, _) => Block.IsExpanded = true; + Block.RetryRequested += OnRetryRequested; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "live"); + } + } + + private void Show(string scenario) + { + switch (scenario) + { + case "finished": + Block.ErrorText = null; + Block.Elapsed = ReasoningBlockDemoData.FinishedElapsed; + Block.Reasoning = ReasoningBlockDemoData.Reasoning; + Block.Status = ReasoningStatus.Done; + Block.IsExpanded = true; + break; + case "interrupted": + Block.Elapsed = ReasoningBlockDemoData.InterruptedElapsed; + Block.Reasoning = ReasoningBlockDemoData.Interrupted; + Block.ErrorText = ReasoningBlockDemoData.InterruptedReason; + Block.Status = ReasoningStatus.Error; + Block.IsExpanded = true; + break; + default: + // With no data of its own the block takes its simulation up again and starts it over; without a + // simulation it reads "Thought process" with nothing to open. + Block.IsExpanded = false; + Block.ClearValue(ReasoningBlock.ErrorTextProperty); + Block.ClearValue(ReasoningBlock.ElapsedProperty); + Block.ClearValue(ReasoningBlock.StatusProperty); + Block.ClearValue(ReasoningBlock.ReasoningProperty); + Block.Simulation = scenario == "empty" ? null : _simulation; + break; + } + } + + private void OnRetryRequested(object? sender, RoutedEventArgs e) + { + Scenario.SelectedValue = "live"; + Show("live"); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/ReasoningBlockDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/ReasoningBlockDemoData.cs new file mode 100644 index 00000000..eef3c681 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/ReasoningBlockDemoData.cs @@ -0,0 +1,27 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented reasoning the reasoning block demos share: a model tracing a double charge to webhook redeliveries. +/// Demo content only; a real app passes what its model has sent. +/// +internal static class ReasoningBlockDemoData +{ + /// The whole reasoning: bold headings, paragraphs under them. + public const string Reasoning = + "**Reading the webhook handler**\n\nThe handler in api/billing/webhook.ts checks the signature and then calls fulfillOrder for every invoice.paid event. Nothing records which events it already handled, so a redelivered event runs the whole path again.\n\n" + + "**Checking when Stripe retries**\n\nStripe sends an event again when the endpoint times out or answers with an error. The logs show fulfillOrder taking 11 seconds, because the receipt PDF renders before the handler answers.\n\n" + + "**Tracing the second charge**\n\nEvery double charge lines up with a redelivery. Each retry calls createUsageCharge again with a key built from Date.now(), so Stripe sees a new request instead of a repeat.\n\n" + + "**Choosing a fix**\n\nRecord event.id in a processed_events table inside the same transaction, derive the idempotency key from the invoice id, and move the receipt render to a queue so the handler answers in well under a second."; + + /// The first two steps, where the interrupted block stopped. + public static readonly string Interrupted = Reasoning[..Reasoning.IndexOf("**Tracing", StringComparison.Ordinal)].TrimEnd(); + + /// Why the interrupted block stopped. + public const string InterruptedReason = "The connection dropped before the model finished."; + + /// How long the finished block thought. + public static readonly TimeSpan FinishedElapsed = TimeSpan.FromSeconds(7); + + /// How long the interrupted block thought before it failed. + public static readonly TimeSpan InterruptedElapsed = TimeSpan.FromSeconds(3); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Stop.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Stop.axaml new file mode 100644 index 00000000..f716ac7a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Stop.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Stop.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Stop.axaml.cs new file mode 100644 index 00000000..f98026f4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReasoningBlock/Stop.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// Streams the reasoning; Stop ends it as Stopped, Replay starts over. +public partial class ReasoningBlockStopDemo : UserControl +{ + public ReasoningBlockStopDemo() + { + InitializeComponent(); + } + + private void OnReplay(object? sender, RoutedEventArgs e) + { + Block.IsExpanded = false; + Block.Simulation?.Restart(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Narrow.axaml new file mode 100644 index 00000000..6bd1d913 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Narrow.axaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Preview.axaml new file mode 100644 index 00000000..085b2a01 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Preview.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Preview.axaml.cs new file mode 100644 index 00000000..b1b47304 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/Preview.axaml.cs @@ -0,0 +1,56 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: a simulated program, a fresh program and one past its milestone. +public partial class ReferralCardPreviewDemo : UserControl +{ + private readonly bool _ready; + + public ReferralCardPreviewDemo() + { + InitializeComponent(); + Show("sample"); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor body runs. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "sample"); + } + } + + private void Show(string scenario) + { + // A new card each time, so the program plays again from the start. The demo gives the data: the link, the reward, + // the milestone and either a program to play (a simulation) or the referrals themselves. + var card = new ReferralCard + { + Link = ReferralCardDemoData.Link, + Reward = ReferralCardDemoData.Reward, + Milestone = ReferralCardDemoData.Milestone + }; + switch (scenario) + { + case "fresh": + card.Referrals = []; + card.Simulate = true; + card.Link = "https://app.example.com/r/sam-k"; + break; + case "unlocked": + card.Referrals = ReferralCardDemoData.Unlocked(DateTimeOffset.Now); + break; + default: + card.Simulation = new ReferralCardSimulation + { + Referrals = ReferralCardDemoData.Program(DateTimeOffset.Now), + Beats = ReferralCardDemoData.Beats + }; + break; + } + Host.Content = card; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/ReferralCardDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/ReferralCardDemoData.cs new file mode 100644 index 00000000..8221a6fd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/ReferralCardDemoData.cs @@ -0,0 +1,67 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// Invented referral programs for the demos (people and addresses are made up; example.com): the program the preview plays +/// (a friend who signed up subscribes, an invited one signs up and subscribes, and the fifth paid referral unlocks the +/// milestone), a program past its goal and one halfway. The reward and the milestone belong to an invented product. +/// +internal static class ReferralCardDemoData +{ + /// The invite link of the preview's program. + public const string Link = "https://app.example.com/r/emma20"; + + /// The credit each side gets, in dollars. + public const int Reward = 20; + + /// Five paid referrals unlock three free months. + public static ReferralMilestone Milestone { get; } = new() + { + Goal = 5, + Reward = "3 months free", + Detail = "Applied to your next invoice" + }; + + /// The preview's program at the start, newest first: three paid, one signed up, two invited. + public static List Program(DateTimeOffset now) => + [ + new() { Id = "p6", Email = "jonas.weber@example.com", Stage = ReferralStage.Invited, InvitedAt = now.AddDays(-1) }, + new() { Id = "p5", Email = "ana.souza@example.com", Stage = ReferralStage.Invited, InvitedAt = now.AddDays(-3) }, + new() { Id = "p4", Name = "Priya Raman", Email = "priya.raman@example.com", Stage = ReferralStage.SignedUp, InvitedAt = now.AddDays(-6) }, + new() { Id = "p3", Name = "Leo Martins", Email = "leo.martins@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-12) }, + new() { Id = "p2", Name = "Hana Sato", Email = "hana.sato@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-19) }, + new() { Id = "p1", Name = "Marcus Bell", Email = "marcus.bell@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-27) }, + ]; + + /// The preview's program as of when the gallery first showed it (the narrow demo shows it without playing). + public static IReadOnlyList StartingProgram { get; } = Program(DateTimeOffset.Now); + + /// The beats the preview plays: Priya subscribes, then Ana signs up and subscribes (the fifth paid referral). + public static IReadOnlyList Beats { get; } = + [ + new(TimeSpan.FromSeconds(1.4), "p4", ReferralStage.Earned), + new(TimeSpan.FromSeconds(2.2), "p5", ReferralStage.SignedUp, "Ana Souza"), + new(TimeSpan.FromSeconds(2.4), "p5", ReferralStage.Earned) + ]; + + /// A program past its goal of five paid referrals. + public static List Unlocked(DateTimeOffset now) => + [ + new() { Id = "u7", Email = "olivia.park@example.com", Stage = ReferralStage.Invited, InvitedAt = now.AddDays(-1) }, + new() { Id = "u6", Name = "Tomás Rivera", Email = "tomas.rivera@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-4) }, + new() { Id = "u5", Name = "Ines Laurent", Email = "ines.laurent@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-8) }, + new() { Id = "u4", Name = "Kofi Mensah", Email = "kofi.mensah@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-11) }, + new() { Id = "u3", Name = "Mei Lin", Email = "mei.lin@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-15) }, + new() { Id = "u2", Name = "Noah Fischer", Email = "noah.fischer@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-21) }, + new() { Id = "u1", Name = "Sara Haddad", Email = "sara.haddad@example.com", Stage = ReferralStage.SignedUp, InvitedAt = now.AddDays(-26) }, + ]; + + /// A program halfway: two paid, one signed up, two invited. + public static List Halfway(DateTimeOffset now) => + [ + new() { Id = "h5", Email = "dmitri.volkov@example.com", Stage = ReferralStage.Invited, InvitedAt = now.AddDays(-2) }, + new() { Id = "h4", Email = "a.very.long.address.for.a.friend@marketing.example.com", Stage = ReferralStage.Invited, InvitedAt = now.AddDays(-3) }, + new() { Id = "h3", Name = "Grace Okafor", Email = "grace.okafor@example.com", Stage = ReferralStage.SignedUp, InvitedAt = now.AddDays(-7) }, + new() { Id = "h2", Name = "Luca Bianchi", Email = "luca.bianchi@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-13) }, + new() { Id = "h1", Name = "Amara Diallo", Email = "amara.diallo@example.com", Stage = ReferralStage.Earned, InvitedAt = now.AddDays(-20) }, + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/YourData.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/YourData.axaml new file mode 100644 index 00000000..262772d9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/YourData.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/YourData.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/YourData.axaml.cs new file mode 100644 index 00000000..aba8dde2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReferralCard/YourData.axaml.cs @@ -0,0 +1,87 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The card driven by the app: its own list, a send that can fail, handled shares and webhook updates. +public partial class ReferralCardYourDataDemo : UserControl +{ + private List _list = []; + private int _next; + + public ReferralCardYourDataDemo() + { + InitializeComponent(); + Card.InviteHandler = SendAsync; + Card.InvitesSent += (_, e) => + { + // Your backend would return the new referrals; here they join the app's own list. + var now = DateTimeOffset.Now; + _list.InsertRange(0, e.Emails.Select(email => new Referral + { + Id = "app-" + ++_next, + Email = email, + Stage = ReferralStage.Invited, + InvitedAt = now + })); + Write($"Sent {e.Emails.Count} invite(s)."); + }; + Card.ShareRequested += (_, e) => + { + // A desktop app would open e.Url in the browser or the mail app here. + e.Handled = true; + Write($"Share {e.Channel}: {e.Url}"); + }; + Card.LinkCopied += (_, e) => Write("Copied " + e.Link); + Card.MilestoneReached += (_, e) => Write("Milestone reached: " + e.Milestone.Reward); + Webhook.Click += (_, _) => Advance(); + Reset.Click += (_, _) => Load(); + Load(); + } + + private void Load() + { + _list = ReferralCardDemoData.Halfway(DateTimeOffset.Now); + Card.Referrals = _list.ToList(); + Write("Events show here."); + } + + private static async Task SendAsync(IReadOnlyList emails, CancellationToken token) + { + await Task.Delay(800, token); + if (emails.Any(e => e.Contains("bounce", StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException("One address bounced. Nothing was sent."); + } + } + + /// The oldest referral that is not paid yet moves one stage on. + private void Advance() + { + for (var i = _list.Count - 1; i >= 0; i--) + { + var referral = _list[i]; + if (referral.Stage == ReferralStage.Earned) + { + continue; + } + var next = referral.WithStage(referral.Stage + 1); + if (next.Name is null) + { + next = next.WithName(NameFrom(next.Email)); + } + _list[i] = next; + Card.Referrals = _list.ToList(); + Write($"{next.Email} is now {next.Stage}."); + return; + } + Write("Everyone has paid."); + } + + private static string NameFrom(string email) + { + var local = email.Split('@')[0].Split('.', '-', '_'); + return string.Join(' ', local.Where(p => p.Length > 0).Take(2).Select(p => char.ToUpperInvariant(p[0]) + p[1..])); + } + + private void Write(string text) => Log.Text = text; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Insert.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Insert.axaml new file mode 100644 index 00000000..93efcdba --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Insert.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Insert.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Insert.axaml.cs new file mode 100644 index 00000000..8e183ad3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Insert.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Insert with a handler that takes a moment. +public partial class ReplyDraftInsertDemo : UserControl +{ + public ReplyDraftInsertDemo() + { + InitializeComponent(); + Draft.InsertHandler = InsertAsync; + } + + private async Task InsertAsync(ReplyDraftEventArgs draft) + { + // Your composer would take the text here; the delay stands in for saving it. + await Task.Delay(900); + Composer.Text = draft.Draft; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Narrow.axaml new file mode 100644 index 00000000..72bf9eeb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/OwnData.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/OwnData.axaml new file mode 100644 index 00000000..a8e722a5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/OwnData.axaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/OwnData.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/OwnData.axaml.cs new file mode 100644 index 00000000..2279d5ea --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/OwnData.axaml.cs @@ -0,0 +1,50 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Your own email, quick replies, model and writer. +public partial class ReplyDraftOwnDataDemo : UserControl +{ + public ReplyDraftOwnDataDemo() + { + InitializeComponent(); + Draft.Message = new ReplyDraftMessage + { + FromName = "Jonas Weber", + FromEmail = "jonas@fabrikam.example", + Subject = "Invoice 4471 charged twice", + Body = "Hello,\n\nOur card was charged twice for invoice 4471 this morning. Can you refund the duplicate?\n\nJonas", + ReceivedAt = DateTimeOffset.Now.AddMinutes(-12), + }; + Draft.Suggestions = + [ + new ReplyDraftSuggestion("refund", "Refund it"), + new ReplyDraftSuggestion("details", "Ask for details"), + ]; + Draft.Model = ReplyDraftDemoData.Model; + Draft.Generator = ReplyDraftStreams.FromText(Write); + Draft.DraftCompleted += (_, e) => Log.Text = $"Draft ready: {e.Mode}, {e.Tone}, {e.Length}, {e.Suggestion?.Label}"; + } + + // Your service would call a model here; the demo picks a canned text. + private static async Task Write(ReplyDraftRequest request) + { + await Task.Delay(500, request.CancellationToken); + var refund = request.Suggestion?.Id != "details"; + var opening = request.Tone == ReplyDraftTone.Formal ? "Dear Jonas," : "Hi Jonas,"; + var body = refund + ? "Sorry about that. I've refunded the duplicate charge for invoice 4471; it should reach your card in 3 to 5 business days." + : "Sorry about that. Could you send the last four digits of the card and the time of both charges so I can find them?"; + if (request.Length == ReplyDraftLength.Long) + { + body += " I've also added a note to your account so this does not happen again at the next renewal."; + } + if (request.Tone == ReplyDraftTone.Concise) + { + body = refund + ? "Refunded the duplicate for invoice 4471. Allow 3 to 5 business days." + : "Please send the card's last four digits and both charge times."; + } + return opening + "\n\n" + body + "\n\nBest,\nAlex"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Preview.axaml new file mode 100644 index 00000000..519cca8b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Preview.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Preview.axaml.cs new file mode 100644 index 00000000..a334f03e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/Preview.axaml.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the self-playing tour, the on-demand state, a failing generator and the card with no data. +public partial class ReplyDraftPreviewDemo : UserControl +{ + private readonly bool _ready; + + public ReplyDraftPreviewDemo() + { + InitializeComponent(); + _ready = true; + Show("tour"); + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor body runs. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "tour"); + } + } + + private void Show(string scenario) + { + Host.Child = scenario switch + { + "demand" => new ReplyDraft { AutoStart = false, AutoPlay = false, Simulation = ReplyDraftDemoData.Simulation() }, + // The demo's email as your own data, with a generator that fails. + "error" => new ReplyDraft + { + Message = ReplyDraftDemoData.Message, + Suggestions = ReplyDraftDemoData.Suggestions, + Model = ReplyDraftDemoData.Model, + Generator = Failing, + IsSimulated = true, + }, + "empty" => new ReplyDraft(), + _ => new ReplyDraft { Simulation = ReplyDraftDemoData.Simulation() }, + }; + } + + // A generator that reads for a moment and then fails, as a dropped connection would. + private static async IAsyncEnumerable Failing(ReplyDraftRequest request) + { + await Task.Delay(700, request.CancellationToken); + yield return "Hi Maya, "; + await Task.Delay(300, request.CancellationToken); + throw new InvalidOperationException("The model stopped responding."); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/ReplyDraftDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/ReplyDraftDemoData.cs new file mode 100644 index 00000000..34217d1b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ReplyDraft/ReplyDraftDemoData.cs @@ -0,0 +1,144 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented email the reply draft demos answer: a request to move a launch review, three quick replies and a reply +/// written for each quick reply, tone and length. Every name and address is made up; the model credited is a real one +/// from . Demo content only. +/// +internal static class ReplyDraftDemoData +{ + /// Who signs the replies. + public const string Signature = "Sam"; + + /// The email: a request to move a launch review. + public static ReplyDraftMessage Message { get; } = new() + { + FromName = "Maya Chen", + FromEmail = "maya.chen@harborline.example", + Subject = "Can we move Thursday's launch review?", + Body = "Hi Sam,\n\n" + + "The pricing page copy came back from legal with a few changes, and I don't think we can hold the launch review on " + + "Thursday at 2pm without them.\n\n" + + "Could we push it to next Tuesday at 10am instead? That gives us time to fold in their notes and rerun the " + + "screenshots. If Tuesday doesn't work, Wednesday afternoon is open on my side too.\n\n" + + "Thanks,\nMaya", + ReceivedAt = new DateTimeOffset(DateTime.Today.AddHours(9).AddMinutes(41)), + }; + + /// The quick replies. + public static IReadOnlyList Suggestions { get; } = + [ + new("tuesday", "Tuesday works"), + new("wednesday", "Try Wednesday"), + new("thursday", "Keep Thursday"), + ]; + + /// The model credited on the reply (a neutral mark; no brand mark is drawn). + public static AgentModel Model { get; } = new(AiModelCatalog.MistralMedium35.Name); + + // Four sentences per quick reply and tone: Short takes the first, Medium the first two, Long all four in two paragraphs. + private static readonly Dictionary<(string Id, ReplyDraftTone Tone), string[]> s_sentences = new() + { + [("tuesday", ReplyDraftTone.Friendly)] = + [ + "Tuesday at 10am works great for me, so let's move the launch review there.", + "Thanks for chasing legal on the pricing copy. Having their notes in first will make the review much smoother.", + "I'll update the invite and ask Priya to rerun the screenshots on Monday afternoon.", + "If anything else comes back from legal before then, just drop it in the doc and I'll take a look.", + ], + [("tuesday", ReplyDraftTone.Concise)] = + [ + "Tuesday at 10am works. I'll move the review.", + "Please share legal's notes once they're in.", + "I'll update the invite today and get the screenshots rerun on Monday.", + "Anything else from legal can go straight into the doc.", + ], + [("tuesday", ReplyDraftTone.Formal)] = + [ + "Thank you for letting me know. Tuesday at 10am suits me, and I will reschedule the launch review accordingly.", + "I appreciate you following up with legal on the pricing copy; reviewing their changes beforehand will make the session more productive.", + "I will send an updated invitation today and arrange for the screenshots to be regenerated on Monday.", + "Should legal raise any further points before then, please add them to the review document and I will read them ahead of the meeting.", + ], + [("wednesday", ReplyDraftTone.Friendly)] = + [ + "Tuesday morning is tricky for me, but Wednesday afternoon would be perfect.", + "How about 2pm? That still gives us a full day to fold in legal's notes.", + "I'll hold the slot on my calendar and move the invite once you confirm.", + "And thanks for flagging this early, it's much easier to shift now than on Thursday morning.", + ], + [("wednesday", ReplyDraftTone.Concise)] = + [ + "Tuesday doesn't work for me. Wednesday afternoon does.", + "Let's do 2pm.", + "I'll move the invite once you confirm.", + "That leaves a day for legal's notes and the screenshots.", + ], + [("wednesday", ReplyDraftTone.Formal)] = + [ + "Thank you for the suggestion. Unfortunately, Tuesday morning is not possible for me; Wednesday afternoon would be preferable.", + "Would 2pm on Wednesday be convenient? It would still allow time to incorporate legal's comments.", + "I will reserve the slot and update the invitation as soon as you confirm.", + "Thank you as well for raising this early, which makes the change considerably easier to arrange.", + ], + [("thursday", ReplyDraftTone.Friendly)] = + [ + "I'd love to keep Thursday at 2pm if we can, since the launch date depends on it.", + "Could we review everything except the pricing page, and pick up legal's changes in a quick follow-up?", + "I'm happy to set up a 15-minute slot on Friday for just the pricing copy.", + "Let me know if that feels too tight and we can figure something else out.", + ], + [("thursday", ReplyDraftTone.Concise)] = + [ + "Let's keep Thursday at 2pm; the launch date depends on it.", + "We can review everything but pricing and cover legal's changes separately.", + "I'll add a 15-minute pricing check on Friday.", + "Tell me if that's too tight.", + ], + [("thursday", ReplyDraftTone.Formal)] = + [ + "If possible, I would prefer to keep the review on Thursday at 2pm, as the launch date depends on it.", + "Perhaps we could review all materials except the pricing page and address legal's changes in a brief follow-up.", + "I would be glad to schedule a 15-minute session on Friday dedicated to the pricing copy.", + "Please let me know if this timeline seems too tight, and we can consider alternatives.", + ], + }; + + /// A reply for every quick reply, tone and length (3 × 3 × 3). + public static IReadOnlyList Replies { get; } = + [ + .. from suggestion in new[] { "tuesday", "wednesday", "thursday" } + from tone in new[] { ReplyDraftTone.Friendly, ReplyDraftTone.Concise, ReplyDraftTone.Formal } + from length in new[] { ReplyDraftLength.Short, ReplyDraftLength.Medium, ReplyDraftLength.Long } + select new ReplyDraftVersion(suggestion, tone, length, Reply(suggestion, tone, length)), + ]; + + /// A new simulation of the email (each card gets its own). + public static ReplyDraftSimulation Simulation() => new() + { + Message = Message, + Suggestions = Suggestions, + Model = Model, + Replies = Replies, + }; + + /// The reply for a quick reply, tone and length, greeting Maya and signed by Sam. + public static string Reply(string suggestionId, ReplyDraftTone tone, ReplyDraftLength length) + { + var sentences = s_sentences[(suggestionId, tone)]; + var greeting = tone == ReplyDraftTone.Formal ? "Dear Maya," : "Hi Maya,"; + var closing = tone switch + { + ReplyDraftTone.Formal => "Kind regards,\n" + Signature, + ReplyDraftTone.Concise => Signature, + _ => "Thanks!\n" + Signature, + }; + var body = length switch + { + ReplyDraftLength.Short => sentences[0], + ReplyDraftLength.Long => sentences[0] + " " + sentences[1] + "\n\n" + sentences[2] + " " + sentences[3], + _ => sentences[0] + " " + sentences[1], + }; + return greeting + "\n\n" + body + "\n\n" + closing; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/OwnRow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/OwnRow.axaml new file mode 100644 index 00000000..0c390188 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/OwnRow.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Persisting.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Persisting.axaml new file mode 100644 index 00000000..640eb320 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Persisting.axaml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Persisting.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Persisting.axaml.cs new file mode 100644 index 00000000..35362875 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Persisting.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Persists feedback through a pretend server call that can be made to fail, and logs every submission. +public partial class ResponseFeedbackPersistingDemo : UserControl +{ + public ResponseFeedbackPersistingDemo() + { + InitializeComponent(); + Card.Reply = ResponseFeedbackDemoData.Reply(); + Card.FeedbackHandler = SaveAsync; + Card.FeedbackSubmitted += (_, e) => Log.Text = Describe(e.Submission); + } + + private async Task SaveAsync(FeedbackSubmission feedback, CancellationToken cancellation) + { + // A real app would POST the submission here and pass the token on, so a closed panel aborts the request. + await Task.Delay(TimeSpan.FromMilliseconds(600), cancellation); + if (FailSwitch.IsChecked == true) + { + FailSwitch.IsChecked = false; + throw new InvalidOperationException("The feedback service is not reachable."); + } + } + + private static string Describe(FeedbackSubmission feedback) + { + var text = $"{feedback.Stage} · {feedback.Rating} · message {feedback.MessageId}"; + if (feedback.Reasons.Count > 0) + { + text += " · " + string.Join(", ", feedback.Reasons); + } + if (feedback.Comment is { } comment) + { + text += $" · \"{comment}\""; + } + return text; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Preview.axaml new file mode 100644 index 00000000..f0bb9510 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Preview.axaml @@ -0,0 +1,16 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Preview.axaml.cs new file mode 100644 index 00000000..7339bddb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Preview.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: a reply with simulated feedback. +public partial class ResponseFeedbackPreviewDemo : UserControl +{ + public ResponseFeedbackPreviewDemo() + { + InitializeComponent(); + // A paragraph, a list and inline code: a reply of your own can be any control. + Card.Reply = ResponseFeedbackDemoData.Reply(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Reasons.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Reasons.axaml new file mode 100644 index 00000000..8e1f6308 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Reasons.axaml @@ -0,0 +1,25 @@ + + + + + + + + Wrong code + Didn't compile + Outdated API + Off topic + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/ResponseFeedbackDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/ResponseFeedbackDemoData.cs new file mode 100644 index 00000000..e05a121c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/ResponseFeedbackDemoData.cs @@ -0,0 +1,88 @@ +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Layout; +using Avalonia.Media; +using TKCode.Arc.DesignTokens; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented exchange the response feedback demos show: a question about a slow Postgres query and an answer with a +/// paragraph, a short list and inline code. The model is a real one from . +/// +internal static class ResponseFeedbackDemoData +{ + /// The model credited with the reply (Anthropic's Claude Sonnet 5.5; checked on ). + public static readonly string ModelName = AiModelCatalog.ClaudeSonnet55.Name; + + /// The person's question. + public const string Prompt = "Why is my Postgres query slow when I filter on created_at?"; + + private static readonly (string Text, bool Code)[] s_intro = + [ + ("A filter on ", false), ("created_at", true), + (" is slow when Postgres has to read every row to find the matches. Run ", false), ("EXPLAIN ANALYZE", true), + (" on the query: a ", false), ("Seq Scan", true), (" on the table means there is no index it can use.", false), + ]; + + private static readonly (string Text, bool Code)[][] s_points = + [ + [("Add an index: ", false), ("CREATE INDEX ON orders (created_at);", true)], + [("Compare the column with a range instead of wrapping it in ", false), ("date()", true), (", so the index applies.", false)], + [("Run ", false), ("ANALYZE", true), (" after large imports so the planner has fresh statistics.", false)], + ]; + + private const string Outro = "With the index in place the same query usually drops from seconds to a few milliseconds."; + + /// The answer as plain text, for the copy action. + public static readonly string CopyText = PlainText(); + + /// Builds the answer (a new control each call): a paragraph, a bulleted list and a closing paragraph, with inline code on a muted fill. + public static Control Reply() + { + var body = new StackPanel { Spacing = 10 }; + body.Children.Add(Paragraph(s_intro)); + var list = new StackPanel { Spacing = 4 }; + foreach (var point in s_points) + { + var row = new Grid { ColumnDefinitions = new ColumnDefinitions("18,*") }; + var bullet = new TextBlock { Text = "•", HorizontalAlignment = HorizontalAlignment.Center }; + ArcTokens.Bind(bullet, TextBlock.ForegroundProperty, ArcDesignTokenKind.TextMuted); + row.Children.Add(bullet); + var text = Paragraph(point); + Grid.SetColumn(text, 1); + row.Children.Add(text); + list.Children.Add(row); + } + body.Children.Add(list); + body.Children.Add(Paragraph([(Outro, false)])); + return body; + } + + private static string PlainText() + { + var lines = new List { string.Concat(s_intro.Select(part => part.Text)), string.Empty }; + lines.AddRange(s_points.Select(point => "- " + string.Concat(point.Select(part => part.Text)))); + lines.Add(string.Empty); + lines.Add(Outro); + return string.Join("\n", lines); + } + + private static SelectableTextBlock Paragraph(IEnumerable<(string Text, bool Code)> parts) + { + var block = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; + block.Inlines ??= []; + foreach (var (text, code) in parts) + { + var run = new Run(text); + if (code) + { + ArcTokens.Bind(run, TextElement.FontFamilyProperty, ArcDesignTokenKind.MonoFontFamily); + ArcTokens.Bind(run, TextElement.BackgroundProperty, ArcDesignTokenKind.SurfaceMuted); + run.FontSize = 13; + } + block.Inlines.Add(run); + } + return block; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Streaming.axaml new file mode 100644 index 00000000..b37646f8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ResponseFeedback/Streaming.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/FollowRequest.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/FollowRequest.axaml new file mode 100644 index 00000000..827a7bb3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/FollowRequest.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/FollowRequest.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/FollowRequest.axaml.cs new file mode 100644 index 00000000..0e346623 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/FollowRequest.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// Maps a simulated backend's progress to Status, the way an app follows a real request. +public partial class SketchToImageFollowRequestDemo : UserControl +{ + // Queued, then early, middle and late steps, then the image is ready: seconds after the request starts (invented). + private static readonly (double At, SketchToImageStatus Status)[] s_steps = + [ + (0.6, SketchToImageStatus.Sketching), + (4.0, SketchToImageStatus.Blocking), + (6.4, SketchToImageStatus.Refining), + (9.5, SketchToImageStatus.Done), + ]; + + private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(100) }; + private DateTime _started; + private int _next; + + public SketchToImageFollowRequestDemo() + { + InitializeComponent(); + _timer.Tick += OnTick; + Card.StageChanged += (_, e) => Log.Text = $"Stage: {e.Stage}"; + Card.Completed += (_, _) => Log.Text = "Image ready."; + Card.ReplayRequested += (_, _) => Start(); + DetachedFromVisualTree += (_, _) => _timer.Stop(); + } + + private void OnGenerate(object? sender, RoutedEventArgs e) => Start(); + + private void Start() + { + Card.Status = SketchToImageStatus.Idle; + _started = DateTime.UtcNow; + _next = 0; + _timer.Start(); + Log.Text = "Queued…"; + } + + private void OnTick(object? sender, EventArgs e) + { + var elapsed = (DateTime.UtcNow - _started).TotalSeconds; + while (_next < s_steps.Length && elapsed >= s_steps[_next].At) + { + Card.Status = s_steps[_next].Status; + _next++; + } + if (_next == s_steps.Length) + { + _timer.Stop(); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Neutral.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Neutral.axaml new file mode 100644 index 00000000..8c5d8ccc --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Neutral.axaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/OwnScene.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/OwnScene.axaml new file mode 100644 index 00000000..59ddae83 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/OwnScene.axaml @@ -0,0 +1,11 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/OwnScene.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/OwnScene.axaml.cs new file mode 100644 index 00000000..15f2de2a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/OwnScene.axaml.cs @@ -0,0 +1,68 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace TKCode.Arc.Gallery.Demos; + +/// A still life drawn from a scene of its own: lines, regions back to front and a light source. +public partial class SketchToImageOwnSceneDemo : UserControl +{ + private const string Bowl = "M180 250 C180 320 420 320 420 250 C380 238 220 238 180 250 Z"; + private const string Pear = "M262 172 C250 172 248 194 244 206 C226 222 226 260 262 262 C298 260 296 222 280 206 C276 194 274 172 262 172 Z"; + private const string Apple = "M308 232 A32 32 0 1 0 372 232 A32 32 0 1 0 308 232 Z"; + + public SketchToImageOwnSceneDemo() + { + InitializeComponent(); + Card.Scene = new SketchToImageScene + { + Alt = "A pear and an apple in a blue bowl on a wooden table", + Lines = + [ + "M0 262 H600", + "M180 250 C180 320 420 320 420 250", + "M180 250 C220 238 380 238 420 250", + Pear, + Apple, + "M262 172 L266 158", + "M340 200 L343 188", + "M150 334 C220 348 380 348 450 334", + ], + Regions = + [ + new SketchToImageRegion + { + Brush = new Rect(0, 0, 600, 270), + Shapes = [new SketchToImageShape("M0 0H600V262H0Z", Color.Parse("#E8DCCB"), Vertical("#F1E8DA", "#D8C7B0"))], + }, + new SketchToImageRegion + { + Brush = new Rect(0, 256, 600, 144), + Shapes = [new SketchToImageShape("M0 262H600V400H0Z", Color.Parse("#8A5A3C"), Vertical("#A06C49", "#6A432D"))], + }, + new SketchToImageRegion + { + Brush = new Rect(222, 164, 156, 104), + Shapes = + [ + new SketchToImageShape(Pear, Color.Parse("#B8C25A"), Vertical("#D2D873", "#8E9A3A")), + new SketchToImageShape(Apple, Color.Parse("#C2483A"), Vertical("#D96150", "#99302A")), + ], + }, + new SketchToImageRegion + { + Brush = new Rect(176, 236, 248, 82), + Shapes = [new SketchToImageShape(Bowl, Color.Parse("#3E6D8E"), Vertical("#5287AB", "#2C4F69"))], + }, + ], + Light = new Point(110, 60), + }; + } + + private static IBrush Vertical(string top, string bottom) => + new ImmutableLinearGradientBrush( + [new ImmutableGradientStop(0, Color.Parse(top)), new ImmutableGradientStop(1, Color.Parse(bottom))], + startPoint: new RelativePoint(0.5, 0, RelativeUnit.Relative), + endPoint: new RelativePoint(0.5, 1, RelativeUnit.Relative)); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Preview.axaml new file mode 100644 index 00000000..7952dd8d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Preview.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Preview.axaml.cs new file mode 100644 index 00000000..71f544a4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Preview.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the card looping on its own, held at each stage, or empty. +public partial class SketchToImagePreviewDemo : UserControl +{ + private readonly bool _ready; + + public SketchToImagePreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnStageChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the card exists. + if (!_ready) + { + return; + } + var value = Stages.SelectedValue as string; + var empty = value == "empty"; + Card.Scene = empty ? null : SketchToImageDemoData.Alpine; + Card.Prompt = empty ? null : SketchToImageDemoData.Prompt; + Card.Status = Enum.TryParse(value, out var status) ? status : null; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Sizes.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Sizes.axaml new file mode 100644 index 00000000..06f7f06a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/Sizes.axaml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/SketchToImageDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/SketchToImageDemoData.cs new file mode 100644 index 00000000..1b065b4a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SketchToImage/SketchToImageDemoData.cs @@ -0,0 +1,209 @@ +using System.Globalization; +using System.Text; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The scene the SketchToImage demos paint: layered alpine ridges at golden hour, a low sun, warm haze, pines on a dark +/// foreground hill, two wisps of cloud and two birds, with its prompt. Everything is drawn in code: the pencil lines trace +/// the same outlines the brush fills, and the finished picture is the same shapes with gradients, haze bands, snow +/// highlights and a sun glow. Invented demo content; no image is downloaded. +/// +internal static class SketchToImageDemoData +{ + /// The prompt shown under the stage label. + public const string Prompt = "Layered alpine ridges at golden hour, warm haze"; + + /// The prompt of the neutral-tone demo. + public const string GraphitePrompt = "A quiet ridge line at dusk, graphite study"; + + private static SketchToImageScene? s_alpine; + + /// The alpine scene (immutable, shared by every demo), built on first use. + public static SketchToImageScene Alpine => s_alpine ??= BuildAlpine(); + + private static readonly Point s_sun = new(420, 178); + + private static readonly Point[] s_far = + [ + new(0, 232), new(40, 214), new(78, 222), new(118, 196), new(150, 206), new(190, 180), new(226, 198), new(262, 188), + new(300, 206), new(336, 192), new(372, 214), new(410, 200), new(452, 216), new(490, 196), new(530, 210), + new(566, 198), new(600, 212), + ]; + + private static readonly Point[] s_mid = + [ + new(0, 268), new(52, 240), new(96, 252), new(140, 222), new(176, 238), new(214, 214), new(250, 234), new(292, 246), + new(330, 226), new(372, 250), new(412, 236), new(456, 258), new(500, 232), new(548, 252), new(600, 238), + ]; + + private static readonly Point[] s_near = + [ + new(0, 312), new(60, 286), new(104, 298), new(156, 270), new(196, 290), new(244, 278), new(290, 300), new(338, 280), + new(384, 296), new(430, 274), new(478, 292), new(528, 280), new(600, 300), + ]; + + private const string ForegroundLine = "M0 352 C80 334 150 344 220 340 C300 336 360 326 440 334 C510 341 560 336 600 330"; + + private static readonly (double X, double Base, double Height)[] s_pines = + [ + (64, 350, 64), (98, 346, 44), (476, 340, 34), (520, 338, 58), (548, 336, 40), + ]; + + private static SketchToImageScene BuildAlpine() + { + var sun = Circle(s_sun, 30); + var lines = new List + { + Polyline(s_far, close: false), + Polyline(s_mid, close: false), + Polyline(s_near, close: false), + ForegroundLine, + sun, + "M190 180 L204 206", + "M214 214 L232 246 L226 268", + "M330 226 L346 256", + "M156 270 L170 300 L164 322", + "M430 274 L446 304", + }; + foreach (var pine in s_pines) + { + lines.Add(Pine(pine.X, pine.Base, pine.Height)); + } + lines.Add("M86 96 C116 86 146 92 172 88 C196 84 220 90 238 88"); + lines.Add("M356 70 C380 62 406 66 430 62"); + lines.Add("M250 120 Q256 114 262 120 Q268 114 274 120"); + lines.Add("M284 104 Q288 100 292 104 Q296 100 300 104"); + + var regions = new List + { + new() + { + Brush = new Rect(0, 0, 600, 250), + Shapes = + [ + new("M0 0H600V400H0Z", Rgb(0xEDCBAE), Vertical((0, 0xFFA7B6D3), (0.5, 0xFFE9C3AA), (0.8, 0xFFF7D9B2), (1, 0xFFFBE3BC))), + new(Circle(s_sun, 120), null, Radial(0xC8FFEBC0, 0x00FFE6B0)), + ], + }, + new() + { + Brush = new Rect(384, 142, 72, 72), + Shapes = [new(sun, Rgb(0xFBE3B0), Radial(0xFFFFF8E4, 0xFFFCD596))], + }, + new() + { + Brush = new Rect(0, 172, 600, 100), + Shapes = [new(Polyline(s_far, close: true), Rgb(0xD3B1B6), Vertical((0, 0xFFE2BFBF), (1, 0xFFC7A3B1)))], + }, + new() + { + Brush = new Rect(0, 208, 600, 108), + Shapes = + [ + new("M0 200H600V300H0Z", null, Vertical((0, 0x00FFF1E0), (0.6, 0x8CFFF1E0), (1, 0x00FFF1E0))), + new(Polyline(s_mid, close: true), Rgb(0xA08AA6), Vertical((0, 0xFFB69AB2), (1, 0xFF85718F))), + new(Snow(140, 222, 30.0 / 44, 16.0 / 36), null, Solid(0xD9E6D6E0)), + new(Snow(214, 214, 24.0 / 38, 20.0 / 36), null, Solid(0xD9E6D6E0)), + new(Snow(330, 226, 20.0 / 38, 24.0 / 42), null, Solid(0xD9E6D6E0)), + new(Snow(500, 232, 26.0 / 44, 20.0 / 48), null, Solid(0xD9E6D6E0)), + ], + }, + new() + { + Brush = new Rect(0, 264, 600, 96), + Shapes = + [ + new("M0 250H600V330H0Z", null, Vertical((0, 0x00FFF1E0), (0.55, 0x60FFF1E0), (1, 0x00FFF1E0))), + new(Polyline(s_near, close: true), Rgb(0x675C7A), Vertical((0, 0xFF776A8C), (1, 0xFF4F4562))), + ], + }, + new() + { + Brush = new Rect(0, 322, 600, 78), + Shapes = [new(ForegroundLine + " V400 H0 Z", Rgb(0x33464A), Vertical((0, 0xFF3E5557), (1, 0xFF22302F)))], + }, + new() + { + Brush = new Rect(36, 276, 532, 76), + Shapes = s_pines + .Select(p => new SketchToImageShape(Pine(p.X, p.Base, p.Height), Rgb(0x1D2B2A), Vertical((0, 0xFF263836), (1, 0xFF172221)))) + .ToArray(), + }, + }; + + return new SketchToImageScene + { + Alt = "Layered mountain ridges under a warm evening sky", + Lines = lines, + Regions = regions, + Light = s_sun, + }; + } + + private static Color Rgb(uint rgb) => Color.FromUInt32(0xFF000000 | rgb); + + private static IBrush Solid(uint argb) => new ImmutableSolidColorBrush(Color.FromUInt32(argb)); + + private static IBrush Vertical(params (double Offset, uint Argb)[] stops) => + new ImmutableLinearGradientBrush( + stops.Select(s => new ImmutableGradientStop(s.Offset, Color.FromUInt32(s.Argb))).ToArray(), + startPoint: new RelativePoint(0.5, 0, RelativeUnit.Relative), + endPoint: new RelativePoint(0.5, 1, RelativeUnit.Relative)); + + private static IBrush Radial(uint inner, uint outer) => + new ImmutableRadialGradientBrush( + [new ImmutableGradientStop(0, Color.FromUInt32(inner)), new ImmutableGradientStop(1, Color.FromUInt32(outer))], + radius: 0.5); + + private static string Polyline(Point[] points, bool close) + { + var builder = new StringBuilder(); + for (var i = 0; i < points.Length; i++) + { + builder.Append(i == 0 ? "M" : " L").Append(Num(points[i].X)).Append(' ').Append(Num(points[i].Y)); + } + if (close) + { + builder.Append(" V400 H0 Z"); + } + return builder.ToString(); + } + + private static string Circle(Point center, double radius) => + $"M{Num(center.X - radius)} {Num(center.Y)} A{Num(radius)} {Num(radius)} 0 1 0 {Num(center.X + radius)} {Num(center.Y)} " + + $"A{Num(radius)} {Num(radius)} 0 1 0 {Num(center.X - radius)} {Num(center.Y)} Z"; + + /// A three-tier pine on a short trunk, symmetric about . + private static string Pine(double x, double baseY, double height) + { + var top = baseY - height; + var w = height * 0.38; + (double Dx, double Y)[] right = + [ + (w * 0.45, top + height * 0.33), (w * 0.22, top + height * 0.33), (w * 0.72, top + height * 0.64), + (w * 0.45, top + height * 0.64), (w, baseY - height * 0.1), (w * 0.12, baseY - height * 0.1), (w * 0.12, baseY), + ]; + var builder = new StringBuilder(); + builder.Append('M').Append(Num(x)).Append(' ').Append(Num(top)); + foreach (var (dx, y) in right) + { + builder.Append(" L").Append(Num(x + dx)).Append(' ').Append(Num(y)); + } + for (var i = right.Length - 1; i >= 0; i--) + { + builder.Append(" L").Append(Num(x - right[i].Dx)).Append(' ').Append(Num(right[i].Y)); + } + return builder.Append(" Z").ToString(); + } + + /// A snow cap hugging a peak whose slopes fall by and per unit. + private static string Snow(double x, double y, double left, double right) => + $"M{Num(x)} {Num(y)} L{Num(x + 13)} {Num(y + 13 * right)} L{Num(x + 7)} {Num(y + 13 * right + 4)} " + + $"L{Num(x + 1)} {Num(y + 6)} L{Num(x - 6)} {Num(y + 12 * left + 3)} L{Num(x - 12)} {Num(y + 12 * left)} Z"; + + private static string Num(double value) => value.ToString("0.##", CultureInfo.InvariantCulture); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/CustomFields.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/CustomFields.axaml new file mode 100644 index 00000000..ab263f66 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/CustomFields.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/CustomFields.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/CustomFields.axaml.cs new file mode 100644 index 00000000..f982cead --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/CustomFields.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A vendor form with its own fields and samples. +public partial class SmartFormFillCustomFieldsDemo : UserControl +{ + public SmartFormFillCustomFieldsDemo() + { + InitializeComponent(); + Card.Samples = + [ + new SmartFormFillSample("Invoice footer", + "Questions about this invoice? Write to billing@harborline.example.com\n" + + "Harborline Supply Co.\n" + + "88 Quarry Lane, Portland\n" + + "+1 503 555 0199 · harborline.example.com"), + ]; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Extractor.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Extractor.axaml new file mode 100644 index 00000000..a05f2fe6 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Extractor.axaml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Extractor.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Extractor.axaml.cs new file mode 100644 index 00000000..cdb2fb1c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Extractor.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app-supplied extractor and save: what a real model call and a real save would plug into. +public partial class SmartFormFillExtractorDemo : UserControl +{ + public SmartFormFillExtractorDemo() + { + InitializeComponent(); + Card.Extract = ExtractAsync; + Card.Submit = SaveAsync; + Card.Filled += (_, e) => Log.Text = $"Read {e.FilledCount} fields; {e.ReviewCount} to review."; + Card.Undone += (_, _) => Log.Text = "Undo restored the values from before the fill."; + Card.Submitted += (_, e) => Log.Text = "Saved: " + string.Join(", ", e.Values.Where(v => v.Value.Length > 0).Select(v => $"{v.Key} = {v.Value}")); + } + + /// + /// Stands in for a model call with structured output: each field gets a value, a confidence from 0 to 1, the exact source + /// substring (it lights up in the pasted text) and a note when the model is unsure. + /// + private static async Task ExtractAsync(string text, CancellationToken token) + { + await Task.Delay(TimeSpan.FromSeconds(1.2), token); + var result = new SmartFormFillResult(); + var email = text.Split([' ', '\n', ',', '/'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(w => w.Contains('@')); + if (email is not null) + { + result["email"] = new SmartFormFillValue(email.Trim('.', ';'), 0.97, email.Trim('.', ';')); + } + var firstLine = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault(); + if (firstLine is { Length: > 0 and < 40 } && !firstLine.Contains('@')) + { + result["name"] = new SmartFormFillValue(firstLine, 0.7, firstLine, "Taken from the first line."); + } + return result; + } + + private static Task SaveAsync(IReadOnlyDictionary values) => Task.Delay(TimeSpan.FromSeconds(0.8)); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Narrow.axaml new file mode 100644 index 00000000..da4ef856 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Narrow.axaml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Preview.axaml new file mode 100644 index 00000000..0f8613f7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/Preview.axaml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/SmartFormFillDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/SmartFormFillDemoData.cs new file mode 100644 index 00000000..feea4853 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SmartFormFill/SmartFormFillDemoData.cs @@ -0,0 +1,58 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented texts the smart form fill demos paste, each with the result a read returns, checked by hand: an email +/// signature and messy call notes. The people, companies and numbers are made up (emails on example.com, phone numbers in +/// the 555 range); the model named is a real one from . Demo content only. +/// +internal static class SmartFormFillDemoData +{ + /// An email signature under a short reply. + public const string Signature = + "Thanks again for the walkthrough today. Happy to set up a pilot with our dispatch team next week.\n\n" + + "Best,\n" + + "Maya Okafor\n" + + "Head of Partnerships | Fernbrook Freight\n" + + "maya.okafor@example.com\n" + + "m. +1 (415) 555-0142 · o. +1 (415) 555-0100\n" + + "500 Harbor Way, Suite 300, San Francisco"; + + /// Messy call notes. + public const string Notes = + "call notes 10/14 - spoke w/ daniel reyes, runs ops at bluepine studio (i think?). wants a demo for his team before Q1.\n" + + "email: d.reyes@example.com / cell 312.555.0187\n" + + "they're moving offices, somewhere in chicago, will confirm"; + + /// The model named beside the paste area. + public static string ModelName => AiModelCatalog.DeepSeekV41Flash.Name; + + /// The samples behind Try sample, each with its hand-checked result. + public static IReadOnlyList Samples { get; } = + [ + new("Email signature", Signature) + { + Result = new SmartFormFillResult + { + ["name"] = new("Maya Okafor", 0.98, "Maya Okafor"), + ["title"] = new("Head of Partnerships", 0.95, "Head of Partnerships"), + ["company"] = new("Fernbrook Freight", 0.96, "Fernbrook Freight"), + ["email"] = new("maya.okafor@example.com", 0.99, "maya.okafor@example.com"), + ["phone"] = new("+1 (415) 555-0142", 0.74, "+1 (415) 555-0142", "Two numbers in the text; picked the mobile."), + ["address"] = new("500 Harbor Way, Suite 300, San Francisco, CA", 0.68, "500 Harbor Way, Suite 300, San Francisco", + "The state was inferred from the city."), + }, + }, + new("Messy notes", Notes) + { + Result = new SmartFormFillResult + { + ["name"] = new("Daniel Reyes", 0.92, "daniel reyes"), + ["title"] = new("Head of Operations", 0.56, "runs ops", "Paraphrased from \"runs ops\"."), + ["company"] = new("Bluepine Studio", 0.71, "bluepine studio", "The notes add \"i think?\"."), + ["email"] = new("d.reyes@example.com", 0.99, "d.reyes@example.com"), + ["phone"] = new("+1 312 555 0187", 0.9, "312.555.0187"), + ["address"] = new("Chicago, IL", 0.45, "somewhere in chicago", "Only the city is known; the office is moving."), + }, + }, + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Corner.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Corner.axaml new file mode 100644 index 00000000..c5e1b116 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Corner.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Corner.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Corner.axaml.cs new file mode 100644 index 00000000..0b74369d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Corner.axaml.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The stack inline or pinned to a window corner. +public partial class SocialProofToastCornerDemo : UserControl +{ + private readonly bool _ready; + + public SocialProofToastCornerDemo() + { + InitializeComponent(); + Placement.SelectionChanged += OnPlacementChanged; + _ready = true; + } + + private void OnPlacementChanged(object? sender, SelectionChangedEventArgs e) + { + // SegmentedControl reports its first selection while the XAML loads. + if (!_ready) + { + return; + } + Stack.Position = (Placement.SelectedValue as string) switch + { + "BottomLeft" => SocialProofToastPosition.BottomLeft, + "BottomRight" => SocialProofToastPosition.BottomRight, + "TopRight" => SocialProofToastPosition.TopRight, + _ => SocialProofToastPosition.Inline, + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Preview.axaml new file mode 100644 index 00000000..6406e10e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Preview.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Preview.axaml.cs new file mode 100644 index 00000000..344ecdd0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Preview.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated feed with two toasts prefilled, a pause switch and a restart button. +public partial class SocialProofToastPreviewDemo : UserControl +{ + public SocialProofToastPreviewDemo() + { + InitializeComponent(); + Paused.IsCheckedChanged += (_, _) => Stack.IsPaused = Paused.IsChecked == true; + } + + private void OnRestart(object? sender, RoutedEventArgs e) => Stack.Restart(); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Selectable.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Selectable.axaml new file mode 100644 index 00000000..bfae6a51 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Selectable.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Selectable.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Selectable.axaml.cs new file mode 100644 index 00000000..781944a3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/Selectable.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Selectable toasts with a session limit, logging Selected, Dismissed and Shown. +public partial class SocialProofToastSelectableDemo : UserControl +{ + private int _shown; + + public SocialProofToastSelectableDemo() + { + InitializeComponent(); + Stack.Selected += (_, e) => Log.Text = "Selected: open the story for " + e.Event.Name; + Stack.Dismissed += (_, e) => Log.Text = "Dismissed: " + e.Event.Name; + Stack.Shown += (_, e) => + { + _shown++; + Log.Text = $"Shown {_shown} of 5: {e.Event.Name}"; + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/SocialProofToastDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/SocialProofToastDemoData.cs new file mode 100644 index 00000000..1c35bd61 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/SocialProofToastDemoData.cs @@ -0,0 +1,33 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented SaaS feed the SocialProofToast demos simulate: people and companies that do not exist, in invented places, +/// with times measured back from the start of each pass. Each access returns fresh events, so every stack gets its own. +/// +internal static class SocialProofToastDemoData +{ + /// Ten events, oldest first. + public static IReadOnlyList Events => + [ + Person("s1", "Sofia", "started a Pro trial", "Madrid", TimeSpan.FromMinutes(14)), + Company("s2", "Contoso Labs", "upgraded to Business", "48 seats", TimeSpan.FromMinutes(12), "Verified purchase"), + Person("s3", "Mateo", "bought the Team plan", "Lisbon", TimeSpan.FromMinutes(11), "Verified purchase"), + Company("s4", "Northwind AI", "started a Business trial", "12 seats", TimeSpan.FromMinutes(9)), + Person("s5", "Amara", "upgraded to Pro", "Nairobi", TimeSpan.FromMinutes(7)), + Company("s6", "Fabrikam", "renewed Enterprise", "120 seats", TimeSpan.FromMinutes(6), "Verified purchase"), + Person("s7", "Lena", "started a Pro trial", "Hamburg", TimeSpan.FromMinutes(4)), + Company("s8", "Harbor & Pine", "upgraded to Business", "26 seats", TimeSpan.FromMinutes(3), "Verified purchase"), + Person("s9", "Kenji", "switched to yearly billing", "Osaka", TimeSpan.FromMinutes(2)), + Person("s10", "Priya", "started a Pro trial", "Pune", TimeSpan.FromSeconds(20)), + ]; + + private static SocialProofToastSimulationEvent Person(string id, string name, string action, string meta, TimeSpan ago, string? verified = null) => + new() { Event = new SocialProofEvent(id, name, action) { Meta = meta, VerifiedLabel = verified }, Ago = ago }; + + private static SocialProofToastSimulationEvent Company(string id, string name, string action, string meta, TimeSpan ago, string? verified = null) => + new() + { + Event = new SocialProofEvent(id, name, action) { Meta = meta, VerifiedLabel = verified, Actor = SocialProofActor.Company }, + Ago = ago, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/YourEvents.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/YourEvents.axaml new file mode 100644 index 00000000..3772b658 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/YourEvents.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/YourEvents.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/YourEvents.axaml.cs new file mode 100644 index 00000000..1a8d6e52 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SocialProofToast/YourEvents.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app-fed stack: events declared in XAML, and Push for one that just happened. +public partial class SocialProofToastYourEventsDemo : UserControl +{ + private static readonly (string Name, string Action, string Meta, SocialProofActor Actor)[] s_purchases = + [ + ("Amara", "bought the Pro plan", "Nairobi", SocialProofActor.Person), + ("Harbor & Pine", "upgraded to Business", "26 seats", SocialProofActor.Company), + ("Kenji", "switched to yearly billing", "Osaka", SocialProofActor.Person), + ("Northwind AI", "added 10 seats", "Business plan", SocialProofActor.Company), + ]; + + private int _count; + + public SocialProofToastYourEventsDemo() + { + InitializeComponent(); + } + + private void OnPush(object? sender, RoutedEventArgs e) + { + var (name, action, meta, actor) = s_purchases[_count % s_purchases.Length]; + _count++; + Stack.Push(new SocialProofEvent("push-" + _count, name, action) + { + Meta = meta, + Actor = actor, + Time = DateTimeOffset.Now, + VerifiedLabel = "Verified purchase", + }); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Narrow.axaml new file mode 100644 index 00000000..a57b529a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Narrow.axaml @@ -0,0 +1,8 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Narrow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Narrow.axaml.cs new file mode 100644 index 00000000..323382eb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Narrow.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Finished results in a narrow container. +public partial class SourceCardsNarrowDemo : UserControl +{ + public SourceCardsNarrowDemo() + { + InitializeComponent(); + Cards.Sources = SourceCardsDemoData.Sources(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Preview.axaml new file mode 100644 index 00000000..a37f45ea --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Preview.axaml.cs new file mode 100644 index 00000000..7a9b4c45 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Preview.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: a simulated search in its three scenarios. +public partial class SourceCardsPreviewDemo : UserControl +{ + private readonly bool _ready; + + public SourceCardsPreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (!_ready || Cards.Simulation is not { } simulation) + { + return; + } + // A new scenario starts the story over. + simulation.Scenario = (Scenario.SelectedValue as string) switch + { + "error" => SourceCardsScenario.Error, + "empty" => SourceCardsScenario.Empty, + _ => SourceCardsScenario.Live + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Reader.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Reader.axaml new file mode 100644 index 00000000..a2ff0766 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Reader.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Reader.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Reader.axaml.cs new file mode 100644 index 00000000..cfb2e3b8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Reader.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// Opens sources in an in-app reader, and shows a failed search with Retry. +public partial class SourceCardsReaderDemo : UserControl +{ + private readonly SourceCardsSource[] _all = SourceCardsDemoData.Sources(); + + public SourceCardsReaderDemo() + { + InitializeComponent(); + Cards.Sources = _all; + } + + private void OnSourceInvoked(object? sender, SourceCardsSourceEventArgs e) + { + // Handled: the card does not open the browser. + e.Handled = true; + Reader.Text = $"[{e.Number}] {e.Item.Title}\n{e.Item.Snippet}"; + } + + private void OnFail(object? sender, RoutedEventArgs e) + { + Cards.Sources = _all[..2]; + Cards.ErrorText = SourceCardsDemoData.ErrorText; + Cards.Status = SourceCardsStatus.Error; + } + + private void OnRetry(object? sender, RoutedEventArgs e) + { + Cards.ErrorText = null; + Cards.Sources = _all; + Cards.Status = SourceCardsStatus.Done; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/SourceCardsDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/SourceCardsDemoData.cs new file mode 100644 index 00000000..0e038bb5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/SourceCardsDemoData.cs @@ -0,0 +1,120 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// Invented search results for the source cards demos: made-up sites on the reserved .example domain with made-up +/// passages. None of the pages exist. +/// +internal static class SourceCardsDemoData +{ + // The simulated search of the Preview ------------------------------------------------------------------------------ + + /// The query the simulated search runs. + public const string SimulatedQuery = "webhook signature verification"; + + /// The query of the simulated empty search. + public const string SimulatedEmptyQuery = "webhook retries on the lunar billing api"; + + /// Why a search fails (the simulated one and the reader demo's). + public const string ErrorText = "The search provider timed out after 8s."; + + /// The search time the simulated search reports. + public static readonly TimeSpan SimulatedDuration = TimeSpan.FromMilliseconds(2100); + + /// The answer the simulated search streams; [n] cites source n. + public const string SimulatedAnswer = + "Read the raw body with request.text() before parsing it [1] [2]. " + + "Compute an HMAC-SHA256 of the timestamp and body with your endpoint secret, then compare it with the header in constant time [3] [6]. " + + "Most mismatches come from a framework parsing the JSON first [4]; on edge runtimes, turn off body parsing for that route [5]."; + + /// Six pages about verifying webhook signatures, in citation order. + public static readonly IReadOnlyList SimulatedSources = + [ + new("Verify webhook signatures", "https://docs.ledgerpay.example/webhooks/signatures") + { + Site = "Ledgerpay Docs", + Published = "Oct 9, 2026", + Snippet = "Compute an HMAC with SHA-256 over the timestamp, a period and the raw request body, using the endpoint's signing secret, and compare it with the signature in the Ledgerpay-Signature header." + }, + new("Request: text() method", "https://webref.example/api/request/text") + { + Site = "WebRef", + Published = "Aug 30, 2026", + Snippet = "The text() method of the Request interface reads the request stream to completion and returns a promise that resolves with the body as a string, exactly as it was sent." + }, + new("Constant-time comparison for HMAC digests", "https://cryptonotes.example/posts/constant-time-compare") + { + Site = "Crypto Notes", + Published = "Jun 2, 2026", + Snippet = "Comparing digests with == stops at the first different byte, which leaks timing. Use a comparison whose time does not depend on where the inputs differ." + }, + new("Why does my webhook signature never match?", "https://devanswers.example/q/48213") + { + Site = "DevAnswers", + Published = "Mar 14, 2026", + Snippet = "Your framework parses the JSON before your handler runs, so you sign a re-serialized body with different spacing. Read the raw bytes for this route and verify those." + }, + new("Reading the raw body in edge functions", "https://edgehost.example/docs/functions/raw-body") + { + Site = "Edgehost Docs", + Published = "Sep 21, 2026", + Snippet = "Edge functions receive a standard Request. Call request.text() once, verify the signature against that string, then JSON.parse it yourself." + }, + new("HMAC", "https://wikiref.example/wiki/HMAC") + { + Site = "WikiRef", + Published = "Jul 7, 2026", + Snippet = "A hash-based message authentication code combines a cryptographic hash function with a secret key to verify both the integrity and the authenticity of a message." + } + ]; + + // The app-driven demos --------------------------------------------------------------------------------------------- + + public const string Query = "postgres connection pool size for serverless"; + + /// Seven results in citation order (a fresh copy each call). + public static SourceCardsSource[] Sources() => + [ + new("Connection pooling in serverless functions", "https://docs.edgehost.example/guides/postgres-pooling") + { + Site = "Edgehost Docs", + Published = "Sep 30, 2026", + Snippet = "Every function instance opens its own connections. Put a pooler between your functions and the database and keep each instance to one or two connections." + }, + new("max_connections and memory", "https://pgnotes.example/config/max-connections") + { + Site = "PG Notes", + Published = "Jul 12, 2026", + Snippet = "Each connection costs a backend process and its working memory. Raising max_connections past a few hundred usually slows the server down instead of serving more clients." + }, + new("Transaction mode versus session mode", "https://poolkeeper.example/docs/modes") + { + Site = "Poolkeeper", + Published = "Aug 3, 2026", + Snippet = "In transaction mode a server connection returns to the pool when each transaction ends, so a few dozen connections can serve thousands of short-lived clients." + }, + new("Why do my lambdas exhaust the database?", "https://devanswers.example/q/51870") + { + Site = "DevAnswers", + Published = "May 18, 2026", + Snippet = "A burst of cold starts opens a burst of connections. Without a pooler the database runs out of slots long before it runs out of CPU." + }, + new("Sizing a pool: a worked example", "https://blog.querylane.example/pool-sizing") + { + Site = "Querylane Blog", + Published = "Oct 2, 2026", + Snippet = "Start from cores × 2 plus the number of disks, measure, then adjust. More connections than the server can run at once only add queueing inside the database." + }, + new("Prepared statements behind a pooler", "https://poolkeeper.example/docs/prepared-statements") + { + Site = "Poolkeeper", + Published = "Aug 3, 2026", + Snippet = "Named prepared statements belong to one server connection. In transaction mode, turn them off in the driver or use a pooler that tracks them." + }, + new("Idle timeouts", "https://pgnotes.example/config/idle-timeouts") + { + Site = "PG Notes", + Published = "Jul 12, 2026", + Snippet = "Set idle_in_transaction_session_timeout so a client that forgot to commit cannot hold a connection forever." + } + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Streaming.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Streaming.axaml new file mode 100644 index 00000000..fe01a816 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Streaming.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Streaming.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Streaming.axaml.cs new file mode 100644 index 00000000..c062d56f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SourceCards/Streaming.axaml.cs @@ -0,0 +1,85 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// Streams invented results and a cited answer into the cards, as an app would. +public partial class SourceCardsStreamingDemo : UserControl +{ + // A word or an [n] citation per tick. + private static readonly string[] s_answer = + [ + "Keep", "each", "function", "instance", "to", "one", "or", "two", "connections", "and", "put", "a", "pooler", + "in", "front", "of", "the", "database", "[1]", "[4].", "Transaction", "mode", "lets", "a", "few", "dozen", + "server", "connections", "serve", "thousands", "of", "clients", "[3],", "but", "turn", "off", "named", + "prepared", "statements", "in", "the", "driver", "[6].", "Size", "the", "pool", "from", "the", "server's", + "cores,", "not", "from", "the", "traffic", "[2]", "[5]." + ]; + + private readonly AvaloniaList _sources = []; + private readonly SourceCardsSource[] _results = SourceCardsDemoData.Sources()[..5]; + private DispatcherTimer? _timer; + private int _step; + + public SourceCardsStreamingDemo() + { + InitializeComponent(); + Cards.Sources = _sources; + Cards.Query = SourceCardsDemoData.Query; + AttachedToVisualTree += (_, _) => Start(); + DetachedFromVisualTree += (_, _) => _timer?.Stop(); + } + + private void OnReplay(object? sender, RoutedEventArgs e) => Start(); + + private void Start() + { + _timer?.Stop(); + _sources.Clear(); + Answer.Inlines?.Clear(); + Cards.IsExpanded = false; + Cards.Duration = null; + Cards.Status = SourceCardsStatus.Searching; + _step = 0; + _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(420) }; + _timer.Tick += (_, _) => Tick(); + _timer.Start(); + } + + private void Tick() + { + if (_sources.Count < _results.Length) + { + _sources.Add(_results[_sources.Count]); + return; + } + if (Cards.Status == SourceCardsStatus.Searching) + { + // The search is over when the model starts answering. + Cards.Duration = TimeSpan.FromMilliseconds(2100); + Cards.Status = SourceCardsStatus.Done; + _timer!.Interval = TimeSpan.FromMilliseconds(60); + return; + } + if (_step >= s_answer.Length) + { + _timer?.Stop(); + return; + } + var token = s_answer[_step++]; + var inlines = Answer.Inlines ??= []; + if (token.StartsWith('[') && token.IndexOf(']') is var close and > 1 && int.TryParse(token[1..close], out var n)) + { + inlines.Add(new InlineUIContainer(new SourceCitation { Number = n }) { BaselineAlignment = BaselineAlignment.Center }); + inlines.Add(new Run(token[(close + 1)..] + " ")); + } + else + { + inlines.Add(new Run(token + " ")); + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Events.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Events.axaml new file mode 100644 index 00000000..2977ba82 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Events.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Events.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Events.axaml.cs new file mode 100644 index 00000000..f9748dfd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Events.axaml.cs @@ -0,0 +1,43 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A caption that follows the spoken word and a log of the player's events. +public partial class SpeechPlayerEventsDemo : UserControl +{ + private readonly List _lines = []; + + public SpeechPlayerEventsDemo() + { + InitializeComponent(); + Player.Played += (_, e) => Add($"Played at {e.Time:m\\:ss} with {e.Voice.Name}, {Speed(e.Speed)}"); + Player.Paused += (_, e) => Add($"Paused at {e.Time:m\\:ss} on word {e.WordIndex + 1}"); + Player.Seeked += (_, e) => Add($"Seeked to {e.Time:m\\:ss}"); + Player.Ended += (_, _) => Add("Ended"); + Player.VoiceChanged += (_, e) => Add($"Voice changed to {e.Voice.Name}"); + Player.SpeedChanged += (_, e) => Add($"Speed changed to {Speed(e.Speed)}"); + Player.Closed += (_, _) => + { + Caption.Text = "Caption: (not reading)"; + Add("Closed"); + }; + Player.WordChanged += (_, e) => Caption.Text = $"Caption: {e.Word}"; + } + + private static string Speed(SpeechSpeed speed) => speed switch + { + SpeechSpeed.OneAndHalf => "1.5×", + SpeechSpeed.Twice => "2×", + _ => "1×" + }; + + private void Add(string line) + { + _lines.Insert(0, line); + if (_lines.Count > 5) + { + _lines.RemoveAt(_lines.Count - 1); + } + Log.Text = string.Join("\n", _lines); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Plain.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Plain.axaml new file mode 100644 index 00000000..08d5cf34 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Plain.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Preview.axaml new file mode 100644 index 00000000..9715de1e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Preview.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Preview.axaml.cs new file mode 100644 index 00000000..069e7444 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/Preview.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the self-playing simulation, a slow voice service, a failing one and the empty player. +public partial class SpeechPlayerPreviewDemo : UserControl +{ + private readonly SpeechSimulation? _simulation; + private readonly bool _ready; + + public SpeechPlayerPreviewDemo() + { + InitializeComponent(); + _simulation = Player.Simulation; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the player is ready. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "sample"); + } + } + + private void Show(string scenario) + { + if (scenario == "empty") + { + // No text and no simulation: the action row waits with Read aloud disabled. + Player.Synthesize = null; + Player.Simulation = null; + return; + } + Player.Simulation = _simulation; + // A new service resets the player to its action row; Open is what Read aloud does. + Player.Synthesize = scenario switch + { + "slow" => request => SpeechSimulation.SynthesizeAsync(request, new SpeechSimulationOptions { Delay = TimeSpan.FromSeconds(3) }), + "error" => request => SpeechSimulation.SynthesizeAsync(request, new SpeechSimulationOptions { Fail = true }), + _ => null + }; + // Both stand-ins are simulations: keep the note that no audio plays. + Player.IsSimulated = true; + Player.Open(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/SpeechPlayerDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/SpeechPlayerDemoData.cs new file mode 100644 index 00000000..655ea908 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/SpeechPlayer/SpeechPlayerDemoData.cs @@ -0,0 +1,29 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// What the SpeechPlayer demos read: an invented travel reply credited to a real model from , +/// and four invented voices (the catalog lists text models only, no text to speech voices). Demo content only: no audio +/// plays and nothing is sent anywhere. +/// +internal static class SpeechPlayerDemoData +{ + /// The reply the preview reads. + public const string Reply = + "Lisbon in early May is a lovely choice. Mornings start near 16°C and afternoons reach the low twenties, so pack light layers and shoes with good grip for the steep, cobbled hills.\n\n" + + "Spend your first morning in Alfama before the crowds arrive, then ride tram 28 across the old town. On day two, take an early train to Sintra and come back in time for sunset at a miradouro."; + + /// The model the preview's reply is credited to. + public static string Author { get; } = AiModelCatalog.ClaudeSonnet55.Name; + + /// The model the chat-column demo's reply is credited to. + public static string ChatAuthor { get; } = AiModelCatalog.ClaudeHaiku55.Name; + + /// Four invented voices with a one-line description each. + public static IReadOnlyList Voices { get; } = + [ + new SpeechVoice("ava", "Ava", "Warm and conversational"), + new SpeechVoice("leo", "Leo", "Calm, lower register"), + new SpeechVoice("maya", "Maya", "Bright and upbeat"), + new SpeechVoice("noah", "Noah", "Clear and steady"), + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Narrow.axaml new file mode 100644 index 00000000..ee4ef639 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Narrow.axaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Narrow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Narrow.axaml.cs new file mode 100644 index 00000000..3b25303d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Narrow.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The demo starters in a narrow column. +public partial class StarterPromptsNarrowDemo : UserControl +{ + public StarterPromptsNarrowDemo() + { + InitializeComponent(); + StarterPromptsDemoData.AddStarters(Prompts); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnComposer.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnComposer.axaml new file mode 100644 index 00000000..8e5cf4d2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnComposer.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnComposer.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnComposer.axaml.cs new file mode 100644 index 00000000..7d0dc20c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnComposer.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Cards paired with the app's own composer. +public partial class StarterPromptsOwnComposerDemo : UserControl +{ + public StarterPromptsOwnComposerDemo() + { + InitializeComponent(); + StarterPromptsDemoData.AddStarters(Prompts); + Prompts.PromptSelected += (_, e) => + { + Composer.Text = e.Prompt.Prompt; + Composer.Focus(); + Composer.CaretIndex = Composer.Text?.Length ?? 0; + Picked.Text = $"Picked “{e.Prompt.Title}” (id: {e.Prompt.Id})."; + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnStarters.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnStarters.axaml new file mode 100644 index 00000000..2ebad6c0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/OwnStarters.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Preview.axaml new file mode 100644 index 00000000..bf982528 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Preview.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Preview.axaml.cs new file mode 100644 index 00000000..95dccfd3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Preview.axaml.cs @@ -0,0 +1,45 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the demo starters, the placeholder cards while they load, and the empty control. +public partial class StarterPromptsPreviewDemo : UserControl +{ + private readonly bool _ready; + + public StarterPromptsPreviewDemo() + { + InitializeComponent(); + // The starters are a list the demos share, so they are added here rather than written out in XAML. + StarterPromptsDemoData.AddStarters(Prompts); + _ready = true; + } + + private void OnStateChanged(object? sender, SelectionChangedEventArgs e) + { + // The segmented control applies SelectedValue while the XAML loads, before Prompts is assigned. + if (!_ready) + { + return; + } + var state = State.SelectedValue as string; + Prompts.IsLoading = state == "loading"; + if (state == "empty") + { + Prompts.Prompts.Clear(); + Prompts.Categories.Clear(); + Prompts.Greeting = null; + Prompts.Description = null; + Prompts.Placeholder = null; + Prompts.ModelName = null; + } + else if (Prompts.Prompts.Count == 0) + { + StarterPromptsDemoData.AddStarters(Prompts); + Prompts.Greeting = StarterPromptsDemoData.Greeting; + Prompts.Description = StarterPromptsDemoData.Description; + Prompts.Placeholder = StarterPromptsDemoData.Placeholder; + Prompts.ModelName = StarterPromptsDemoData.ModelName; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Sending.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Sending.axaml new file mode 100644 index 00000000..d47a9f29 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Sending.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Sending.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Sending.axaml.cs new file mode 100644 index 00000000..6f357301 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/Sending.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app-driven send with failure, retry and stop. +public partial class StarterPromptsSendingDemo : UserControl +{ + public StarterPromptsSendingDemo() + { + InitializeComponent(); + StarterPromptsDemoData.AddStarters(Prompts); + Prompts.SubmitRequested += (_, e) => + { + var from = e.Prompt is { } prompt ? $"from “{prompt.Title}”" : "written by hand"; + Log.Text = $"Sending {e.Message.Length} characters, {from}."; + e.Sending = SendAsync(FailNext.IsChecked == true, e.CancellationToken); + FailNext.IsChecked = false; + }; + } + + /// Stands in for a chat endpoint: two seconds, then success or an error the footer shows. + private async Task SendAsync(bool fail, CancellationToken token) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(2), token); + } + catch (OperationCanceledException) + { + Log.Text = "Stopped. The text stays in the composer."; + throw; + } + if (fail) + { + Log.Text = "The send failed."; + throw new InvalidOperationException("Couldn't send. Try again."); + } + Log.Text = "Sent."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/StarterPromptsDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/StarterPromptsDemoData.cs new file mode 100644 index 00000000..6aacf391 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/StarterPrompts/StarterPromptsDemoData.cs @@ -0,0 +1,89 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented starters the starter prompts demos show: 24 cards over Write, Code, Analyze and Plan, some of them using a +/// connected app, with the greeting, description and placeholder around them. Names, numbers and projects are made up; +/// the model name is a real one from . +/// +internal static class StarterPromptsDemoData +{ + public const string Greeting = "What are we working on, Maya?"; + + public const string Description = "Start from a starter, or ask anything. Connected apps are marked on the card."; + + public const string Placeholder = "Ask anything, or pick a starter above"; + + /// The model in the composer's footer: Alibaba Cloud's Qwen3.8-Max (checked on ). + public static readonly string ModelName = AiModelCatalog.Qwen38Max.Name; + + public static readonly IReadOnlyList Categories = + [ + new("write", "Write", ArcIcons.PenLine), + new("code", "Code", ArcIcons.Code), + new("analyze", "Analyze", ArcIcons.ChartBar), + new("plan", "Plan", ArcIcons.ListTodo), + ]; + + /// The starters, in tab order. + public static readonly IReadOnlyList Prompts = + [ + P("launch-email", "write", "Draft the launch email", "For the 1,200 beta customers", StarterPromptApp.Mail, + "Draft a launch email for the 1,200 customers in the beta list. Lead with the two features they asked for most, keep it under 150 words and end with one clear call to action."), + P("tighten-post", "write", "Tighten this blog post", "Cut it to a 4 minute read", null, + "Edit the blog post I paste next so it reads in about 4 minutes. Keep my voice, cut repetition, and list the three biggest changes you made."), + P("reply-threads", "write", "Answer the open threads", "6 customer emails waiting", StarterPromptApp.Mail, + "Draft replies to the 6 open customer threads in my inbox. Match each sender's tone, answer the question they asked first, and flag any that need a refund decision."), + P("release-notes", "write", "Write the release notes", "From last week's merged changes", StarterPromptApp.Repository, + "Write release notes from the changes merged last week. Group them into New, Improved and Fixed, use plain words, and leave out internal refactors."), + P("team-update", "write", "Write the weekly update", "Wins, risks and asks", StarterPromptApp.Docs, + "Turn this week's project notes into a team update with three sections: wins, risks and asks. Keep each item to one sentence."), + P("job-post", "write", "Draft a job post", "Senior product designer", null, + "Draft a job post for a senior product designer on a small, remote team. Describe the work in the first 90 days, the must-haves and what we offer, without buzzwords."), + P("review-pr", "code", "Review my open pull request", "Find bugs before review", StarterPromptApp.Repository, + "Review my open pull request. Point out bugs, missing tests and unclear names first, then smaller style notes, each with the file and line."), + P("explain-code", "code", "Explain this function", "Line by line, in plain words", null, + "Explain the function I paste next line by line in plain words, then describe one input that would make it fail."), + P("write-tests", "code", "Write tests for the parser", "Cover the edge cases", null, + "Write unit tests for the date parser I paste next. Cover empty input, time zones, leap days and invalid formats, and name each test after the behavior it checks."), + P("fix-issue", "code", "Fix the top bug", "The oldest open crash report", StarterPromptApp.Issues, + "Look at the oldest open crash report in the tracker, explain the likely cause, and propose a minimal fix with a test that reproduces it."), + P("sql-query", "code", "Write a SQL query", "Weekly active users by plan", null, + "Write a SQL query for weekly active users by plan for the last 12 weeks, from an events table with user_id, plan and created_at. Explain any assumptions."), + P("refactor", "code", "Refactor a long component", "Split it without changing behavior", null, + "Refactor the component I paste next into smaller pieces without changing what it renders. Show the new files and explain why each split helps."), + P("churn", "analyze", "Find why customers churned", "From last quarter's exit survey", StarterPromptApp.Docs, + "Read last quarter's exit survey answers and group the reasons customers left into five themes, with a count and one representative quote for each."), + P("metrics", "analyze", "Summarize this week's metrics", "What moved and why", null, + "Summarize this week's product metrics I paste next: what moved more than 5%, the likely reason, and one question worth asking the team."), + P("compare-plans", "analyze", "Compare our pricing plans", "Against what customers use", null, + "Compare our three pricing plans against the usage data I paste next. Which limits do customers hit first, and which plan looks underpriced?"), + P("bug-trends", "analyze", "Spot trends in bug reports", "The last 30 days", StarterPromptApp.Issues, + "Look at the bug reports filed in the last 30 days and tell me which areas of the product get the most reports, and whether any of them is getting worse."), + P("survey", "analyze", "Read the survey results", "412 answers, sample data", null, + "Analyze the 412 survey answers I paste next. Give me the three strongest findings, any surprising splits by plan, and what we should not conclude from it."), + P("meeting-load", "analyze", "Audit my meeting load", "Where the week went", StarterPromptApp.Calendar, + "Look at my calendar for the last two weeks and show how my time split between meetings, focus time and one-on-ones, and which recurring meetings I could drop."), + P("plan-week", "plan", "Plan my week", "Around the Thursday deadline", StarterPromptApp.Calendar, + "Plan my week around the Thursday deadline. Block focus time for the proposal, keep my recurring meetings, and leave Friday afternoon free."), + P("roadmap", "plan", "Draft a Q1 roadmap", "Three themes, six bets", StarterPromptApp.Docs, + "Draft a Q1 roadmap with three themes and two bets under each, with the problem, the expected outcome and a rough size for every bet."), + P("launch-plan", "plan", "Plan the launch", "Two weeks, step by step", StarterPromptApp.Issues, + "Plan a two week launch for the new reporting feature as a checklist of issues: owners, due dates, and what blocks what."), + P("offsite", "plan", "Plan a team offsite", "Eight people, two days", null, + "Plan a two day offsite for eight people. Suggest an agenda that mixes planning and downtime, and list what to book three weeks ahead."), + P("onboarding", "plan", "Plan a new hire's first week", "An engineer starting Monday", StarterPromptApp.Calendar, + "Plan the first week for an engineer starting Monday: meetings to set up, the codebase tour, a first small task and who to pair with each day."), + P("migration", "plan", "Break down the migration", "Into small, safe steps", StarterPromptApp.Repository, + "Break the database migration into small steps we can ship one at a time, each with a rollback plan and a check that it worked."), + ]; + + /// Gives the demo's tabs and starters (they are only read, so the demos share them). + public static void AddStarters(StarterPrompts control) + { + control.Categories.AddRange(Categories); + control.Prompts.AddRange(Prompts); + } + + private static StarterPrompt P(string id, string category, string title, string description, StarterPromptApp? app, string prompt) => + new() { Id = id, Category = category, Title = title, Description = description, App = app, Prompt = prompt }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Disabled.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Disabled.axaml new file mode 100644 index 00000000..3fe6a1ff --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Disabled.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Preview.axaml new file mode 100644 index 00000000..4ecb4e7b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Preview.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Preview.axaml.cs new file mode 100644 index 00000000..7eee5b40 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Preview.axaml.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the switcher live, loading a workspace, after a failed switch, and empty. +public partial class TeamSwitcherPreviewDemo : UserControl +{ + private readonly TeamSwitcherSimulation? _simulation; + private readonly bool _ready; + + public TeamSwitcherPreviewDemo() + { + InitializeComponent(); + _simulation = Switcher.Simulation; + Switcher.ValueChanged += (_, e) => Status.Text = "Active workspace: " + e.Workspace.Name; + Switcher.SwitchFailed += (_, e) => Status.Text = "Stayed on the current workspace: " + e.Error.Message; + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "live"); + } + } + + private void Show(string scenario) + { + Switcher.SwitchHandler = null; + // Empty: no workspaces and no simulation, so the trigger says there is no workspace. + Switcher.Simulation = scenario == "empty" ? null : _simulation; + Switcher.Value = scenario == "empty" ? null : "northwind"; + switch (scenario) + { + case "switching": + // A slow load: the trigger shows Contoso with a shimmering "Switching…" and a spinner. + Switcher.SwitchHandler = (_, token) => Task.Delay(TimeSpan.FromSeconds(3), token); + _ = Switcher.SwitchToAsync("contoso"); + break; + case "failed": + // The load throws once: the trigger goes back to Northwind and shows the message. + Switcher.SwitchHandler = (workspace, _) => + { + Switcher.SwitchHandler = null; + return Task.FromException(new InvalidOperationException("Your access to " + workspace.Name + " has expired.")); + }; + _ = Switcher.SwitchToAsync("fabrikam"); + break; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Sidebar.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Sidebar.axaml new file mode 100644 index 00000000..31627b66 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/Sidebar.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/TeamSwitcherDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/TeamSwitcherDemoData.cs new file mode 100644 index 00000000..74da9dab --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/TeamSwitcherDemoData.cs @@ -0,0 +1,32 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented workspaces the TeamSwitcher demos simulate, and the two invites their simulated join accepts. Every name +/// here is invented. Each property returns fresh workspaces, so every switcher gets its own. +/// +internal static class TeamSwitcherDemoData +{ + /// The plan of a workspace the simulated create makes. + public const string CreatedPlan = "Free"; + + /// Your role in a workspace the simulated create makes. + public const string CreatedRole = "Owner"; + + /// Six invented workspaces. + public static IReadOnlyList Workspaces => + [ + new("northwind", "Northwind", "Enterprise", 214, "Admin"), + new("northwind-labs", "Northwind Labs", "Pro", 12, "Owner"), + new("contoso", "Contoso", "Business", 86, "Member"), + new("fabrikam", "Fabrikam", "Pro", 31, "Admin"), + new("wide-world", "Wide World Importers", "Free", 4, "Owner"), + new("proseware", "Proseware", "Enterprise", 1280, "Guest"), + ]; + + /// The invite codes the simulated join accepts, and the workspace each one joins. + public static IReadOnlyList Invites => + [ + new() { Code = "ADATUM-4821", Workspace = new("adatum", "Adatum", "Pro", 18, "Member") }, + new() { Code = "TAILSPIN-0917", Workspace = new("tailspin", "Tailspin Toys", "Business", 57, "Member") }, + ]; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/YourWorkspaces.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/YourWorkspaces.axaml new file mode 100644 index 00000000..ca334201 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/YourWorkspaces.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/YourWorkspaces.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/YourWorkspaces.axaml.cs new file mode 100644 index 00000000..704439f5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TeamSwitcher/YourWorkspaces.axaml.cs @@ -0,0 +1,43 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// App-supplied workspaces with delegates that stand in for a real service. +public partial class TeamSwitcherYourWorkspacesDemo : UserControl +{ + public TeamSwitcherYourWorkspacesDemo() + { + InitializeComponent(); + + // Loading a workspace: prefetch its routes, swap the session. Keep it short; the trigger shows a loading state. + Switcher.SwitchHandler = (workspace, token) => Task.Delay(TimeSpan.FromMilliseconds(500), token); + + Switcher.CreateHandler = async (name, token) => + { + await Task.Delay(TimeSpan.FromMilliseconds(700), token); + if (name.Length < 3) + { + throw new InvalidOperationException("Use at least three characters."); + } + return new TeamSwitcherWorkspace("ws-" + Guid.NewGuid().ToString("N")[..8], name, "Free", 1, "Owner"); + }; + + Switcher.JoinHandler = async (invite, token) => + { + await Task.Delay(TimeSpan.FromMilliseconds(700), token); + if (!invite.Trim().EndsWith("KESTREL-2026", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("That invite has expired. Ask for a new one."); + } + return new TeamSwitcherWorkspace("kestrel", "Kestrel Labs", "Pro", 23, "Member"); + }; + + // Keep your own list in sync; the switcher de-duplicates by id. + Switcher.WorkspaceAdded += (_, e) => + { + Switcher.Workspaces.Add(e.Workspace); + Log.Text = (e.Kind == TeamSwitcherAddKind.Created ? "Created " : "Joined ") + e.Workspace.Name + "."; + }; + Switcher.ValueChanged += (_, e) => Log.Text = "Now in " + e.Workspace.Name + " (id: " + e.Id + ")."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Narrow.axaml new file mode 100644 index 00000000..d8889329 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Narrow.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Preview.axaml new file mode 100644 index 00000000..f78a113f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Preview.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Preview.axaml.cs new file mode 100644 index 00000000..73885741 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/Preview.axaml.cs @@ -0,0 +1,69 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated inbox live, a classifier that fails once, a queue that runs out and the empty state. +public partial class TicketRouterPreviewDemo : UserControl +{ + private readonly bool _ready; + + public TicketRouterPreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the router exists. + if (_ready) + { + Show(Scenario.SelectedValue as string ?? "live"); + } + } + + private void Show(string scenario) + { + Router.IsPaused = false; + switch (scenario) + { + case "error": + // A demo classifier: it reads the simulation's answers after a short wait and fails on the second ticket. + var calls = 0; + Router.Loop = true; + Router.Classifier = async (message, token) => + { + await Task.Delay(700, token); + calls++; + if (calls == 2) + { + throw new InvalidOperationException("The classifier timed out."); + } + return message.Sample ?? new TicketClassification(TicketIntent.Question, TicketSentiment.Neutral, 0.5); + }; + Router.InitialRouted = null; + Router.Tickets = null; + break; + case "caughtup": + Router.Classifier = null; + Router.Loop = false; + Router.InitialRouted = []; + Router.Tickets = [.. TicketRouterDemoData.Tickets.Take(3)]; + break; + case "empty": + // No tickets of your own: the router rests on "No tickets yet" in the default lanes. + Router.Classifier = null; + Router.Loop = null; + Router.InitialRouted = []; + Router.Tickets = []; + break; + default: + Router.Classifier = null; + Router.Loop = null; + Router.InitialRouted = null; + Router.Tickets = null; + break; + } + Router.Restart(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/TicketRouterDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/TicketRouterDemoData.cs new file mode 100644 index 00000000..6cf80d85 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/TicketRouterDemoData.cs @@ -0,0 +1,107 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The inbox the ticket router demos share: four lanes, a stream of ten messages from made-up customers with the answers +/// the simulated classifier gives (two of them below the review threshold) and the tickets already in the lanes. The +/// classifier is a real model (); people, companies and numbers are invented. Demo content only. +/// +internal static class TicketRouterDemoData +{ + /// The classifier named in the header: a real model. + public static string Model => AiModelCatalog.MistralSmall4.Name; + + /// The model named by the demo with its own classifier. + public static string YourModel => AiModelCatalog.Ministral38B.Name; + + /// The lanes: Billing, Engineering, Success and Needs review, with invented assignees and earlier counts. + public static IList Lanes { get; } = + [ + new TicketLane(TicketRouter.BillingLaneId, "Billing", "Andre Williams", "Priya Nair") { Count = 9 }, + new TicketLane(TicketRouter.EngineeringLaneId, "Engineering", "Mei Tanaka", "Jonas Berg", "Ada Okafor") { Count = 14 }, + new TicketLane(TicketRouter.SuccessLaneId, "Success", "Sofia Alvarez", "Liam Chen") { Count = 6 }, + new TicketLane(TicketRouter.ReviewLaneId, "Needs review") { IsReview = true }, + ]; + + /// The stream, in arrival order, each with the simulated classifier's answer. + public static IList Tickets { get; } = + [ + Message("t-101", TicketChannel.Email, "Hannah Brooks", "Lumen Bakery", "Charged twice for the March invoice", + "Hi, our card shows two charges of $49 on the 3rd and we only have one workspace.", "now", + TicketIntent.Billing, TicketSentiment.Negative, 0.94), + Message("t-102", TicketChannel.Chat, "Kofi Mensah", "Harbor Labs", "Export to CSV times out on large boards", + "Every export over about 5,000 rows spins and then fails with a timeout.", "1m", + TicketIntent.Bug, TicketSentiment.Negative, 0.91), + Message("t-103", TicketChannel.Messages, "Elena Petrova", "Northgate Studio", "Could we get dark mode in the editor?", + "The team works late and the bright canvas is rough. Any plans?", "2m", + TicketIntent.Feature, TicketSentiment.Positive, 0.88), + Message("t-104", TicketChannel.HelpDesk, "Marcus Reed", "Pinewood Dental", "Not sure this is the right place", + "We switched plans last week and some things look different now, can someone check?", "3m", + TicketIntent.Billing, TicketSentiment.Neutral, 0.54), + Message("t-105", TicketChannel.Email, "Yuki Sato", "Orbit Freight", "Refund for the unused annual seats", + "We removed eight seats in January. Can the unused months be refunded?", "4m", + TicketIntent.Refund, TicketSentiment.Neutral, 0.9), + Message("t-106", TicketChannel.Chat, "Grace Liu", "Fernway Health", "SSO login loops back to the sign-in page", + "After the identity provider redirect we land on sign-in again, every browser.", "5m", + TicketIntent.Bug, TicketSentiment.Negative, 0.96), + Message("t-107", TicketChannel.Messages, "Omar Haddad", "Quill & Co", "How do I add a guest to one project?", + "A freelancer needs access to a single project only, not the whole workspace.", "6m", + TicketIntent.Question, TicketSentiment.Neutral, 0.83), + Message("t-108", TicketChannel.HelpDesk, "Nora Lindqvist", "Brightside Schools", "Love the new reports, one idea", + "The weekly report is great. Being able to schedule it per team would be even better.", "7m", + TicketIntent.Feature, TicketSentiment.Positive, 0.62), + Message("t-109", TicketChannel.Email, "Diego Ramos", "Cobalt Coffee", "Invoice needs our VAT number", + "Our accountant needs the VAT number on the invoices from now on.", "8m", + TicketIntent.Billing, TicketSentiment.Neutral, 0.89), + Message("t-110", TicketChannel.Chat, "Amara Nwosu", "Tidewater Co-op", "Webhooks stopped firing overnight", + "No events since about 2am. Nothing changed on our side as far as we know.", "9m", + TicketIntent.Bug, TicketSentiment.Negative, 0.93), + ]; + + /// The tickets already in the lanes when the inbox opens, newest first. + public static IList Routed { get; } = CreateRouted(); + + private static RoutedTicket[] CreateRouted() + { + var earlier = new[] + { + Message("t-091", TicketChannel.Email, "Ravi Kapoor", "Summit Outdoors", "Upgrade to the team plan", + "We'd like to move from monthly to the yearly team plan.", "12m", TicketIntent.Billing, TicketSentiment.Positive, 0.92), + Message("t-092", TicketChannel.Chat, "Lea Fischer", "Atelier Nord", "Images fail to upload from iPad", + "Uploads stop at 90% from the tablet app.", "14m", TicketIntent.Bug, TicketSentiment.Negative, 0.9), + Message("t-093", TicketChannel.Messages, "Sam Carter", "Maple Analytics", "Keyboard shortcuts for comments", + "Would love a shortcut to jump to the next comment.", "15m", TicketIntent.Feature, TicketSentiment.Positive, 0.86), + Message("t-094", TicketChannel.HelpDesk, "Ines Duarte", "Velo Repair", "Question about our account", + "Something changed with our account, not sure what.", "17m", TicketIntent.Question, TicketSentiment.Neutral, 0.48), + Message("t-095", TicketChannel.Email, "Tom Becker", "Lakeside Clinic", "Receipt for last payment", + "Could you send the receipt for the last payment again?", "19m", TicketIntent.Billing, TicketSentiment.Neutral, 0.91), + Message("t-096", TicketChannel.Chat, "Zara Ahmed", "Kite Studio", "Search misses new documents", + "Documents created today do not show up in search.", "21m", TicketIntent.Bug, TicketSentiment.Negative, 0.88), + }; + return + [ + new RoutedTicket(earlier[0], earlier[0].Sample, TicketRouter.BillingLaneId, "Priya Nair"), + new RoutedTicket(earlier[1], earlier[1].Sample, TicketRouter.EngineeringLaneId, "Jonas Berg"), + new RoutedTicket(earlier[2], earlier[2].Sample, TicketRouter.SuccessLaneId, "Liam Chen"), + new RoutedTicket(earlier[3], earlier[3].Sample, TicketRouter.ReviewLaneId, null, TicketRouter.SuccessLaneId), + new RoutedTicket(earlier[4], earlier[4].Sample, TicketRouter.BillingLaneId, "Andre Williams"), + new RoutedTicket(earlier[5], earlier[5].Sample, TicketRouter.EngineeringLaneId, "Mei Tanaka"), + ]; + } + + private static TicketMessage Message(string id, TicketChannel channel, string customer, string company, string subject, + string preview, string receivedAt, TicketIntent intent, TicketSentiment sentiment, double confidence) => + new(id, channel, customer, company, subject, preview) + { + ReceivedAt = receivedAt, + Sample = new TicketClassification(intent, sentiment, confidence), + }; + + /// A new simulation of the inbox (tests and code use it; the XAML demos spell it out). + public static TicketRouterSimulation Simulation() => new() + { + Model = Model, + Lanes = Lanes, + Tickets = Tickets, + Routed = Routed, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/YourClassifier.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/YourClassifier.axaml new file mode 100644 index 00000000..220ae4a4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/YourClassifier.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/YourClassifier.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/YourClassifier.axaml.cs new file mode 100644 index 00000000..b3b71e06 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TicketRouter/YourClassifier.axaml.cs @@ -0,0 +1,74 @@ +using System.Collections.ObjectModel; +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// Your own lanes, tickets and an async classifier; the routing events written under the card. +public partial class TicketRouterYourClassifierDemo : UserControl +{ + private static readonly string[] s_extra = + [ + "Refund the duplicate charge please", + "App crashes when I open settings", + "Idea: let us pin favorite reports", + "Where do I change my email address?", + ]; + + private readonly ObservableCollection _tickets = + [ + new("q-1", TicketChannel.Chat, "Rosa Diaz", "Fieldstone", "Invoice shows the wrong company name", "Our new legal name is not on the invoice."), + new("q-2", TicketChannel.Email, "Ken Ito", "Bluebird Labs", "Password reset email never arrives", "Tried three times, nothing in spam either."), + new("q-3", TicketChannel.HelpDesk, "Ava Moss", "Glenbrook", "Hello, quick thing", "Not urgent, whenever someone has a minute."), + ]; + + private int _added; + + public TicketRouterYourClassifierDemo() + { + InitializeComponent(); + Router.Lanes = + [ + new TicketLane("billing", "Billing", "Noor Hassan", "Eli Brandt"), + new TicketLane("engineering", "Engineering", "Rina Kato"), + new TicketLane("success", "Success", "Tomas Vidal", "June Park"), + new TicketLane("review", "Triage") { IsReview = true }, + ]; + Router.Loop = false; + Router.Classifier = ClassifyAsync; + Router.Tickets = _tickets; + Router.TicketRouted += (_, e) => Log.Text = e.Ticket.Assignee is { } who + ? $"TicketRouted: {e.Ticket.Message.Subject} → {e.Ticket.LaneId} ({who})" + : $"TicketRouted: {e.Ticket.Message.Subject} → {e.Ticket.LaneId}, suggested {e.Ticket.SuggestedLaneId}"; + Router.ReviewConfirmed += (_, e) => Log.Text = $"ReviewConfirmed: {e.Ticket.Message.Subject} → {e.LaneId} ({e.Ticket.Assignee})"; + Add.Click += (_, _) => + { + var subject = s_extra[_added % s_extra.Length]; + _added++; + _tickets.Add(new TicketMessage($"x-{_added}", TicketChannel.Messages, "Sam Rivera", "Northwind", subject, "Sent from the demo.")); + }; + } + + /// A stand-in for your model call: waits, then reads a few words. Cancelled when the router pauses. + private static async Task ClassifyAsync(TicketMessage message, CancellationToken token) + { + await Task.Delay(600, token); + var text = (message.Subject + " " + message.Preview).ToLowerInvariant(); + if (text.Contains("refund")) + { + return new TicketClassification(TicketIntent.Refund, TicketSentiment.Neutral, 0.93); + } + if (text.Contains("invoice") || text.Contains("charge")) + { + return new TicketClassification(TicketIntent.Billing, TicketSentiment.Neutral, 0.9); + } + if (text.Contains("crash") || text.Contains("never arrives") || text.Contains("error")) + { + return new TicketClassification(TicketIntent.Bug, TicketSentiment.Negative, 0.88); + } + if (text.Contains("idea")) + { + return new TicketClassification(TicketIntent.Feature, TicketSentiment.Positive, 0.85); + } + return new TicketClassification(TicketIntent.Question, TicketSentiment.Neutral, 0.42); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Failure.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Failure.axaml new file mode 100644 index 00000000..45d3c86b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Failure.axaml @@ -0,0 +1,10 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Failure.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Failure.axaml.cs new file mode 100644 index 00000000..e271ff8e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Failure.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A summarizer whose first request fails, so the card shows its alert and Try again. +public partial class TldrToggleFailureDemo : UserControl +{ + private readonly TldrToggleSimulation _simulation = TldrToggleDemoData.Simulation(); + private int _attempts; + + public TldrToggleFailureDemo() + { + InitializeComponent(); + Card.Article = TldrToggleDemoData.Article; + Card.Model = TldrToggleDemoData.Model; + // The demo's written summaries stand in for a model; every other request fails after the thinking pause. + Card.Summarizer = request => _simulation.Summarize(request, fail: _attempts++ % 2 == 0); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/KeyPoints.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/KeyPoints.axaml new file mode 100644 index 00000000..59d8d9cc --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/KeyPoints.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Narrow.axaml new file mode 100644 index 00000000..a7df5a88 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Narrow.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Preview.axaml new file mode 100644 index 00000000..182b739d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/Preview.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/TldrToggleDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/TldrToggleDemoData.cs new file mode 100644 index 00000000..f509f851 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/TldrToggleDemoData.cs @@ -0,0 +1,94 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented article the TL;DR toggle demos play: a Northwind engineering post about making a desktop app start +/// faster, a summary written for each of the five lengths and seven key points ranked by weight. The company, the people +/// and every number are invented; the model credited is a real one from . Demo content only. +/// +internal static class TldrToggleDemoData +{ + public static TldrArticle Article { get; } = new() + { + Title = "How we made our desktop app start four times faster", + SourceName = "Northwind Engineering", + AuthorName = "Priya Raman", + Date = "Oct 21, 2026", + Paragraphs = + [ + new("intro", + "Last spring our desktop app took 4.2 seconds to show its first workspace on a typical laptop. People opened it in the " + + "morning, switched to email while it loaded and sometimes never came back. We set ourselves one goal for the quarter: " + + "show something useful in under a second, on the same hardware, without dropping a single feature."), + new("measure", + "We started by measuring instead of guessing. A trace of a thousand cold starts showed that almost half the time went to " + + "loading modules nobody needed yet: the editor, the calendar, the settings pages and three charting libraries. Another " + + "quarter went to waiting on the network before drawing anything at all, even when the data on disk was only minutes old."), + new("bootstrap", + "Partial bootstrapping loads only what the first screen needs. The shell, the sidebar and the list view now ship in one " + + "small bundle, and everything else is fetched in the background after the first paint, in the order people usually open " + + "it. Opening the editor before it has arrived shows a short skeleton instead of blocking the whole window."), + new("cache", + "Next we drew from the local cache first. The app now renders the last known state of the workspace straight from disk " + + "and marks anything older than a few minutes as stale. Fresh data streams in behind it, and rows update in place so " + + "nothing jumps under the pointer."), + new("sync", + "Sync was the hardest part. The old client asked the server for everything since the last session and waited for the " + + "full answer. The new one asks for a compact list of changed ids first, applies them in small batches and only then " + + "fetches the full records for what is on screen."), + new("results", + "The results held up outside the lab. Median cold start dropped from 4.2 seconds to 0.9, and the slowest tenth of " + + "machines went from 11 seconds to 2.6. Support tickets about a slow launch fell by two thirds in the first month, and " + + "daily use rose on the older hardware we had worried about most."), + new("next", + "We are not done. Search still builds its index on the main thread, and large workspaces with thousands of projects take " + + "longer to sync than we would like. Both are next on our list, and we will share what we learn."), + ], + }; + + private const string Lead = + "The desktop app took 4.2 seconds to show a workspace, so the team set out to show something useful in under a second " + + "without dropping features. Traces showed half the time went to modules nobody needed yet and a quarter to waiting on the " + + "network. Partial bootstrapping now loads only the first screen while the rest arrives in the background, and the app renders " + + "the last known state from disk as fresh data streams in. A leaner sync applies changed ids first."; + + private const string Outcome = + " The median cold start fell to 0.9 seconds, and support tickets about slow launches dropped by two thirds."; + + /// The summaries written for lengths 1 to 5 (about 35, 60, 85, 100 and 130 words). + public static IReadOnlyList Summaries { get; } = + [ + "The team cut the app's median cold start from 4.2 seconds to 0.9 by loading only what the first screen needs, drawing the " + + "last known state from disk and syncing changed ids before full records.", + "Cold starts took 4.2 seconds, and traces showed most of that went to loading modules nobody needed yet and to waiting on the " + + "network. Partial bootstrapping now ships only the shell, sidebar and list view first, the app draws its cached state straight " + + "from disk, and sync fetches changed ids before full records. The median start is now 0.9 seconds.", + Lead, + Lead + Outcome, + Lead + Outcome + " The slowest tenth of machines went from 11 seconds to 2.6, and daily use rose on the older hardware the " + + "team had worried about most. Search indexing and very large workspaces are next.", + ]; + + /// Seven key points, most important first; a list of N takes the first N and shows them in article order. + public static IReadOnlyList KeyPoints { get; } = + [ + new("bootstrap", "Partial bootstrapping loads only the shell, sidebar and list view first; everything else arrives after the first paint."), + new("results", "Median cold start fell from 4.2 seconds to 0.9, and the slowest machines went from 11 seconds to 2.6."), + new("cache", "The app draws the last known workspace from disk and updates stale rows in place."), + new("measure", "Traces showed half of startup went to modules nobody needed yet and a quarter to the network."), + new("sync", "Sync fetches a list of changed ids first, then full records only for what is on screen."), + new("intro", "The goal was something useful on screen in under a second, with no features dropped."), + new("next", "Search indexing on the main thread and very large workspaces are next."), + ]; + + /// The model credited in the footer (a neutral mark; no brand mark is drawn). + public static AgentModel Model { get; } = new(AiModelCatalog.Gpt6Luna.Name, ArcIcons.Sparkles); + + /// A new simulation of the post (each card gets its own). + public static TldrToggleSimulation Simulation() => new() + { + Article = Article, + Model = Model, + Summaries = Summaries, + KeyPoints = KeyPoints, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/YourSummarizer.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/YourSummarizer.axaml new file mode 100644 index 00000000..827db60b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/YourSummarizer.axaml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/YourSummarizer.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/YourSummarizer.axaml.cs new file mode 100644 index 00000000..9283fa05 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TldrToggle/YourSummarizer.axaml.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app article with a summarizer that returns whole results, paced by TldrStreams.FromResult. +public partial class TldrToggleYourSummarizerDemo : UserControl +{ + private static readonly TldrArticle s_article = new() + { + Title = "Release notes: offline mode for the field app", + SourceName = "Contoso Labs help center", + AuthorName = "Tomas Berg", + Date = "Oct 28, 2026", + Paragraphs = + [ + new("what", "Offline mode keeps the field app working when the signal drops. Forms, photos and signatures are saved on " + + "the device and sent as soon as a connection comes back, in the order they were made."), + new("setup", "Admins turn it on per team under Settings, Devices. Each device then downloads the forms and the last " + + "thirty days of jobs, which takes about a minute on a typical connection."), + new("conflicts", "When two people edit the same job while offline, the later change wins field by field, and the " + + "earlier value is kept in the job's history so nothing is lost."), + new("limits", "Large attachments over 50 MB wait for Wi-Fi, and reports that need live data stay unavailable until " + + "the device is back online."), + ], + }; + + private static readonly string[] s_summaries = + [ + "Offline mode saves forms, photos and signatures on the device and sends them when the signal returns.", + "Offline mode keeps the field app working without signal: work is saved on the device and sent in order once a " + + "connection returns. Admins enable it per team, and conflicting edits resolve field by field with history kept.", + ]; + + private static readonly TldrKeyPoint[] s_points = + [ + new("what", "Work is saved on the device and sent in order when the signal returns."), + new("setup", "Admins turn it on per team; devices download forms and 30 days of jobs."), + new("conflicts", "Conflicting edits resolve field by field and keep the earlier value in history."), + new("limits", "Attachments over 50 MB wait for Wi-Fi; live reports need a connection."), + ]; + + public TldrToggleYourSummarizerDemo() + { + InitializeComponent(); + Card.Article = s_article; + Card.Model = TldrToggleDemoData.Model; + Card.Summarizer = Summarize; + Card.Completed += (_, e) => Log.Text = $"Completed: {e.Mode}, length {e.Length}, {e.Words} words."; + } + + private static IAsyncEnumerable Summarize(TldrRequest request) + { + // A provider that returns whole results: the card still streams them, paced by FromResult. + if (request.Mode == TldrMode.KeyPoints) + { + return TldrStreams.FromResult(s_points.Take(request.PointCount).ToArray(), request.CancellationToken); + } + return TldrStreams.FromResult(s_summaries[request.Length <= 2 ? 0 : 1], request.CancellationToken); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Failure.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Failure.axaml new file mode 100644 index 00000000..e4bea0f3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Failure.axaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Failure.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Failure.axaml.cs new file mode 100644 index 00000000..5d843dc1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Failure.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A rewrite function that fails every other call, to show Couldn't rewrite and Retry. +public partial class ToneTunerFailureDemo : UserControl +{ + private const string Draft = """ + Hi Sofia, + + Thank you for letting us know about the duplicate charge on INV-20481. We have refunded the duplicate $249.00 to your card ending in 4417. + + Best, + Maya + """; + + private int _calls; + + public ToneTunerFailureDemo() + { + InitializeComponent(); + Tuner.Text = Draft; + Tuner.Rewrite = RewriteAsync; + // Starts formal, so the first request runs (and fails) as soon as the card is shown. + Tuner.Value = new ToneSettings(2, 1, ToneVoice.Neutral); + } + + private async Task RewriteAsync(ToneRewriteRequest request, CancellationToken token) + { + await Task.Delay(TimeSpan.FromMilliseconds(600), token); + if (_calls++ % 2 == 0) + { + throw new InvalidOperationException("The model did not respond."); + } + var greeting = request.Formality == 2 ? "Dear Ms. Ramirez," : "Hi Sofia,"; + var signOff = request.Formality == 2 ? "Kind regards,\nMaya Chen" : "Best,\nMaya"; + return $"{greeting}\n\nThank you for letting us know about the duplicate charge on INV-20481. We have issued a full refund of the duplicate $249.00 to your card ending in 4417.\n\n{signOff}"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Narrow.axaml new file mode 100644 index 00000000..96139746 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Narrow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Preview.axaml new file mode 100644 index 00000000..6584c5ca --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/Preview.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/ToneTunerDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/ToneTunerDemoData.cs new file mode 100644 index 00000000..7cd73c9a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/ToneTunerDemoData.cs @@ -0,0 +1,147 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The invented session the tone tuner demos play: a billing reply to Sofia Ramirez about a duplicate charge, written out +/// for every formality, detail and voice. Sentences a dial does not touch are the same string in every version, so only the +/// changed ones crossfade. The people, invoice, amounts and card are invented; the model is a real one from +/// . Demo content only. +/// +internal static class ToneTunerDemoData +{ + public const string RecipientName = "Sofia Ramirez"; + + public const string RecipientSubject = "Re: Duplicate charge on INV-20481"; + + /// The model the status names while a rewrite runs. + public static string Model => AiModelCatalog.ClaudeSonnet55.Name; + + /// The draft the session starts from: the reply at the starting tone. + public static string Draft { get; } = Compose(ToneSettings.Default); + + /// The reply in every other tone (3 formalities × 3 details × 4 voices, the starting tone left out). + public static IReadOnlyList Versions { get; } = + [ + .. from f in Enumerable.Range(0, 3) + from d in Enumerable.Range(0, 3) + from v in new[] { ToneVoice.Neutral, ToneVoice.Friendly, ToneVoice.Confident, ToneVoice.Empathetic } + let settings = new ToneSettings(f, d, v) + where settings != ToneSettings.Default + select new ToneTunerVersion(settings, Compose(settings)), + ]; + + /// The reply in 's tone. + public static string Compose(ToneSettings value) + { + var f = Math.Clamp(value.Formality, 0, 2); + var d = Math.Clamp(value.Detail, 0, 2); + var v = value.Voice; + var paragraphs = new List { Greeting(f, v) }; + var body = new List { Acknowledge(f, v), d >= 1 ? Cause(f, d) : null, Refund(f, v), Timing(f, d), d == 2 ? Prevention(f) : null }; + paragraphs.Add(string.Join(" ", body.Where(sentence => sentence is not null))); + if (d >= 1) + { + var closer = Closer(f, v); + if (v == ToneVoice.Empathetic) + { + closer += " " + Patience(f); + } + paragraphs.Add(closer); + } + paragraphs.Add(SignOff(f, v)); + return string.Join("\n\n", paragraphs); + } + + private static string Greeting(int f, ToneVoice v) => (f, v) switch + { + (2, _) => "Dear Ms. Ramirez,", + (_, ToneVoice.Friendly) => "Hi Sofia!", + (0, _) => "Hey Sofia,", + _ => "Hi Sofia,", + }; + + private static string Acknowledge(int f, ToneVoice v) => (v, f) switch + { + (ToneVoice.Friendly, 0) => "Thanks so much for flagging the double charge on INV-20481!", + (ToneVoice.Friendly, 1) => "Thanks so much for letting us know about the duplicate charge on INV-20481.", + (ToneVoice.Friendly, _) => "Thank you very much for bringing the duplicate charge on invoice INV-20481 to our attention.", + (ToneVoice.Confident, 0) => "You're right, INV-20481 was charged twice.", + (ToneVoice.Confident, 1) => "You're right: INV-20481 was charged twice.", + (ToneVoice.Confident, _) => "You are correct that invoice INV-20481 was charged twice.", + (ToneVoice.Empathetic, 0) => "I'm so sorry you got charged twice for INV-20481.", + (ToneVoice.Empathetic, 1) => "I'm sorry you were charged twice for INV-20481, and I understand how frustrating that is.", + (ToneVoice.Empathetic, _) => "I am sorry that you were charged twice for invoice INV-20481, and I understand how frustrating this must be.", + (_, 0) => "Thanks for flagging the double charge on INV-20481.", + (_, 1) => "Thank you for letting us know about the duplicate charge on INV-20481.", + _ => "Thank you for bringing the duplicate charge on invoice INV-20481 to our attention.", + }; + + private static string Cause(int f, int d) => (f, d) switch + { + (0, 1) => "A payment retry on our side went through twice.", + (0, _) => "A payment retry on our side went through twice after our processor timed out on March 3.", + (1, 1) => "A payment retry on our side went through twice after a timeout.", + (1, _) => "A payment retry on our side went through twice after our processor timed out on March 3.", + (_, 1) => "A payment retry on our side was processed twice following a timeout.", + _ => "A payment retry on our side was processed twice after our payment processor timed out on March 3.", + }; + + private static string Refund(int f, ToneVoice v) => (v == ToneVoice.Confident, f) switch + { + (true, 0) => "We've already refunded the extra $249.00 to your card ending in 4417.", + (true, 1) => "We've already refunded the duplicate $249.00 to your card ending in 4417.", + (true, _) => "The duplicate $249.00 has already been refunded to your card ending in 4417.", + (_, 0) => "We've refunded the extra $249.00 to your card ending in 4417.", + (_, 1) => "We have refunded the duplicate $249.00 to your card ending in 4417.", + _ => "We have issued a full refund of the duplicate $249.00 to your card ending in 4417.", + }; + + private static string Timing(int f, int d) => (d, f) switch + { + (0, 0) => "It'll show up in 5–7 business days.", + (0, 1) => "Expect it within 5–7 business days.", + (0, _) => "Please allow 5–7 business days.", + (1, 0) => "It should show up within 5–7 business days.", + (1, _) => "It should appear on your statement within 5–7 business days.", + (_, 0) => "It should show up within 5–7 business days, depending on your bank.", + (_, 1) => "It should appear on your statement within 5–7 business days, depending on your bank.", + _ => "It should appear on your statement within 5–7 business days, depending on your bank's processing time.", + }; + + private static string Prevention(int f) => f switch + { + 0 => "We've also added a check so a retry can't charge you twice again.", + 1 => "We've also added a safeguard so a retried payment can't be charged twice again.", + _ => "We have also added a safeguard to ensure that a retried payment cannot be charged twice in future.", + }; + + private static string Closer(int f, ToneVoice v) => (v, f) switch + { + (ToneVoice.Friendly, 0) => "If anything still looks off, just reply here and I'll sort it out!", + (ToneVoice.Friendly, 1) => "If anything still looks off, just reply here and I'll happily sort it out.", + (ToneVoice.Friendly, _) => "Should anything else look incorrect, please reply to this email and I will gladly look into it personally.", + (ToneVoice.Confident, 0) => "If anything still looks off, reply here and I'll fix it the same day.", + (ToneVoice.Confident, 1) => "If anything still looks off, reply to this email and I'll fix it the same day.", + (ToneVoice.Confident, _) => "Should anything else look incorrect, reply to this email and I will resolve it the same day.", + (ToneVoice.Empathetic, 0) => "If anything still looks off, just reply here, I'm happy to help.", + (ToneVoice.Empathetic, 1) => "If anything still looks off, reply to this email and I'll make sure it's put right.", + (ToneVoice.Empathetic, _) => "Should anything else look incorrect, please reply to this email and I will make sure it is put right.", + (_, 0) => "If anything still looks off, just reply here.", + (_, 1) => "If anything still looks off, reply to this email and I'll take care of it.", + _ => "Should anything else look incorrect, please reply to this email and I will look into it personally.", + }; + + private static string Patience(int f) => f switch + { + 0 => "Thanks for bearing with us.", + 1 => "Thank you for your patience.", + _ => "Thank you for your patience while we resolved this.", + }; + + private static string SignOff(int f, ToneVoice v) => (f, v) switch + { + (2, _) => "Kind regards,\nMaya Chen\nBilling Support", + (_, ToneVoice.Friendly) or (0, _) => "Cheers,\nMaya", + (_, ToneVoice.Empathetic) => "Warm regards,\nMaya", + _ => "Best,\nMaya", + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/YourModel.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/YourModel.axaml new file mode 100644 index 00000000..957f9487 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/YourModel.axaml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/YourModel.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/YourModel.axaml.cs new file mode 100644 index 00000000..00cd8429 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToneTuner/YourModel.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A draft of your own with an async rewrite function (canned versions stand in for a model). +public partial class ToneTunerYourModelDemo : UserControl +{ + private const string Draft = """ + Hi Jordan, + + Thanks for the report. The export got stuck because one of the 12 sheets has a formula that loops. I removed it and restarted the export, so the file should reach your inbox within 10 minutes. + + Best, + Priya + """; + + public ToneTunerYourModelDemo() + { + InitializeComponent(); + Tuner.Text = Draft; + Tuner.Rewrite = RewriteAsync; + Tuner.Rewritten += (_, e) => Log.Text = $"Rewritten ({e.Value.Formality}, {e.Value.Detail}, {e.Value.Voice}): {e.Text.Split('\n')[2]}"; + } + + /// + /// Stands in for your model: in an app, send request.Instructions and request.Text to it and return its + /// text. The token is cancelled when a newer change supersedes this request. + /// + private static async Task RewriteAsync(ToneRewriteRequest request, CancellationToken token) + { + await Task.Delay(TimeSpan.FromMilliseconds(700), token); + var greeting = request.Formality switch + { + 2 => "Dear Jordan,", + 0 => "Hey Jordan,", + _ => "Hi Jordan,", + }; + var opener = request.Voice switch + { + ToneVoice.Friendly => "Thanks so much for the report!", + ToneVoice.Confident => "Found it.", + ToneVoice.Empathetic => "Sorry the export left you waiting, and thanks for the report.", + _ => request.Formality == 2 ? "Thank you for the report." : "Thanks for the report.", + }; + var cause = request.Detail == 0 + ? "One of the 12 sheets had a looping formula." + : request.Formality == 2 + ? "The export stopped because one of the 12 sheets contains a formula that refers to itself." + : "The export got stuck because one of the 12 sheets has a formula that loops."; + var fix = request.Formality == 0 + ? "I pulled it out and restarted the export, so the file should land in your inbox within 10 minutes." + : "I removed it and restarted the export, so the file should reach your inbox within 10 minutes."; + var more = request.Detail == 2 ? " Next time, the export will name the sheet that blocks it." : string.Empty; + var signOff = request.Formality == 2 ? "Kind regards,\nPriya" : "Best,\nPriya"; + return $"{greeting}\n\n{opener} {cause} {fix}{more}\n\n{signOff}"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Narrow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Narrow.axaml.cs index e9dcea84..1ef2e3b3 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Narrow.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Narrow.axaml.cs @@ -7,6 +7,6 @@ public partial class ToolCallNarrowDemo : UserControl public ToolCallNarrowDemo() { InitializeComponent(); - Group.Items.AddRange([ToolCallSamples.TicketSearch(), ToolCallSamples.ChecklistRead(), ToolCallSamples.JobLogQuery(ToolCallStatus.Error)]); + Group.Items.AddRange([ToolCallDemoData.TicketSearch(), ToolCallDemoData.ChecklistRead(), ToolCallDemoData.JobLogQuery(ToolCallStatus.Error)]); } } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Outputs.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Outputs.axaml.cs index 0659168c..148a1d98 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Outputs.axaml.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Outputs.axaml.cs @@ -7,10 +7,10 @@ public partial class ToolCallOutputsDemo : UserControl public ToolCallOutputsDemo() { InitializeComponent(); - var list = ToolCallSamples.TicketSearch(); - var table = ToolCallSamples.JobLogQuery(); - var json = ToolCallSamples.ChecklistRead(); - var text = ToolCallSamples.StatusNote(ToolCallStatus.Success); + var list = ToolCallDemoData.TicketSearch(); + var table = ToolCallDemoData.JobLogQuery(); + var json = ToolCallDemoData.ChecklistRead(); + var text = ToolCallDemoData.StatusNote(ToolCallStatus.Success); foreach (var item in new[] { list, table, json, text }) { Host.Children.Add(new ToolCall { Item = item, IsExpanded = true }); diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Preview.axaml index ed0cb62e..7a137ec9 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Preview.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Preview.axaml @@ -1,9 +1,11 @@ - + - + + + + + The default simulated turn: calls appear and run, one fails until Retry, one waits for Allow or Deny.
+/// The simulated turn: calls appear and run, one fails until Retry, one waits for Allow or Deny. public partial class ToolCallPreviewDemo : UserControl { - private readonly ToolCallSimulation _simulation = new(); private readonly DispatcherTimer _stream = new() { Interval = TimeSpan.FromMilliseconds(40) }; private string[] _chunks = []; private int _shown; @@ -15,46 +14,31 @@ public partial class ToolCallPreviewDemo : UserControl public ToolCallPreviewDemo() { InitializeComponent(); - Group.ItemsSource = _simulation.Items; - _simulation.PropertyChanged += (_, e) => - { - if (e.Property == ToolCallSimulation.IsWorkingProperty) - { - Group.IsActive = _simulation.IsWorking; - } - }; - _simulation.Changed += OnChanged; + // The group plays the simulation; the demo writes the transcript around it. + Group.Simulation!.Changed += OnChanged; _stream.Tick += OnStream; - AttachedToVisualTree += (_, _) => Start(); - DetachedFromVisualTree += (_, _) => - { - _stream.Stop(); - _simulation.Stop(); - }; - } - - private void Start() - { - _stream.Stop(); - Answer.IsVisible = false; - Answer.Text = null; - Again.IsVisible = false; - Group.ClearValue(ToolCallGroup.IsExpandedProperty); - Intro.Text = "Let me look at what is holding up the close."; - Intro.Opacity = 1; - _simulation.Start(); + DetachedFromVisualTree += (_, _) => _stream.Stop(); } private void OnChanged(object? sender, ToolCallSimulationEventArgs e) { + if (e.Phase == ToolCallSimulationPhase.Started) + { + _stream.Stop(); + Answer.IsVisible = false; + Answer.Text = null; + Again.IsVisible = false; + Group.ClearValue(ToolCallGroup.IsExpandedProperty); + Intro.Text = ToolCallDemoData.Intro; + Intro.Opacity = 1; + return; + } if (e.Phase != ToolCallSimulationPhase.Finished) { return; } // The group folds 450ms after the last call; the answer starts streaming in once it has. - _chunks = _simulation.WasDenied - ? ["Two things hold up the March close: ", "the export bug #482 and ", "job 0112, which ", "has not finished on the replica. ", "I did not post to #finance-ops."] - : ["Two things hold up the March close: ", "the export bug #482 and ", "job 0112, which ", "has not finished on the replica. ", "I posted a note in #finance-ops."]; + _chunks = ToolCallDemoData.Answer(Group.Simulation!.WasDenied); _shown = 0; DispatcherTimer.RunOnce(() => { @@ -75,5 +59,5 @@ private void OnStream(object? sender, EventArgs e) Again.IsVisible = true; } - private void OnRunAgain(object? sender, RoutedEventArgs e) => Start(); + private void OnRunAgain(object? sender, RoutedEventArgs e) => Group.Simulation?.Start(); } diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Statuses.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Statuses.axaml index f8a5755e..f5593888 100644 --- a/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Statuses.axaml +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/Statuses.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:arc="https://github.com/AIDotNet/TKCode.Arc" x:Class="TKCode.Arc.Gallery.Demos.ToolCallStatusesDemo"> - + + diff --git a/src/TKCode.Arc.Special/Ai/ToolCall/ToolCallSamples.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/ToolCallDemoData.cs similarity index 71% rename from src/TKCode.Arc.Special/Ai/ToolCall/ToolCallSamples.cs rename to gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/ToolCallDemoData.cs index 8b87a0df..56be5c06 100644 --- a/src/TKCode.Arc.Special/Ai/ToolCall/ToolCallSamples.cs +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/ToolCall/ToolCallDemoData.cs @@ -1,16 +1,29 @@ using System.Text.Json.Nodes; -namespace TKCode.Arc; +namespace TKCode.Arc.Gallery.Demos; /// -/// The built-in sample data (invented: a finance team asks what holds up its March books close). A -/// without an item shows , a without calls shows -/// , and plays . Every call returned is new -/// and marked as sample data: Allow and Retry on it are simulated when nothing handles them. +/// The tool calls the demos show (invented: a finance team asks what holds up its March books close; the tools, tickets, +/// jobs, channel and people are made up). Each method returns new calls, since rows change them in place; +/// is the turn the preview's plays (a simulation appends copies of +/// its calls, so the script can be shared). Demo content only. /// -public static class ToolCallSamples +internal static class ToolCallDemoData { - /// The tool names used by the samples. + /// The preview's simulated turn (shared: every run appends copies of these calls). + public static IReadOnlyList Script { get; } = NewScript(); + + /// What the agent says before its first call. + public const string Intro = "Let me look at what is holding up the close."; + + /// The answer streamed after the calls, in chunks; the last chunk depends on whether the note was posted. + public static string[] Answer(bool denied) => + [ + "Two things hold up the March close: ", "the export bug #482 and ", "job 0112, which ", "has not finished on the replica. ", + denied ? "I did not post to #finance-ops." : "I posted a note in #finance-ops." + ]; + + /// The tool names used by the demos. public const string TrackerTool = "Tracker"; /// @@ -22,29 +35,6 @@ public static class ToolCallSamples /// public const string ChatTool = "Chat"; - /// - public const string WebTool = "Web"; - - /// A finished web search with three results (what a lone shows by default). - public static ToolCallItem WebSearch() - { - var item = new ToolCallItem("web-search", WebTool, "Searched the web for spring easing curves", ToolCallStatus.Success, ArcIcons.Globe) - { - RunningTitle = "Searching the web for spring easing curves", - Output = new ToolCallListOutput( - [ - new ToolCallListEntry("1", "Critically damped springs for interface motion", "guide"), - new ToolCallListEntry("2", "Choosing a visual duration instead of stiffness", "article"), - new ToolCallListEntry("3", "Why springs keep their velocity when retargeted", "notes") - ]), - Duration = TimeSpan.FromMilliseconds(640), - IsSample = true - }; - item.Input.Add(new ToolCallArgument("query", "spring easing curves interface motion")); - item.Input.Add(new ToolCallArgument("limit", "3")); - return item; - } - /// Call 1: a ticket search with four results (1180ms). public static ToolCallItem TicketSearch(ToolCallStatus status = ToolCallStatus.Success) { @@ -59,8 +49,7 @@ public static ToolCallItem TicketSearch(ToolCallStatus status = ToolCallStatus.S new ToolCallListEntry("#507", "Export button stays grey in one browser", "web") ]), Summary = "4 results", - Duration = TimeSpan.FromMilliseconds(1180), - IsSample = true + Duration = TimeSpan.FromMilliseconds(1180) }; item.Input.Add(new ToolCallArgument("project", "finance/close")); item.Input.Add(new ToolCallArgument("query", "status:open tag:close-blocker month:march")); @@ -76,8 +65,7 @@ public static ToolCallItem ChecklistRead(ToolCallStatus status = ToolCallStatus. RunningTitle = "Reading the March close checklist", Output = new ToolCallJsonOutput(ChecklistJson()), Summary = "2 blocked", - Duration = TimeSpan.FromMilliseconds(760), - IsSample = true + Duration = TimeSpan.FromMilliseconds(760) }; item.Input.Add(new ToolCallArgument("board", "March close")); item.Input.Add(new ToolCallArgument("team", "FIN")); @@ -101,8 +89,7 @@ public static ToolCallItem JobLogQuery(ToolCallStatus status = ToolCallStatus.Su ["0113", "write_csv_batches", null] ]), Summary = "2 rows", - Duration = TimeSpan.FromMilliseconds(420), - IsSample = true + Duration = TimeSpan.FromMilliseconds(420) }; item.Input.Add(new ToolCallArgument("database", "reporting_replica")); item.Input.Add(new ToolCallArgument("sql", "select job, step, finished_at from export_runs where finished_at is null order by job")); @@ -120,8 +107,7 @@ public static ToolCallItem StatusNote(ToolCallStatus status = ToolCallStatus.App Message = "Sends as Ledger bot to 36 people in #finance-ops", Output = new ToolCallTextOutput("Sent to #finance-ops at 09:15 as Ledger bot."), Summary = "1 message", - Duration = TimeSpan.FromMilliseconds(610), - IsSample = true + Duration = TimeSpan.FromMilliseconds(610) }; item.Input.Add(new ToolCallArgument("channel", "#finance-ops")); item.Input.Add(new ToolCallArgument("text", @@ -129,8 +115,7 @@ public static ToolCallItem StatusNote(ToolCallStatus status = ToolCallStatus.App return item; } - /// The four calls of the sample turn, all finished (1180 + 760 + 420 + 610ms = "3.0s"): what a group shows - /// when it is given no calls. + /// The four calls of the turn, all finished (1180 + 760 + 420 + 610ms = "3.0s"). public static IReadOnlyList SettledCalls() => [ TicketSearch(), @@ -140,12 +125,12 @@ public static IReadOnlyList SettledCalls() => ]; /// - /// The simulated turn plays by default: the first call is appended 630ms after the + /// The simulated turn the preview's plays: the first call is appended 630ms after the /// start and each later one about 225ms after the one before settles (265ms after the retried query); the ticket /// search and the checklist read succeed, the job log query times out after 1.4s and waits for Retry (the retry runs /// 700ms), and the status note waits for Allow or Deny. /// - public static IReadOnlyList Script() => + public static IReadOnlyList NewScript() => [ new(TicketSearch()) { Delay = TimeSpan.FromMilliseconds(630) }, new(ChecklistRead()), @@ -153,7 +138,7 @@ public static IReadOnlyList Script() => new(StatusNote(), ToolCallSimulationOutcome.NeedsApproval) { Delay = TimeSpan.FromMilliseconds(265) } ]; - /// The checklist the sample planner returns (35 lines once indented). + /// The checklist the planner returns (35 lines once indented). private static JsonNode ChecklistJson() => new JsonObject { ["board"] = "March close", diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Finished.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Finished.axaml new file mode 100644 index 00000000..744c9b06 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Finished.axaml @@ -0,0 +1,16 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Finished.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Finished.axaml.cs new file mode 100644 index 00000000..e4a41276 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Finished.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// A completed run loaded from history, with its own results and a deploy handler. +public partial class TrainingRunFinishedDemo : UserControl +{ + public TrainingRunFinishedDemo() + { + InitializeComponent(); + Run.BaseModel = new AgentModel("Base model"); + Run.Details = ["LoRA r8", "2× H200"]; + Run.Dataset = new TrainingRunDataset("fabrikam/ticket-triage", 12480); + for (var step = 20; step <= 1200; step += 20) + { + var train = 0.31 + 1.6 * Math.Exp(-step / 260.0) + 0.02 * Math.Sin(step / 37.0); + double? validation = step % 100 == 0 ? Math.Round(0.36 + 1.55 * Math.Exp(-step / 280.0), 4) : null; + Run.Loss.Add(new TrainingRunLossPoint(step, Math.Round(train, 4), validation)); + } + Run.Events.Add(new TrainingRunEvent("1", TrainingRunEventKind.Epoch, "Epoch 1 of 2 complete", 600)); + Run.Events.Add(new TrainingRunEvent("2", TrainingRunEventKind.Eval, "Eval · accuracy 91.4% (sample)", 1200)); + Run.Events.Add(new TrainingRunEvent("3", TrainingRunEventKind.Done, "Training complete", 1200)); + Run.Metrics = + [ + new TrainingRunMetric("Validation loss", "0.371", "best at step 1,200"), + new TrainingRunMetric("Accuracy", "91.4%", "+6.2 pts vs base (sample)"), + new TrainingRunMetric("Steps", "1,200", "2 epochs"), + new TrainingRunMetric("Duration", "9m 12s"), + ]; + Run.DeployHandler = async token => + { + await Task.Delay(1200, token); + return "https://inference.example.com/v1/triage-v2"; + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/HostDriven.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/HostDriven.axaml new file mode 100644 index 00000000..f60a1b61 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/HostDriven.axaml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/HostDriven.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/HostDriven.axaml.cs new file mode 100644 index 00000000..6c8016ee --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/HostDriven.axaml.cs @@ -0,0 +1,112 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace TKCode.Arc.Gallery.Demos; + +/// A run driven by app code: a timer stands in for the job API and the card's buttons are answered here. +public partial class TrainingRunHostDrivenDemo : UserControl +{ + private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(400) }; + private int _ticks; + private int _step; + + public TrainingRunHostDrivenDemo() + { + InitializeComponent(); + Run.BaseModel = new AgentModel("Base model"); + Run.Details = ["LoRA r16", "4× H200"]; + Run.Dataset = new TrainingRunDataset("northwind/receipts-2026", 9312); + Run.PauseRequested += (_, _) => Answer(TrainingRunStatus.Paused, "Paused by you"); + Run.ResumeRequested += (_, _) => Answer(TrainingRunStatus.Running, "Resumed"); + Run.CancelRequested += (_, _) => Answer(TrainingRunStatus.Cancelled, "Cancelled"); + Run.RestartRequested += OnRestart; + Run.DeployHandler = async token => + { + await Task.Delay(1500, token); + return "https://inference.example.com/v1/receipts-v1"; + }; + _timer.Tick += OnTick; + AttachedToVisualTree += (_, _) => _timer.Start(); + DetachedFromVisualTree += (_, _) => _timer.Stop(); + } + + private void Answer(TrainingRunStatus status, string what) + { + Run.Apply(new TrainingRunProgress + { + Status = status, + Event = new TrainingRunEvent(Guid.NewGuid().ToString("N"), TrainingRunEventKind.Info, what, _step), + }); + Log.Text = what + " at step " + _step + "."; + } + + private void OnRestart(object? sender, RoutedEventArgs e) + { + _step = 0; + _ticks = 0; + Run.Loss.Clear(); + Run.Events.Clear(); + Run.Step = 0; + Run.Elapsed = null; + Run.LearningRate = null; + Run.Eta = TimeSpan.FromMinutes(6.5); + Run.Status = TrainingRunStatus.Queued; + Log.Text = "Queued again."; + } + + private void OnTick(object? sender, EventArgs e) + { + _ticks++; + switch (Run.Status) + { + case TrainingRunStatus.Queued when _ticks > 4: + Run.Apply(new TrainingRunProgress { Status = TrainingRunStatus.Running, Elapsed = TimeSpan.Zero, Activity = "Loading the base model" }); + Log.Text = "The job started."; + break; + case TrainingRunStatus.Running: + Burst(); + break; + } + } + + /// Five samples at once, as a job API that reports every two seconds would send them. + private void Burst() + { + if (_step == 0) + { + Run.Events.Add(new TrainingRunEvent("start", TrainingRunEventKind.Start, "Training started on 4× H200", 0)); + } + for (var i = 0; i < 5 && _step < Run.TotalSteps; i++) + { + _step += 4; + var train = 0.4 + 1.7 * Math.Exp(-_step / 140.0) + 0.04 * Math.Sin(_step * 1.7); + double? validation = _step % 60 == 0 ? Math.Round(0.46 + 1.62 * Math.Exp(-_step / 150.0), 4) : null; + Run.Apply(new TrainingRunProgress { Loss = new TrainingRunLossPoint(_step, Math.Round(train, 4), validation) }); + if (_step % 300 == 0 && _step < Run.TotalSteps) + { + Run.Events.Add(new TrainingRunEvent("epoch" + _step, TrainingRunEventKind.Epoch, "Epoch 1 of 2 complete", _step)); + } + else if (_step % 120 == 0) + { + Run.Events.Add(new TrainingRunEvent("ckpt" + _step, TrainingRunEventKind.Checkpoint, "Checkpoint saved", _step)); + } + } + Run.Apply(new TrainingRunProgress + { + Step = _step, + LearningRate = 1e-4 * (1 - _step / (double)Run.TotalSteps) + 1e-5, + Elapsed = TimeSpan.FromSeconds(_step * 0.6), + Eta = TimeSpan.FromSeconds((Run.TotalSteps - _step) * 0.6), + }); + if (_step >= Run.TotalSteps) + { + Run.Apply(new TrainingRunProgress + { + Status = TrainingRunStatus.Completed, + Event = new TrainingRunEvent("done", TrainingRunEventKind.Done, "Training complete", _step), + }); + Log.Text = "The job finished. Deploy is simulated."; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Narrow.axaml new file mode 100644 index 00000000..f1dbef1d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Narrow.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Preview.axaml new file mode 100644 index 00000000..7eca9c54 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Preview.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Preview.axaml.cs new file mode 100644 index 00000000..c0f8263b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/Preview.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the simulated run, healthy or failing once with out of memory. +public partial class TrainingRunPreviewDemo : UserControl +{ + private readonly bool _ready; + + public TrainingRunPreviewDemo() + { + InitializeComponent(); + _ready = true; + } + + private void OnScenarioChanged(object? sender, SelectionChangedEventArgs e) + { + // The control applies SelectedValue while the XAML loads, before the constructor has finished. + if (_ready) + { + Run.Scenario = Scenario.SelectedValue as string == "oom" ? TrainingRunScenario.Oom : TrainingRunScenario.Healthy; + } + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/TrainingRunDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/TrainingRunDemoData.cs new file mode 100644 index 00000000..94d6c666 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrainingRun/TrainingRunDemoData.cs @@ -0,0 +1,38 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The fine-tuning job the training run demos play: a LoRA run on 8× H200 against an invented support dataset. The base +/// model keeps a neutral label (which models a vendor lets you fine-tune is not something these demos can state); the job, +/// the dataset and the endpoint are invented. Demo content only. +/// +internal static class TrainingRunDemoData +{ + /// The job's name. + public const string JobName = "support-v4"; + + /// The base model, by a neutral label. + public static AgentModel BaseModel { get; } = new("Base model"); + + /// The method and the hardware after the model. + public static IReadOnlyList Details { get; } = ["LoRA r16", "8× H200"]; + + /// The training data: an invented dataset of support tickets. + public static TrainingRunDataset Dataset { get; } = new("contoso/support-tickets-oct", 48210, null); + + /// The hardware the job queues for and trains on. + public const string Hardware = "8× H200"; + + /// The address the simulated deploy returns. + public const string Endpoint = "https://inference.example.com/v1/support-v4"; + + /// A new simulation of the job (code uses it; the XAML demos spell it out). + public static TrainingRunSimulation Simulation() => new() + { + JobName = JobName, + BaseModel = BaseModel, + Details = Details, + Dataset = Dataset, + Hardware = Hardware, + Endpoint = Endpoint, + }; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Card.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Card.axaml new file mode 100644 index 00000000..b11417ec --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Card.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Checkout.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Checkout.axaml new file mode 100644 index 00000000..d8e78f0c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Checkout.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Checkout.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Checkout.axaml.cs new file mode 100644 index 00000000..0988bc31 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Checkout.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// An app's own checkout handler (simulated here), with the events the banner raises. +public partial class TrialBannerCheckoutDemo : UserControl +{ + public TrialBannerCheckoutDemo() + { + InitializeComponent(); + TrialBannerDemoData.Apply(Banner); + Banner.AddPaymentHandler = AddPaymentAsync; + Banner.ComparePlansRequested += (_, e) => + { + // A real app navigates to its plans page here. + e.Handled = true; + Log.Text = "Compare plans: the app opens its plans page."; + }; + Banner.ManageBillingRequested += (_, e) => + { + e.Handled = true; + Log.Text = "Manage billing: the app opens its billing portal."; + }; + Banner.Snoozed += (_, e) => Log.Text = $"Snoozed ({e.Option}) until {e.Until:MMM d}. Persist this and hide the banner until then."; + Banner.Dismissed += (_, _) => Log.Text = "Dismissed. Persist this."; + } + + /// Stands in for a call to your billing service: 1.2s, then success or a declined card. + private async Task AddPaymentAsync(CancellationToken cancellation) + { + Log.Text = "Opening checkout…"; + await Task.Delay(TimeSpan.FromMilliseconds(1200), cancellation); + if (Decline.IsChecked == true) + { + Log.Text = "The card was declined (sample)."; + throw new InvalidOperationException("The card was declined."); + } + Log.Text = "Payment method on file."; + } + + private void OnReset(object? sender, RoutedEventArgs e) + { + Banner.IsUpgraded = false; + Banner.IsCollapsed = false; + Log.Text = "Nothing yet."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Narrow.axaml new file mode 100644 index 00000000..f1a13113 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Narrow.axaml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Pill.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Pill.axaml new file mode 100644 index 00000000..d2d50d53 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Pill.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Preview.axaml new file mode 100644 index 00000000..68243c34 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Preview.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Preview.axaml.cs new file mode 100644 index 00000000..8d340033 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/Preview.axaml.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +/// The default demo: the trial banner with 9 days, 2 days, an ended trial or none, as a banner, a card or a pill. +public partial class TrialBannerPreviewDemo : UserControl +{ + private readonly bool _ready; + + public TrialBannerPreviewDemo() + { + InitializeComponent(); + TrialBannerDemoData.Apply(Banner); + _ready = true; + Apply(); + } + + private void OnChanged(object? sender, SelectionChangedEventArgs e) + { + // The segmented controls apply SelectedValue while the XAML loads, before the banner field is set. + if (_ready) + { + Apply(); + } + } + + private void Apply() + { + // "No trial" leaves the days unset: the banner's empty state. + Banner.DaysLeft = (Days.SelectedValue as string) switch + { + "none" => null, + var value => int.TryParse(value, out var days) ? days : 9 + }; + Banner.Variant = (Variant.SelectedValue as string) switch + { + "card" => TrialBannerVariant.Card, + "pill" => TrialBannerVariant.Pill, + _ => TrialBannerVariant.Banner + }; + } + + private void OnReset(object? sender, RoutedEventArgs e) + { + Banner.IsUpgraded = false; + Banner.IsCollapsed = false; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/TrialBannerDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/TrialBannerDemoData.cs new file mode 100644 index 00000000..2aa3bf8c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/TrialBanner/TrialBannerDemoData.cs @@ -0,0 +1,36 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The trial the banner demos show: an invented product's Pro plan on the invented Northwind workspace, which would lose +/// SAML SSO, team chat alerts, issue sync and its 12 seats. Demo content only (no real product is named). +/// +internal static class TrialBannerDemoData +{ + /// The plan on trial. + public const string Plan = "Pro"; + + /// The workspace on trial. + public const string Workspace = "Northwind"; + + /// The short loss phrase of the banner copy. + public const string LossSummary = "SAML SSO and 12 seats"; + + /// The four rows of what the workspace would lose (new instances each call: a banner owns its rows). + public static IReadOnlyList Features() => + [ + new("sso", "SAML single sign-on", "for every member", ArcIcons.Key), + new("alerts", "Alerts in team chat", "incidents and mentions", ArcIcons.Bell), + new("sync", "Two-way issue sync", "status and comments", ArcIcons.RefreshCw), + new("seats", "Team seats", "12 seats", ArcIcons.Users), + ]; + + /// Gives the demo's trial: the plan, the workspace, the loss and the features. + public static void Apply(TrialBanner banner) + { + banner.Plan = Plan; + banner.Workspace = Workspace; + banner.LossSummary = LossSummary; + banner.Features.Clear(); + banner.Features.AddRange(Features()); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Checkout.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Checkout.axaml new file mode 100644 index 00000000..441c7558 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Checkout.axaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Checkout.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Checkout.axaml.cs new file mode 100644 index 00000000..20315199 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Checkout.axaml.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class UpgradePromptCheckoutDemo : UserControl +{ + private int _attempts; + + public UpgradePromptCheckoutDemo() + { + InitializeComponent(); + Prompt.UpgradeHandler = UpgradeAsync; + Prompt.IntervalChanged += (_, e) => Log.Text = $"Billing: {e.Interval}."; + Prompt.UpgradeFailed += (_, e) => Log.Text = $"Failed: {e.Error.Message}"; + Prompt.Upgraded += (_, e) => + Log.Text = $"Upgraded to {e.Selection.Plan.Name}, {e.Selection.Interval}, {e.Selection.Amount:0.##} {e.Selection.Currency} today (sample)."; + Prompt.Dismissed += (_, _) => Log.Text = "Snoozed until the limit resets."; + Prompt.ContinueRequested += OnContinue; + } + + // Your server charges the plan and interval here; this sample only waits and declines the first attempt. + private async Task UpgradeAsync(UpgradePromptSelection selection, CancellationToken cancellationToken) + { + await Task.Delay(900, cancellationToken); + if (_attempts++ == 0) + { + throw new InvalidOperationException("Your card was declined. Try another card."); + } + } + + private void OnContinue(object? sender, RoutedEventArgs e) + { + // A real app closes the prompt and retries the blocked action; the sample starts over. + Log.Text = "Continue: close the prompt and retry the blocked action."; + _attempts = 0; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Narrow.axaml new file mode 100644 index 00000000..ef15e65c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Narrow.axaml @@ -0,0 +1,11 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Preview.axaml new file mode 100644 index 00000000..4ded5d66 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Preview.axaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Preview.axaml.cs new file mode 100644 index 00000000..54830fbf --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/Preview.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class UpgradePromptPreviewDemo : UserControl +{ + public UpgradePromptPreviewDemo() + { + InitializeComponent(); + // The second card runs the built-in simulated upgrade as soon as it is shown, so it lands on the success state. + Upgraded.Loaded += OnUpgradedLoaded; + } + + private async void OnUpgradedLoaded(object? sender, RoutedEventArgs e) + { + Upgraded.Loaded -= OnUpgradedLoaded; + await Upgraded.UpgradeAsync(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/ReducedMotion.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/ReducedMotion.axaml new file mode 100644 index 00000000..996daa8e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/ReducedMotion.axaml @@ -0,0 +1,11 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/UpgradePromptDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/UpgradePromptDemoData.cs new file mode 100644 index 00000000..0c33311d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/UpgradePromptDemoData.cs @@ -0,0 +1,45 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The offer the upgrade prompt demos show: an invented product's Pro plan at $20 a month or $16 a month billed yearly, +/// with 2,000 AI credits, a priority queue and access to real models from . The plan, its +/// prices and limits belong to the invented product; they are not a model vendor's prices. Demo content only; the model +/// marks are Lucide glyphs, not the vendors' logos. +/// +internal static class UpgradePromptDemoData +{ + /// The mark for Claude Opus 5.5: Lucide sparkles in violet. + private static readonly IBrush s_topModel = new ImmutableSolidColorBrush(Color.FromRgb(0x7C, 0x5C, 0xE6)); + + /// The mark for image input with Gemini 4 Argon: Lucide eye in teal. + private static readonly IBrush s_vision = new ImmutableSolidColorBrush(Color.FromRgb(0x0E, 0x95, 0x8A)); + + /// The Pro plan the demos offer (shared: the prompt only reads it). + public static UpgradePromptPlan ProPlan { get; } = new() + { + Name = "Pro", + MonthlyPrice = 20, + YearlyPrice = 16, + Currency = "USD", + Limit = 2000, + Features = + [ + new UpgradePromptFeature("credits", "2,000 AI credits every month", ArcIcons.Zap), + new UpgradePromptFeature("model", $"{AiModelCatalog.ClaudeOpus55.Name} for your hardest tasks", ArcIcons.Sparkles, s_topModel), + new UpgradePromptFeature("queue", "Priority queue, no waiting at peak times", ArcIcons.Timer) + ], + Limits = + [ + new UpgradePromptLimit("AI credits", "2,000 a month"), + new UpgradePromptLimit("Models", AiModelCatalog.ClaudeOpus55.Name, ArcIcons.Sparkles, s_topModel), + new UpgradePromptLimit("Image input", AiModelCatalog.Gemini4Argon.Name, ArcIcons.Eye, s_vision), + new UpgradePromptLimit("Queue", "Priority") + ] + }; + + /// The Team plan's shared-workspace unlock, naming a real model. + public static string SharedWorkspace { get; } = $"Shared {AiModelCatalog.ClaudeOpus55.Name} workspace"; +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/YourPlan.axaml b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/YourPlan.axaml new file mode 100644 index 00000000..5a9b61d7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Ai/UpgradePrompt/YourPlan.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/DomainVerifyDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/DomainVerifyDemoData.cs new file mode 100644 index 00000000..e0783d6e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/DomainVerifyDemoData.cs @@ -0,0 +1,39 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// +/// The Domain verify demos' data (all invented): the records a website domain and a sending domain would need, and a +/// stand-in resolver that lets them appear one by one. +/// +public static class DomainVerifyDemoData +{ + /// A CNAME to the edge, an ownership TXT and a CAA for a subdomain of a website. + public static IReadOnlyList WebsiteRecords(string domain) + { + var label = domain.Split('.')[0]; + return + [ + new("route", "CNAME", label, "edge.northwind-sites.net", "Routes traffic to Northwind"), + new("owner", "TXT", "_northwind", "northwind-verify=7f3a91c0e2b4d658", "Proves you own the domain"), + new("caa", "CAA", label, "0 issue \"letsencrypt.org\"", "Lets Let's Encrypt issue the certificate"), + ]; + } + + /// MX, SPF, DKIM and DMARC for a domain that sends mail. + public static IReadOnlyList EmailRecords(string domain) => + [ + new("mx", "MX", domain, "10 inbound.northwind-mail.net", "Receives replies and bounces"), + new("spf", "TXT", domain, "v=spf1 include:spf.northwind-mail.net ~all", "Says who may send for the domain"), + new("dkim", "CNAME", "nw1._domainkey", "nw1.dkim.northwind-mail.net", "Signs every message"), + new("dmarc", "TXT", "_dmarc", "v=DMARC1; p=quarantine; rua=mailto:dmarc@northwind.dev", "Tells receivers what to do with fakes"), + ]; + + /// Record i becomes valid on poll i + 1, so the list fills in order; regions follow the number of valid records. + public static DomainVerifyResult Propagation(IReadOnlyList records, int attempt) + { + var checks = records + .Select((r, i) => new DomainRecordCheck(r.Id, i < attempt ? DomainRecordStatus.Valid : DomainRecordStatus.Missing)) + .ToList(); + var valid = checks.Count(c => c.Status == DomainRecordStatus.Valid); + return new DomainVerifyResult(checks, new DomainRegions(valid == records.Count ? 6 : Math.Min(5, valid * 2), 6)); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Handlers.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Handlers.axaml new file mode 100644 index 00000000..cdddcbd3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Handlers.axaml @@ -0,0 +1,17 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Handlers.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Handlers.axaml.cs new file mode 100644 index 00000000..55ae6003 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Handlers.axaml.cs @@ -0,0 +1,41 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class DomainVerifyHandlersDemo : UserControl +{ + public DomainVerifyHandlersDemo() + { + InitializeComponent(); + + Block.GetRecordsAction = async (domain, cancellation) => + { + await Task.Delay(600, cancellation); + return DomainVerifyDemoData.WebsiteRecords(domain); + }; + + // The resolver pool is busy during the first run, so that check throws; the second run sees the records + // spread one by one over three polls. + Block.VerifyAction = async (domain, records, context, cancellation) => + { + await Task.Delay(500, cancellation); + if (context.Run == 1) + { + throw new InvalidOperationException("The resolver pool is busy right now. Check again in a moment."); + } + return DomainVerifyDemoData.Propagation(records, context.Attempt); + }; + + Block.IssueCertificateAction = async (domain, cancellation) => + { + await Task.Delay(1200, cancellation); + return new DomainCertificate("Let's Encrypt", "in 60 days"); + }; + + Block.PollInterval = TimeSpan.FromMilliseconds(900); + Block.PhaseChanged += (_, e) => Log.Text = $"Phase: {e.OldPhase} → {e.NewPhase}"; + Block.Failed += (_, e) => Log.Text = $"{e.Domain}: {e.Exception.Message}"; + Block.Verified += (_, e) => Log.Text = $"{e.Domain} is live, certificate by {e.Certificate.Issuer}, renews {e.Certificate.Renews}."; + Block.Removed += (_, e) => Log.Text = $"{e.Domain} was removed."; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Narrow.axaml new file mode 100644 index 00000000..0f272e26 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Narrow.axaml @@ -0,0 +1,12 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Narrow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Narrow.axaml.cs new file mode 100644 index 00000000..16be371d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Narrow.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class DomainVerifyNarrowDemo : UserControl +{ + public DomainVerifyNarrowDemo() + { + InitializeComponent(); + + Block.GetRecordsAction = async (domain, cancellation) => + { + await Task.Delay(400, cancellation); + return DomainVerifyDemoData.WebsiteRecords(domain); + }; + + // The CAA record holds another authority's value, so the stacked row shows what was found and the fix. + Block.VerifyAction = async (domain, records, context, cancellation) => + { + await Task.Delay(400, cancellation); + var checks = records + .Select(r => r.Id == "caa" && context.Run == 1 + ? new DomainRecordCheck(r.Id, DomainRecordStatus.Invalid, + Found: "0 issue \"other-ca.example\"", + Fix: "Replace the CAA value with the one above so the certificate can be issued.") + : new DomainRecordCheck(r.Id, DomainRecordStatus.Valid)) + .ToList(); + return new DomainVerifyResult(checks, new DomainRegions(6, 6)); + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Preview.axaml new file mode 100644 index 00000000..aa30c277 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/Preview.axaml @@ -0,0 +1,7 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/SendingDomain.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/SendingDomain.axaml new file mode 100644 index 00000000..6968920a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/SendingDomain.axaml @@ -0,0 +1,12 @@ + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/SendingDomain.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/SendingDomain.axaml.cs new file mode 100644 index 00000000..73090a4f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/DomainVerify/SendingDomain.axaml.cs @@ -0,0 +1,42 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class DomainVerifySendingDemo : UserControl +{ + public DomainVerifySendingDemo() + { + InitializeComponent(); + + // An email domain needs MX, SPF, DKIM and DMARC instead of a CNAME and a CAA, and no SSL certificate: the last step + // switches delivery on, so the card's words change with Labels. + Block.Labels = new DomainVerifyLabels + { + AddHeading = "Add a sending domain", + AddHint = "Mail is sent from addresses at this domain", + FooterRecords = "Add these records, then check that mail can be signed", + FooterVerified = "Delivery is on", + IssuingCertificate = "Turning on delivery", + CertificateIssued = (issuer, renews) => $"Delivery on · signed by {issuer}", + CertificateIssuedShort = issuer => $"Signed by {issuer}", + CertificateAnnouncement = issuer => $"Delivery is on, signed by {issuer}", + }; + + Block.GetRecordsAction = async (domain, cancellation) => + { + await Task.Delay(500, cancellation); + return DomainVerifyDemoData.EmailRecords(domain); + }; + Block.VerifyAction = async (domain, records, context, cancellation) => + { + await Task.Delay(400, cancellation); + return DomainVerifyDemoData.Propagation(records, context.Attempt + 1); + }; + Block.IssueCertificateAction = async (domain, cancellation) => + { + await Task.Delay(900, cancellation); + return new DomainCertificate("Northwind Mail", "yearly"); + }; + Block.PollInterval = TimeSpan.FromMilliseconds(700); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Failing.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Failing.axaml new file mode 100644 index 00000000..4713e92b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Failing.axaml @@ -0,0 +1,16 @@ + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Failing.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Failing.axaml.cs new file mode 100644 index 00000000..8e865f86 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Failing.axaml.cs @@ -0,0 +1,54 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class WebhookTesterFailingDemo : UserControl +{ + public WebhookTesterFailingDemo() + { + InitializeComponent(); + + // A real app passes the signed request to its server, which does the POST. Here the "server" is a delay and a rule: + // paths ending in /legacy answer 500, /slow never answers in time, everything else is a 200. + Block.SendAction = async (request, cancellation) => + { + await Task.Delay(700, cancellation); + if (request.Url.EndsWith("/slow", StringComparison.Ordinal)) + { + return new WebhookTestResponse { Status = 0, DurationMs = 10000, TimedOut = true, Error = "No answer within 10 seconds." }; + } + if (request.Url.EndsWith("/legacy", StringComparison.Ordinal)) + { + return new WebhookTestResponse + { + Status = 500, + DurationMs = 701, + Body = "{\"error\":\"legacy handler is switched off\"}", + Headers = [new("content-type", "application/json")], + }; + } + return new WebhookTestResponse + { + Status = 200, + DurationMs = 143, + Body = "{\"received\":true}", + Headers = [new("content-type", "application/json")], + }; + }; + + // Retry goes to the same server, here always through: the retry reaches the (now fixed) handler. + Block.RetryAction = async (delivery, cancellation) => + { + await Task.Delay(500, cancellation); + return new WebhookTestResponse { Status = 200, DurationMs = 97, Body = "{\"received\":true}" }; + }; + + Block.Delivered += (_, e) => + { + var response = e.Delivery.Response; + Log.Text = response.IsSuccess + ? $"{e.Delivery.Event}: {response.Status} in {response.DurationMs} ms." + : $"{e.Delivery.Event} failed ({(response.TimedOut ? "timed out" : response.Status)}). Press Retry in the list."; + }; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Narrow.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Narrow.axaml new file mode 100644 index 00000000..02874dcc --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Narrow.axaml @@ -0,0 +1,11 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Narrow.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Narrow.axaml.cs new file mode 100644 index 00000000..21ead8be --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Narrow.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class WebhookTesterNarrowDemo : UserControl +{ + public WebhookTesterNarrowDemo() + { + InitializeComponent(); + Block.Deliveries.AddRange(WebhookTesterDemoData.Deliveries()); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/OtherProduct.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/OtherProduct.axaml new file mode 100644 index 00000000..1ef3dd4a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/OtherProduct.axaml @@ -0,0 +1,12 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/OtherProduct.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/OtherProduct.axaml.cs new file mode 100644 index 00000000..83a52d15 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/OtherProduct.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class WebhookTesterOtherProductDemo : UserControl +{ + public WebhookTesterOtherProductDemo() + { + InitializeComponent(); + Block.Source = new WebhookTesterSource { Name = "Relay Mail" }; + Block.Events.AddRange(WebhookTesterDemoData.RelayEvents()); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Preview.axaml new file mode 100644 index 00000000..44aa5d9d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Preview.axaml @@ -0,0 +1,7 @@ + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Preview.axaml.cs new file mode 100644 index 00000000..c5e51dfe --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/Preview.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class WebhookTesterPreviewDemo : UserControl +{ + public WebhookTesterPreviewDemo() + { + InitializeComponent(); + Block.Deliveries.AddRange(WebhookTesterDemoData.Deliveries()); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/WebhookTesterDemoData.cs b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/WebhookTesterDemoData.cs new file mode 100644 index 00000000..25a7cc1e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Blocks/WebhookTester/WebhookTesterDemoData.cs @@ -0,0 +1,64 @@ +namespace TKCode.Arc.Gallery.Demos; + +/// Sample deliveries and a fake endpoint for the WebhookTester demos (all names and values are made up). +public static class WebhookTesterDemoData +{ + private static readonly DateTimeOffset s_day = new(2026, 10, 28, 0, 0, 0, TimeSpan.Zero); + + /// Five earlier attempts, newest first: two of them failed. + public static IEnumerable Deliveries(string url = "https://api.northwind.dev/webhooks/billing") + { + yield return Make("dlv_5", "order.paid", new TimeSpan(14, 28, 6), url, 200, 184, "{\n \"received\": true\n}"); + yield return Make("dlv_4", "subscription.updated", new TimeSpan(14, 11, 42), url, 500, 1204, + "{\n \"error\": \"upstream unavailable\"\n}"); + yield return Make("dlv_3", "customer.created", new TimeSpan(13, 57, 19), url, 200, 96, "{\n \"received\": true\n}"); + yield return Make("dlv_2", "payment.failed", new TimeSpan(13, 40, 3), url, 0, 10000, null, timedOut: true); + yield return Make("dlv_1", "checkout.completed", new TimeSpan(13, 22, 51), url, 200, 212, "{\n \"received\": true\n}"); + } + + private static WebhookTestDelivery Make(string id, string type, TimeSpan at, string url, int status, int ms, string? body, + bool timedOut = false) + { + var time = s_day + at; + var payload = $"{{\n \"id\": \"evt_{id}\",\n \"type\": \"{type}\"\n}}"; + var timestamp = time.ToUnixTimeSeconds(); + var signature = $"t={timestamp},v1={WebhookTesterDemoSigner.Hex(id)}"; + var request = new WebhookTestRequest(url, type, payload, timestamp, signature, "x-webhook-signature", + [ + new("content-type", "application/json"), + new("user-agent", "Acme-Webhooks/1.0"), + new("x-webhook-event", type), + new("x-webhook-signature", signature), + ]); + var response = new WebhookTestResponse + { + Status = status, + DurationMs = ms, + Body = body, + TimedOut = timedOut, + Error = timedOut ? "The endpoint did not answer within 10 seconds." : null, + Headers = status == 0 + ? null + : [new("content-type", "application/json"), new("x-request-id", "req_" + id)], + }; + return new WebhookTestDelivery(id, type, time, request, response); + } + + /// The events of the "other product" demo: made-up names, nothing from a real service. + public static IEnumerable RelayEvents() + { + yield return new WebhookTestEvent("message.delivered", """{"id":"evt_9f2a","type":"message.delivered","data":{"message":"msg_0184","to":"ada@northwind.dev","latency_ms":412}}"""); + yield return new WebhookTestEvent("message.bounced", """{"id":"evt_9f2b","type":"message.bounced","data":{"message":"msg_0185","to":"nobody@example.org","reason":"mailbox full"}}"""); + yield return new WebhookTestEvent("domain.verified", """{"id":"evt_9f2c","type":"domain.verified","data":{"domain":"mail.northwind.dev","checked":["spf","dkim","dmarc"]}}"""); + } +} + +/// A stand-in for a signature in the sample deliveries (not a real HMAC; the block signs real sends itself). +internal static class WebhookTesterDemoSigner +{ + public static string Hex(string seed) + { + var hash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(seed)); + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Center.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Center.axaml new file mode 100644 index 00000000..adf3448a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Center.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Connect.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Connect.axaml new file mode 100644 index 00000000..7a61ffcd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Connect.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Connect.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Connect.axaml.cs new file mode 100644 index 00000000..8cf7a8b1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Connect.axaml.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class OrbitLogosConnectDemo : UserControl +{ + private int _attempts; + + public OrbitLogosConnectDemo() + { + InitializeComponent(); + Orbit.ConnectHandler = ConnectAsync; + } + + private async Task ConnectAsync(OrbitLogo logo) + { + // Stand-in for your own sign-in or API call: the first attempt is refused so the Try again state shows. + await Task.Delay(1200); + if (_attempts++ == 0) + { + throw new InvalidOperationException("Refused for the demo."); + } + } + + private void OnOpenParcel(object? sender, RoutedEventArgs e) => Orbit.SelectedId = "parcel"; + + private void OnClose(object? sender, RoutedEventArgs e) => Orbit.SelectedId = null; + + private void OnSelectionChanged(object? sender, OrbitLogosSelectionChangedEventArgs e) + { + Status.Text = e.Logo is null ? "Closed" : $"Opened {e.Logo.Name} (index {e.Index})"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Preview.axaml new file mode 100644 index 00000000..1f74c4d3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/OrbitLogos/Preview.axaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Compact.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Compact.axaml new file mode 100644 index 00000000..9c0c537b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Compact.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Preview.axaml new file mode 100644 index 00000000..25a55e19 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Preview.axaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Preview.axaml.cs new file mode 100644 index 00000000..09a04271 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Preview.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class PageCurlPreviewDemo : UserControl +{ + public PageCurlPreviewDemo() + { + InitializeComponent(); + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Template.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Template.axaml new file mode 100644 index 00000000..4a834fe2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Template.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + Ferns + Unfurl in damp shade and need very little. + Lavender + Wants sun, dry feet and a hard prune in spring. + Mint + Give it a pot of its own; it will take the bed otherwise. + Thyme + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Template.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Template.axaml.cs new file mode 100644 index 00000000..c0d74064 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/PageCurl/Template.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class PageCurlTemplateDemo : UserControl +{ + public PageCurlTemplateDemo() + { + InitializeComponent(); + } + + private void OnSpreadChanged(object? sender, PageCurlSpreadChangedEventArgs e) + { + Readout.Text = $"Spread {e.Spread} of {Book.SpreadCount - 1}"; + } +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Badges.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Badges.axaml new file mode 100644 index 00000000..a5faa2a9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Badges.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Preview.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Preview.axaml new file mode 100644 index 00000000..275f1ac1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Preview.axaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Preview.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Preview.axaml.cs new file mode 100644 index 00000000..d5d30c4f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Preview.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class StickerPeelPreviewDemo : UserControl +{ + public StickerPeelPreviewDemo() + { + InitializeComponent(); + } + + // Clearing the positions sends every sticker gliding back to where its item starts. + private void OnReset(object? sender, RoutedEventArgs e) => Lid.Reset(); +} diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Saved.axaml b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Saved.axaml new file mode 100644 index 00000000..5c4e21c8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Saved.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Saved.axaml.cs b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Saved.axaml.cs new file mode 100644 index 00000000..ccd82e02 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Demos/Playful/StickerPeel/Saved.axaml.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace TKCode.Arc.Gallery.Demos; + +public partial class StickerPeelSavedDemo : UserControl +{ + private IReadOnlyDictionary? _saved; + + public StickerPeelSavedDemo() + { + InitializeComponent(); + } + + private void OnPositionsChanged(object? sender, StickerPeelPositionsChangedEventArgs e) + { + _saved = e.Positions; + var placed = e.Positions[e.Id]; + Readout.Text = string.Create(CultureInfo.InvariantCulture, + $"Saved {e.Id}: x {placed.X:0.00}, y {placed.Y:0.00}"); + } + + private void OnRestore(object? sender, RoutedEventArgs e) + { + if (_saved is not null) + { + Board.Positions = _saved; + } + } + + private void OnReset(object? sender, RoutedEventArgs e) => Board.Reset(); +} diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/en-US.md new file mode 100644 index 00000000..a6b68751 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/en-US.md @@ -0,0 +1,208 @@ +--- +title: Agent cursor +summary: A live document where an AI agent edits like a collaborator: its labeled cursor selects, rewrites and fixes text while a teammate reads along, and the changes stay tinted until someone accepts or undoes them. +--- + + + +## When to use + +`AgentCursor` shows an AI agent editing a document the way a colleague would. Its labeled cursor comes out of its avatar +in the header, drag-selects a paragraph and streams in a rewrite, adds a heading and fixes a typo, while a teammate's +cursor reads along. Every change stays tinted in the agent's color until someone accepts or undoes it from a floating +review bar. Without data of its own it plays a simulation you give it (in the preview, an invented launch brief, an +agent named after a real model and a teammate), looping. + +- **An AI writing assistant** that edits a document next to people, with Accept and Undo. +- **Landing and onboarding sections** that show a collaborative AI editor in motion. +- **A product surface that streams structured edits** from an agent and needs a human review step. + +## When not to use + +- **Free form chat or questions and answers**: use a chat transcript. +- **A real rich text editor**: use your editor's own collaboration cursors; this card renders read-only blocks. +- **Long documents with many sections**: the card is built for a short excerpt of five to ten blocks. +- **A single text diff**: use a diff view. + +## Usage + +Give the card the document as `Blocks` (title, heading and paragraph blocks with stable ids) and the edits as `Edits`. +Edits address blocks by id: a rewrite replaces a block's text, an insert adds a block after one, and a fix replaces an +exact word inside a block. The card plays the edits in order and stops in review. + +```csharp +card.Blocks = +[ + new AgentCursorBlock("title", AgentCursorBlockKind.Title, "Weekly update"), + new AgentCursorBlock("shipped", AgentCursorBlockKind.Paragraph, "This week the team shipped the new onboarding flow."), +]; +card.Edits = +[ + AgentCursorEdit.Rewrite("shipped", "We shipped the new onboarding flow.", "Making it shorter"), + AgentCursorEdit.Insert("shipped", AgentCursorBlockKind.Heading, "Next week"), +]; +``` + +With no `Blocks`, `Edits`, `Run` and `Simulation` the card shows an empty document; an agent without a name is called +"Agent" (`AgentCursorLabels.Agent`) and nobody reads along without a `Collaborator`. + +### Simulation + +To show the card without an agent (a demo, a design review, an onboarding tour), give it an `AgentCursorSimulation`: the +document, the edits, the agent, a teammate and the meta line. While the card has no `Blocks`, `Edits` or `Run` it plays +the simulation, says that nothing was saved and, with full motion, loops. Your own data wins over the simulation, and +setting `Simulation` to null stops it and shows the empty document. + +```csharp +card.Simulation = new AgentCursorSimulation +{ + Blocks = [new AgentCursorBlock("summary", AgentCursorBlockKind.Paragraph, "Shared inbox is a new place where teams answer email.")], + Edits = [AgentCursorEdit.Rewrite("summary", "Shared inbox lets a team answer email together.", "Tightening the summary")], + Agent = new AgentModel("Claude Opus 5.5", ArcIcons.Sparkles), + Collaborator = new AgentCursorCollaborator("Maya Ortiz"), +}; +``` + +The preview above plays a simulation whose brief, edits, agent and teammate are the gallery's own demo data. + +### Anatomy + +1. **Header**: the agent's avatar (its mark on a tint of its color) with the teammate's avatar beside it, a status line + that says what the agent is doing, and Ask, Stop, Run again or Retry. +2. **Document**: the title, an optional muted `Meta` line, headings and paragraphs. +3. **Agent cursor**: an arrow in the agent's color with its name. It selects, streams and flags typos. +4. **Teammate cursor**: a second arrow with the person's initials and first name, reading along. +5. **Review bar**: floats at the bottom with the count of changes, Undo and Accept changes. + +### Your document and edits + + + +`AutoPlay="False"` keeps the card still until someone presses Ask (or you call `Ask()`). `Agent` names the agent and +gives it a mark and, through a solid `MarkBrush`, the color of its cursor, selection and change tint; without one they +follow the accent. `Collaborator` is the person reading along; set `IsCollaboratorVisible="False"` to hide them. + +Store the document from `Accepted` and pass it back as `Blocks`. The card compares blocks by content, so passing back the +document it just showed changes nothing; a different document replaces the one on screen once no run is using it. + +### Fetching edits from your agent + + + +`Run` takes a function that fetches the edits. The cursor reads the document while it waits. Pressing Stop cancels the +token; a failed task ends in the error state with a Retry button, and its message is announced. + +```csharp +card.Run = async cancellation => +{ + var response = await http.PostAsJsonAsync("/api/agent/edits", document, cancellation); + return await response.Content.ReadFromJsonAsync>(cancellation) ?? []; +}; +``` + +### Review, accept and undo + +Finished edits stay tinted and the review bar counts them. Accept changes clears the tints and raises `Accepted` with the +edited document; Undo puts the text back, closes inserted blocks and raises `Undone`. Stop keeps finished edits for review +and puts back an edit that was half done. `Phase` and `PhaseChanged` follow the run: Idle, Thinking, Editing, Review, +Accepted, Reverted, Stopped, Empty and Error. `Accept()`, `Undo()` and `Stop()` do the same from code. + +A simulation loops: the teammate accepts after a short pause, then the document resets and the run starts over. The pause +never runs out while the pointer is over the card or focus is inside it. Set `Loop` to replay your own run; it never +loops below full motion. + +### Narrow containers + + + +The card is fluid up to 680px. Under 480px the document padding tightens and the review bar drops "by" and the agent's name so it stays +on one line. Text wraps, selections and cursor targets are measured from the live lines, and a cursor's tag moves to the +left of its arrow near the right edge. + +### Your own words + +`Labels` holds every word the card shows or says. Texts with a name or a count are functions: + +```csharp +card.Labels = new AgentCursorLabels +{ + Reading = agent => agent + " lit le document", + Ready = count => count + " modifications prêtes", + Accept = "Accepter", +}; +``` + +### Content guidelines + +- Keep the excerpt short: five to ten blocks. +- Give each edit a `Description` in a few words ("Tightening the summary"); the status line and screen readers use it. +- Fix edits need the exact word to replace inside the block text. + +### Best practices + +- Persist the text from `Accepted` and pass it back as `Blocks`. +- Keep block ids stable between runs so edits find their blocks. +- Use `Speed` for a faster demo rather than shortening edits. + +### Common questions + +**Can people type in the document?** No. The blocks are read-only; wire your editor's own collaboration cursors for +real editing. + +**What happens to an edit whose block or word is missing?** It is skipped. + +## API reference + + + + + + + + + + + + + + + + + +## Accessibility + +- A polite live region announces each step: reading, the current edit, the edits ready, accepted, undone and errors. A + simulated run adds "Simulated, nothing was saved". +- Cursors, selections and tints are decorative and hidden from assistive technology; the document stays real text blocks. +- Status never relies on color: every end state pairs an icon with a label, and pending changes are counted in the review + bar. +- Focus inside the card or a pointer over it holds the looping simulation in review, so nobody loses the bar mid reach. +- Every word is in `AgentCursorLabels`. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves between Ask, Stop, Run again or Retry in the header, then Undo and Accept changes in the review bar | +| Enter, Space | Activates the focused button | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Cursor travel | Glides on a gentle arc with an ease-in-out curve, 280 to 600ms by distance | Jumps | Jumps | +| Cursor appears or leaves | Grows out of the avatar and fades over 200ms | Fades | At once | +| Selection | Fills line by line in step with the drag | Fills line by line | Fills line by line | +| Rewrite | Old text fades out in 110ms, then 3 to 6 words every 34ms, each chunk fading in over 140ms | New text appears whole | New text appears whole | +| Block height | Follows the content on a bounce-free 0.24s spring; inserted blocks grow from zero, then their text fades in | At once | At once | +| Status line | Crossfades, 100ms out and 160ms in, shimmering while the agent works | Short crossfade | At once | +| Review bar | Rises 8px and fades in over 200ms | Fades | At once | +| Loop and teammate drift | Loop after review; the teammate drifts a little while reading | Off | Off | + +Playback pauses while the card is off screen. Cursors move by their render transform, so travel never lays anything out. + +## Related + +- `AgentStatus` for the live status of a whole run. +- `ToolCall` for the tool calls an agent makes. +- `AgentPlan` for the plan an agent writes before it acts. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/zh-CN.md new file mode 100644 index 00000000..ef4d8c32 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/zh-CN.md @@ -0,0 +1,184 @@ +--- +title: Agent cursor +summary: 一份实时文档,AI 智能体像协作者一样在其中编辑:它带名字标签的光标选中、改写并修正文字,同时一位同事的光标跟着阅读,所有改动保持着色,直到有人接受或撤销。 +source-hash: 281ae90d39e9 +--- + + + +## 何时使用 + +`AgentCursor` 展示 AI 智能体像同事一样编辑文档。它带名字标签的光标从页头的头像中出来,拖动选中一个段落并流式写入改写后的文字,添加一个标题,再修正一个拼写错误,同时一位同事的光标跟着阅读。每处改动都以智能体的颜色着色,直到有人在浮动的审阅栏中接受或撤销。没有自己的数据时,它会循环播放你提供的模拟(预览中是一份虚构的发布简报、一个以真实模型命名的智能体和一位同事)。 + +- **AI 写作助手**:与人一起编辑文档,并提供接受与撤销。 +- **落地页和新手引导**:展示一个正在运行的协作式 AI 编辑器。 +- **从智能体流式接收结构化编辑的产品界面**:需要一个人工审阅步骤。 + +## 何时不用 + +- **自由对话或问答**:使用聊天记录组件。 +- **真正的富文本编辑器**:使用编辑器自带的协作光标;这张卡片渲染的是只读的块。 +- **包含很多章节的长文档**:卡片面向五到十个块的简短摘录。 +- **单段文字的差异**:使用差异视图。 + +## 用法 + +把文档作为 `Blocks`(带稳定 id 的标题、小标题和段落块)交给卡片,把编辑作为 `Edits`。编辑按 id 指向块:改写替换一个块的文字,插入在某个块之后添加一个块,修正替换块文字中的一个确切的词。卡片按顺序播放这些编辑,并停在审阅状态。 + +```csharp +card.Blocks = +[ + new AgentCursorBlock("title", AgentCursorBlockKind.Title, "Weekly update"), + new AgentCursorBlock("shipped", AgentCursorBlockKind.Paragraph, "This week the team shipped the new onboarding flow."), +]; +card.Edits = +[ + AgentCursorEdit.Rewrite("shipped", "We shipped the new onboarding flow.", "Making it shorter"), + AgentCursorEdit.Insert("shipped", AgentCursorBlockKind.Heading, "Next week"), +]; +``` + +既没有 `Blocks`、`Edits`、`Run` 也没有 `Simulation` 时,卡片显示一份空文档;没有名称的智能体称为 "Agent"(`AgentCursorLabels.Agent`),没有 `Collaborator` 时也没有人跟着阅读。 + +### 模拟 + +想在没有智能体的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `AgentCursorSimulation`:文档、编辑、智能体、一位同事和元信息行。卡片没有 `Blocks`、`Edits` 或 `Run` 时会播放这个模拟,说明什么都没有保存,并在动效为完整时循环。你自己的数据优先于模拟;把 `Simulation` 设为 null 会停止模拟并显示空文档。 + +```csharp +card.Simulation = new AgentCursorSimulation +{ + Blocks = [new AgentCursorBlock("summary", AgentCursorBlockKind.Paragraph, "Shared inbox is a new place where teams answer email.")], + Edits = [AgentCursorEdit.Rewrite("summary", "Shared inbox lets a team answer email together.", "Tightening the summary")], + Agent = new AgentModel("Claude Opus 5.5", ArcIcons.Sparkles), + Collaborator = new AgentCursorCollaborator("Maya Ortiz"), +}; +``` + +上方的预览播放的就是一个模拟,其简报、编辑、智能体和同事来自图库自己的演示数据。 + +### 结构 + +1. **页头**:智能体的头像(以其颜色为底的标记),旁边是同事的头像;一行状态说明智能体正在做什么;以及 Ask、Stop、Run again 或 Retry 按钮。 +2. **文档**:标题、可选的弱化 `Meta` 行、小标题和段落。 +3. **智能体光标**:智能体颜色的箭头和它的名字。它负责选中、流式写入和标记拼写错误。 +4. **同事光标**:第二个箭头,带有这个人的姓名首字母和名字,跟着阅读。 +5. **审阅栏**:浮在底部,显示改动数量、Undo 和 Accept changes。 + +### 你自己的文档和编辑 + + + +`AutoPlay="False"` 让卡片保持静止,直到有人按下 Ask(或你调用 `Ask()`)。`Agent` 为智能体命名,并给它一个标记;通过纯色的 `MarkBrush` 还决定其光标、选区和改动着色的颜色,没有时它们跟随强调色。`Collaborator` 是跟着阅读的人;设置 `IsCollaboratorVisible="False"` 可以隐藏他。 + +从 `Accepted` 中保存文档,再作为 `Blocks` 传回。卡片按内容比较块,所以传回它刚显示的文档不会有任何变化;不同的文档会在没有运行使用它时替换屏幕上的文档。 + +### 从你的智能体获取编辑 + + + +`Run` 接受一个获取编辑的函数。等待期间,光标会阅读文档。按下 Stop 会取消令牌;失败的任务会以错误状态结束并显示 Retry 按钮,其消息会被播报。 + +```csharp +card.Run = async cancellation => +{ + var response = await http.PostAsJsonAsync("/api/agent/edits", document, cancellation); + return await response.Content.ReadFromJsonAsync>(cancellation) ?? []; +}; +``` + +### 审阅、接受与撤销 + +完成的编辑保持着色,审阅栏会统计它们。Accept changes 清除着色,并以编辑后的文档引发 `Accepted`;Undo 恢复文字、收起插入的块,并引发 `Undone`。Stop 会保留已完成的编辑以供审阅,并恢复进行到一半的编辑。`Phase` 和 `PhaseChanged` 跟随运行过程:Idle、Thinking、Editing、Review、Accepted、Reverted、Stopped、Empty 和 Error。`Accept()`、`Undo()` 和 `Stop()` 在代码中完成同样的操作。 + +模拟会循环:同事在短暂停顿后接受,然后文档重置,运行重新开始。指针停在卡片上或焦点在卡片内时,这段停顿不会结束。设置 `Loop` 可以重放你自己的运行;低于完整动效时从不循环。 + +### 窄容器 + + + +卡片在 680px 以内随容器伸缩。低于 480px 时,文档内边距收紧,审阅栏去掉 "by" 和智能体的名称,以保持在一行内。文字会换行,选区和光标目标按实时的行来测量,靠近右边缘时光标的标签会移到箭头左侧。 + +### 你自己的措辞 + +`Labels` 保存卡片显示或朗读的每一个词。包含名字或数量的文字是函数: + +```csharp +card.Labels = new AgentCursorLabels +{ + Reading = agent => agent + " lit le document", + Ready = count => count + " modifications prêtes", + Accept = "Accepter", +}; +``` + +### 内容准则 + +- 摘录保持简短:五到十个块。 +- 用几个词为每个编辑写一个 `Description`("Tightening the summary");状态行和屏幕阅读器会用到它。 +- 修正编辑需要块文字中要替换的确切的词。 + +### 最佳实践 + +- 保存来自 `Accepted` 的文字,并作为 `Blocks` 传回。 +- 在多次运行之间保持块 id 稳定,以便编辑找到它们的块。 +- 想让演示更快时使用 `Speed`,而不是缩短编辑。 + +### 常见问题 + +**用户可以在文档中输入吗?** 不可以。这些块是只读的;真正的编辑请使用编辑器自带的协作光标。 + +**如果编辑指向的块或词不存在会怎样?** 该编辑会被跳过。 + +## API 参考 + + + + + + + + + + + + + + + + + +## 无障碍 + +- 一个礼貌级别的实时区域播报每一步:阅读、当前编辑、编辑就绪、已接受、已撤销以及错误。模拟运行会附加 "Simulated, nothing was saved"。 +- 光标、选区和着色是装饰性的,对辅助技术隐藏;文档仍然是真实的文字块。 +- 状态从不只依赖颜色:每个结束状态都同时带有图标和文字,待审阅的改动在审阅栏中计数。 +- 焦点在卡片内或指针停在卡片上时,循环播放的模拟会停在审阅状态,这样没人会在伸手去点时失去审阅栏。 +- 每个词都在 `AgentCursorLabels` 中。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 在页头的 Ask、Stop、Run again 或 Retry 之间移动,然后到审阅栏中的 Undo 和 Accept changes | +| Enter、Space | 激活获得焦点的按钮 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 光标移动 | 沿一条平缓的弧线以缓入缓出曲线滑动,按距离 280 到 600ms | 直接跳到目标 | 直接跳到目标 | +| 光标出现或离开 | 从头像中长出并在 200ms 内淡入淡出 | 淡入淡出 | 立即 | +| 选区 | 随拖动逐行填充 | 逐行填充 | 逐行填充 | +| 改写 | 旧文字在 110ms 内淡出,然后每 34ms 写入 3 到 6 个词,每段在 140ms 内淡入 | 新文字整体出现 | 新文字整体出现 | +| 块高度 | 以无回弹的 0.24s 弹簧跟随内容;插入的块从零高度展开,然后文字淡入 | 立即 | 立即 | +| 状态行 | 交叉淡化,100ms 淡出、160ms 淡入,智能体工作时带微光 | 短暂交叉淡化 | 立即 | +| 审阅栏 | 上升 8px 并在 200ms 内淡入 | 淡入 | 立即 | +| 循环与同事漂移 | 审阅后循环;同事阅读时轻微漂移 | 关闭 | 关闭 | + +卡片不在屏幕上时,播放会暂停。光标通过渲染变换移动,因此移动从不触发布局。 + +## 相关组件 + +- `AgentStatus`:显示整个运行的实时状态。 +- `ToolCall`:显示智能体发起的工具调用。 +- `AgentPlan`:显示智能体行动前写下的计划。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/zh-TW.md new file mode 100644 index 00000000..4650d051 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentCursor/zh-TW.md @@ -0,0 +1,184 @@ +--- +title: Agent cursor +summary: 一份即時文件,AI 代理像協作者一樣在其中編輯:它帶名字標籤的游標選取、改寫並修正文字,同時一位同事的游標跟著閱讀,所有變更保持著色,直到有人接受或復原。 +source-hash: 281ae90d39e9 +--- + + + +## 何時使用 + +`AgentCursor` 展示 AI 代理像同事一樣編輯文件。它帶名字標籤的游標從頁首的大頭貼中出來,拖曳選取一個段落並串流寫入改寫後的文字,新增一個標題,再修正一個錯字,同時一位同事的游標跟著閱讀。每處變更都以代理的顏色著色,直到有人在浮動的審閱列中接受或復原。沒有自己的資料時,它會循環播放你提供的模擬(預覽中是一份虛構的發佈簡報、一個以真實模型命名的代理和一位同事)。 + +- **AI 寫作助理**:與人一起編輯文件,並提供接受與復原。 +- **到達頁和新手導覽**:展示一個正在運作的協作式 AI 編輯器。 +- **從代理串流接收結構化編輯的產品介面**:需要一個人工審閱步驟。 + +## 何時不用 + +- **自由對話或問答**:使用聊天紀錄元件。 +- **真正的 RTF 編輯器**:使用編輯器內建的協作游標;這張卡片呈現的是唯讀的區塊。 +- **包含許多章節的長文件**:卡片針對五到十個區塊的簡短摘錄。 +- **單段文字的差異**:使用差異檢視。 + +## 用法 + +把文件以 `Blocks`(帶穩定 id 的標題、小標題和段落區塊)交給卡片,把編輯以 `Edits` 交給它。編輯以 id 指向區塊:改寫取代一個區塊的文字,插入在某個區塊之後新增一個區塊,修正取代區塊文字中一個確切的字詞。卡片依序播放這些編輯,並停在審閱狀態。 + +```csharp +card.Blocks = +[ + new AgentCursorBlock("title", AgentCursorBlockKind.Title, "Weekly update"), + new AgentCursorBlock("shipped", AgentCursorBlockKind.Paragraph, "This week the team shipped the new onboarding flow."), +]; +card.Edits = +[ + AgentCursorEdit.Rewrite("shipped", "We shipped the new onboarding flow.", "Making it shorter"), + AgentCursorEdit.Insert("shipped", AgentCursorBlockKind.Heading, "Next week"), +]; +``` + +既沒有 `Blocks`、`Edits`、`Run` 也沒有 `Simulation` 時,卡片顯示一份空白文件;沒有名稱的代理稱為 "Agent"(`AgentCursorLabels.Agent`),沒有 `Collaborator` 時也沒有人跟著閱讀。 + +### 模擬 + +想在沒有代理的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `AgentCursorSimulation`:文件、編輯、代理、一位同事和中繼資訊行。卡片沒有 `Blocks`、`Edits` 或 `Run` 時會播放這個模擬,說明什麼都沒有儲存,並在動態效果為完整時循環。你自己的資料優先於模擬;把 `Simulation` 設為 null 會停止模擬並顯示空白文件。 + +```csharp +card.Simulation = new AgentCursorSimulation +{ + Blocks = [new AgentCursorBlock("summary", AgentCursorBlockKind.Paragraph, "Shared inbox is a new place where teams answer email.")], + Edits = [AgentCursorEdit.Rewrite("summary", "Shared inbox lets a team answer email together.", "Tightening the summary")], + Agent = new AgentModel("Claude Opus 5.5", ArcIcons.Sparkles), + Collaborator = new AgentCursorCollaborator("Maya Ortiz"), +}; +``` + +上方的預覽播放的就是一個模擬,其簡報、編輯、代理和同事來自圖庫自己的示範資料。 + +### 結構 + +1. **頁首**:代理的大頭貼(以其顏色為底的標記),旁邊是同事的大頭貼;一行狀態說明代理正在做什麼;以及 Ask、Stop、Run again 或 Retry 按鈕。 +2. **文件**:標題、選用的淡化 `Meta` 行、小標題和段落。 +3. **代理游標**:代理顏色的箭頭和它的名字。它負責選取、串流寫入和標示錯字。 +4. **同事游標**:第二個箭頭,帶有這個人的姓名縮寫和名字,跟著閱讀。 +5. **審閱列**:浮在底部,顯示變更數量、Undo 和 Accept changes。 + +### 你自己的文件和編輯 + + + +`AutoPlay="False"` 讓卡片保持靜止,直到有人按下 Ask(或你呼叫 `Ask()`)。`Agent` 為代理命名並給它一個標記;透過純色的 `MarkBrush` 也決定其游標、選取範圍和變更著色的顏色,沒有時它們跟隨強調色。`Collaborator` 是跟著閱讀的人;設定 `IsCollaboratorVisible="False"` 可以隱藏他。 + +從 `Accepted` 儲存文件,再以 `Blocks` 傳回。卡片依內容比較區塊,所以傳回它剛顯示的文件不會有任何改變;不同的文件會在沒有執行使用它時取代畫面上的文件。 + +### 從你的代理取得編輯 + + + +`Run` 接受一個取得編輯的函式。等待期間,游標會閱讀文件。按下 Stop 會取消權杖;失敗的工作會以錯誤狀態結束並顯示 Retry 按鈕,其訊息會被播報。 + +```csharp +card.Run = async cancellation => +{ + var response = await http.PostAsJsonAsync("/api/agent/edits", document, cancellation); + return await response.Content.ReadFromJsonAsync>(cancellation) ?? []; +}; +``` + +### 審閱、接受與復原 + +完成的編輯保持著色,審閱列會計算它們。Accept changes 清除著色,並以編輯後的文件引發 `Accepted`;Undo 還原文字、收合插入的區塊,並引發 `Undone`。Stop 會保留已完成的編輯以供審閱,並還原進行到一半的編輯。`Phase` 和 `PhaseChanged` 跟隨執行過程:Idle、Thinking、Editing、Review、Accepted、Reverted、Stopped、Empty 和 Error。`Accept()`、`Undo()` 和 `Stop()` 在程式碼中完成同樣的操作。 + +模擬會循環:同事在短暫停頓後接受,然後文件重設,執行重新開始。指標停在卡片上或焦點在卡片內時,這段停頓不會結束。設定 `Loop` 可以重播你自己的執行;低於完整動態效果時從不循環。 + +### 窄容器 + + + +卡片在 680px 以內隨容器伸縮。低於 480px 時,文件內距收緊,審閱列去掉 "by" 和代理的名稱,以保持在一行內。文字會換行,選取範圍和游標目標依即時的行來量測,靠近右邊緣時游標的標籤會移到箭頭左側。 + +### 你自己的用語 + +`Labels` 保存卡片顯示或朗讀的每一個字詞。包含名字或數量的文字是函式: + +```csharp +card.Labels = new AgentCursorLabels +{ + Reading = agent => agent + " lit le document", + Ready = count => count + " modifications prêtes", + Accept = "Accepter", +}; +``` + +### 內容準則 + +- 摘錄保持簡短:五到十個區塊。 +- 用幾個字為每個編輯寫一個 `Description`("Tightening the summary");狀態行和螢幕閱讀器會用到它。 +- 修正編輯需要區塊文字中要取代的確切字詞。 + +### 最佳做法 + +- 儲存來自 `Accepted` 的文字,並以 `Blocks` 傳回。 +- 在多次執行之間保持區塊 id 穩定,讓編輯找到它們的區塊。 +- 想讓示範更快時使用 `Speed`,而不是縮短編輯。 + +### 常見問題 + +**使用者可以在文件中輸入嗎?** 不行。這些區塊是唯讀的;真正的編輯請使用編輯器內建的協作游標。 + +**如果編輯指向的區塊或字詞不存在會怎樣?** 該編輯會被略過。 + +## API 參考 + + + + + + + + + + + + + + + + + +## 無障礙 + +- 一個禮貌層級的即時區域播報每一步:閱讀、目前的編輯、編輯就緒、已接受、已復原以及錯誤。模擬執行會附加 "Simulated, nothing was saved"。 +- 游標、選取範圍和著色是裝飾性的,對輔助科技隱藏;文件仍然是真實的文字區塊。 +- 狀態從不只依賴顏色:每個結束狀態都同時帶有圖示和文字,待審閱的變更在審閱列中計數。 +- 焦點在卡片內或指標停在卡片上時,循環播放的模擬會停在審閱狀態,這樣沒人會在伸手去點時失去審閱列。 +- 每個字詞都在 `AgentCursorLabels` 中。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 在頁首的 Ask、Stop、Run again 或 Retry 之間移動,然後到審閱列中的 Undo 和 Accept changes | +| Enter、Space | 啟用取得焦點的按鈕 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 游標移動 | 沿一條平緩的弧線以緩入緩出曲線滑動,依距離 280 到 600ms | 直接跳到目標 | 直接跳到目標 | +| 游標出現或離開 | 從大頭貼中長出並在 200ms 內淡入淡出 | 淡入淡出 | 立即 | +| 選取範圍 | 隨拖曳逐行填滿 | 逐行填滿 | 逐行填滿 | +| 改寫 | 舊文字在 110ms 內淡出,然後每 34ms 寫入 3 到 6 個字詞,每段在 140ms 內淡入 | 新文字整體出現 | 新文字整體出現 | +| 區塊高度 | 以無回彈的 0.24s 彈簧跟隨內容;插入的區塊從零高度展開,然後文字淡入 | 立即 | 立即 | +| 狀態行 | 交叉淡化,100ms 淡出、160ms 淡入,代理工作時帶微光 | 短暫交叉淡化 | 立即 | +| 審閱列 | 上升 8px 並在 200ms 內淡入 | 淡入 | 立即 | +| 循環與同事漂移 | 審閱後循環;同事閱讀時輕微漂移 | 關閉 | 關閉 | + +卡片不在畫面上時,播放會暫停。游標透過轉譯變換移動,因此移動從不觸發版面配置。 + +## 相關元件 + +- `AgentStatus`:顯示整個執行的即時狀態。 +- `ToolCall`:顯示代理發起的工具呼叫。 +- `AgentPlan`:顯示代理行動前寫下的計畫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/en-US.md new file mode 100644 index 00000000..98f66364 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/en-US.md @@ -0,0 +1,206 @@ +--- +title: Agent handoff +summary: A support chat where an AI assistant hands off to a person: a card shows the summary it passes to the team and the typical wait, then the person joins, with a thin timeline marking each step. +--- + + + +## When to use + +`AgentHandoff` is a support conversation where an AI assistant answers first and hands off to a person. When it +transfers, a card in the thread shows the summary it passes to the team and the typical wait. The person then joins and +takes over, and a thin timeline on the left marks each step: the assistant, the handoff, the person joining and the +resolution. + +- **A support widget** where the AI answers first and escalates to a human agent. +- **Showing customers what the assistant shared** with the team, so they do not repeat themselves. +- **Marketing or docs pages** that explain an AI to human handoff flow. + +## When not to use + +- **A general chat with one assistant** and no handoff: use `ChatThread`. +- **An agent's internal steps or tool calls**: use `AgentTimeline` or `ToolCall`. +- **A full help desk inbox for agents**: build it from list and detail layouts instead. + +## Usage + +With no `Stage` and no simulation the card is an empty chat: the header, the assistant's step and nothing else. Wire +it to your support backend by setting `Stage` and the data, and answering the events: + +```csharp +chat.Title = "Order NW-48213"; +chat.Subtitle = "Northwind Home support"; +chat.Summary = new AgentHandoffSummary( + "Desk arrived with a cracked leg. Customer wants a full refund.", + new AgentHandoffField("Order", "NW-48213"), + new AgentHandoffField("Asks for", "Full refund")); +chat.Agent = new AgentHandoffAgent("Jordan Reyes", "Support specialist", "14:05"); +chat.ShowsComposer = true; +chat.AllowsRequestHuman = true; +chat.SendRequested += (_, e) => backend.Send(e.Text); +chat.HumanRequested += (_, _) => backend.Escalate(); + +// Then follow the backend. +chat.Stage = conversation.Stage; +``` + +Keep the assistant's messages in `Messages` and everything after the handoff card in `AgentMessages`: notes the customer +adds while waiting and the conversation with the person. The join step is placed before the person's first message. + +### Stages + + + +Drive `Stage` from your backend: `Transferring` when you create the ticket, `Connected` when a person accepts, +`Resolved` when they close it and `Unavailable` when the queue times out. + +| Stage | Header status | Handoff card | +|---|---|---| +| `Assistant` | A sparkle and "AI assistant" | Not shown; Talk to a person can sit under the last answer | +| `Transferring` | A spinner and "Transferring" | "Transferring you to a person", the typical wait (`ReplyEstimate`), Cancel | +| `Connected` | A person and "With Jordan" | "Jordan joined from Help desk", the person, the summary | +| `Resolved` | A check and "Resolved" | "Resolved by Jordan"; a last step closes the timeline | +| `Unavailable` | An alert and "No one available" | "No one is available right now", Try again | + +### Actions and the composer + +Each affordance shows when you turn it on, and raises a bubbling routed event: + +- `AllowsRequestHuman`: a Talk to a person reply under the assistant's last finished answer, raising `HumanRequested`. +- `AllowsCancelTransfer`: Cancel on the card while transferring, raising `CancelTransferRequested`. +- `AllowsRetry`: Try again on the card when nobody is available, raising `RetryRequested`. +- `ShowsComposer`: the composer. Enter or the send button raises `SendRequested` with the trimmed text; append the + message to `Messages` or `AgentMessages` yourself. The placeholder follows the stage ("Message Jordan") unless you set + `Placeholder`. + +### Streaming and typing + +Stream assistant text by updating the message's `Text` with `IsStreaming` set; the card fades each appended chunk in. +Set `Typing` to `Assistant` for a short shimmering Thinking line, or to `Agent` for typing dots. + +```csharp +var reply = new AgentHandoffMessage(AgentHandoffSender.Assistant, string.Empty) { IsStreaming = true }; +chat.Messages.Add(reply); +await foreach (var chunk in stream) +{ + reply.Text += chunk; +} +reply.IsStreaming = false; +``` + +### Narrow containers + + + +The card fills its container up to 560px and holds up at 360px with tighter padding and wider bubbles. It has a fixed +height (600 by default; set `Height`) and the thread scrolls inside, pinned to the newest message, so the page never +shifts as the thread grows. Long names and meta lines truncate with an ellipsis; message text wraps. + +### Marks and avatars + +`AssistantMark` and `ProviderMark` take a 24 × 24 stroked glyph (a Lucide icon or your own path) for the assistant on +the first step and the help desk next to the handoff card; `AssistantMarkBrush` and `ProviderMarkBrush` color them. The +person's avatar shows their initials, or the photo in `AgentHandoffAgent.Avatar`. + +### The simulation + +`AgentHandoffSimulation` plays a scripted chat for demos and prototypes, as in the preview above. Give it the words in +an `AgentHandoffScript` (a line left null is skipped) and set it as the card's `Simulation`: the card plays it while +`Stage` is unset, from the top each time it is shown, with the composer and the Talk to a person, Cancel and Try again +actions working. `Scenario="Unavailable"` stops when nobody picks the transfer up, so Try again shows; changing the +scenario starts the story over. It exposes `Stage`, `Messages`, `AgentMessages`, `Typing`, `Summary`, `Agent` and +`Notice`, and takes `RequestHuman`, `CancelTransfer`, `Retry` and `Send`. Setting `Stage` hands the card back to your +data. Do not use it outside demos. + +```xml + + + + + +``` + +```csharp +chat.Simulation = new AgentHandoffSimulation +{ + Script = new AgentHandoffScript + { + CustomerOpening = "My order arrived damaged.", + AssistantAnswer = "Sorry about that. I can send a replacement part.", + CustomerRequest = "Can I talk to a person?", + AssistantHandoff = "Of course, I'll pass this to our team with a summary.", + Summary = new AgentHandoffSummary("Order arrived damaged; wants a refund."), + Agent = new AgentHandoffAgent("Sam Lee", "Support specialist"), + AgentGreeting = "Hi, I'm Sam. I've read the summary.", + }, +}; +``` + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +chat.Labels = new AgentHandoffLabels +{ + Transferring = "Übergabe", + TalkToPerson = "Mit einer Person sprechen", + With = name => "Mit " + name, +}; +``` + +### Best practices + +- Pass `Summary` with the same facts you send to the help desk, so the customer sees what was shared. +- For very long threads pass only the recent messages; the card is meant for a live conversation, not an archive. +- Use `Notice` for a one-off result such as "Refund issued"; it is announced once. + +## API reference + + + + + + + + + + + +## Accessibility + +- The thread is a focusable log named "Conversation", so keyboard users can scroll it; it does not announce every + streamed chunk. +- Stage changes and `Notice` are announced once through a polite status region, including that the demo is simulated. +- Every status pairs an icon with a label: Transferring, With Jordan, Resolved, No one available. Color is never the + only signal. +- The summary toggle reports expanded or collapsed. The typing dots are named, such as "Jordan is typing". +- Marks and avatars on the timeline are decorative next to their visible labels. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the thread, Talk to a person, the card's action, the summary toggle and the composer | +| Up, Down, Page Up, Page Down, Home, End | Scroll the focused thread | +| Enter | Sends the composer message, or presses the focused button | +| Space | Toggles the summary when the summary button has focus | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Assistant reply streams | Chunks of 3 to 8 words about every 34ms; each chunk only fades in over 140ms | The reply appears whole | The reply appears whole | +| A row arrives or a line wraps | The thread follows its measured height on a bounce-free 0.24s spring, pushing up without a snap | At once | At once | +| Transferring | A small accent dot travels down the timeline beside the handoff card | No travel dot | No travel dot | +| Status labels change | Old label fades out over 100ms, new label fades in over 160ms and rises 2px | Short crossfade | At once | +| Typing dots, spinner | Breathe and turn | Hold still | Hold still | +| The simulated chat | Plays and starts over | Plays once | Plays once | + +Loops pause off screen. + +## Related + +- `ChatThread` for a chat with one assistant or between people. +- `AgentTimeline` for the steps of a multi-agent run. +- `ToolCall` for the tool calls an agent makes. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/zh-CN.md new file mode 100644 index 00000000..70e7c267 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/zh-CN.md @@ -0,0 +1,186 @@ +--- +title: Agent handoff +summary: AI 助手把对话转交给真人的客服聊天:一张卡片显示它交给团队的摘要和通常的等待时间,随后真人加入,左侧一条细时间线标出每一步。 +source-hash: a81b332d51fd +--- + + + +## 何时使用 + +`AgentHandoff` 是一段客服对话:AI 助手先回答,再把对话转交给真人。转接时,对话里的一张卡片会显示助手交给团队的摘要和通常的等待时间。随后真人加入并接手,左侧一条细时间线标出每一步:助手、转接、真人加入和问题解决。 + +- **客服小组件**:AI 先回答,需要时升级给人工客服。 +- **向客户展示助手交给团队的内容**,让他们不必重复说明。 +- **营销或文档页面**,讲解从 AI 到真人的转接流程。 + +## 何时不用 + +- **只有一个助手、没有转接的普通聊天**:用 `ChatThread`。 +- **展示智能体的内部步骤或工具调用**:用 `AgentTimeline` 或 `ToolCall`。 +- **给客服人员用的完整工单收件箱**:用列表和详情布局搭建。 + +## 用法 + +既没有 `Stage` 也没有模拟时,卡片是一个空对话:只有标题栏和助手的步骤。要接入你的客服后端,设置 `Stage` 和数据,并处理事件: + +```csharp +chat.Title = "Order NW-48213"; +chat.Subtitle = "Northwind Home support"; +chat.Summary = new AgentHandoffSummary( + "Desk arrived with a cracked leg. Customer wants a full refund.", + new AgentHandoffField("Order", "NW-48213"), + new AgentHandoffField("Asks for", "Full refund")); +chat.Agent = new AgentHandoffAgent("Jordan Reyes", "Support specialist", "14:05"); +chat.ShowsComposer = true; +chat.AllowsRequestHuman = true; +chat.SendRequested += (_, e) => backend.Send(e.Text); +chat.HumanRequested += (_, _) => backend.Escalate(); + +// Then follow the backend. +chat.Stage = conversation.Stage; +``` + +助手的消息放在 `Messages` 里,转接卡片之后的一切放在 `AgentMessages` 里:客户等待时补充的说明,以及与真人的对话。加入步骤会放在真人的第一条消息之前。 + +### 阶段 + + + +由后端驱动 `Stage`:创建工单时设为 `Transferring`,有人接手时设为 `Connected`,对方关闭对话时设为 `Resolved`,排队超时设为 `Unavailable`。 + +| 阶段 | 标题栏状态 | 转接卡片 | +|---|---|---| +| `Assistant` | 星芒图标和“AI assistant” | 不显示;最后一条回答下方可以出现 Talk to a person | +| `Transferring` | 加载圈和“Transferring” | “Transferring you to a person”、通常的等待时间(`ReplyEstimate`)、Cancel | +| `Connected` | 人像图标和“With Jordan” | “Jordan joined from Help desk”、接手的人、摘要 | +| `Resolved` | 对勾和“Resolved” | “Resolved by Jordan”;最后一步收尾时间线 | +| `Unavailable` | 警示图标和“No one available” | “No one is available right now”、Try again | + +### 操作与输入框 + +每个操作在你打开对应属性后才显示,并触发一个冒泡的路由事件: + +- `AllowsRequestHuman`:在助手最后一条已完成的回答下方显示 Talk to a person 快捷回复,触发 `HumanRequested`。 +- `AllowsCancelTransfer`:转接中在卡片上显示 Cancel,触发 `CancelTransferRequested`。 +- `AllowsRetry`:无人接手时在卡片上显示 Try again,触发 `RetryRequested`。 +- `ShowsComposer`:显示输入框。按 Enter 或发送按钮会触发 `SendRequested`,携带去掉首尾空白的文本;消息要由你自己追加到 `Messages` 或 `AgentMessages`。占位文字随阶段变化(“Message Jordan”),除非你设置了 `Placeholder`。 + +### 流式输出与正在输入 + +要流式输出助手的文字,在设置了 `IsStreaming` 的消息上更新 `Text`;卡片会让每次追加的片段淡入。把 `Typing` 设为 `Assistant` 会显示一行简短的闪光 Thinking,设为 `Agent` 会显示正在输入的圆点。 + +```csharp +var reply = new AgentHandoffMessage(AgentHandoffSender.Assistant, string.Empty) { IsStreaming = true }; +chat.Messages.Add(reply); +await foreach (var chunk in stream) +{ + reply.Text += chunk; +} +reply.IsStreaming = false; +``` + +### 窄容器 + + + +卡片填满容器,最宽 560px;在 360px 宽时依然可用,内边距更紧凑,气泡更宽。卡片高度固定(默认 600,可设置 `Height`),对话在内部滚动并固定在最新消息处,所以对话变长时页面不会跳动。过长的名字和元信息行以省略号截断,消息文字会换行。 + +### 标识与头像 + +`AssistantMark` 和 `ProviderMark` 接受一个 24 × 24 的描边图形(Lucide 图标或你自己的路径),分别用于第一步的助手和转接卡片旁的客服系统;`AssistantMarkBrush` 和 `ProviderMarkBrush` 设置它们的颜色。接手的人的头像显示姓名首字母,或 `AgentHandoffAgent.Avatar` 中的照片。 + +### 模拟 + +`AgentHandoffSimulation` 为演示和原型播放一段按剧本进行的对话,就像上面的预览。把台词放进 `AgentHandoffScript`(留空为 null 的台词会被跳过),再把它设为卡片的 `Simulation`:卡片在未设置 `Stage` 时播放它,每次显示时从头开始,输入框以及 Talk to a person、Cancel 和 Try again 操作都能使用。`Scenario="Unavailable"` 会停在无人接手的时候,于是显示 Try again;切换场景会让故事从头开始。它公开 `Stage`、`Messages`、`AgentMessages`、`Typing`、`Summary`、`Agent` 和 `Notice`,并接受 `RequestHuman`、`CancelTransfer`、`Retry` 和 `Send`。设置 `Stage` 后,卡片就回到你的数据。不要在演示之外使用它。 + +```xml + + + + + +``` + +```csharp +chat.Simulation = new AgentHandoffSimulation +{ + Script = new AgentHandoffScript + { + CustomerOpening = "My order arrived damaged.", + AssistantAnswer = "Sorry about that. I can send a replacement part.", + CustomerRequest = "Can I talk to a person?", + AssistantHandoff = "Of course, I'll pass this to our team with a summary.", + Summary = new AgentHandoffSummary("Order arrived damaged; wants a refund."), + Agent = new AgentHandoffAgent("Sam Lee", "Support specialist"), + AgentGreeting = "Hi, I'm Sam. I've read the summary.", + }, +}; +``` + +### 你自己的文案 + +`Labels` 包含卡片显示或朗读的每一个词。替换整个对象即可本地化或改写: + +```csharp +chat.Labels = new AgentHandoffLabels +{ + Transferring = "Übergabe", + TalkToPerson = "Mit einer Person sprechen", + With = name => "Mit " + name, +}; +``` + +### 最佳实践 + +- `Summary` 里放你发给客服系统的同样事实,让客户看到分享了什么。 +- 对话很长时只传最近的消息;这张卡片用于进行中的对话,不是存档。 +- 一次性的结果(例如 “Refund issued”)用 `Notice`,它只会被朗读一次。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 对话区是一个名为 “Conversation” 的可聚焦日志,键盘用户可以滚动它;它不会朗读每一个流式片段。 +- 阶段变化和 `Notice` 通过礼貌的状态区域朗读一次,其中包括演示是模拟的这一点。 +- 每个状态都由图标和文字搭配:Transferring、With Jordan、Resolved、No one available。颜色从来不是唯一的信号。 +- 摘要切换按钮会报告展开或收起。正在输入的圆点有名称,例如 “Jordan is typing”。 +- 时间线上的标识和头像在可见文字旁只起装饰作用。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次移到对话区、Talk to a person、卡片上的操作、摘要切换按钮和输入框 | +| Up、Down、Page Up、Page Down、Home、End | 滚动获得焦点的对话区 | +| Enter | 发送输入框里的消息,或按下获得焦点的按钮 | +| Space | 摘要按钮获得焦点时展开或收起摘要 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 助手回复流式输出 | 约每 34ms 一段,每段 3 到 8 个词;每段只做 140ms 的淡入 | 回复整段出现 | 回复整段出现 | +| 新行到来或文字换行 | 对话区以无回弹的 0.24s 弹簧跟随测得的高度,向上推开而不跳动 | 立即 | 立即 | +| 转接中 | 一个强调色小圆点沿转接卡片旁的时间线向下移动 | 没有移动的圆点 | 没有移动的圆点 | +| 状态文字变化 | 旧文字 100ms 淡出,新文字 160ms 淡入并上升 2px | 短暂交叉淡化 | 立即 | +| 正在输入的圆点、加载圈 | 呼吸、旋转 | 保持静止 | 保持静止 | +| 模拟对话 | 播放并循环 | 只播放一次 | 只播放一次 | + +离开屏幕时循环暂停。 + +## 相关组件 + +- `ChatThread`:与单个助手或真人之间的聊天。 +- `AgentTimeline`:多智能体运行的各个步骤。 +- `ToolCall`:智能体发起的工具调用。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/zh-TW.md new file mode 100644 index 00000000..d3755c7b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentHandoff/zh-TW.md @@ -0,0 +1,186 @@ +--- +title: Agent handoff +summary: AI 助理把對話轉交給真人的客服聊天:一張卡片顯示它交給團隊的摘要與一般的等候時間,接著真人加入,左側一條細時間軸標出每一步。 +source-hash: a81b332d51fd +--- + + + +## 何時使用 + +`AgentHandoff` 是一段客服對話:AI 助理先回答,再把對話轉交給真人。轉接時,對話裡的一張卡片會顯示助理交給團隊的摘要與一般的等候時間。接著真人加入並接手,左側一條細時間軸標出每一步:助理、轉接、真人加入與問題解決。 + +- **客服小工具**:AI 先回答,需要時升級給真人客服。 +- **讓客戶看到助理交給團隊的內容**,不必再說一次。 +- **行銷或文件頁面**,說明從 AI 到真人的轉接流程。 + +## 何時不用 + +- **只有一個助理、沒有轉接的一般聊天**:用 `ChatThread`。 +- **呈現代理的內部步驟或工具呼叫**:用 `AgentTimeline` 或 `ToolCall`。 +- **給客服人員用的完整工單收件匣**:用清單與詳細資料版面組合。 + +## 用法 + +既沒有 `Stage` 也沒有模擬時,卡片是一個空對話:只有標題列和助理的步驟。要接上你的客服後端,設定 `Stage` 與資料,並處理事件: + +```csharp +chat.Title = "Order NW-48213"; +chat.Subtitle = "Northwind Home support"; +chat.Summary = new AgentHandoffSummary( + "Desk arrived with a cracked leg. Customer wants a full refund.", + new AgentHandoffField("Order", "NW-48213"), + new AgentHandoffField("Asks for", "Full refund")); +chat.Agent = new AgentHandoffAgent("Jordan Reyes", "Support specialist", "14:05"); +chat.ShowsComposer = true; +chat.AllowsRequestHuman = true; +chat.SendRequested += (_, e) => backend.Send(e.Text); +chat.HumanRequested += (_, _) => backend.Escalate(); + +// Then follow the backend. +chat.Stage = conversation.Stage; +``` + +助理的訊息放在 `Messages`,轉接卡片之後的一切放在 `AgentMessages`:客戶等候時補充的說明,以及與真人的對話。加入步驟會放在真人的第一則訊息之前。 + +### 階段 + + + +由後端驅動 `Stage`:建立工單時設為 `Transferring`,有人接手時設為 `Connected`,對方結束對話時設為 `Resolved`,排隊逾時設為 `Unavailable`。 + +| 階段 | 標題列狀態 | 轉接卡片 | +|---|---|---| +| `Assistant` | 星芒圖示與「AI assistant」 | 不顯示;最後一則回答下方可以出現 Talk to a person | +| `Transferring` | 載入圈與「Transferring」 | 「Transferring you to a person」、一般的等候時間(`ReplyEstimate`)、Cancel | +| `Connected` | 人像圖示與「With Jordan」 | 「Jordan joined from Help desk」、接手的人、摘要 | +| `Resolved` | 勾號與「Resolved」 | 「Resolved by Jordan」;最後一步為時間軸收尾 | +| `Unavailable` | 警示圖示與「No one available」 | 「No one is available right now」、Try again | + +### 動作與輸入框 + +每個動作要在你開啟對應屬性後才會顯示,並觸發一個冒泡的路由事件: + +- `AllowsRequestHuman`:在助理最後一則已完成的回答下方顯示 Talk to a person 快速回覆,觸發 `HumanRequested`。 +- `AllowsCancelTransfer`:轉接中在卡片上顯示 Cancel,觸發 `CancelTransferRequested`。 +- `AllowsRetry`:無人接手時在卡片上顯示 Try again,觸發 `RetryRequested`。 +- `ShowsComposer`:顯示輸入框。按 Enter 或傳送按鈕會觸發 `SendRequested`,附帶去除前後空白的文字;訊息要由你自己加到 `Messages` 或 `AgentMessages`。提示文字會隨階段改變(「Message Jordan」),除非你設定了 `Placeholder`。 + +### 串流輸出與輸入中 + +要串流輸出助理的文字,在設定了 `IsStreaming` 的訊息上更新 `Text`;卡片會讓每次附加的片段淡入。把 `Typing` 設為 `Assistant` 會顯示一行簡短的閃光 Thinking,設為 `Agent` 會顯示輸入中的圓點。 + +```csharp +var reply = new AgentHandoffMessage(AgentHandoffSender.Assistant, string.Empty) { IsStreaming = true }; +chat.Messages.Add(reply); +await foreach (var chunk in stream) +{ + reply.Text += chunk; +} +reply.IsStreaming = false; +``` + +### 窄容器 + + + +卡片填滿容器,最寬 560px;在 360px 寬時依然好用,內距更緊湊、對話泡泡更寬。卡片高度固定(預設 600,可設定 `Height`),對話在內部捲動並固定在最新訊息,所以對話變長時頁面不會跳動。過長的名稱與中繼資訊列會以刪節號截斷,訊息文字會換行。 + +### 標誌與大頭貼 + +`AssistantMark` 與 `ProviderMark` 接受一個 24 × 24 的描邊圖形(Lucide 圖示或你自己的路徑),分別用於第一步的助理與轉接卡片旁的客服系統;`AssistantMarkBrush` 與 `ProviderMarkBrush` 設定它們的顏色。接手者的大頭貼顯示姓名縮寫,或 `AgentHandoffAgent.Avatar` 中的相片。 + +### 模擬 + +`AgentHandoffSimulation` 為示範和原型播放一段依劇本進行的對話,就像上方的預覽。把台詞放進 `AgentHandoffScript`(留為 null 的台詞會被略過),再把它設為卡片的 `Simulation`:卡片在未設定 `Stage` 時播放它,每次顯示時從頭開始,輸入框以及 Talk to a person、Cancel 與 Try again 操作都能使用。`Scenario="Unavailable"` 會停在無人接手的時候,於是顯示 Try again;切換情境會讓故事從頭開始。它公開 `Stage`、`Messages`、`AgentMessages`、`Typing`、`Summary`、`Agent` 與 `Notice`,並接受 `RequestHuman`、`CancelTransfer`、`Retry` 與 `Send`。設定 `Stage` 後,卡片就回到你的資料。不要在示範以外使用它。 + +```xml + + + + + +``` + +```csharp +chat.Simulation = new AgentHandoffSimulation +{ + Script = new AgentHandoffScript + { + CustomerOpening = "My order arrived damaged.", + AssistantAnswer = "Sorry about that. I can send a replacement part.", + CustomerRequest = "Can I talk to a person?", + AssistantHandoff = "Of course, I'll pass this to our team with a summary.", + Summary = new AgentHandoffSummary("Order arrived damaged; wants a refund."), + Agent = new AgentHandoffAgent("Sam Lee", "Support specialist"), + AgentGreeting = "Hi, I'm Sam. I've read the summary.", + }, +}; +``` + +### 你自己的文案 + +`Labels` 包含卡片顯示或朗讀的每一個字詞。替換整個物件即可在地化或改寫: + +```csharp +chat.Labels = new AgentHandoffLabels +{ + Transferring = "Übergabe", + TalkToPerson = "Mit einer Person sprechen", + With = name => "Mit " + name, +}; +``` + +### 最佳做法 + +- `Summary` 放入你傳給客服系統的同一批事實,讓客戶看到分享了什麼。 +- 對話很長時只傳最近的訊息;這張卡片是給進行中的對話用的,不是封存。 +- 一次性的結果(例如「Refund issued」)用 `Notice`,它只會朗讀一次。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 對話區是一個名為「Conversation」的可聚焦記錄,鍵盤使用者可以捲動它;它不會朗讀每一個串流片段。 +- 階段變化與 `Notice` 會透過禮貌的狀態區域朗讀一次,其中包括示範是模擬的這件事。 +- 每個狀態都由圖示與文字搭配:Transferring、With Jordan、Resolved、No one available。顏色從來不是唯一的訊號。 +- 摘要切換按鈕會回報展開或收合。輸入中的圓點有名稱,例如「Jordan is typing」。 +- 時間軸上的標誌與大頭貼在可見文字旁只是裝飾。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序移到對話區、Talk to a person、卡片上的動作、摘要切換按鈕與輸入框 | +| Up、Down、Page Up、Page Down、Home、End | 捲動取得焦點的對話區 | +| Enter | 傳送輸入框中的訊息,或按下取得焦點的按鈕 | +| Space | 摘要按鈕取得焦點時展開或收合摘要 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 助理回覆串流輸出 | 約每 34ms 一段,每段 3 到 8 個字詞;每段只做 140ms 的淡入 | 回覆整段出現 | 回覆整段出現 | +| 新列到來或文字換行 | 對話區以無回彈的 0.24s 彈簧跟隨量得的高度,往上推開而不跳動 | 立即 | 立即 | +| 轉接中 | 一個強調色小圓點沿著轉接卡片旁的時間軸往下移動 | 沒有移動的圓點 | 沒有移動的圓點 | +| 狀態文字變化 | 舊文字 100ms 淡出,新文字 160ms 淡入並上升 2px | 短暫交叉淡化 | 立即 | +| 輸入中的圓點、載入圈 | 呼吸、旋轉 | 保持靜止 | 保持靜止 | +| 模擬對話 | 播放並重複 | 只播放一次 | 只播放一次 | + +離開畫面時循環會暫停。 + +## 相關元件 + +- `ChatThread`:與單一助理或真人之間的聊天。 +- `AgentTimeline`:多代理執行的各個步驟。 +- `ToolCall`:代理發出的工具呼叫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/en-US.md index 2a93dd01..2e8c16fc 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/en-US.md @@ -19,8 +19,8 @@ fails. to quiet text, and a footer that ends with the total time and a one-line summary. - **Work that can fail halfway**: a failed step stops the run and shows its reason with Retry and Skip, instead of hiding the failure in a log. -- **Trying the whole journey in a page or a demo**: with no data and no status the card plays an invented plan on a - built-in simulation, so every state can be seen without an agent. +- **Trying the whole journey in a page or a demo**: give the card an `AgentPlanSimulation` with your own script and it + plays the plan, so every state can be seen without an agent. ## When not to use @@ -35,8 +35,11 @@ fails. ## Usage -With no `Steps` and no `Status`, the card shows an invented draft (adding full-text search to a help center) and runs -the built-in simulation when it is approved. +With no `Steps`, no `Status` and no `Simulation`, the card is an empty draft: "Nothing is planned yet. Add a step, or let +the agent keep planning." with Add step, Keep planning and a disabled Approve plan; its buttons only raise their events. +The package ships no sample plan. The preview above gives the card a simulation the gallery writes: GPT-6.1 Sol (a real +model's name) drafts a plan for adding full-text search to a help center (an invented task), and the simulation runs it +once it is approved. ```xml ` to an `AgentPlanSimulation` whose properties come from `{x:Static}`, as the +preview's Code tab shows. Setting `Simulation` back to null takes its draft away and leaves the plan empty. -Give the simulation your own steps, suggestions and summary to demo another journey: +Here the words are replaced too, and the simulation is set from code: @@ -217,8 +227,8 @@ verbs with `AgentPlanActivityIcons.Set`. ### Common questions -**Does the plan run anything?** No. It shows a plan and, with no `Status`, plays an invented simulation. Real work is -yours: answer `Approved` and update the steps. +**Does the plan run anything?** No. It shows a plan and, with no `Status`, plays the `Simulation` you give it, if any. +Real work is yours: answer `Approved` and update the steps. **How do I show a step the agent added?** Insert an `AgentPlanStep` with `NoteKind = AgentPlanNoteKind.AddedByAgent` while the plan runs. It slides in, gets a wash and a note, and is announced. @@ -264,12 +274,9 @@ The enums: | `AgentPlanChangeKind` | `Rename`, `Move`, `Remove`, `Add`, `SuggestionStreamed` | | `AgentPlanStepActionKind` | `Retry`, `Skip` | -`AgentPlanSamples` holds the invented sample (`Steps`, `Suggestions`, `GenericScript`, `Goal`, `Summary`, -`CreateModel()`, `CreateSteps()`), so a page can start from it and change one step. - ## Accessibility -- The card is a group named after the model and the goal ("Lumen 2 Pro plan: Add full-text search to the help center"). +- The card is a group named after the model and the goal ("GPT-6.1 Sol plan: Add full-text search to the help center"). The steps are an ordered list, so a screen reader announces the count and each position. - While the plan runs, every step announces its state in words (Pending, In progress, Done, Failed, Skipped) before its title. Each state also has its own shape, so it never rests on color. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-CN.md index 7c539a86..5ecbff0a 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-CN.md @@ -1,7 +1,7 @@ --- title: Agent plan summary: 将智能体的计划呈现为可编辑的清单:草稿阶段可以重排、改名和添加步骤,批准后则逐步跟随运行过程。 -source-hash: 2ce58e71cf24 +source-hash: 001769f62b45 --- @@ -13,7 +13,7 @@ source-hash: 2ce58e71cf24 - **编码或运维智能体的计划模式**:智能体提出步骤,人编辑并批准,卡片随后跟踪工作进展。批准之前不会运行任何内容。 - **跟踪多步任务的侧边面板或聊天消息**:显示当前的一个步骤,已完成的步骤回落为安静的文字;底部的页脚以总耗时和一行摘要收尾。 - **可能中途失败的工作**:失败的步骤会停止运行,并显示失败原因以及 Retry 和 Skip,而不是把失败隐藏在日志里。 -- **在页面或演示中体验完整流程**:在没有数据且没有状态时,卡片会在内置的模拟中播放一份虚构的计划,因此无需智能体即可看到每一种状态。 +- **在页面或演示中体验完整流程**:给卡片一个带有你自己脚本的 `AgentPlanSimulation`,它就会播放这份计划,因此无需智能体即可看到每一种状态。 ## 何时不用 @@ -25,7 +25,7 @@ source-hash: 2ce58e71cf24 ## 用法 -在没有 `Steps` 也没有 `Status` 时,卡片会显示一份虚构的草稿(为帮助中心添加全文搜索),并在批准后运行内置的模拟。 +在没有 `Steps`、`Status` 和 `Simulation` 时,卡片是一份空草稿:"Nothing is planned yet. Add a step, or let the agent keep planning.",并带有 Add step、Keep planning 和处于禁用状态的 Approve plan;它的按钮只引发各自的事件。包里不附带示例计划。上方的预览给卡片设置了一个由展示库编写的模拟:GPT-6.1 Sol(真实的模型名称)为帮助中心添加全文搜索起草计划(任务是虚构的),批准后由模拟运行。 ```xml agent.Decide(e.Step, e.Kind); ### 模拟 -`AgentPlanSimulation` 是内置的智能体,在 `Status` 为 null 时运行。它由事件驱动:每次状态变化只等待一个计时器,计划空闲时不运行任何内容;Pause 会保留剩余时间,Resume 则从那里继续。它响应的按钮与宿主相同。其节拍依据测得的流程(1× 速度): +`AgentPlanSimulation` 是用于预览、新手引导和测试的模拟智能体:把它设为 `Simulation`,它会在 `Status` 为 null 时运行。它由事件驱动:每次状态变化只等待一个计时器,计划空闲时不运行任何内容;Pause 会保留剩余时间,Resume 则从那里继续。它响应的按钮与宿主相同。其节拍依据测得的流程(1× 速度): | 节拍 | 时间 | |---|---| @@ -111,9 +111,9 @@ plan.StepAction += (_, e) => agent.Decide(e.Step, e.Kind); | Retry → 第一条新行,然后 577ms,再 465ms → 完成 | 555ms | | Skip → 下一个步骤开始 | 277ms | -在示例中,智能体会在第三个步骤的第二行之后(417ms 后)插入一个步骤,而测试步骤在其最后一行之后失败一次,然后无限期地等待 Retry 或 Skip。没有脚本的步骤(即你添加的步骤)会运行 `GenericScript`:三条活动行和 30 秒。脚本通过步骤 id 查找,因此改名后的步骤会保留其脚本,被删除的步骤则会被跳过。Start over 会回到运行开始时的草稿,并让所有建议重新可用。 +模拟所说的一切都由你的应用编写:`Steps`(草稿,每一项是一个 `AgentPlanScriptedStep`,带有由活动行和时间组成的 `AgentPlanStepScript`、可选的失败以及智能体可选插入的步骤)、Keep planning 用的 `Suggestions`、没有脚本的步骤(即你添加的步骤)所用的 `GenericScript`(未设置时,这样的步骤完成时没有活动行,也没有时间),以及 `Goal`、`Model` 和 `Summary`(计划未设置自己的值时显示)。在预览中,智能体会在第三个步骤的第二行之后(417ms 后)插入一个步骤,而测试步骤在其最后一行之后失败一次,然后无限期地等待 Retry 或 Skip。脚本通过步骤 id 查找,因此改名后的步骤会保留其脚本,被删除的步骤则会被跳过。Start over 会回到运行开始时的草稿,并让所有建议重新可用。计划挂接模拟时读取这些数据;每个计划各建一个模拟,因为它保存运行状态。在 XAML 中,把 `` 设为一个 `AgentPlanSimulation`,其属性来自 `{x:Static}`,写法见预览的 Code 标签。把 `Simulation` 设回 null 会带走它的草稿,计划变为空。 -将你自己的步骤、建议和摘要交给模拟,即可演示另一个流程: +下面的示例还替换了文字,并在代码中设置模拟: @@ -169,7 +169,7 @@ plan.Simulation = new AgentPlanSimulation ### 常见问题 -**计划会运行任何东西吗?** 不会。它只显示计划,在没有 `Status` 时播放一段虚构的模拟。真正的工作由你负责:响应 `Approved` 并更新步骤。 +**计划会运行任何东西吗?** 不会。它只显示计划,在没有 `Status` 时播放你给它的 `Simulation`(如果有)。真正的工作由你负责:响应 `Approved` 并更新步骤。 **如何展示智能体添加的步骤?** 在计划运行时插入一个 `AgentPlanStep`,并设置 `NoteKind = AgentPlanNoteKind.AddedByAgent`。它会滑入,带有底纹和备注,并且会被公告。 @@ -213,11 +213,9 @@ plan.Simulation = new AgentPlanSimulation | `AgentPlanChangeKind` | `Rename`、`Move`、`Remove`、`Add`、`SuggestionStreamed` | | `AgentPlanStepActionKind` | `Retry`、`Skip` | -`AgentPlanSamples` 保存虚构的示例(`Steps`、`Suggestions`、`GenericScript`、`Goal`、`Summary`、`CreateModel()`、`CreateSteps()`),因此页面可以从它开始,只修改其中一个步骤。 - ## 无障碍 -- 卡片是一个以模型名称和目标命名的分组("Lumen 2 Pro plan: Add full-text search to the help center")。步骤是一个有序列表,因此屏幕阅读器会读出总数以及每个位置。 +- 卡片是一个以模型名称和目标命名的分组("GPT-6.1 Sol plan: Add full-text search to the help center")。步骤是一个有序列表,因此屏幕阅读器会读出总数以及每个位置。 - 计划运行时,每个步骤在标题之前都会以文字播报其状态(Pending、In progress、Done、Failed、Skipped)。每种状态还有自己的形状,因此不只依赖颜色。 - 失败的步骤会读出 Failed 一词、失败原因,以及两个带标签的按钮(Retry 和 Skip,各自以步骤名称命名)。 - 标题是一个带标签的文本区域("Step 3"),并由一条提示描述,提示中说明 Alt 加方向键以及 Escape。手柄和删除按钮以其位置和步骤命名("Move step 3: …")。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-TW.md index 2c860d00..1df7abc7 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentPlan/zh-TW.md @@ -1,7 +1,7 @@ --- title: Agent plan summary: "代理的計畫是一份可編輯的檢查清單:草稿階段可以重新排序、改名與新增步驟,核准後則依序追蹤執行過程。" -source-hash: 2ce58e71cf24 +source-hash: 001769f62b45 --- @@ -13,7 +13,7 @@ source-hash: 2ce58e71cf24 - **編碼或維運代理的計畫模式**:代理提出步驟,人員編輯並核准,卡片再追蹤工作進度。核准之前什麼都不會執行。 - **追蹤多步驟任務的側邊面板或聊天訊息**:一個目前步驟,已完成的步驟會沉降為安靜的文字,頁尾則以總時間與一行摘要收尾。 - **可能中途失敗的工作**:失敗的步驟會停止執行,並顯示原因與 Retry 和 Skip,而不是把失敗藏在記錄裡。 -- **在頁面或示範中試用完整流程**:沒有資料也沒有狀態時,卡片會在內建模擬上播放一個虛構的計畫,因此不需要代理也能看到每一種狀態。 +- **在頁面或示範中試用完整流程**:給卡片一個帶有你自己腳本的 `AgentPlanSimulation`,它就會播放這份計畫,因此不需要代理也能看到每一種狀態。 ## 何時不用 @@ -25,7 +25,7 @@ source-hash: 2ce58e71cf24 ## 用法 -未設定 `Steps` 與 `Status` 時,卡片會顯示一份虛構的草稿(為說明中心新增全文搜尋),並在核准時執行內建模擬。 +未設定 `Steps`、`Status` 與 `Simulation` 時,卡片是一份空草稿:"Nothing is planned yet. Add a step, or let the agent keep planning.",並帶有 Add step、Keep planning 與停用中的 Approve plan;它的按鈕只會引發各自的事件。套件不附帶範例計畫。上方的預覽為卡片設定了一個由展示庫撰寫的模擬:GPT-6.1 Sol(真實的模型名稱)為說明中心新增全文搜尋起草計畫(任務是虛構的),核准後由模擬執行。 ```xml agent.Decide(e.Step, e.Kind); ### 模擬 -`AgentPlanSimulation` 是內建的代理,當 `Status` 為 null 時執行。它由事件驅動:每次狀態變化只等待一個計時器,計畫閒置時不執行任何東西;Pause 會保留剩餘時間,Resume 則從該處繼續。它會回應主機會發出的同樣按鈕。節拍依照實測的流程(1× 速度)設定: +`AgentPlanSimulation` 是用於預覽、新手導覽與測試的模擬代理:把它設為 `Simulation`,它會在 `Status` 為 null 時執行。它由事件驅動:每次狀態變化只等待一個計時器,計畫閒置時不執行任何東西;Pause 會保留剩餘時間,Resume 則從該處繼續。它會回應主機會發出的同樣按鈕。節拍依照實測的流程(1× 速度)設定: | 節拍 | 時間 | |---|---| @@ -111,9 +111,9 @@ plan.StepAction += (_, e) => agent.Decide(e.Step, e.Kind); | Retry → 第一行新活動,接著 577ms,再 465ms → 完成 | 555ms | | Skip → 下一個步驟開始 | 277ms | -在範例中,代理會在第三個步驟的第二行之後(417ms 之後)插入一個步驟,而測試步驟會在最後一行之後失敗一次,接著只要需要就會一直等待 Retry 或 Skip。沒有腳本的步驟(例如你新增的)會執行 `GenericScript`:三行活動與 30 秒。腳本是依步驟 id 尋找的,因此改名的步驟會保留其腳本,被移除的步驟則會被略過。Start over 會回到執行開始前的草稿,所有建議都會再次可用。 +模擬所說的一切都由你的應用程式撰寫:`Steps`(草稿,每一項是一個 `AgentPlanScriptedStep`,帶有由活動行與時間組成的 `AgentPlanStepScript`、可選的失敗,以及代理可選插入的步驟)、Keep planning 用的 `Suggestions`、沒有腳本的步驟(例如你新增的)所用的 `GenericScript`(未設定時,這樣的步驟完成時沒有活動行,也沒有時間),以及 `Goal`、`Model` 與 `Summary`(計畫未設定自己的值時顯示)。在預覽中,代理會在第三個步驟的第二行之後(417ms 之後)插入一個步驟,而測試步驟會在最後一行之後失敗一次,接著只要需要就會一直等待 Retry 或 Skip。腳本是依步驟 id 尋找的,因此改名的步驟會保留其腳本,被移除的步驟則會被略過。Start over 會回到執行開始前的草稿,所有建議都會再次可用。計畫掛接模擬時讀取這些資料;每個計畫各建一個模擬,因為它保存執行狀態。在 XAML 中,把 `` 設為一個 `AgentPlanSimulation`,其屬性來自 `{x:Static}`,寫法見預覽的 Code 分頁。把 `Simulation` 設回 null 會帶走它的草稿,計畫變為空。 -給模擬你自己的步驟、建議與摘要,以示範另一段流程: +下面的示範還替換了文字,並在程式碼中設定模擬: @@ -169,7 +169,7 @@ plan.Simulation = new AgentPlanSimulation ### 常見問題 -**計畫會執行任何東西嗎?** 不會。它只顯示計畫;沒有 `Status` 時,它會播放虛構的模擬。實際工作由你負責:回應 `Approved` 並更新步驟。 +**計畫會執行任何東西嗎?** 不會。它只顯示計畫;沒有 `Status` 時,它會播放你給它的 `Simulation`(如果有)。實際工作由你負責:回應 `Approved` 並更新步驟。 **如何顯示代理新增的步驟?** 計畫執行期間,插入一個 `NoteKind = AgentPlanNoteKind.AddedByAgent` 的 `AgentPlanStep`。它會滑入、帶有底色與註記,並會被播報。 @@ -213,11 +213,9 @@ plan.Simulation = new AgentPlanSimulation | `AgentPlanChangeKind` | `Rename`, `Move`, `Remove`, `Add`, `SuggestionStreamed` | | `AgentPlanStepActionKind` | `Retry`, `Skip` | -`AgentPlanSamples` 保存虛構的範例(`Steps`、`Suggestions`、`GenericScript`、`Goal`、`Summary`、`CreateModel()`、`CreateSteps()`),讓頁面能從它開始,並只修改其中一個步驟。 - ## 無障礙 -- 卡片是一個群組,以模型與目標命名(例如 "Lumen 2 Pro plan: Add full-text search to the help center")。步驟是有序清單,因此螢幕閱讀器會朗讀總數與每個步驟的位置。 +- 卡片是一個群組,以模型與目標命名(例如 "GPT-6.1 Sol plan: Add full-text search to the help center")。步驟是有序清單,因此螢幕閱讀器會朗讀總數與每個步驟的位置。 - 計畫執行時,每個步驟都會在標題前以文字播報其狀態(Pending、In progress、Done、Failed、Skipped)。每種狀態也有自己的形狀,因此絕不只靠顏色傳達。 - 失敗的步驟會朗讀 Failed 一字、其原因,以及兩個有標籤的按鈕(Retry 與 Skip,各以步驟名稱命名)。 - 標題是一個有標籤的文字區域("Step 3"),並有一段提示說明 Alt 加方向鍵與 Escape。把手與移除按鈕則以其位置與步驟命名("Move step 3: …")。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/en-US.md index 59554ddf..2b35f5dc 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/en-US.md @@ -33,10 +33,12 @@ moves. ## Usage -With nothing set, the pill plays a sample run on its own: an invented coding agent that works through six steps and -stops on an approval that waits for as long as it takes. The preview above adds a switch to a second sample (a research -agent whose third step fails once, so Retry can be seen) and a Replay button; those two controls belong to the demo, -not to the pill. +With nothing set, the pill rests at "Ready": the default mark, named "Agent", with no ring and no timer. The package +ships no sample run. The preview above plays a simulated run that the gallery writes: a coding agent named after +Claude Sonnet 5.5 works through six steps and stops on an approval that waits for as long as it takes. Its switch +moves to a second run (a research agent named after Gemini 4 Argon, whose third step fails once, so Retry can be seen) +and Replay starts over; the switch, the Replay button and the runs belong to the demo, not to the pill. The model +names are real; the tasks, files and counts are invented. ```xml ` and set its `Scenario` from +`{x:Static}`, as the preview's Code tab shows (the parameterless simulation has an empty run, which stays at Ready). The start delay is 0.9 seconds (`StartDelay`) and 0.5 seconds after `Replay()` (`ReplayDelay`). A +finished or stopped run stays as it is until the pill settles it. The timers run on a `TimeProvider`, so a test can step +the run with a manual clock; the pill takes its own `TimeProvider` property for the timer and the settle delay. Setting +`Simulation` back to null leaves the pill at Ready; the run itself goes on until you `Dispose()` it. ### Narrow containers @@ -258,7 +273,9 @@ the pill's footprint. The pill keeps its place in the layout. **How do I show the timer for a run that already started?** Set `StartedAt` to the real start. The pill counts from there. -**What if I give `Status` and a `Simulation`?** The simulation drives the pill and `Status` is ignored. +**What if I give `Status` and a `Simulation`?** Your data wins: while `Status`, `Activity`, `StartedAt` or a step is +set, the pill shows them and leaves the simulation alone (it does not start it, and the buttons only raise their +events). `Agent` alone does not count: it names the simulated run's agent. **Why does a count not roll like a new activity?** Only a new label and icon roll. A count changes in place, so it can tick several times a second. @@ -283,8 +300,6 @@ tick several times a second. - - The enums: | Type | Values | @@ -299,8 +314,8 @@ The agent is an `AgentModel` (name, a 24 by 24 stroked mark and its brush), shar ## Accessibility - The pill is a button with an expanded state. Its name reads the agent, the activity with its count and the elapsed - time ("Juniper Code 3: Checking specs 8 of 12, 0:09 elapsed"); a finished run reads its result. -- A polite live region announces each new activity and each outcome ("Juniper Code 3: done in 14 seconds, 5 files + time ("Claude Sonnet 5.5: Checking specs 8 of 12, 0:09 elapsed"); a finished run reads its result. +- A polite live region announces each new activity and each outcome ("Claude Sonnet 5.5: done in 14 seconds, 5 files touched"), and the result of an action through `Notice`. It does not speak counts or timer ticks. - The open panel is a region named "{agent} run details"; the progress bar is a progress bar named "Run progress" with a value such as "4 of 6 steps finished". diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-CN.md index 911c2835..0c64382b 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-CN.md @@ -1,7 +1,7 @@ --- title: Agent status summary: 运行中智能体的实时状态胶囊:标识位于旋转的环中,显示当前步骤与计时;按一下即展开为最近的步骤,并带有暂停、停止与审批控件。 -source-hash: 6d0078b450f9 +source-hash: f143ca47a5cb --- @@ -26,7 +26,7 @@ source-hash: 6d0078b450f9 ## 用法 -未设置任何内容时,胶囊会自行播放一次示例运行:一个虚构的编码智能体依次完成六个步骤,并停在一个等待多久都可以的审批上。上方的预览还加了一个开关,可切换到第二个示例(研究智能体,其第三步会失败一次,以便看到重试),以及一个 Replay 按钮;这两个控件属于演示本身,而不属于胶囊。 +未设置任何内容时,胶囊停在“就绪”(Ready)状态:显示名为 "Agent" 的默认标记,没有环,也没有计时器。包里不附带任何示例运行。上方的预览播放的是展示库编写的模拟运行:一个以 Claude Sonnet 5.5 命名的编码智能体依次完成六个步骤,并停在一个等待多久都可以的审批上。它的开关可切换到第二个运行(以 Gemini 4 Argon 命名的研究智能体,其第三步会失败一次,以便看到重试),Replay 则从头开始;开关、Replay 按钮和这些运行都属于演示本身,而不属于胶囊。模型名称是真实的;任务、文件和计数是虚构的。 ```xml ### 模拟 -示例运行不属于控件的一部分。`AgentRunSimulation` 是一个没有界面的驱动器,它按计时器播放一个 `AgentRunScenario`:列出步骤、推进计数、无限期等待审批或失败,并将 Pause、Resume、Stop、Approve、Deny、Retry 与 Open run 应用到它自己的状态上。把它交给 `AgentStatus.Simulation`,胶囊就会显示它并把按钮操作路由回去;你也可以自行读取它的 `Status`、`Activity`、`Steps` 与 `Notice`,以驱动其他任何视图。 +模拟运行用于预览、新手引导和测试,由你的应用编写。`AgentRunScenario` 包含智能体、步骤、结果和通知;每个 `AgentRunScenarioStep` 都有一个现在时的标签、一个过去时的标签、一个图标、一段时长、一个可选的计数、一个可选的审批关卡,以及首次尝试时可选的失败。`AgentRunSimulation` 是一个没有界面的驱动器,它按计时器播放该场景:列出步骤、推进计数、无限期等待审批或失败,并将 Pause、Resume、Stop、Approve、Deny、Retry 与 Open run 应用到它自己的状态上。把它交给 `AgentStatus.Simulation`,胶囊出现在屏幕上后就会启动它、显示它并把按钮操作路由回去;你也可以自行读取它的 `Status`、`Activity`、`Steps` 与 `Notice`,以驱动其他任何视图。 ```csharp -var simulation = new AgentRunSimulation(AgentStatusSamples.Research); +var run = new AgentRunScenario +{ + Agent = new AgentModel("Release bot", ArcIcons.Sparkles), + Outcome = "2 files touched", + Steps = + [ + new() { Id = "read", ActiveLabel = "Reading 12 files", DoneLabel = "Read 12 files", + Icon = ArcIcons.Search, Duration = TimeSpan.FromSeconds(2) }, + new() { Id = "push", ActiveLabel = "Pushing the branch", DoneLabel = "Pushed the branch", + Icon = ArcIcons.CloudUpload, Duration = TimeSpan.FromSeconds(1.5), + Request = "Waiting on you to push", WaitingLabel = "Approval needed to push" } + ] +}; +var simulation = new AgentRunSimulation(run); pill.Simulation = simulation; simulation.Replay(); // clears the run and starts again after 0.5 s -simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 s +simulation.Scenario = otherRun; // starts over after 0.9 s ``` -开始延迟为 0.9 秒(`StartDelay`),`Replay()` 之后为 0.5 秒(`ReplayDelay`)。已完成或已停止的运行会保持原样,直到胶囊将其结算。计时器运行在 `TimeProvider` 上,因此测试可以用手动时钟推进运行;胶囊自身也有一个 `TimeProvider` 属性,用于计时和结算延迟。 - -`AgentStatusSamples` 包含两个虚构的场景、它们的智能体(`CodingAgent`、`ResearchAgent`)以及两种活动。你可以用 `AgentRunScenarioStep` 构建自己的 `AgentRunScenario`:每个步骤都有一个现在时的标签、一个过去时的标签、一个图标、一段时长、一个可选的计数、一个可选的审批关卡,以及首次尝试时可选的失败。 +在 XAML 中,请在 `` 里为每个胶囊创建一个模拟,并用 `{x:Static}` 设置它的 `Scenario`,写法见预览的 Code 标签(无参数的模拟只有一个空运行,会停在就绪状态)。开始延迟为 0.9 秒(`StartDelay`),`Replay()` 之后为 0.5 秒(`ReplayDelay`)。已完成或已停止的运行会保持原样,直到胶囊将其结算。计时器运行在 `TimeProvider` 上,因此测试可以用手动时钟推进运行;胶囊自身也有一个 `TimeProvider` 属性,用于计时和结算延迟。把 `Simulation` 设回 null 后,胶囊停在就绪状态;运行本身会继续,直到你调用 `Dispose()`。 ### 窄容器 @@ -198,7 +209,7 @@ simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 **如何为已经开始的运行显示计时器?** 将 `StartedAt` 设为真实的开始时间,胶囊会从那里开始计时。 -**如果同时设置了 `Status` 和 `Simulation` 会怎样?** 由模拟驱动胶囊,`Status` 会被忽略。 +**如果同时设置了 `Status` 和 `Simulation` 会怎样?** 以你的数据为准:只要设置了 `Status`、`Activity`、`StartedAt` 或某个步骤,胶囊就显示它们,不理会模拟(不会启动它,按钮也只引发各自的事件)。单独设置 `Agent` 不算:它只是为模拟运行的智能体命名。 **为什么计数不像新活动那样滚动?** 只有新的文字和图标会滚动。计数是原地更改的,因此它可以每秒跳动好几次。 @@ -222,8 +233,6 @@ simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 - - 枚举如下: | 类型 | 取值 | @@ -237,8 +246,8 @@ simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 ## 无障碍 -- 胶囊是一个带有展开状态的按钮。它的名称会读出智能体、带计数的活动以及已经过的时间(如 "Juniper Code 3: Checking specs 8 of 12, 0:09 elapsed");运行完成后则读出其结果。 -- 礼貌级别的实时区域会播报每个新的活动和每个结果(如 "Juniper Code 3: done in 14 seconds, 5 files touched"),以及通过 `Notice` 传出的操作结果。它不播报计数或计时器的跳动。 +- 胶囊是一个带有展开状态的按钮。它的名称会读出智能体、带计数的活动以及已经过的时间(如 "Claude Sonnet 5.5: Checking specs 8 of 12, 0:09 elapsed");运行完成后则读出其结果。 +- 礼貌级别的实时区域会播报每个新的活动和每个结果(如 "Claude Sonnet 5.5: done in 14 seconds, 5 files touched"),以及通过 `Notice` 传出的操作结果。它不播报计数或计时器的跳动。 - 打开的面板是一个名为 "{agent} run details" 的区域;进度条是一个名为 "Run progress" 的进度条,其值如 "4 of 6 steps finished"。 - 状态从不只依赖颜色:每种状态都有一个文字("Ready"、"Paused"、"Done in"、"Stopped after")以及不同的环或图标;步骤在运行时使用现在时,完成后使用过去时。 - 窄容器中的图标按钮都有无障碍名称。被截断的文字在胶囊名称和面板的步骤列表中保持完整。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-TW.md index d8866550..260d4ec1 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentStatus/zh-TW.md @@ -1,7 +1,7 @@ --- title: Agent status summary: 執行中代理的即時狀態膠囊:其標記位於旋轉的環中,顯示目前的活動與計時器;按一下即展開成近期步驟,並提供暫停、停止與核准控制。 -source-hash: 6d0078b450f9 +source-hash: f143ca47a5cb --- @@ -26,7 +26,7 @@ source-hash: 6d0078b450f9 ## 用法 -未設定任何內容時,膠囊會自行播放一個範例執行:一個虛構的程式代理,依序完成六個步驟,並在一個等待多久都可以的核准處停下。上方的預覽另外加了一個切換開關,可切換到第二個範例(研究代理,其第三步會失敗一次,好讓你看到重試),以及一個「重播」按鈕;這兩個控制項屬於示範本身,不屬於膠囊。 +未設定任何內容時,膠囊停在「就緒」(Ready)狀態:顯示名為 "Agent" 的預設標記,沒有環,也沒有計時器。套件不附帶任何範例執行。上方的預覽播放的是展示庫撰寫的模擬執行:一個以 Claude Sonnet 5.5 命名的程式代理,依序完成六個步驟,並在一個等待多久都可以的核准處停下。它的切換開關可切換到第二個執行(以 Gemini 4 Argon 命名的研究代理,其第三步會失敗一次,好讓你看到重試),「重播」則從頭開始;切換開關、「重播」按鈕與這些執行都屬於示範本身,不屬於膠囊。模型名稱是真實的;任務、檔案與計數是虛構的。 ```xml ### 模擬 -範例執行不屬於控制項的一部分。`AgentRunSimulation` 是沒有 UI 的驅動器,它會以計時器播放一個 `AgentRunScenario`:列出步驟、推進計數、無限期等待核准或失敗,並把暫停、繼續、停止、核准、拒絕、重試與開啟執行套用到它自己的狀態上。把它交給 `AgentStatus.Simulation`,膠囊就會顯示它並把按鈕的操作導回它;你也可以自行讀取其 `Status`、`Activity`、`Steps` 與 `Notice`,以驅動任何其他檢視。 +模擬執行用於預覽、新手導覽與測試,由你的應用程式撰寫。`AgentRunScenario` 包含代理、步驟、結果與通知;每個 `AgentRunScenarioStep` 都有一個現在式標籤、一個過去式標籤、一個圖示、一段時長、一個可選的計數、一個可選的核准關卡,以及可選的首次嘗試失敗。`AgentRunSimulation` 是沒有 UI 的驅動器,它會以計時器播放該情境:列出步驟、推進計數、無限期等待核准或失敗,並把暫停、繼續、停止、核准、拒絕、重試與開啟執行套用到它自己的狀態上。把它交給 `AgentStatus.Simulation`,膠囊出現在畫面上後就會啟動它、顯示它並把按鈕的操作導回它;你也可以自行讀取其 `Status`、`Activity`、`Steps` 與 `Notice`,以驅動任何其他檢視。 ```csharp -var simulation = new AgentRunSimulation(AgentStatusSamples.Research); +var run = new AgentRunScenario +{ + Agent = new AgentModel("Release bot", ArcIcons.Sparkles), + Outcome = "2 files touched", + Steps = + [ + new() { Id = "read", ActiveLabel = "Reading 12 files", DoneLabel = "Read 12 files", + Icon = ArcIcons.Search, Duration = TimeSpan.FromSeconds(2) }, + new() { Id = "push", ActiveLabel = "Pushing the branch", DoneLabel = "Pushed the branch", + Icon = ArcIcons.CloudUpload, Duration = TimeSpan.FromSeconds(1.5), + Request = "Waiting on you to push", WaitingLabel = "Approval needed to push" } + ] +}; +var simulation = new AgentRunSimulation(run); pill.Simulation = simulation; simulation.Replay(); // clears the run and starts again after 0.5 s -simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 s +simulation.Scenario = otherRun; // starts over after 0.9 s ``` -起始延遲為 0.9 秒(`StartDelay`),呼叫 `Replay()` 後為 0.5 秒(`ReplayDelay`)。已完成或已停止的執行會保持原樣,直到膠囊將其回復。計時器執行於 `TimeProvider` 之上,因此測試可以用手動時鐘推進執行過程;膠囊另外提供自己的 `TimeProvider` 屬性,用於計時器與回復延遲。 - -`AgentStatusSamples` 提供兩個虛構的情境、它們的代理(`CodingAgent`、`ResearchAgent`)與兩個活動。你可以用 `AgentRunScenarioStep` 建立自己的 `AgentRunScenario`:每個步驟都有一個現在式標籤、一個過去式標籤、一個圖示、一段時長、一個可選的計數、一個可選的核准關卡,以及可選的首次嘗試失敗。 +在 XAML 中,請在 `` 裡為每個膠囊建立一個模擬,並用 `{x:Static}` 設定它的 `Scenario`,寫法見預覽的 Code 分頁(無參數的模擬只有一個空執行,會停在就緒狀態)。起始延遲為 0.9 秒(`StartDelay`),呼叫 `Replay()` 後為 0.5 秒(`ReplayDelay`)。已完成或已停止的執行會保持原樣,直到膠囊將其回復。計時器執行於 `TimeProvider` 之上,因此測試可以用手動時鐘推進執行過程;膠囊另外提供自己的 `TimeProvider` 屬性,用於計時器與回復延遲。把 `Simulation` 設回 null 後,膠囊停在就緒狀態;執行本身會繼續,直到你呼叫 `Dispose()`。 ### 窄容器 @@ -198,7 +209,7 @@ simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 **如何為已經開始的執行顯示計時器?** 把 `StartedAt` 設為真正的開始時間,膠囊就會從那裡開始計算。 -**如果同時設定 `Status` 與 `Simulation` 會怎樣?** 由模擬驅動膠囊,`Status` 會被忽略。 +**如果同時設定 `Status` 與 `Simulation` 會怎樣?** 以你的資料為準:只要設定了 `Status`、`Activity`、`StartedAt` 或某個步驟,膠囊就顯示它們,不理會模擬(不會啟動它,按鈕也只引發各自的事件)。單獨設定 `Agent` 不算:它只是為模擬執行的代理命名。 **為什麼計數不像新活動那樣滾動?** 只有新的標籤與圖示會滾動。計數原地變更,因此每秒可以跳動數次。 @@ -222,8 +233,6 @@ simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 - - 列舉: | 類型 | 值 | @@ -237,8 +246,8 @@ simulation.Scenario = AgentStatusSamples.Coding; // starts over after 0.9 ## 無障礙 -- 膠囊是一個具有展開狀態的按鈕。其名稱會朗讀代理、帶計數的活動與已經經過的時間("Juniper Code 3: Checking specs 8 of 12, 0:09 elapsed");執行結束後則朗讀其結果。 -- 禮貌即時區域會播報每個新的活動與每個結果("Juniper Code 3: done in 14 seconds, 5 files touched"),以及透過 `Notice` 傳來的動作結果。它不會播報計數或計時器的跳動。 +- 膠囊是一個具有展開狀態的按鈕。其名稱會朗讀代理、帶計數的活動與已經經過的時間("Claude Sonnet 5.5: Checking specs 8 of 12, 0:09 elapsed");執行結束後則朗讀其結果。 +- 禮貌即時區域會播報每個新的活動與每個結果("Claude Sonnet 5.5: done in 14 seconds, 5 files touched"),以及透過 `Notice` 傳來的動作結果。它不會播報計數或計時器的跳動。 - 展開的面板是一個名為 "{agent} run details" 的區域;進度條是名為 "Run progress" 的進度條,其值例如 "4 of 6 steps finished"。 - 狀態從不只靠顏色:每種狀態都有一個文字(Ready、Paused、Done in、Stopped after)以及不同的環或字符;步驟在執行中使用現在式,完成後使用過去式。 - 窄容器中的圖示按鈕都有無障礙名稱。被截斷的標籤在膠囊的名稱中以及面板的步驟清單中保持完整。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentTimeline/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentTimeline/en-US.md index 264d9466..18adb6cf 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentTimeline/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AgentTimeline/en-US.md @@ -35,8 +35,9 @@ parallel speedup. ## Usage -Given no data, the timeline shows an invented sample run and replays it the first time it is about a fifth in view. -Register the package with `UseArcSpecial()` next to `UseArcControls()`. +Given no data, the timeline is empty: the title "Agent run" over an empty axis, with nothing to play. The package ships +no sample run. The preview above gives it a run the gallery writes and sets `AutoPlay="True"`, so it replays the first +time it is about a fifth in view. Register the package with `UseArcSpecial()` next to `UseArcControls()`. ```xml @@ -25,7 +25,7 @@ source-hash: 5696d9478d3e ## 用法 -没有提供数据时,时间线会展示一次虚构的示例运行,并在它首次有大约五分之一进入视野时开始回放。请在 `UseArcControls()` 旁调用 `UseArcSpecial()` 注册该包。 +没有提供数据时,时间线为空:在空的时间轴上方显示标题 "Agent run",没有可播放的内容。包里不附带示例运行。上方的预览给它一个由展示库编写的运行并设置 `AutoPlay="True"`,因此它会在首次有大约五分之一进入视野时开始回放。请在 `UseArcControls()` 旁调用 `UseArcSpecial()` 注册该包。 ```xml @@ -25,7 +25,7 @@ source-hash: 5696d9478d3e ## 用法 -未提供資料時,時間軸會顯示一次虛構的範例執行過程,並在第一次它有約五分之一進入可視範圍時開始重播。請在 `UseArcControls()` 旁呼叫 `UseArcSpecial()` 來註冊該套件。 +未提供資料時,時間軸為空:在空的時間軸上方顯示標題 "Agent run",沒有可播放的內容。套件不附帶範例執行過程。上方的預覽給它一個由展示庫撰寫的執行過程並設定 `AutoPlay="True"`,因此它會在第一次有約五分之一進入可視範圍時開始重播。請在 `UseArcControls()` 旁呼叫 `UseArcSpecial()` 來註冊該套件。 ```xml + +## When to use + +`AiCommandPalette` is a command palette that can also answer questions. Typing searches your pages and actions; Tab +turns the query into a question for the model, and the answer streams inside the palette with numbered sources. The +next steps the model suggests, such as filing an issue or opening a doc, are rows you run with Enter, right there. + +- **A global Cmd+K** in a product where people both jump to pages and ask about their workspace. +- **Search that falls back to an answer**: when nothing matches, the last row asks the model about the query. +- **Answers that lead to an action** in another tool, like filing an issue or posting a summary to a channel. + +## When not to use + +- **A long conversation with follow-ups**: use a chat thread; the palette answers one question at a time. +- **Plain navigation with no AI**: use the `CommandPalette` block. +- **Suggestions while typing in a form field**: use an autocomplete input. + +## Usage + +Give the palette your pages and actions as `AiCommandPaletteItem`s and answer questions with `Responder`. Without +either it is an empty palette, and a question gets no answer. `ModelName` is null by default. + +```csharp +palette.ItemsSource = commands; +palette.ItemSelected += (_, e) => Navigate(e.Item.Id); +palette.Responder = async context => +{ + var reply = await api.AskAsync(context.Question, context.CancellationToken); + context.SetSources(reply.Sources); // the chips land first + await foreach (var delta in reply.Stream) + { + context.Append(delta); // cite sources as [1], [2] + } + return new AiCommandPaletteAnswer + { + Actions = [new AiCommandPaletteAction("issue", "Create issue in Linear", "ENG", ArcIcons.CircleDot, + token => CreateIssueAsync(context.Question, token))], + }; +}; +``` + +### Your pages and actions + + + +Each item has a label, an optional `Group` that sections the results, a 16px `Icon`, quiet `Meta` text, a `Shortcut` +shown as key caps and extra `Keywords`. Items marked `IsSuggested` are listed while the query is empty. The search is a +synchronous word match: every word of the query has to start a word of the label, group, meta or keywords, so it suits a +few hundred rows; filter on your server beyond that. Choosing a row raises `ItemSelected` and runs the item's `Command`. + +### Asking + +`Responder` receives an `AiCommandPaletteAskContext` with the question and a cancellation token. Stream text with +`Append` in pieces of any size, and call `SetSources` before the text that cites them. Resolve with an +`AiCommandPaletteAnswer` holding the suggested actions (and the whole `Text` if you did not stream it). Throw to show +the failed state with a "Try again" row. The token is cancelled when the person asks again, goes back to search or the +palette leaves the window. + +Citations are written as `[1]`, `[2]` in the text and become small buttons; hovering or focusing one lights its source +chip, and the other way round. Choosing either raises `SourceSelected`; a source with an `Href` opens it with the system +launcher unless you handle the event. + +### Suggested actions + +Each `AiCommandPaletteAction` runs in place: the row shows a spinner and its running label, then a check that draws once +and the label `Run` returned ("Created ENG-2481"), or an alert and the error label, where Enter retries. +`ActionCompleted` reports the result. Set `DoneMeta` to say what happened, for example that a simulated action sent +nothing. + +### As a dialog + + + +`AiCommandPaletteDialog` shows the palette over the page, near the top of the window, over a dimmed scrim. Cmd+K (Ctrl+K +elsewhere) toggles it from anywhere in the window, `IsOpen` is two-way, and `arc:AiCommandPaletteDialog.Opens` on a button +opens it. Escape in an empty search, a press on the scrim and choosing a page close it. Put your own palette inside: + +```xml + + + +``` + +Inline, `IsElevated` adds the floating shadow and `IsHotkeyEnabled` makes Cmd+K or Ctrl+K focus the input. +`CloseRequested` is raised by Escape in an empty search. + +### Narrow containers + + + +The palette fills its container up to 680px and holds up at 360px. Long labels and meta text truncate with an ellipsis, +meta first; below 540px the navigate hint leaves the footer, and the source chips wrap. `ResultsHeight` sets the height +of the results area (392 by default), which never changes while typing, switching modes or streaming. + +### Your own words + +`Labels` holds every word the palette shows or says, including the placeholders, the "Searching your workspace" status +and the footer hints. `ModelName` and `ModelMark` name the model in the footer, in the input and beside each answer. + +### Simulated workspace + +For a demo or a prototype without a backend, give the palette an `AiCommandPaletteSimulation`: its `Items`, `Questions` +and canned `Answers` fill in what the palette lacks. The simulated model picks the answer whose `Keywords` the question +mentions most (an answer without keywords takes the rest), "searches" for 700ms, lands the sources and streams the text; +its actions pretend to run and say "Simulated, nothing was sent". With no items and no responder of its own the palette +also plays a walkthrough while it is on screen: it searches for `SearchQuery`, opens Ask AI, asks the first question and +runs the first action; the first real click or key press inside stops it for good. + +```csharp +palette.ModelName = "Your model"; +palette.Simulation = new AiCommandPaletteSimulation +{ + Items = [new AiCommandPaletteItem("inbox", "Inbox", "Pages", ArcIcons.Inbox) { IsSuggested = true }], + Questions = ["What changed this week?"], + SearchQuery = "in", + Answers = + [ + new AiCommandPaletteSimulationAnswer + { + Sources = [new AiCommandPaletteSource("notes", "Release notes")], + Text = "Saved views shipped on Tuesday [1].", + Actions = [new AiCommandPaletteSimulationAction("open", "Open release notes", "Docs", ArcIcons.FileText, "Opened")], + }, + ], +}; +``` + +```csharp +palette.Labels = new AiCommandPaletteLabels +{ + SearchPlaceholder = "Rechercher des pages et des actions", + AskMode = "Demander à l’IA", + Thinking = "Recherche dans votre espace", +}; +``` + +### Best practices + +- Keep item labels short and put context in `Meta`; use `Group` to section the results. +- Return sources before text so the chips land first, and cite them in the text. +- Give each action a `Run` that honors the token, and return the created object's name as the done label. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The input keeps the focus the whole time and reads as a combo box; the rows are list items, and the highlighted one + reports itself selected, so arrows never move focus out of the input. +- A polite live region announces the question, the finished answer, action results and simulated runs ("Simulated, + nothing was sent"). +- Status is never color alone: a running, done or failed action changes both its icon and its label. +- Source chips and citations are buttons named with the source's full description; hovering or focusing one lights its + partner. +- The mode toggle is a pair of pressed-state buttons that do not take focus from the input. + +### Keyboard + +| Keys | Action | +|---|---| +| Up, Down | Move through results, questions or suggested actions (wrapping at the ends) | +| Enter | Open the page, run the action, ask the question, or retry a failed answer or action; in Ask AI with text, ask it | +| Tab | Ask AI about the current query; with an empty query, open Ask AI with suggested questions, where Tab goes back to search | +| Escape | From Ask AI, back to search (cancels a streaming answer); in search, clear the query, then raise `CloseRequested` | +| Cmd+K, Ctrl+K | Focus the input with `IsHotkeyEnabled`; toggle an `AiCommandPaletteDialog` | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Views (results, questions, answer) | Old view fades out over 100ms, new one fades in over 160ms and rises 2px | 120ms fade | At once | +| Highlight between rows | Glides in 130ms; jumps without travel when the list itself changes | At once | At once | +| Answer text | 3 to 7 words every 34ms, each batch fading in over 140ms; the view follows the text down unless you scroll up | Up to 24 words every 240ms, no fade | Same as reduced | +| Source chips, suggested actions | Fade in over 160ms | At once | At once | +| Action labels, status icons, footer hints | One crossfade; the done check draws once | Short fade | At once | +| Walkthrough | Plays while on screen, pauses off screen and starts over clean | Never plays | Never plays | + +The palette never changes size: the results area keeps its height in every view. + +## Related + +- `CommandPalette` (block) for a command menu without AI. +- `ToolCall` for the tool calls an agent makes. +- `AgentStatus` for the live status of a running agent. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiCommandPalette/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiCommandPalette/zh-CN.md new file mode 100644 index 00000000..f385319e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiCommandPalette/zh-CN.md @@ -0,0 +1,196 @@ +--- +title: AI command +summary: 带 AI 模式的 Cmd+K 命令面板:搜索页面与操作,或按 Tab 提问,回答就地流式呈现,附带编号来源,以及可直接在列表中执行的后续步骤。 +source-hash: 5fdd634d1cb1 +--- + + + +## 何时使用 + +`AiCommandPalette` 是一个也能回答问题的命令面板。输入内容会搜索你的页面和操作;按 Tab 会把查询变成向模型提出的问题, +回答在面板内流式呈现,并附带编号来源。模型建议的后续步骤(例如提交问题单或打开文档)会作为列表行出现,按 Enter 就地执行。 + +- **全局 Cmd+K**:在一个人们既要跳转页面、又要询问工作区情况的产品中。 +- **无结果时回退到回答的搜索**:没有匹配项时,最后一行会就该查询询问模型。 +- **引出其他工具中操作的回答**,例如提交问题单或把摘要发到频道。 + +## 何时不用 + +- **带追问的长对话**:使用聊天线程;面板一次只回答一个问题。 +- **没有 AI 的普通导航**:使用 `CommandPalette` 区块。 +- **在表单字段中输入时的建议**:使用自动补全输入框。 + +## 用法 + +把你的页面和操作作为 `AiCommandPaletteItem` 交给面板,并用 `Responder` 回答问题。两者都不提供时,它是一个空面板,提问也不会得到回答。`ModelName` 默认为 null。 + +```csharp +palette.ItemsSource = commands; +palette.ItemSelected += (_, e) => Navigate(e.Item.Id); +palette.Responder = async context => +{ + var reply = await api.AskAsync(context.Question, context.CancellationToken); + context.SetSources(reply.Sources); // the chips land first + await foreach (var delta in reply.Stream) + { + context.Append(delta); // cite sources as [1], [2] + } + return new AiCommandPaletteAnswer + { + Actions = [new AiCommandPaletteAction("issue", "Create issue in Linear", "ENG", ArcIcons.CircleDot, + token => CreateIssueAsync(context.Question, token))], + }; +}; +``` + +### 你自己的页面和操作 + + + +每个条目有一个标签、一个用于给结果分区的可选 `Group`、一个 16px 的 `Icon`、低调的 `Meta` 文字、以键帽显示的 `Shortcut`, +以及额外的 `Keywords`。标记为 `IsSuggested` 的条目会在查询为空时列出。搜索是同步的按词匹配:查询中的每个词都必须是标签、 +分组、附注或关键词中某个词的开头,因此适合几百行的规模;超出时请在服务器端过滤。选择一行会引发 `ItemSelected`,并执行该条目的 `Command`。 + +### 提问 + +`Responder` 接收一个 `AiCommandPaletteAskContext`,其中包含问题和一个取消令牌。用 `Append` 以任意大小的片段流式输出文字, +并在引用来源的文字之前调用 `SetSources`。最后返回一个 `AiCommandPaletteAnswer`,其中包含建议的操作(如果没有流式输出, +还要包含完整的 `Text`)。抛出异常会显示失败状态和一行“Try again”。当用户再次提问、返回搜索或面板离开窗口时,令牌会被取消。 + +引用在文字中写作 `[1]`、`[2]`,并会变成小按钮;悬停或聚焦其中一个会点亮对应的来源标签,反之亦然。选择任意一个都会引发 +`SourceSelected`;带有 `Href` 的来源会用系统启动器打开,除非你处理了该事件。 + +### 建议的操作 + +每个 `AiCommandPaletteAction` 都就地执行:该行先显示加载圈和执行中的标签,然后显示只绘制一次的对勾和 `Run` 返回的标签 +(“Created ENG-2481”),或者显示警示图标和错误标签,此时按 Enter 会重试。`ActionCompleted` 报告结果。设置 `DoneMeta` +说明发生了什么,例如模拟操作没有发送任何内容。 + +### 作为对话框 + + + +`AiCommandPaletteDialog` 把面板显示在页面上方、靠近窗口顶部,并带有变暗的遮罩。Cmd+K(其他平台为 Ctrl+K)可在窗口任意位置 +切换它,`IsOpen` 是双向的,按钮上的 `arc:AiCommandPaletteDialog.Opens` 可以打开它。在空搜索中按 Escape、点击遮罩或选择页面 +都会关闭它。把你自己的面板放进去: + +```xml + + + +``` + +内嵌使用时,`IsElevated` 会加上浮层阴影,`IsHotkeyEnabled` 会让 Cmd+K 或 Ctrl+K 聚焦输入框。在空搜索中按 Escape 会引发 +`CloseRequested`。 + +### 窄容器 + + + +面板会填满容器,最宽 680px,窄到 360px 也能正常使用。过长的标签和附注会以省略号截断,附注先截断;宽度低于 540px 时, +页脚中的导航提示会隐藏,来源标签会换行。`ResultsHeight` 设置结果区域的高度(默认 392),在输入、切换模式或流式输出时都不会改变。 + +### 你自己的措辞 + +`Labels` 包含面板显示或朗读的所有文字,包括占位文字、“Searching your workspace”状态和页脚提示。`ModelName` 和 `ModelMark` +在页脚、输入框和每条回答旁边标明所用模型。 + +### 模拟工作区 + +用于没有后端的演示或原型时,给面板一个 `AiCommandPaletteSimulation`:它的 `Items`、`Questions` 和预设的 `Answers` 会补上面板缺少的部分。模拟模型会选出问题中提到其 `Keywords` 最多的回答(没有关键词的回答接住其余问题),“搜索” 700ms,放出来源并流式输出文字;它的操作只是假装运行,并显示 “Simulated, nothing was sent”。面板自己既没有条目也没有 responder 时,还会在可见时播放演示:搜索 `SearchQuery`,打开 Ask AI,提出第一个问题并运行第一个操作;在面板内第一次真实的点击或按键会永久停止演示。 + +```csharp +palette.ModelName = "Your model"; +palette.Simulation = new AiCommandPaletteSimulation +{ + Items = [new AiCommandPaletteItem("inbox", "Inbox", "Pages", ArcIcons.Inbox) { IsSuggested = true }], + Questions = ["What changed this week?"], + SearchQuery = "in", + Answers = + [ + new AiCommandPaletteSimulationAnswer + { + Sources = [new AiCommandPaletteSource("notes", "Release notes")], + Text = "Saved views shipped on Tuesday [1].", + Actions = [new AiCommandPaletteSimulationAction("open", "Open release notes", "Docs", ArcIcons.FileText, "Opened")], + }, + ], +}; +``` + +```csharp +palette.Labels = new AiCommandPaletteLabels +{ + SearchPlaceholder = "Rechercher des pages et des actions", + AskMode = "Demander à l’IA", + Thinking = "Recherche dans votre espace", +}; +``` + +### 最佳实践 + +- 条目标签保持简短,把上下文放进 `Meta`;用 `Group` 给结果分区。 +- 先返回来源再返回文字,让来源标签先出现,并在文字中引用它们。 +- 给每个操作提供一个遵守令牌的 `Run`,并把所创建对象的名称作为完成标签返回。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 焦点始终留在输入框中,输入框被读作组合框;各行是列表项,高亮的那一行会报告自己处于选中状态,因此方向键永远不会把焦点移出输入框。 +- 一个礼貌型实时区域会播报问题、完成的回答、操作结果和模拟执行(“Simulated, nothing was sent”)。 +- 状态从不只靠颜色表达:执行中、已完成或失败的操作会同时改变图标和标签。 +- 来源标签和引用都是按钮,以来源的完整说明命名;悬停或聚焦其中一个会点亮与之对应的另一个。 +- 模式切换是一对带按下状态的按钮,不会从输入框夺走焦点。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Up、Down | 在结果、问题或建议的操作之间移动(到头时循环) | +| Enter | 打开页面、执行操作、提出问题,或重试失败的回答或操作;在 Ask AI 中有文字时提出该问题 | +| Tab | 就当前查询询问 AI;查询为空时打开带建议问题的 Ask AI,在那里按 Tab 返回搜索 | +| Escape | 从 Ask AI 返回搜索(取消正在流式输出的回答);在搜索中先清空查询,再引发 `CloseRequested` | +| Cmd+K、Ctrl+K | 开启 `IsHotkeyEnabled` 时聚焦输入框;切换 `AiCommandPaletteDialog` | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 视图(结果、问题、回答) | 旧视图在 100ms 内淡出,新视图在 160ms 内淡入并上升 2px | 120ms 淡入淡出 | 立即 | +| 行间高亮 | 130ms 滑动;列表本身变化时直接跳到位、不移动 | 立即 | 立即 | +| 回答文字 | 每 34ms 出现 3 到 7 个词,每批在 140ms 内淡入;视图随文字向下跟随,除非你向上滚动 | 每 240ms 最多 24 个词,不淡入 | 与减弱相同 | +| 来源标签、建议的操作 | 160ms 淡入 | 立即 | 立即 | +| 操作标签、状态图标、页脚提示 | 一次交叉淡化;完成的对勾只绘制一次 | 短暂淡入淡出 | 立即 | +| 演示 | 可见时播放,不可见时暂停,回来后从干净状态重新开始 | 从不播放 | 从不播放 | + +面板的尺寸从不改变:结果区域在每个视图中都保持同样的高度。 + +## 相关组件 + +- `CommandPalette`(区块):不带 AI 的命令菜单。 +- `ToolCall`:智能体发起的工具调用。 +- `AgentStatus`:运行中智能体的实时状态。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiCommandPalette/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiCommandPalette/zh-TW.md new file mode 100644 index 00000000..ef5e6bb4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiCommandPalette/zh-TW.md @@ -0,0 +1,196 @@ +--- +title: AI command +summary: 具備 AI 模式的 Cmd+K 命令面板:搜尋頁面與動作,或按 Tab 提問,回答就地串流呈現,附有編號來源,以及可直接在清單中執行的後續步驟。 +source-hash: 5fdd634d1cb1 +--- + + + +## 何時使用 + +`AiCommandPalette` 是一個也能回答問題的命令面板。輸入內容會搜尋你的頁面和動作;按 Tab 會把查詢變成向模型提出的問題, +回答在面板內串流呈現,並附有編號來源。模型建議的後續步驟(例如建立議題或開啟文件)會以清單列的形式出現,按 Enter 就地執行。 + +- **全域 Cmd+K**:在一個使用者既要跳到頁面、又要詢問工作區狀況的產品中。 +- **找不到結果時改為回答的搜尋**:沒有符合項目時,最後一列會針對該查詢詢問模型。 +- **引導到其他工具中動作的回答**,例如建立議題或把摘要發到頻道。 + +## 何時不用 + +- **帶有追問的長篇對話**:請使用聊天串;面板一次只回答一個問題。 +- **沒有 AI 的一般導覽**:請使用 `CommandPalette` 區塊。 +- **在表單欄位輸入時的建議**:請使用自動完成輸入框。 + +## 用法 + +把你的頁面和動作以 `AiCommandPaletteItem` 交給面板,並用 `Responder` 回答問題。兩者都沒有提供時,它是一個空的面板,提問也不會得到回答。`ModelName` 預設為 null。 + +```csharp +palette.ItemsSource = commands; +palette.ItemSelected += (_, e) => Navigate(e.Item.Id); +palette.Responder = async context => +{ + var reply = await api.AskAsync(context.Question, context.CancellationToken); + context.SetSources(reply.Sources); // the chips land first + await foreach (var delta in reply.Stream) + { + context.Append(delta); // cite sources as [1], [2] + } + return new AiCommandPaletteAnswer + { + Actions = [new AiCommandPaletteAction("issue", "Create issue in Linear", "ENG", ArcIcons.CircleDot, + token => CreateIssueAsync(context.Question, token))], + }; +}; +``` + +### 你自己的頁面和動作 + + + +每個項目都有一個標籤、一個用來替結果分區的選用 `Group`、一個 16px 的 `Icon`、低調的 `Meta` 文字、以鍵帽顯示的 `Shortcut`, +以及額外的 `Keywords`。標記為 `IsSuggested` 的項目會在查詢為空時列出。搜尋是同步的逐字比對:查詢中的每個字詞都必須是標籤、 +分組、附註或關鍵字中某個字詞的開頭,因此適合幾百列的規模;超過時請在伺服器端篩選。選取一列會引發 `ItemSelected`,並執行該項目的 `Command`。 + +### 提問 + +`Responder` 會收到一個 `AiCommandPaletteAskContext`,其中包含問題和一個取消權杖。用 `Append` 以任意大小的片段串流輸出文字, +並在引用來源的文字之前呼叫 `SetSources`。最後傳回一個 `AiCommandPaletteAnswer`,其中包含建議的動作(如果沒有串流輸出, +還要包含完整的 `Text`)。擲回例外狀況會顯示失敗狀態和一列「Try again」。當使用者再次提問、返回搜尋或面板離開視窗時,權杖會被取消。 + +引用在文字中寫成 `[1]`、`[2]`,並會變成小按鈕;滑鼠停留或聚焦其中一個會點亮對應的來源標籤,反之亦然。選取任一個都會引發 +`SourceSelected`;帶有 `Href` 的來源會用系統啟動器開啟,除非你處理了該事件。 + +### 建議的動作 + +每個 `AiCommandPaletteAction` 都就地執行:該列先顯示載入圈和執行中的標籤,接著顯示只繪製一次的勾號和 `Run` 傳回的標籤 +(「Created ENG-2481」),或顯示警示圖示和錯誤標籤,這時按 Enter 會重試。`ActionCompleted` 會回報結果。設定 `DoneMeta` +說明發生了什麼,例如模擬的動作沒有送出任何內容。 + +### 作為對話方塊 + + + +`AiCommandPaletteDialog` 會把面板顯示在頁面上方、靠近視窗頂端,並加上調暗的遮罩。Cmd+K(其他平台為 Ctrl+K)可在視窗任何位置 +切換它,`IsOpen` 是雙向的,按鈕上的 `arc:AiCommandPaletteDialog.Opens` 可以開啟它。在空白搜尋中按 Escape、點按遮罩或選取頁面 +都會關閉它。把你自己的面板放進去: + +```xml + + + +``` + +內嵌使用時,`IsElevated` 會加上浮層陰影,`IsHotkeyEnabled` 會讓 Cmd+K 或 Ctrl+K 聚焦輸入框。在空白搜尋中按 Escape 會引發 +`CloseRequested`。 + +### 窄容器 + + + +面板會填滿容器,最寬 680px,窄到 360px 也能正常使用。過長的標籤和附註會以刪節號截斷,附註先截斷;寬度低於 540px 時, +頁尾的導覽提示會隱藏,來源標籤會換行。`ResultsHeight` 設定結果區域的高度(預設 392),在輸入、切換模式或串流輸出時都不會改變。 + +### 你自己的用語 + +`Labels` 包含面板顯示或朗讀的所有文字,包括預留位置文字、「Searching your workspace」狀態和頁尾提示。`ModelName` 和 +`ModelMark` 會在頁尾、輸入框和每則回答旁標示所用的模型。 + +### 模擬工作區 + +用於沒有後端的示範或原型時,給面板一個 `AiCommandPaletteSimulation`:它的 `Items`、`Questions` 和預設的 `Answers` 會補上面板缺少的部分。模擬模型會選出問題中提到其 `Keywords` 最多的回答(沒有關鍵字的回答接住其餘問題),「搜尋」700ms,放出來源並串流輸出文字;它的動作只是假裝執行,並顯示「Simulated, nothing was sent」。面板本身既沒有項目也沒有 responder 時,還會在畫面上可見時播放示範:搜尋 `SearchQuery`,開啟 Ask AI,提出第一個問題並執行第一個動作;在面板內第一次真正的點按或按鍵會永久停止示範。 + +```csharp +palette.ModelName = "Your model"; +palette.Simulation = new AiCommandPaletteSimulation +{ + Items = [new AiCommandPaletteItem("inbox", "Inbox", "Pages", ArcIcons.Inbox) { IsSuggested = true }], + Questions = ["What changed this week?"], + SearchQuery = "in", + Answers = + [ + new AiCommandPaletteSimulationAnswer + { + Sources = [new AiCommandPaletteSource("notes", "Release notes")], + Text = "Saved views shipped on Tuesday [1].", + Actions = [new AiCommandPaletteSimulationAction("open", "Open release notes", "Docs", ArcIcons.FileText, "Opened")], + }, + ], +}; +``` + +```csharp +palette.Labels = new AiCommandPaletteLabels +{ + SearchPlaceholder = "Rechercher des pages et des actions", + AskMode = "Demander à l’IA", + Thinking = "Recherche dans votre espace", +}; +``` + +### 最佳做法 + +- 項目標籤保持簡短,把脈絡放進 `Meta`;用 `Group` 替結果分區。 +- 先傳回來源再傳回文字,讓來源標籤先出現,並在文字中引用它們。 +- 為每個動作提供一個遵守權杖的 `Run`,並把所建立物件的名稱作為完成標籤傳回。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 焦點始終留在輸入框中,輸入框會被讀成下拉式方塊;各列是清單項目,反白的那一列會回報自己處於選取狀態,因此方向鍵永遠不會把焦點移出輸入框。 +- 一個禮貌型即時區域會播報問題、完成的回答、動作結果和模擬執行(「Simulated, nothing was sent」)。 +- 狀態從不只靠顏色表達:執行中、已完成或失敗的動作會同時改變圖示和標籤。 +- 來源標籤和引用都是按鈕,以來源的完整說明命名;滑鼠停留或聚焦其中一個會點亮與它對應的另一個。 +- 模式切換是一對帶有按下狀態的按鈕,不會從輸入框搶走焦點。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Up、Down | 在結果、問題或建議的動作之間移動(到底時循環) | +| Enter | 開啟頁面、執行動作、提出問題,或重試失敗的回答或動作;在 Ask AI 中有文字時提出該問題 | +| Tab | 針對目前的查詢詢問 AI;查詢為空時開啟附有建議問題的 Ask AI,在那裡按 Tab 會返回搜尋 | +| Escape | 從 Ask AI 返回搜尋(取消串流中的回答);在搜尋中先清除查詢,再引發 `CloseRequested` | +| Cmd+K、Ctrl+K | 開啟 `IsHotkeyEnabled` 時聚焦輸入框;切換 `AiCommandPaletteDialog` | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 檢視(結果、問題、回答) | 舊檢視在 100ms 內淡出,新檢視在 160ms 內淡入並上升 2px | 120ms 淡入淡出 | 立即 | +| 列與列之間的反白 | 130ms 滑動;清單本身改變時直接跳到定位、不移動 | 立即 | 立即 | +| 回答文字 | 每 34ms 出現 3 到 7 個字詞,每批在 140ms 內淡入;檢視會跟著文字往下,除非你往上捲動 | 每 240ms 最多 24 個字詞,不淡入 | 與減少相同 | +| 來源標籤、建議的動作 | 160ms 淡入 | 立即 | 立即 | +| 動作標籤、狀態圖示、頁尾提示 | 一次交叉淡化;完成的勾號只繪製一次 | 短暫淡入淡出 | 立即 | +| 示範 | 可見時播放,不可見時暫停,回來後從乾淨的狀態重新開始 | 從不播放 | 從不播放 | + +面板的尺寸從不改變:結果區域在每個檢視中都維持同樣的高度。 + +## 相關元件 + +- `CommandPalette`(區塊):不含 AI 的命令選單。 +- `ToolCall`:代理程式發出的工具呼叫。 +- `AgentStatus`:執行中代理程式的即時狀態。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/en-US.md new file mode 100644 index 00000000..736cdd30 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/en-US.md @@ -0,0 +1,173 @@ +--- +title: AI mascot +summary: A small assistant character that shows what the AI is doing: it breathes when idle, leans in to listen, watches its dot orbit while thinking, reads along as it writes, squints when done and shakes once on an error. +--- + + + +## When to use + +`AiMascot` gives an assistant a face that shows what it is doing. It is a soft pebble with two pill eyes, a quiet +shade on its lower right and a small companion dot. Each of its six states is a continuous motion with no loop seam, +and each one also changes the face, so the state reads at 24px. + +- **Reply headers and composers**: a 24 to 32px mascot next to the assistant's name thinks while you wait for the first + token and reads along while the reply streams. +- **Side panels**: a 40 to 64px mascot in the header of an assistant panel. +- **Empty states**: a 96 to 160px mascot that breathes, blinks and watches the pointer anywhere in the window. +- **Image canvases and long jobs**: something friendly and alive while the model works, instead of a spinner. + +## When not to use + +- **People need the steps, tools and elapsed time of a run**: use `AgentStatus`. The mascot shows a mood, not progress. +- **Dense interfaces**: a plain shimmering "Thinking" line distracts less than a character. +- **Known progress**: use a progress bar when you know the percentage. + +## Usage + +```xml + +``` + +`AiMascot` is a `TemplatedControl` in the Special package (register it with `UseArcSpecial()`). Map your assistant's +phases to `Status`: the microphone is open → `Listening`, waiting for the first token → `Thinking`, streaming → +`Writing`, finished → `Done`, failed → `Error`, otherwise `Idle`. Keep thinking short and switch to writing on the +first token, so the mascot reads what it writes. + +Leave `Status` unset and the mascot plays through every state on a loop (idle, listening, thinking, writing, done, +error), raising `StatusChanged` each time it moves on. That is for showcases; in a product, always set `Status`. + +### Anatomy + +1. **Body**: a soft pebble, anchored at its bottom center. It breathes, floats, leans and tilts around that point. +2. **Form shade**: a quiet crescent along the lower right that gives the body volume. +3. **Eyes**: two pill shapes that blink, glance, widen, look up or down, close into a line and turn into a happy squint. +4. **Brows**: only in the error state, with their inner ends raised. +5. **Companion dot**: floats beside the head, pulses like a voice level, orbits the head, writes, droops, and turns into + the done sparkle. +6. **Ground**: a soft contact shadow and the line the dot writes, drawn only at 56px and up. + +### States + +| Status | Face | Body | Dot | +|---|---|---|---| +| `Idle` | Blinks every 2.4 to 5.2s and glances around | Breathes and floats | Drifts beside the head | +| `Listening` | Wider eyes | Leans in | Pulses like a voice level | +| `Thinking` | Looks up and follows the dot | Tilts the head | Orbits the head, passing behind and in front | +| `Writing` | Looks down and reads along | Leans toward the pen | Writes a line, then lifts and returns | +| `Done` | Happy squint | Lifts a little | Shrinks while a sparkle grows in its place | +| `Error` | Worried brows, smaller eyes | Shakes once | Droops and dims | + + + +### In a reply header + +At 24px next to the assistant's name. The demo simulates the phases: thinking, writing while the words stream, then +done. `Announce` is on because this mascot carries the status. + + + +### Sizes + +`Size` is the width and height in px, from 24 (inline) to 160 (empty state); the default is 96. Below 56 a compact +drawing takes over: tighter framing, larger eyes, dot and sparkle, and no ground. The mascot is a fixed square that +never follows its container, and the drawing stays crisp at any scale. + + + +### Tones and colors + +`Tone` picks the body color from the theme: `Accent` (the accent with accent-foreground eyes), `Ink` (the foreground +with background eyes) or `Soft` (a muted surface with a hairline border and foreground eyes). `BodyColor` overrides +the tone; the eyes then become a dark mix of it unless `EyeColor` is set. `DotColor` changes the companion dot and the +sparkle. + + + +### Following the pointer + +The eyes follow the pointer while it is over the mascot (`FollowPointer="Hover"`, the default), anywhere in the window +(`Page`, good for empty states) or never (`Never`). Touch is ignored. Thinking and writing keep their own gaze. + + + +### Companion and ground + +`ShowCompanion="False"` hides the dot and the sparkle. `ShowGround="False"` hides the contact shadow and the written +line; below 56px they are never drawn. + +### Pausing + +`IsPaused` freezes the mascot on the frame on screen. It also pauses by itself when it is scrolled out of view, +hidden or detached, and resumes on the same frame instead of jumping. + +### Reduced motion + + + +Under reduced motion each state is a still, representative pose (eyes up with the dot over the head, half a written +line, the squint and the sparkle, the brows), changes switch in place, and there are no loops, glances, blinks or +shakes. Pointer following is off. The tour of a mascot without `Status` still moves from state to state. + +### Common questions + +**Why does the mascot not move in a screenshot test?** Tests and screenshots usually run with motion off; every state +then shows its still pose. + +**Can I use several mascots on one page?** Yes. Each runs its own clock, pauses off screen and has its own blink and +glance rhythm. Turn `Announce` on for one of them at most. + +## API reference + + + + + + + +The enums: + +| Type | Values | +|---|---| +| `AiMascotStatus` | `Idle`, `Listening`, `Thinking`, `Writing`, `Done`, `Error` | +| `AiMascotTone` | `Accent`, `Ink`, `Soft` | +| `AiMascotPointerFollow` | `Hover`, `Page`, `Never` | + +## Accessibility + +- The drawing is an image whose name is built from the assistant's name and the state, for example "Atlas, thinking". + It updates with every state. `Label` replaces the whole name; `AccessibleLabel` reads the name in use. +- With `Announce`, state changes are read through a polite live region outside the image. Use it on one mascot per + view so screen readers do not hear duplicates. +- The state never rests on motion alone: each state changes the face (wide eyes, eyes up, eyes down, the happy squint, + worried brows) and the name. Show a text status next to it where the state matters. +- Pointer following ignores touch and is off under reduced motion. + +### Keyboard + +| Keys | Action | +|---|---| +| None | The mascot is a status image and takes no focus. Pair it with a real control when people act on what it shows. | + +## Motion + +- Every state is a continuous function of time (sums of slow sines, an orbit angle, a writing cycle whose start and + end meet at zero length), so loops never restart and have no seam. +- A status change blends the old pose into the new one over about 420ms, without a bounce. Each state's weight rides a + critically damped spring, so a change in the middle of a blend starts from the frame on screen and keeps its speed. + The first paint is the still pose, and the loop eases out of it over the same 420ms. +- The eyes ride their own critically damped spring: quick saccades when idle, a soft follow while thinking, tight + tracking while writing. Nothing bounces. +- Done passes the eyes through a closed line into the happy squint while the dot shrinks and the sparkle grows in its + place. Error shakes once, dying down in about 0.7s. +- The dot is drawn behind and in front of the body and switches only at the ends of its orbit, where it is clear of + the body. While it crosses the crown in front, it fades into the eye color so it stays visible on the body. +- Blinks come every 2.4 to 5.2s, sometimes twice, and take 160ms. +- The clock runs only while the mascot is shown; off screen it stops and resumes on the same frame. Under reduced + motion each state shows a still pose and nothing loops. + +## Related + +- `AgentStatus` for the steps, tools and elapsed time of a running agent. +- `ToolCall` for one tool call in an agent transcript. +- `ContextWindow` for what fills a model's context window. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/zh-CN.md new file mode 100644 index 00000000..ad09d0f4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/zh-CN.md @@ -0,0 +1,143 @@ +--- +title: AI 吉祥物 +summary: 一个小小的助手角色,展示 AI 正在做什么:空闲时呼吸,倾听时向前凑近,思考时看着小圆点绕头旋转,书写时跟着读,完成时眯眼微笑,出错时摇晃一次。 +source-hash: 7df5fd7569f6 +--- + + + +## 何时使用 + +`AiMascot` 给助手一张脸,用来表现它正在做什么。它是一块柔和的鹅卵石,有两只胶囊形的眼睛、右下方一道安静的阴影和一个小小的伴随圆点。六种状态各是一段没有循环接缝的连续动作,并且每种状态都会改变表情,所以在 24px 下也能看懂。 + +- **回复标题与输入框**:在助手名字旁放一个 24 到 32px 的吉祥物,等待第一个 token 时思考,回复流式输出时跟着读。 +- **侧边面板**:在助手面板的标题里放一个 40 到 64px 的吉祥物。 +- **空状态**:一个 96 到 160px 的吉祥物,呼吸、眨眼,并在整个窗口内注视指针。 +- **图像画布与耗时任务**:模型工作时用一个友好、有生气的角色代替加载圈。 + +## 何时不用 + +- **需要展示运行的步骤、工具和耗时**:使用 `AgentStatus`。吉祥物表现的是情绪,不是进度。 +- **信息密集的界面**:一行带微光的"Thinking"比一个角色更不打扰。 +- **已知进度**:知道百分比时使用进度条。 + +## 用法 + +```xml + +``` + +`AiMascot` 是 Special 包中的 `TemplatedControl`(用 `UseArcSpecial()` 注册)。把助手的阶段映射到 `Status`:麦克风打开 → `Listening`,等待第一个 token → `Thinking`,流式输出 → `Writing`,完成 → `Done`,失败 → `Error`,其他情况 → `Idle`。思考阶段尽量短,收到第一个 token 就切换到书写,让吉祥物读它写下的内容。 + +不设置 `Status` 时,吉祥物会循环播放所有状态(空闲、倾听、思考、书写、完成、出错),每切换一次就引发 `StatusChanged`。这只用于展示;在产品中请始终设置 `Status`。 + +### 结构 + +1. **身体**:一块柔和的鹅卵石,以底部中心为锚点。它围绕这个点呼吸、浮动、前倾和侧倾。 +2. **形体阴影**:右下边缘一道安静的月牙形阴影,让身体有体积感。 +3. **眼睛**:两个胶囊形状,会眨眼、张望、睁大、向上或向下看、闭成一条线,再变成开心的眯眼。 +4. **眉毛**:只在出错状态出现,内侧端抬起。 +5. **伴随圆点**:在头旁漂浮,像音量一样跳动,绕头旋转,书写,下垂,并变成完成时的闪光。 +6. **地面**:柔和的接触阴影和圆点写下的线,只在 56px 及以上绘制。 + +### 状态 + +| Status | 表情 | 身体 | 圆点 | +|---|---|---|---| +| `Idle` | 每 2.4 到 5.2 秒眨一次眼,四处张望 | 呼吸并浮动 | 在头旁漂移 | +| `Listening` | 眼睛睁大 | 向前凑近 | 像音量一样跳动 | +| `Thinking` | 向上看并追随圆点 | 歪头 | 绕头旋转,从后方和前方经过 | +| `Writing` | 向下看并跟着读 | 向笔尖倾斜 | 写下一行,然后抬笔返回 | +| `Done` | 开心地眯眼 | 稍稍抬起 | 缩小,同时在原处长出一个闪光 | +| `Error` | 担忧的眉毛,眼睛变小 | 摇晃一次 | 下垂并变暗 | + + + +### 在回复标题中 + +以 24px 放在助手名字旁。示例模拟了各个阶段:思考,文字流式输出时书写,然后完成。因为这个吉祥物承载状态,所以打开了 `Announce`。 + + + +### 尺寸 + +`Size` 是以 px 为单位的宽和高,从 24(行内)到 160(空状态),默认 96。小于 56 时改用紧凑画法:取景更紧,眼睛、圆点和闪光更大,不画地面。吉祥物是固定大小的正方形,从不跟随容器尺寸,图形在任意缩放下都保持清晰。 + + + +### 色调与颜色 + +`Tone` 从主题中选择身体颜色:`Accent`(强调色,眼睛用强调前景色)、`Ink`(前景色,眼睛用背景色)或 `Soft`(柔和的表面色加一条细边框,眼睛用前景色)。`BodyColor` 会覆盖色调;此时眼睛变成它的深色混合,除非设置了 `EyeColor`。`DotColor` 改变伴随圆点和闪光的颜色。 + + + +### 跟随指针 + +指针在吉祥物上方时眼睛会跟随它(`FollowPointer="Hover"`,默认),或在整个窗口内跟随(`Page`,适合空状态),或从不跟随(`Never`)。触摸会被忽略。思考和书写保持自己的视线。 + + + +### 伴随圆点与地面 + +`ShowCompanion="False"` 隐藏圆点和闪光。`ShowGround="False"` 隐藏接触阴影和写下的线;小于 56px 时它们从不绘制。 + +### 暂停 + +`IsPaused` 让吉祥物停在屏幕上的当前帧。它被滚出视口、被隐藏或被移出可视树时也会自行暂停,恢复时从同一帧继续,不会跳变。 + +### 减弱动效 + + + +减弱动效时,每个状态都是一个静止、有代表性的姿势(眼睛向上、圆点在头顶,写了一半的线,眯眼和闪光,眉毛),切换在原地完成,没有循环、张望、眨眼或摇晃。指针跟随关闭。没有设置 `Status` 的吉祥物仍会在状态之间轮换。 + +### 常见问题 + +**为什么截图测试里吉祥物不动?** 测试和截图通常在关闭动效的情况下运行;每个状态此时显示它的静止姿势。 + +**一个页面能放多个吉祥物吗?** 可以。每个都有自己的时钟,离开屏幕时暂停,并有自己的眨眼和张望节奏。最多为其中一个打开 `Announce`。 + +## API 参考 + + + + + + + +枚举: + +| 类型 | 值 | +|---|---| +| `AiMascotStatus` | `Idle`、`Listening`、`Thinking`、`Writing`、`Done`、`Error` | +| `AiMascotTone` | `Accent`、`Ink`、`Soft` | +| `AiMascotPointerFollow` | `Hover`、`Page`、`Never` | + +## 无障碍 + +- 图形是一张图像,它的名称由助手名字和状态组成,例如"Atlas, thinking",并随每次状态变化更新。`Label` 替换整个名称;`AccessibleLabel` 读取当前使用的名称。 +- 打开 `Announce` 后,状态变化会通过图像之外的礼貌型实时区域朗读。每个视图只在一个吉祥物上使用,避免屏幕阅读器重复朗读。 +- 状态从不只靠动作表达:每个状态都会改变表情(睁大的眼睛、向上看、向下看、开心的眯眼、担忧的眉毛)和名称。状态重要时,请在旁边显示文字状态。 +- 指针跟随忽略触摸,并在减弱动效时关闭。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| 无 | 吉祥物是状态图像,不获取焦点。当用户需要根据它的显示进行操作时,请搭配一个真正的控件。 | + +## 动效 + +- 每个状态都是时间的连续函数(缓慢正弦波之和、一个轨道角度、一个起点和终点长度都为零的书写周期),因此循环从不重新开始,也没有接缝。 +- 状态切换在约 420ms 内把旧姿势混合到新姿势,没有回弹。每个状态的权重由一个临界阻尼弹簧驱动,所以混合进行中再次切换时,会从屏幕上的当前帧开始并保持速度。首次绘制是静止姿势,循环在同样的 420ms 内从它缓出。 +- 眼睛由自己的临界阻尼弹簧驱动:空闲时快速扫视,思考时柔和跟随,书写时紧密追踪。没有任何回弹。 +- 完成时眼睛先闭成一条线再变成开心的眯眼,同时圆点缩小、闪光在原处长出。出错时摇晃一次,约 0.7 秒内平息。 +- 圆点在身体后方和前方各画一次,只在轨道两端、离开身体的位置切换图层。它从前方经过头顶时会渐变为眼睛的颜色,以便在身体上保持可见。 +- 每 2.4 到 5.2 秒眨一次眼,有时连眨两次,每次 160ms。 +- 时钟只在吉祥物显示时运行;离开屏幕时停止,并从同一帧恢复。减弱动效时每个状态显示静止姿势,不做任何循环。 + +## 相关组件 + +- `AgentStatus`:展示运行中智能体的步骤、工具和耗时。 +- `ToolCall`:智能体记录中的一次工具调用。 +- `ContextWindow`:模型上下文窗口中装了什么。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/zh-TW.md new file mode 100644 index 00000000..75fe7182 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiMascot/zh-TW.md @@ -0,0 +1,143 @@ +--- +title: AI 吉祥物 +summary: 一個小小的助理角色,呈現 AI 正在做什麼:閒置時呼吸,聆聽時往前靠近,思考時看著小圓點繞頭旋轉,書寫時跟著讀,完成時瞇眼微笑,出錯時搖晃一次。 +source-hash: 7df5fd7569f6 +--- + + + +## 何時使用 + +`AiMascot` 給助理一張臉,用來呈現它正在做什麼。它是一顆柔和的鵝卵石,有兩隻膠囊形的眼睛、右下方一道安靜的陰影和一個小小的伴隨圓點。六種狀態各是一段沒有循環接縫的連續動作,而且每種狀態都會改變表情,所以在 24px 下也看得懂。 + +- **回覆標題與輸入框**:在助理名稱旁放一個 24 到 32px 的吉祥物,等待第一個 token 時思考,回覆串流輸出時跟著讀。 +- **側邊面板**:在助理面板的標題裡放一個 40 到 64px 的吉祥物。 +- **空白狀態**:一個 96 到 160px 的吉祥物,呼吸、眨眼,並在整個視窗內注視指標。 +- **圖像畫布與耗時工作**:模型運作時用一個友善、有生氣的角色取代載入圈。 + +## 何時不用 + +- **需要呈現執行的步驟、工具和經過時間**:使用 `AgentStatus`。吉祥物呈現的是情緒,不是進度。 +- **資訊密集的介面**:一行帶微光的「Thinking」比一個角色更不干擾。 +- **已知進度**:知道百分比時使用進度列。 + +## 用法 + +```xml + +``` + +`AiMascot` 是 Special 套件中的 `TemplatedControl`(用 `UseArcSpecial()` 註冊)。把助理的階段對應到 `Status`:麥克風開啟 → `Listening`,等待第一個 token → `Thinking`,串流輸出 → `Writing`,完成 → `Done`,失敗 → `Error`,其他情況 → `Idle`。思考階段盡量短,收到第一個 token 就切換到書寫,讓吉祥物讀它寫下的內容。 + +不設定 `Status` 時,吉祥物會循環播放所有狀態(閒置、聆聽、思考、書寫、完成、出錯),每切換一次就引發 `StatusChanged`。這只用於展示;在產品中請一律設定 `Status`。 + +### 結構 + +1. **身體**:一顆柔和的鵝卵石,以底部中心為錨點。它繞著這個點呼吸、浮動、前傾和側傾。 +2. **形體陰影**:右下邊緣一道安靜的月牙形陰影,讓身體有立體感。 +3. **眼睛**:兩個膠囊形狀,會眨眼、張望、睜大、往上或往下看、閉成一條線,再變成開心的瞇眼。 +4. **眉毛**:只在出錯狀態出現,內側端抬起。 +5. **伴隨圓點**:在頭旁漂浮,像音量一樣跳動,繞頭旋轉,書寫,下垂,並變成完成時的閃光。 +6. **地面**:柔和的接觸陰影和圓點寫下的線,只在 56px 以上繪製。 + +### 狀態 + +| Status | 表情 | 身體 | 圓點 | +|---|---|---|---| +| `Idle` | 每 2.4 到 5.2 秒眨一次眼,四處張望 | 呼吸並浮動 | 在頭旁漂移 | +| `Listening` | 眼睛睜大 | 往前靠近 | 像音量一樣跳動 | +| `Thinking` | 往上看並追隨圓點 | 歪頭 | 繞頭旋轉,從後方和前方經過 | +| `Writing` | 往下看並跟著讀 | 朝筆尖傾斜 | 寫下一行,然後提筆返回 | +| `Done` | 開心地瞇眼 | 稍微抬起 | 縮小,同時在原處長出一個閃光 | +| `Error` | 擔憂的眉毛,眼睛變小 | 搖晃一次 | 下垂並變暗 | + + + +### 在回覆標題中 + +以 24px 放在助理名稱旁。範例模擬了各個階段:思考,文字串流輸出時書寫,然後完成。因為這個吉祥物承載狀態,所以開啟了 `Announce`。 + + + +### 尺寸 + +`Size` 是以 px 為單位的寬和高,從 24(行內)到 160(空白狀態),預設 96。小於 56 時改用精簡畫法:取景更緊,眼睛、圓點和閃光更大,不畫地面。吉祥物是固定大小的正方形,從不跟隨容器尺寸,圖形在任何縮放下都保持清晰。 + + + +### 色調與顏色 + +`Tone` 從主題中選擇身體顏色:`Accent`(強調色,眼睛用強調前景色)、`Ink`(前景色,眼睛用背景色)或 `Soft`(柔和的表面色加一條細框線,眼睛用前景色)。`BodyColor` 會覆寫色調;此時眼睛變成它的深色混合,除非設定了 `EyeColor`。`DotColor` 改變伴隨圓點和閃光的顏色。 + + + +### 跟隨指標 + +指標在吉祥物上方時眼睛會跟隨它(`FollowPointer="Hover"`,預設),或在整個視窗內跟隨(`Page`,適合空白狀態),或從不跟隨(`Never`)。觸控會被忽略。思考和書寫保持自己的視線。 + + + +### 伴隨圓點與地面 + +`ShowCompanion="False"` 隱藏圓點和閃光。`ShowGround="False"` 隱藏接觸陰影和寫下的線;小於 56px 時它們從不繪製。 + +### 暫停 + +`IsPaused` 讓吉祥物停在螢幕上的目前影格。它被捲出可視範圍、被隱藏或被移出視覺樹時也會自行暫停,恢復時從同一影格繼續,不會跳動。 + +### 減少動態效果 + + + +減少動態效果時,每個狀態都是一個靜止、具代表性的姿勢(眼睛往上、圓點在頭頂,寫了一半的線,瞇眼和閃光,眉毛),切換在原地完成,沒有循環、張望、眨眼或搖晃。指標跟隨關閉。沒有設定 `Status` 的吉祥物仍會在狀態之間輪替。 + +### 常見問題 + +**為什麼截圖測試裡吉祥物不動?** 測試和截圖通常在關閉動態效果的情況下執行;每個狀態此時顯示它的靜止姿勢。 + +**一個頁面能放多個吉祥物嗎?** 可以。每個都有自己的時鐘,離開螢幕時暫停,並有自己的眨眼和張望節奏。最多為其中一個開啟 `Announce`。 + +## API 參考 + + + + + + + +列舉: + +| 型別 | 值 | +|---|---| +| `AiMascotStatus` | `Idle`、`Listening`、`Thinking`、`Writing`、`Done`、`Error` | +| `AiMascotTone` | `Accent`、`Ink`、`Soft` | +| `AiMascotPointerFollow` | `Hover`、`Page`、`Never` | + +## 無障礙 + +- 圖形是一張圖像,它的名稱由助理名稱和狀態組成,例如「Atlas, thinking」,並隨每次狀態變化更新。`Label` 取代整個名稱;`AccessibleLabel` 讀取目前使用的名稱。 +- 開啟 `Announce` 後,狀態變化會透過圖像之外的禮貌型即時區域朗讀。每個畫面只在一個吉祥物上使用,避免螢幕閱讀器重複朗讀。 +- 狀態從不只靠動作表達:每個狀態都會改變表情(睜大的眼睛、往上看、往下看、開心的瞇眼、擔憂的眉毛)和名稱。狀態重要時,請在旁邊顯示文字狀態。 +- 指標跟隨會忽略觸控,並在減少動態效果時關閉。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| 無 | 吉祥物是狀態圖像,不取得焦點。當使用者需要依據它的顯示進行操作時,請搭配一個真正的控制項。 | + +## 動態效果 + +- 每個狀態都是時間的連續函數(緩慢正弦波之和、一個軌道角度、一個起點和終點長度都為零的書寫週期),因此循環從不重新開始,也沒有接縫。 +- 狀態切換在約 420ms 內把舊姿勢混合到新姿勢,沒有回彈。每個狀態的權重由一個臨界阻尼彈簧驅動,所以混合進行中再次切換時,會從螢幕上的目前影格開始並保持速度。首次繪製是靜止姿勢,循環在同樣的 420ms 內從它緩出。 +- 眼睛由自己的臨界阻尼彈簧驅動:閒置時快速掃視,思考時柔和跟隨,書寫時緊密追蹤。沒有任何回彈。 +- 完成時眼睛先閉成一條線再變成開心的瞇眼,同時圓點縮小、閃光在原處長出。出錯時搖晃一次,約 0.7 秒內平息。 +- 圓點在身體後方和前方各畫一次,只在軌道兩端、離開身體的位置切換圖層。它從前方經過頭頂時會漸變為眼睛的顏色,以便在身體上保持可見。 +- 每 2.4 到 5.2 秒眨一次眼,有時連眨兩次,每次 160ms。 +- 時鐘只在吉祥物顯示時執行;離開螢幕時停止,並從同一影格恢復。減少動態效果時每個狀態顯示靜止姿勢,不做任何循環。 + +## 相關元件 + +- `AgentStatus`:呈現執行中代理的步驟、工具和經過時間。 +- `ToolCall`:代理紀錄中的一次工具呼叫。 +- `ContextWindow`:模型上下文視窗中裝了什麼。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/en-US.md new file mode 100644 index 00000000..bfa4748a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/en-US.md @@ -0,0 +1,171 @@ +--- +title: AI spark +summary: An animated spark mark for AI status: four tapered rays and four lighter diagonals breathe at rest, ripple while thinking, pulse like a heartbeat while writing, then gather and settle when done. +--- + + + +## When to use + +`AiSpark` is a brand-quality mark that shows an AI at work: four tapered primary rays and four lighter diagonal rays +around a soft core. Each status has its own motion, and every change blends from the pose the mark is already in, so +states never hard cut. + +- **Above a streaming reply**, to show that the assistant is thinking or writing. +- **Inside a send or generate button** that turns into a working indicator in a composer. +- **Over an image or canvas tool** that needs a calm, branded working mark while a render runs. +- **Empty states and onboarding**, where a 64 to 96px hero mark gives the AI feature an identity. + +## When not to use + +- **Determinate progress** with a known percentage: use a progress bar. +- **Multi step agent runs** where each step needs its own state and detail: use `AgentStatus` or `AgentPlan`. +- **Plain text loading lines** with no mark: use a shimmering label instead. + +## Usage + +Map your stream's lifecycle to `Status`: request sent or tool running is `Thinking`, first token until the last is +`Generating`, finished is `Done`, failed or aborted is `Error`. Return to `Idle` when the composer is ready again. + +```xml + +``` + +```csharp +spark.Status = AiSparkStatus.Thinking; // request sent +spark.Status = AiSparkStatus.Generating; // first token +spark.Status = AiSparkStatus.Done; // last token +``` + +Leave `Status` unset and the spark plays through idle, thinking, writing and done on its own. That is meant for demos +and marketing pages; `EffectiveStatus` reports the step it is showing. + +### Anatomy + +1. **Primary rays**: four tapered rays on the axes, in the mark's color. +2. **Diagonals**: four shorter, lighter rays between them; under 18px they become dots. +3. **Core**: a soft circle in the lighter tone. +4. **Particles**: two dots orbiting just outside the rays while writing, from 40px up. +5. **Label** (pill only) and **tile** (avatar only). + +### States + +| Status | Motion | Still pose (reduced motion) | +|---|---|---| +| `Idle` | A 6.4s breath of the whole mark while one diagonal at a time reaches out | Rest | +| `Thinking` | One crest ripples around the ring every 1.9s; the mark turns at 26 degrees per second | A crest at the top | +| `Generating` | A lub dub heartbeat every 1.04s, axes firing in sequence; 46 degrees per second | Fuller rays and core | +| `Done` | The rays gather toward the center, then settle at rest; the spin glides home | Rest | +| `Error` | One small decaying shake; shorter rays, dimmer diagonals, a muted danger tone | Short rays, danger tone | + +### Pill + + + +`Variant="Pill"` adds a status label (Ready, Thinking, Writing, Done, Couldn't finish) in a quiet bordered capsule 28px +high. Labels crossfade and the capsule eases to the new width; while thinking or writing the label shimmers. `Label` +replaces the built-in word with specific progress such as "Searching 4 sources"; keep it to one or two words. + +### Avatar + + + +`Variant="Avatar"` sets the mark in a round, lightly tinted tile for message avatars and icon buttons. `Size` sets the +tile (32 by default) and the mark is 56% of it. + +### Inline in text + + + +Set `Size` to the text size and `Tone="Inherit"` so the mark takes the color of the text or button around it. + +### Sizes + + + +The mark is crisp from 12px to 120px and beyond. Ray weight is hinted on a log scale by the rendered size: small marks +get heavier rays, and under 18px the diagonals become dots. The mark has a fixed footprint in every status, so it never +moves the layout around it. + +### Color + + + +`Tone` picks the semantic color: `Accent` (default), `Foreground`, `Muted` or `Inherit`. `MarkBrush` overrides it with +any color. The diagonals and the core always use a lighter tone of the same color, and an error eases to a muted danger +tone whatever the color. + +### Hero mark + + + +A 64 to 96px idle mark gives an empty state an identity. `Twinkle="False"` makes the idle mark fully still. + +### Speed and pause + + + +`Speed` multiplies every loop and spin speed. `IsPaused` freezes the mark in place; resuming continues from the same +phase. + +### Content guidelines + +- Pill labels are one or two words that say what is happening ("Searching 4 sources", "Drawing"). +- Use one animated spark per active generation. For many rows, show a still mark (`Status="Idle"`, `Twinkle="False"`) + and animate only the active row. + +### Best practices + +- Put controls such as Stop next to the spark; the spark itself takes no input. +- Set `Announce="False"` on decorative copies so screen readers hear one voice. + +### Common questions + +**Can I pass a CSS length such as 1em?** Set `Size` to the font size of the surrounding text instead. + +**Does a spark off screen cost anything?** No. The loop runs only while the mark moves and is on screen, and stops +entirely once a resting status has settled. + +## API reference + + + + + +## Accessibility + +- With `Announce` on and a status set, the spark is a polite live region: a status change is announced once, as "AI is + thinking" for the mark or as the label text for the pill. +- With `Announce="False"`, and while it plays on its own, it presents as an image with an accessible name, so + decorative copies stay quiet. +- `AccessibleName` replaces the name; every word is in `AiSparkLabels`. +- Status is never color alone: each status has its own shape and motion, an error shortens the rays and dims the + diagonals, and the pill adds a text label. + +### Keyboard + +| Keys | Action | +|---|---| +| None | The spark is a status indicator and takes no focus. Put controls such as Stop next to it. | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Idle | 6.4s breath, one diagonal reaching out at a time | Still | Still | +| Thinking | Crest around the ring every 1.9s, 26 degrees per second spin | Still pose | Still pose | +| Generating | Heartbeat every 1.04s, 46 degrees per second spin, two orbiting particles (40px and up) | Still pose | Still pose | +| Done | Rays gather in 170ms, ease back to rest over 510ms without overshoot; the spin glides home on an ease-out whose speed only falls | Still pose | Still pose | +| Error | One decaying shake over 460ms; the tone eases to muted danger over 320ms | Still pose, tone over 200ms | At once | +| Status change | Blends over about 480ms from the current pose and velocity; an interrupted blend is the next one's start | 200ms blend | At once | +| Pill label | Old label fades out over 100ms, the new one fades in over 160ms and rises 2px while the width eases | Short crossfade | At once | + +One frame loop per spark moves eight ray paths, the core and the transforms in place and allocates nothing. It pauses +while the spark is off screen or hidden and stops entirely once a resting status has settled. The gather and the shake +always finish smoothly, even if the status changes again. + +## Related + +- `AgentStatus` for the live status of a whole agent run. +- `ToolCall` for the tool calls an agent makes. +- `AgentPlan` for the plan an agent writes before it acts. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/zh-CN.md new file mode 100644 index 00000000..456917de --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/zh-CN.md @@ -0,0 +1,153 @@ +--- +title: AI spark +summary: 表示 AI 状态的动画火花标记:四条渐细的主光线与四条较浅的斜线,静止时呼吸,思考时涟漪流转,书写时像心跳一样搏动,完成时向中心收拢后归于平静。 +source-hash: 6689c5961e9c +--- + + + +## 何时使用 + +`AiSpark` 是一个品牌级的标记,用来表示 AI 正在工作:四条渐细的主光线和四条较浅的斜向光线围绕着柔和的核心。每种状态都有自己的动效,每次切换都从标记当前所处的姿态过渡过去,状态之间从不生硬切换。 + +- **流式回复的上方**,表示助手正在思考或书写。 +- **发送或生成按钮内部**,在输入框中变成工作指示。 +- **图像或画布工具之上**,渲染进行时需要一个安静、带品牌感的工作标记。 +- **空状态与新手引导**,用 64 到 96px 的大号标记给 AI 功能一个形象。 + +## 何时不用 + +- **确定的进度**,已知百分比时:使用进度条。 +- **多步骤的智能体运行**,每一步都需要自己的状态与细节时:使用 `AgentStatus` 或 `AgentPlan`。 +- **没有标记的纯文字加载行**:改用闪烁的文字标签。 + +## 用法 + +把流的生命周期对应到 `Status`:请求已发送或工具运行中为 `Thinking`,从第一个词元到最后一个为 `Generating`,完成为 `Done`,失败或中止为 `Error`。输入框再次就绪时回到 `Idle`。 + +```xml + +``` + +```csharp +spark.Status = AiSparkStatus.Thinking; // request sent +spark.Status = AiSparkStatus.Generating; // first token +spark.Status = AiSparkStatus.Done; // last token +``` + +不设置 `Status` 时,火花会自行依次播放空闲、思考、书写和完成。这只用于演示和营销页面;`EffectiveStatus` 报告它正在显示的那一步。 + +### 结构 + +1. **主光线**:位于四个轴向的渐细光线,使用标记的颜色。 +2. **斜线**:主光线之间四条较短、较浅的光线;小于 18px 时变成圆点。 +3. **核心**:较浅色调的柔和圆形。 +4. **粒子**:书写时在光线外侧环绕的两个小点,40px 及以上才显示。 +5. **标签**(仅胶囊)与**底块**(仅头像)。 + +### 状态 + +| 状态 | 动效 | 静止姿态(减少动效) | +|---|---|---| +| `Idle` | 整个标记以 6.4s 为周期呼吸,斜线轮流向外伸展 | 静止 | +| `Thinking` | 一道波峰每 1.9s 绕环一周;标记以每秒 26 度旋转 | 波峰停在顶部 | +| `Generating` | 每 1.04s 一次"扑通"心跳,两条轴依次搏动;每秒 46 度 | 更饱满的光线与核心 | +| `Done` | 光线向中心收拢后回到静止;旋转滑回原位 | 静止 | +| `Error` | 一次逐渐减弱的轻微摇晃;光线变短、斜线变暗、转为柔和的危险色 | 短光线,危险色 | + +### 胶囊 + + + +`Variant="Pill"` 在一个 28px 高、带细边框的安静胶囊里加上状态标签(Ready、Thinking、Writing、Done、Couldn't finish)。标签交叉淡化,胶囊平滑过渡到新宽度;思考或书写时标签带有闪光。`Label` 用具体进度替换内置文字,例如 "Searching 4 sources";保持一两个词。 + +### 头像 + + + +`Variant="Avatar"` 把标记放在一个圆形、浅色着色的底块中,用于消息头像和图标按钮。`Size` 设置底块大小(默认 32),标记占其 56%。 + +### 嵌入文字 + + + +把 `Size` 设为文字大小,并设置 `Tone="Inherit"`,标记就会采用周围文字或按钮的颜色。 + +### 尺寸 + + + +标记从 12px 到 120px 乃至更大都清晰锐利。光线粗细按渲染尺寸在对数刻度上做光学修正:小标记的光线更粗,小于 18px 时斜线变成圆点。标记在任何状态下占位都固定,绝不会挤动周围布局。 + +### 颜色 + + + +`Tone` 选择语义颜色:`Accent`(默认)、`Foreground`、`Muted` 或 `Inherit`。`MarkBrush` 可以用任意颜色覆盖它。斜线和核心始终使用同一颜色的较浅色调,出错时无论原本是什么颜色,都会过渡到柔和的危险色。 + +### 大号标记 + + + +64 到 96px 的空闲标记能给空状态一个形象。`Twinkle="False"` 让空闲标记完全静止。 + +### 速度与暂停 + + + +`Speed` 让所有循环与旋转速度成倍变化。`IsPaused` 让标记原地定格;恢复后从同一相位继续。 + +### 内容指南 + +- 胶囊标签用一两个词说明正在发生什么("Searching 4 sources"、"Drawing")。 +- 每个进行中的生成只用一个动画火花。列表有很多行时,显示静止标记(`Status="Idle"`、`Twinkle="False"`),只让活动的那一行动起来。 + +### 最佳实践 + +- 把"停止"等控件放在火花旁边;火花本身不接受输入。 +- 在装饰性的副本上设置 `Announce="False"`,让读屏软件只听到一个声音。 + +### 常见问题 + +**能传入 1em 这样的 CSS 长度吗?** 改为把 `Size` 设成周围文字的字号。 + +**不在屏幕上的火花有开销吗?** 没有。循环只在标记运动且位于屏幕内时运行,静止状态稳定后完全停止。 + +## API 参考 + + + + + +## 无障碍 + +- `Announce` 开启且设置了状态时,火花是一个礼貌型(polite)实时区域:状态变化只播报一次,标记读作 "AI is thinking",胶囊读作标签文字。 +- 设置 `Announce="False"` 时,以及自行播放期间,它以带可访问名称的图像呈现,装饰性副本因此保持安静。 +- `AccessibleName` 替换名称;所有文字都在 `AiSparkLabels` 中。 +- 状态从不只靠颜色区分:每种状态都有自己的形状与动效,出错时光线变短、斜线变暗,胶囊还有文字标签。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| 无 | 火花是状态指示,不获取焦点。把"停止"等控件放在它旁边。 | + +## 动效 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 空闲 | 6.4s 呼吸,斜线轮流向外伸展 | 静止 | 静止 | +| 思考 | 波峰每 1.9s 绕环一周,每秒 26 度旋转 | 静止姿态 | 静止姿态 | +| 书写 | 每 1.04s 一次心跳,每秒 46 度旋转,两个环绕粒子(40px 及以上) | 静止姿态 | 静止姿态 | +| 完成 | 光线 170ms 内收拢,510ms 内无回弹地回到静止;旋转以只减不增的缓出速度滑回原位 | 静止姿态 | 静止姿态 | +| 出错 | 460ms 内一次逐渐减弱的摇晃;色调在 320ms 内过渡到柔和的危险色 | 静止姿态,色调 200ms | 立即 | +| 状态切换 | 从当前姿态与速度出发约 480ms 过渡;被打断的过渡成为下一次的起点 | 200ms 过渡 | 立即 | +| 胶囊标签 | 旧标签 100ms 淡出,新标签 160ms 淡入并上升 2px,同时宽度平滑变化 | 短暂交叉淡化 | 立即 | + +每个火花只有一个帧循环,原地更新八条光线路径、核心与变换,不产生任何分配。火花不在屏幕上或被隐藏时循环暂停,静止状态稳定后完全停止。收拢与摇晃即使状态再次改变也总会平滑地完成。 + +## 相关组件 + +- `AgentStatus`:整个智能体运行的实时状态。 +- `ToolCall`:智能体发起的工具调用。 +- `AgentPlan`:智能体行动前写下的计划。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/zh-TW.md new file mode 100644 index 00000000..5b8173e4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiSpark/zh-TW.md @@ -0,0 +1,153 @@ +--- +title: AI spark +summary: 表示 AI 狀態的動畫火花標記:四道漸細的主光線與四道較淺的斜線,靜止時呼吸,思考時漣漪流轉,書寫時像心跳一樣搏動,完成時向中心收攏後歸於平靜。 +source-hash: 6689c5961e9c +--- + + + +## 何時使用 + +`AiSpark` 是一個品牌級的標記,用來表示 AI 正在工作:四道漸細的主光線和四道較淺的斜向光線圍繞著柔和的核心。每種狀態都有自己的動態效果,每次切換都從標記目前所處的姿態過渡過去,狀態之間從不生硬切換。 + +- **串流回覆的上方**,表示助理正在思考或書寫。 +- **傳送或產生按鈕內部**,在輸入框中變成工作指示。 +- **影像或畫布工具之上**,算圖進行時需要一個安靜、具品牌感的工作標記。 +- **空白狀態與新手導覽**,用 64 到 96px 的大型標記給 AI 功能一個形象。 + +## 何時不用 + +- **確定的進度**,已知百分比時:使用進度列。 +- **多步驟的代理程式執行**,每一步都需要自己的狀態與細節時:使用 `AgentStatus` 或 `AgentPlan`。 +- **沒有標記的純文字載入列**:改用閃爍的文字標籤。 + +## 用法 + +把串流的生命週期對應到 `Status`:請求已送出或工具執行中為 `Thinking`,從第一個詞元到最後一個為 `Generating`,完成為 `Done`,失敗或中止為 `Error`。輸入框再次就緒時回到 `Idle`。 + +```xml + +``` + +```csharp +spark.Status = AiSparkStatus.Thinking; // request sent +spark.Status = AiSparkStatus.Generating; // first token +spark.Status = AiSparkStatus.Done; // last token +``` + +不設定 `Status` 時,火花會自行依序播放閒置、思考、書寫和完成。這只用於示範和行銷頁面;`EffectiveStatus` 回報它正在顯示的那一步。 + +### 結構 + +1. **主光線**:位於四個軸向的漸細光線,使用標記的顏色。 +2. **斜線**:主光線之間四道較短、較淺的光線;小於 18px 時變成圓點。 +3. **核心**:較淺色調的柔和圓形。 +4. **粒子**:書寫時在光線外側環繞的兩個小點,40px 以上才顯示。 +5. **標籤**(僅膠囊)與**底塊**(僅頭像)。 + +### 狀態 + +| 狀態 | 動態效果 | 靜止姿態(減少動態效果) | +|---|---|---| +| `Idle` | 整個標記以 6.4s 為週期呼吸,斜線輪流向外伸展 | 靜止 | +| `Thinking` | 一道波峰每 1.9s 繞環一圈;標記以每秒 26 度旋轉 | 波峰停在頂部 | +| `Generating` | 每 1.04s 一次「撲通」心跳,兩條軸依序搏動;每秒 46 度 | 更飽滿的光線與核心 | +| `Done` | 光線向中心收攏後回到靜止;旋轉滑回原位 | 靜止 | +| `Error` | 一次逐漸減弱的輕微搖晃;光線變短、斜線變暗、轉為柔和的危險色 | 短光線,危險色 | + +### 膠囊 + + + +`Variant="Pill"` 在一個 28px 高、帶細邊框的安靜膠囊裡加上狀態標籤(Ready、Thinking、Writing、Done、Couldn't finish)。標籤交叉淡化,膠囊平滑過渡到新寬度;思考或書寫時標籤帶有閃光。`Label` 用具體進度取代內建文字,例如 "Searching 4 sources";保持一兩個詞。 + +### 頭像 + + + +`Variant="Avatar"` 把標記放在一個圓形、淺色著色的底塊中,用於訊息頭像和圖示按鈕。`Size` 設定底塊大小(預設 32),標記佔其 56%。 + +### 嵌入文字 + + + +把 `Size` 設為文字大小,並設定 `Tone="Inherit"`,標記就會採用周圍文字或按鈕的顏色。 + +### 尺寸 + + + +標記從 12px 到 120px 乃至更大都清晰銳利。光線粗細依算繪尺寸在對數刻度上做光學修正:小標記的光線更粗,小於 18px 時斜線變成圓點。標記在任何狀態下佔位都固定,絕不會擠動周圍版面。 + +### 顏色 + + + +`Tone` 選擇語意顏色:`Accent`(預設)、`Foreground`、`Muted` 或 `Inherit`。`MarkBrush` 可以用任意顏色覆寫它。斜線和核心一律使用同一顏色的較淺色調,出錯時無論原本是什麼顏色,都會過渡到柔和的危險色。 + +### 大型標記 + + + +64 到 96px 的閒置標記能給空白狀態一個形象。`Twinkle="False"` 讓閒置標記完全靜止。 + +### 速度與暫停 + + + +`Speed` 讓所有循環與旋轉速度成倍變化。`IsPaused` 讓標記原地定格;恢復後從同一相位繼續。 + +### 內容指南 + +- 膠囊標籤用一兩個詞說明正在發生什麼("Searching 4 sources"、"Drawing")。 +- 每個進行中的產生只用一個動畫火花。清單有很多列時,顯示靜止標記(`Status="Idle"`、`Twinkle="False"`),只讓作用中的那一列動起來。 + +### 最佳做法 + +- 把「停止」等控制項放在火花旁邊;火花本身不接受輸入。 +- 在裝飾性的副本上設定 `Announce="False"`,讓螢幕報讀器只聽到一個聲音。 + +### 常見問題 + +**能傳入 1em 這樣的 CSS 長度嗎?** 改為把 `Size` 設成周圍文字的字級。 + +**不在畫面上的火花有負擔嗎?** 沒有。循環只在標記運動且位於畫面內時執行,靜止狀態穩定後完全停止。 + +## API 參考 + + + + + +## 無障礙 + +- `Announce` 開啟且設定了狀態時,火花是一個禮貌型(polite)即時區域:狀態變化只播報一次,標記讀作 "AI is thinking",膠囊讀作標籤文字。 +- 設定 `Announce="False"` 時,以及自行播放期間,它以帶無障礙名稱的影像呈現,裝飾性副本因此保持安靜。 +- `AccessibleName` 取代名稱;所有文字都在 `AiSparkLabels` 中。 +- 狀態從不只靠顏色區分:每種狀態都有自己的形狀與動態效果,出錯時光線變短、斜線變暗,膠囊還有文字標籤。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| 無 | 火花是狀態指示,不取得焦點。把「停止」等控制項放在它旁邊。 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 閒置 | 6.4s 呼吸,斜線輪流向外伸展 | 靜止 | 靜止 | +| 思考 | 波峰每 1.9s 繞環一圈,每秒 26 度旋轉 | 靜止姿態 | 靜止姿態 | +| 書寫 | 每 1.04s 一次心跳,每秒 46 度旋轉,兩個環繞粒子(40px 以上) | 靜止姿態 | 靜止姿態 | +| 完成 | 光線 170ms 內收攏,510ms 內無回彈地回到靜止;旋轉以只減不增的緩出速度滑回原位 | 靜止姿態 | 靜止姿態 | +| 出錯 | 460ms 內一次逐漸減弱的搖晃;色調在 320ms 內過渡到柔和的危險色 | 靜止姿態,色調 200ms | 立即 | +| 狀態切換 | 從目前姿態與速度出發約 480ms 過渡;被打斷的過渡成為下一次的起點 | 200ms 過渡 | 立即 | +| 膠囊標籤 | 舊標籤 100ms 淡出,新標籤 160ms 淡入並上升 2px,同時寬度平滑變化 | 短暫交叉淡化 | 立即 | + +每個火花只有一個影格循環,原地更新八道光線路徑、核心與變換,不產生任何配置。火花不在畫面上或被隱藏時循環暫停,靜止狀態穩定後完全停止。收攏與搖晃即使狀態再次改變也總會平滑地完成。 + +## 相關元件 + +- `AgentStatus`:整個代理程式執行的即時狀態。 +- `ToolCall`:代理程式發起的工具呼叫。 +- `AgentPlan`:代理程式行動前寫下的計畫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/en-US.md new file mode 100644 index 00000000..5443d5ff --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/en-US.md @@ -0,0 +1,228 @@ +--- +title: AI translate +summary: A message card where picking a language streams the AI translation in place, sentence by sentence over the dimmed original, with a detected-language chip, a Show original toggle and a status line. +--- + + + +## When to use + +`AiTranslate` translates one message or paragraph on demand, inside your product. A byline with the author and a chip +naming the text's language sits above the text; below it are a Translate picker, a Show original toggle and a status line +led by the model's mark. Picking a language dims the text on screen and streams the translation in place: each new +sentence that starts retires one sentence of the old text, while the card's height springs to fit. Finished translations +are kept, so Show original and switching back crossfade at once. + +- **Messages, comments, reviews or support tickets** written in another language. +- **Release notes or announcements** that readers in several regions open in their own language. +- **Any place an AI translation should feel inline and reversible**, with the original one click away. + +## When not to use + +- **Translating a whole page or app interface**: use your localization framework. +- **Long documents with headings and lists**: the card renders one plain paragraph. +- **Live captions or speech**: use a realtime stream component. + +## Usage + +Give the card its `Text` and wire `Translator` to your model. The translator receives an `AiTranslateRequest` (the text, +the source and target languages and a cancellation token) and returns the translation as an async stream of text deltas. +Yield deltas as your model streams them, or yield the whole translation once: the card paces whatever arrives into chunks +of 3 to 6 words every 34ms either way. + +```csharp +card.Text = message.Body; +card.Translator = TranslateAsync; + +async IAsyncEnumerable TranslateAsync(AiTranslateRequest request) +{ + // Pass the token on: it is cancelled when the reader picks another language or shows the original. + await foreach (var delta in model.StreamAsync(request.Text, request.From, request.To, request.CancellationToken)) + { + yield return delta; + } +} +``` + +Without a translator the card simulates the stream from `Translations` when you pass ready translations. With no `Text` +and no `Simulation` the card is empty: no byline or chip, an empty paragraph, and the Translate picker off until a text +arrives. + +### Simulation + +To show the card without a translator (a demo, a design review, an onboarding tour), give it an `AiTranslateSimulation`: +the message, its byline, the model to credit and translations made ahead of time. Picking a language streams the ready +translation in place after a short wait (a language without one shows the failure), the status says it is simulated and +nothing was sent, and the card tours `Tour` by itself until someone presses a pointer or a key inside it. Your own `Text` +wins over the simulation, and setting `Simulation` to null stops it and shows the empty card. + +```csharp +card.Simulation = new AiTranslateSimulation +{ + Text = "La version 4.2 est disponible.", + Author = new AiTranslateAuthor { Name = "Camille Laurent", Role = "Product lead" }, + ModelName = "Gemini 3.8 Flash", + Translations = new Dictionary + { + [AiTranslateLanguage.English] = "Version 4.2 is out.", + [AiTranslateLanguage.German] = "Version 4.2 ist da.", + }, + Tour = [AiTranslateLanguage.English, AiTranslateLanguage.German], +}; +``` + +The preview above plays a simulation whose release note and translations are the gallery's own demo data. + +### Streaming from your model + + + +The source language is detected from the text with a light check of common words when `SourceLanguage` is not set, and +the chip reads "Detected · German". A failure (an exception from your translator) puts the previous text back, shows +"Couldn't translate" with Retry, keeps the exception's message as the status tooltip and raises `TranslationFailed`. +Cancelled requests never raise it. + +### Ready translations + + + +Pass translations you made ahead of time in `Translations`; the card streams them with its simulation. A language that +is missing shows the failure. Set `SourceLanguage` when you know it: the chip then reads "Written in Spanish". Setting +`Value` to a language other than the source translates as soon as the card is shown. Without an `Author` the card shows +no byline; the chip stays. + +### Following a preferred language + + + +`Value` is the language on screen (two-way); null or the source language means the original. Render one card per message +and set every card's `Value` from a shared preferred language. `ValueChanged` is raised only when the reader picks a +language or toggles Show original, so you can remember the choice. Show original remembers the last translation for +Show translation. + +### Narrow + + + +Below 470px the card pads 16px and the status line drops under the controls, in line with the text; below 380px the chip +shows just the language name. Names and meta trim with an ellipsis. + +### Anatomy + +1. **Byline**: the avatar with initials, the name over the role and timestamp, and the language chip at the far end. +2. **Paragraph**: the text in the language on screen. While a translation streams, the new text comes first and what is + left of the old text follows, dimmed. +3. **Controls**: the Translate picker (the current language's own name once translated), the Show original toggle and, + after a failure, Retry. Each is 32px high and reserves the width of its longest label. +4. **Status line**: the model's mark and a label: the model's name at rest, "Translating to German", "Translated from + French", "Showing original", or a warning icon and "Couldn't translate". + +### States + +| `State` | Paragraph | Status | +|---|---|---| +| `Original` | The text as written | The model's name, then "Showing original" | +| `Translating` | The translation streaming in over the dimmed text | "Translating to German" | +| `Translated` | The translation | "Translated from French" | +| `Failed` | The previous text, back | A warning icon and "Couldn't translate", with Retry | + +### Persisting translations + +`Translated` is raised once a translation has fully arrived, before the card caches it. The cache lives per card and per +text; setting a new `Text` clears it and shows the new original. To keep translations across sessions, store them from +`Translated` and pass them back in `Translations`. + +### Your own words and mark + +`Labels` holds every word the card shows or says, including the language names. `ModelName` is the idle status and +`ModelMark` the 24 × 24 stroked glyph that leads every status; pass your provider's mark. + +```csharp +card.ModelName = "Gemini 3.8 Flash"; +card.Labels = new AiTranslateLabels +{ + Translate = "Traduire", + ShowOriginal = "Voir l'original", + TranslatingTo = language => "Traduction en " + language, +}; +``` + +### Autoplay + +A card playing a simulation tours the simulation's `Tour` (in the preview English, Deutsch, 日本語 and Português), then +the original, and loops; without a `Tour` it visits every language that has a translation. It pauses off screen and stops +for good on the first pointer or key press inside the card. `AutoPlay` turns it on or off for any card (a card of your own +tours every language in its picker); with reduced motion it translates once and stops. `IsSimulated` adds "Simulated, +nothing was sent" to the announcements (on by default without a translator or while a simulation plays). + +### Content guidelines + +- Keep one paragraph or message per card. For a thread, render one card per message. +- The chip names the source language; the picker lists each language by its own name, with the source marked Original. +- Credit the model you actually call in `ModelName`. + +### Best practices + +- Always pass the request's cancellation token to your network call, so switching languages stops the old request. +- Yield deltas as they arrive rather than waiting for the whole translation. +- Give `SourceLanguage` when you know it; detection is a light check meant for short messages. + +### Common questions + +**How are sentences split?** On `.`, `!` and `?` followed by a space, and on `。`, `!` and `?`. Decimals such as 4.2 stay +whole. + +**Does switching languages mid-stream waste the request?** It cancels it: the token is cancelled and the previous text +comes back before the new language streams. + +## API reference + + + + + + + + + + + +## Accessibility + +- The card's accessible name is "Message from" and the author; the paragraph's name is the text on screen, and it reports + busy while a translation streams. +- Starting, finishing, showing the original and failures are announced in a polite status region; while simulated, the + announcement says that nothing was sent. +- The language menu is a group of radio rows with the current language checked. The Show original toggle reports pressed + and its name switches to Show translation. +- Status is never color alone: the model's mark with a label, or a warning icon with "Couldn't translate". +- Every control is at least 32px tall and labels reserve the width of their longest version, so nothing moves under the + pointer. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the Translate picker, the Show original toggle and, after a failure, Retry | +| Enter, Space, Arrow down | On the picker, opens the language menu on the current language | +| Arrow keys, Home, End | Move through the languages; typing a letter jumps to a language. The list loops | +| Enter, Space | Picks the highlighted language and closes the menu. Picking the language that just failed retries it | +| Escape | Closes the menu with focus back on the picker | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A chunk arrives | 3 to 6 words (about 10 characters in Japanese) every 34ms, each fading in over 140ms; no blur, slide or spring | At once | At once | +| Translating starts | The text on screen dims to 45% over 200ms | At once | At once | +| A new sentence starts | One dimmed old sentence leaves | Same | Same | +| A cached translation or the original | One crossfade: the old text leaves in 100ms, the new one fades in over 160ms with a 2px rise | At once | At once | +| The paragraph's height | Follows the text on a critically damped spring (0.24s, no bounce); a window resize cuts straight to the new height | At once | At once | +| Status, picker and toggle labels | Crossfade in their reserved width (100ms out, 160ms in) | Short fade | At once | +| Autoplay | Loops | Translates once and stops | Translates once and stops | + +## Related + +- `ToolCall` for the tool calls an agent makes in the same transcript. +- `DropdownMenu` for a focused list of actions on a trigger. +- `Badge` for small labels like the language chip. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/zh-CN.md new file mode 100644 index 00000000..2a8c387d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/zh-CN.md @@ -0,0 +1,190 @@ +--- +title: AI translate +summary: 一张消息卡片:选择一种语言后,AI 译文在原处流式出现,逐句覆盖变暗的原文,并带有检测到的语言标签、“显示原文”开关和状态行。 +source-hash: 0c94b955a2d4 +--- + + + +## 何时使用 + +`AiTranslate` 在产品内按需翻译一条消息或一段文字。文字上方是作者署名和一个标明文字语言的标签;下方是 Translate 选择器、Show original 开关,以及以模型标志开头的状态行。选择一种语言后,屏幕上的文字会变暗,译文在原处流式出现:每开始一个新句子,就撤下一句旧文字,同时卡片高度以弹簧动画适配内容。完成的译文会被保留,因此“显示原文”和切换回来时都会立即交叉淡化。 + +- **用另一种语言写的消息、评论、评价或工单。** +- **多个地区的读者会用自己的语言打开的发布说明或公告。** +- **任何希望 AI 翻译显得内联、可撤回的地方**,原文只需一次点击。 + +## 何时不用 + +- **翻译整个页面或应用界面**:使用你的本地化框架。 +- **带标题和列表的长文档**:卡片只渲染一段纯文本。 +- **实时字幕或语音**:使用实时流组件。 + +## 用法 + +给卡片设置 `Text`,并把 `Translator` 接到你的模型。翻译函数接收一个 `AiTranslateRequest`(文字、源语言、目标语言和取消令牌),并以文本增量的异步流返回译文。可以在模型流式输出时逐段 yield,也可以一次 yield 整段译文:无论哪种方式,卡片都会把收到的内容按每 34ms 一块、每块 3 到 6 个词的节奏放出。 + +```csharp +card.Text = message.Body; +card.Translator = TranslateAsync; + +async IAsyncEnumerable TranslateAsync(AiTranslateRequest request) +{ + // Pass the token on: it is cancelled when the reader picks another language or shows the original. + await foreach (var delta in model.StreamAsync(request.Text, request.From, request.To, request.CancellationToken)) + { + yield return delta; + } +} +``` + +没有翻译函数时,如果传了现成译文,卡片会用 `Translations` 模拟流式输出。既没有 `Text` 也没有 `Simulation` 时,卡片是空的:没有署名行和语言标签,段落为空,翻译选择器处于关闭状态,直到有文字传入。 + +### 模拟 + +想在没有翻译函数的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `AiTranslateSimulation`:消息、署名、要标注的模型以及预先做好的译文。选择一种语言后,卡片会在短暂等待后原地流式输出现成译文(没有译文的语言会显示失败状态),状态会说明这是模拟、没有发送任何内容,并且卡片会自动依次播放 `Tour`,直到有人在卡片内按下指针或按键。你自己的 `Text` 优先于模拟;把 `Simulation` 设为 null 会停止模拟并显示空卡片。 + +```csharp +card.Simulation = new AiTranslateSimulation +{ + Text = "La version 4.2 est disponible.", + Author = new AiTranslateAuthor { Name = "Camille Laurent", Role = "Product lead" }, + ModelName = "Gemini 3.8 Flash", + Translations = new Dictionary + { + [AiTranslateLanguage.English] = "Version 4.2 is out.", + [AiTranslateLanguage.German] = "Version 4.2 ist da.", + }, + Tour = [AiTranslateLanguage.English, AiTranslateLanguage.German], +}; +``` + +上方的预览播放的就是一个模拟,其版本说明和译文来自图库自己的演示数据。 + +### 从你的模型流式输出 + + + +未设置 `SourceLanguage` 时,卡片会通过检查常见词来粗略检测源语言,标签显示“Detected · German”。失败(翻译函数抛出异常)时,之前的文字会恢复,显示“Couldn't translate”和 Retry,异常消息保留为状态行的提示,并引发 `TranslationFailed`。被取消的请求不会引发它。 + +### 现成译文 + + + +把预先做好的译文传给 `Translations`,卡片会用模拟流式输出它们。缺少的语言会显示失败状态。已知源语言时请设置 `SourceLanguage`,标签会显示“Written in Spanish”。把 `Value` 设为源语言以外的语言时,卡片一显示就开始翻译。没有 `Author` 时卡片不显示署名行,语言标签保留。 + +### 跟随偏好语言 + + + +`Value` 是屏幕上的语言(双向);null 或源语言表示原文。每条消息渲染一张卡片,并用共享的偏好语言设置每张卡片的 `Value`。只有读者选择语言或切换“显示原文”时才会引发 `ValueChanged`,因此可以在其中记住读者的选择。“显示原文”会记住上一次的译文,供“显示译文”使用。 + +### 窄屏 + + + +宽度小于 470px 时,卡片内边距变为 16px,状态行移到控件下方,与正文左对齐;小于 380px 时,标签只显示语言名称。名字和附加信息超出时以省略号截断。 + +### 结构 + +1. **署名行**:带首字母的头像、名字及其下方的角色和时间,最右侧是语言标签。 +2. **正文**:屏幕上语言的文字。译文流式输出时,新文字在前,剩下的旧文字变暗跟在后面。 +3. **控件**:Translate 选择器(翻译后显示当前语言的本地名称)、Show original 开关,以及失败后出现的 Retry。每个控件高 32px,并预留其最长标签的宽度。 +4. **状态行**:模型标志加一个标签:空闲时是模型名称,然后是“Translating to German”“Translated from French”“Showing original”,或警告图标加“Couldn't translate”。 + +### 状态 + +| `State` | 正文 | 状态行 | +|---|---|---| +| `Original` | 原样的文字 | 模型名称,之后是“Showing original” | +| `Translating` | 译文在变暗的文字上流式出现 | “Translating to German” | +| `Translated` | 译文 | “Translated from French” | +| `Failed` | 之前的文字,已恢复 | 警告图标和“Couldn't translate”,附 Retry | + +### 保存译文 + +译文完整到达后、卡片缓存之前会引发 `Translated`。缓存按卡片、按文字保存;设置新的 `Text` 会清空缓存并显示新的原文。要跨会话保留译文,请在 `Translated` 中存储,再通过 `Translations` 传回。 + +### 自定义文字和标志 + +`Labels` 包含卡片显示或朗读的每个词,包括语言名称。`ModelName` 是空闲时的状态,`ModelMark` 是每个状态开头的 24 × 24 描边图形;传入你的服务商的标志。 + +```csharp +card.ModelName = "Gemini 3.8 Flash"; +card.Labels = new AiTranslateLabels +{ + Translate = "Traduire", + ShowOriginal = "Voir l'original", + TranslatingTo = language => "Traduction en " + language, +}; +``` + +### 自动播放 + +播放模拟的卡片会依次切换到模拟的 `Tour` 中的语言(预览中是 English、Deutsch、日本語 和 Português),再回到原文,并循环播放;没有 `Tour` 时会依次播放每种有译文的语言。卡片离开屏幕时暂停,在卡片内第一次按下指针或按键后永久停止。`AutoPlay` 可为任意卡片开启或关闭(你自己的卡片会依次播放选择器中的每种语言);减少动态效果时只翻译一次就停止。`IsSimulated` 会在播报中加上“Simulated, nothing was sent”(没有翻译函数或正在播放模拟时默认开启)。 + +### 内容指南 + +- 每张卡片放一段或一条消息。对话线程中每条消息渲染一张卡片。 +- 标签写源语言;选择器用每种语言的本地名称列出语言,源语言标为 Original。 +- 在 `ModelName` 中写你实际调用的模型。 + +### 最佳实践 + +- 始终把请求的取消令牌传给网络调用,这样切换语言时旧请求会停止。 +- 收到增量就 yield,不要等整段译文完成。 +- 已知源语言时提供 `SourceLanguage`;检测只是为短消息准备的粗略检查。 + +### 常见问题 + +**句子怎么切分?** 在后跟空格的 `.`、`!` 和 `?` 处切分,也在 `。`、`!` 和 `?` 处切分。4.2 这样的小数保持完整。 + +**流式输出途中切换语言会浪费请求吗?** 会取消它:令牌被取消,之前的文字恢复,然后新语言开始流式输出。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 卡片的无障碍名称是“Message from”加作者名;正文的名称是屏幕上的文字,译文流式输出时报告为忙碌。 +- 开始、完成、显示原文和失败都会在礼貌型状态区域中播报;模拟时,播报会说明没有发送任何内容。 +- 语言菜单是一组单选行,当前语言处于选中状态。Show original 开关报告按下状态,名称切换为 Show translation。 +- 状态从不只靠颜色表达:模型标志加标签,或警告图标加“Couldn't translate”。 +- 每个控件至少高 32px,标签预留其最长版本的宽度,指针下的内容不会移动。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过 Translate 选择器、Show original 开关,失败后还有 Retry | +| Enter、Space、Arrow down | 在选择器上打开语言菜单,定位到当前语言 | +| 方向键、Home、End | 在语言间移动;输入字母跳到对应语言。列表循环 | +| Enter、Space | 选择高亮的语言并关闭菜单。选择刚失败的语言会重试 | +| Escape | 关闭菜单,焦点回到选择器 | + +## 动效 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 一块文字到达 | 每 34ms 一块,每块 3 到 6 个词(日语约 10 个字符),各自在 140ms 内淡入;没有模糊、滑动或弹簧 | 立即 | 立即 | +| 开始翻译 | 屏幕上的文字在 200ms 内变暗到 45% | 立即 | 立即 | +| 新句子开始 | 撤下一句变暗的旧句子 | 相同 | 相同 | +| 已缓存的译文或原文 | 一次交叉淡化:旧文字在 100ms 内离开,新文字在 160ms 内淡入并上升 2px | 立即 | 立即 | +| 正文高度 | 以临界阻尼弹簧(0.24s,无回弹)跟随文字;窗口尺寸变化时直接跳到新高度 | 立即 | 立即 | +| 状态、选择器和开关的标签 | 在预留宽度内交叉淡化(100ms 淡出,160ms 淡入) | 短淡化 | 立即 | +| 自动播放 | 循环 | 翻译一次后停止 | 翻译一次后停止 | + +## 相关 + +- `ToolCall`:同一对话记录中智能体发起的工具调用。 +- `DropdownMenu`:锚定在触发器上的一组聚焦操作。 +- `Badge`:类似语言标签的小标签。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/zh-TW.md new file mode 100644 index 00000000..ac0a67ce --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiTranslate/zh-TW.md @@ -0,0 +1,190 @@ +--- +title: AI translate +summary: 一張訊息卡片:選擇一種語言後,AI 譯文會在原處串流出現,逐句覆蓋變暗的原文,並附有偵測到的語言標籤、「顯示原文」開關和狀態列。 +source-hash: 0c94b955a2d4 +--- + + + +## 何時使用 + +`AiTranslate` 在產品內依需求翻譯一則訊息或一段文字。文字上方是作者署名和一個標明文字語言的標籤;下方是 Translate 選擇器、Show original 開關,以及以模型標誌開頭的狀態列。選擇一種語言後,畫面上的文字會變暗,譯文在原處串流出現:每開始一個新句子,就撤下一句舊文字,同時卡片高度以彈簧動畫配合內容。完成的譯文會被保留,因此「顯示原文」和切換回來時都會立即交叉淡化。 + +- **以另一種語言撰寫的訊息、留言、評論或客服單。** +- **多個地區的讀者會以自己的語言開啟的版本說明或公告。** +- **任何希望 AI 翻譯顯得內嵌、可還原的地方**,原文只需點一下。 + +## 何時不用 + +- **翻譯整個頁面或應用程式介面**:請使用你的在地化框架。 +- **含標題和清單的長文件**:卡片只呈現一段純文字。 +- **即時字幕或語音**:請使用即時串流元件。 + +## 用法 + +為卡片設定 `Text`,並把 `Translator` 接到你的模型。翻譯函式會收到一個 `AiTranslateRequest`(文字、來源語言、目標語言和取消權杖),並以文字增量的非同步串流傳回譯文。可以在模型串流輸出時逐段 yield,也可以一次 yield 整段譯文:不論哪種方式,卡片都會把收到的內容以每 34ms 一塊、每塊 3 到 6 個字詞的節奏放出。 + +```csharp +card.Text = message.Body; +card.Translator = TranslateAsync; + +async IAsyncEnumerable TranslateAsync(AiTranslateRequest request) +{ + // Pass the token on: it is cancelled when the reader picks another language or shows the original. + await foreach (var delta in model.StreamAsync(request.Text, request.From, request.To, request.CancellationToken)) + { + yield return delta; + } +} +``` + +沒有翻譯函式時,如果傳入現成譯文,卡片會用 `Translations` 模擬串流輸出。既沒有 `Text` 也沒有 `Simulation` 時,卡片是空的:沒有署名列和語言標籤,段落為空,翻譯選擇器處於關閉狀態,直到有文字傳入。 + +### 模擬 + +想在沒有翻譯函式的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `AiTranslateSimulation`:訊息、署名、要標示的模型以及預先準備好的譯文。選擇一種語言後,卡片會在短暫等待後原地串流輸出現成譯文(沒有譯文的語言會顯示失敗狀態),狀態會說明這是模擬、沒有送出任何內容,並且卡片會自動依序播放 `Tour`,直到有人在卡片內按下指標或按鍵。你自己的 `Text` 優先於模擬;把 `Simulation` 設為 null 會停止模擬並顯示空卡片。 + +```csharp +card.Simulation = new AiTranslateSimulation +{ + Text = "La version 4.2 est disponible.", + Author = new AiTranslateAuthor { Name = "Camille Laurent", Role = "Product lead" }, + ModelName = "Gemini 3.8 Flash", + Translations = new Dictionary + { + [AiTranslateLanguage.English] = "Version 4.2 is out.", + [AiTranslateLanguage.German] = "Version 4.2 ist da.", + }, + Tour = [AiTranslateLanguage.English, AiTranslateLanguage.German], +}; +``` + +上方的預覽播放的就是一個模擬,其版本說明和譯文來自圖庫自己的示範資料。 + +### 從你的模型串流輸出 + + + +未設定 `SourceLanguage` 時,卡片會檢查常見字詞來粗略偵測來源語言,標籤顯示「Detected · German」。失敗(翻譯函式擲回例外)時,先前的文字會還原,顯示「Couldn't translate」和 Retry,例外訊息保留為狀態列的提示,並引發 `TranslationFailed`。已取消的要求不會引發它。 + +### 現成譯文 + + + +把預先準備好的譯文傳給 `Translations`,卡片會以模擬串流輸出它們。缺少的語言會顯示失敗狀態。已知來源語言時請設定 `SourceLanguage`,標籤會顯示「Written in Spanish」。把 `Value` 設為來源語言以外的語言時,卡片一顯示就開始翻譯。沒有 `Author` 時卡片不顯示署名列,語言標籤仍保留。 + +### 跟隨偏好語言 + + + +`Value` 是畫面上的語言(雙向);null 或來源語言代表原文。每則訊息呈現一張卡片,並以共用的偏好語言設定每張卡片的 `Value`。只有讀者選擇語言或切換「顯示原文」時才會引發 `ValueChanged`,因此可以在其中記住讀者的選擇。「顯示原文」會記住上一次的譯文,供「顯示譯文」使用。 + +### 窄螢幕 + + + +寬度小於 470px 時,卡片內距變為 16px,狀態列移到控制項下方,與內文靠左對齊;小於 380px 時,標籤只顯示語言名稱。名字和附加資訊過長時以刪節號截斷。 + +### 結構 + +1. **署名列**:顯示姓名縮寫的頭像、名字及其下方的職稱和時間,最右側是語言標籤。 +2. **內文**:畫面上語言的文字。譯文串流輸出時,新文字在前,剩下的舊文字變暗接在後面。 +3. **控制項**:Translate 選擇器(翻譯後顯示目前語言的原生名稱)、Show original 開關,以及失敗後出現的 Retry。每個控制項高 32px,並預留其最長標籤的寬度。 +4. **狀態列**:模型標誌加一個標籤:閒置時是模型名稱,接著是「Translating to German」「Translated from French」「Showing original」,或警告圖示加「Couldn't translate」。 + +### 狀態 + +| `State` | 內文 | 狀態列 | +|---|---|---| +| `Original` | 原本的文字 | 模型名稱,之後是「Showing original」 | +| `Translating` | 譯文在變暗的文字上串流出現 | 「Translating to German」 | +| `Translated` | 譯文 | 「Translated from French」 | +| `Failed` | 先前的文字,已還原 | 警告圖示和「Couldn't translate」,附 Retry | + +### 保存譯文 + +譯文完整送達後、卡片快取之前會引發 `Translated`。快取依卡片、依文字保存;設定新的 `Text` 會清除快取並顯示新的原文。要跨工作階段保留譯文,請在 `Translated` 中儲存,再透過 `Translations` 傳回。 + +### 自訂文字和標誌 + +`Labels` 包含卡片顯示或朗讀的每個字詞,包括語言名稱。`ModelName` 是閒置時的狀態,`ModelMark` 是每個狀態開頭的 24 × 24 描邊圖形;請傳入你的服務供應商的標誌。 + +```csharp +card.ModelName = "Gemini 3.8 Flash"; +card.Labels = new AiTranslateLabels +{ + Translate = "Traduire", + ShowOriginal = "Voir l'original", + TranslatingTo = language => "Traduction en " + language, +}; +``` + +### 自動播放 + +播放模擬的卡片會依序切換到模擬的 `Tour` 中的語言(預覽中是 English、Deutsch、日本語 和 Português),再回到原文,並循環播放;沒有 `Tour` 時會依序播放每種有譯文的語言。卡片離開畫面時暫停,在卡片內第一次按下指標或按鍵後永久停止。`AutoPlay` 可為任何卡片開啟或關閉(你自己的卡片會依序播放選擇器中的每種語言);減少動態效果時只翻譯一次就停止。`IsSimulated` 會在朗讀中加上「Simulated, nothing was sent」(沒有翻譯函式或正在播放模擬時預設開啟)。 + +### 內容指南 + +- 每張卡片放一段文字或一則訊息。討論串中每則訊息呈現一張卡片。 +- 標籤寫來源語言;選擇器以每種語言的原生名稱列出語言,來源語言標為 Original。 +- 在 `ModelName` 中寫你實際呼叫的模型。 + +### 最佳做法 + +- 一律把要求的取消權杖傳給網路呼叫,這樣切換語言時舊要求就會停止。 +- 收到增量就 yield,不要等整段譯文完成。 +- 已知來源語言時請提供 `SourceLanguage`;偵測只是為短訊息準備的粗略檢查。 + +### 常見問題 + +**句子怎麼切分?** 在後面接空白的 `.`、`!` 和 `?` 處切分,也在 `。`、`!` 和 `?` 處切分。4.2 這類小數保持完整。 + +**串流途中切換語言會浪費要求嗎?** 會取消它:權杖被取消,先前的文字還原,接著新語言開始串流輸出。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 卡片的無障礙名稱是「Message from」加作者名稱;內文的名稱是畫面上的文字,譯文串流輸出時回報為忙碌。 +- 開始、完成、顯示原文和失敗都會在禮貌型狀態區域中朗讀;模擬時,朗讀會說明沒有傳送任何內容。 +- 語言選單是一組單選列,目前語言處於選取狀態。Show original 開關會回報按下狀態,名稱切換為 Show translation。 +- 狀態從不只靠顏色表達:模型標誌加標籤,或警告圖示加「Couldn't translate」。 +- 每個控制項至少高 32px,標籤預留其最長版本的寬度,指標下的內容不會移動。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過 Translate 選擇器、Show original 開關,失敗後還有 Retry | +| Enter、Space、Arrow down | 在選擇器上開啟語言選單,定位到目前語言 | +| 方向鍵、Home、End | 在語言間移動;輸入字母可跳到對應語言。清單會循環 | +| Enter、Space | 選擇反白的語言並關閉選單。選擇剛失敗的語言會重試 | +| Escape | 關閉選單,焦點回到選擇器 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 一塊文字送達 | 每 34ms 一塊,每塊 3 到 6 個字詞(日文約 10 個字元),各自在 140ms 內淡入;沒有模糊、滑動或彈簧 | 立即 | 立即 | +| 開始翻譯 | 畫面上的文字在 200ms 內變暗到 45% | 立即 | 立即 | +| 新句子開始 | 撤下一句變暗的舊句子 | 相同 | 相同 | +| 已快取的譯文或原文 | 一次交叉淡化:舊文字在 100ms 內離開,新文字在 160ms 內淡入並上升 2px | 立即 | 立即 | +| 內文高度 | 以臨界阻尼彈簧(0.24s,無回彈)跟隨文字;視窗大小改變時直接跳到新高度 | 立即 | 立即 | +| 狀態、選擇器和開關的標籤 | 在預留寬度內交叉淡化(100ms 淡出,160ms 淡入) | 短暫淡化 | 立即 | +| 自動播放 | 循環 | 翻譯一次後停止 | 翻譯一次後停止 | + +## 相關 + +- `ToolCall`:同一段對話紀錄中代理發起的工具呼叫。 +- `DropdownMenu`:錨定在觸發器上的一組聚焦操作。 +- `Badge`:類似語言標籤的小標籤。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/en-US.md new file mode 100644 index 00000000..43efe178 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/en-US.md @@ -0,0 +1,261 @@ +--- +title: AI workflow +summary: An automation card where a trigger, an AI step, a branch and actions sit on one rail, a test run glides down it opening each step's output, and steps are added from a compact command menu. +--- + + + +## When to use + +`AiWorkflow` shows a short automation that mixes an app trigger, a model step and app actions, and lets people test it in +place. Steps are compact 32px rows on one hairline rail. Test run moves one thing at a time: a small accent dot glides +down to the next step, the step shimmers while it runs, then it shows its duration and a preview of what it produced. +A branch picks one of its paths and marks the other one Skipped. Steps are added from a small command menu and removed +with a button on their row. + +- **Automation and integrations pages** where people put together a trigger, an AI step and a few actions. +- **Onboarding and product tours** that show where an AI step fits between the apps a team already uses. +- **Reviewing a workflow**: what it does, and what a sample run produced at every step. + +## When not to use + +- **Large graphs** with many branches, loops or merges: use `WorkflowCanvas` from the Workflow package. +- **One tool call, or the live steps of an agent**: use `ToolCall` or `AgentStatus`. +- **The plan an agent wrote**: use `AgentPlan`. +- **Real products with autoplay on**: set `AutoPlay="False"`; the repeating run is for showcases. + +## Usage + +Give the card your flow in `Steps`. With no steps of its own it plays its `Simulation`, a flow you supply. With neither +it shows the empty state: the frame with Test run unavailable and "No steps yet", or the catalog's triggers to start +from when `Catalog` has some. The header says "Simulated" whenever no runner is wired. + +```xml + +``` + +### Steps + +`Steps` is the flow, in order. Each `AiWorkflowStep` has an `Id`, a `Kind` (`Trigger`, `Ai`, `Action` or `Branch`), an +`App` (one of the built-in `AiWorkflowApp`s or your own name and mark), a `Title`, and optionally a `Detail`, the +`Activity` it shows while running, and the `Output`, `Duration` and `Error` a simulated run uses. A branch carries +`Paths` (each an `AiWorkflowBranchPath` with an id, a tab label and its own steps) and must be the last top-level step. + +```csharp +flow.Steps = +[ + new AiWorkflowStep { Id = "mail", Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Mail, Title = "New email", Detail = "support@example.com" }, + new AiWorkflowStep { Id = "label", Kind = AiWorkflowStepKind.Ai, App = AiWorkflowApp.Ai, Title = "Classify with Claude Sonnet 5.5", Activity = "Reading the email" }, + new AiWorkflowStep { Id = "issue", Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Tracker, Title = "Create issue", Detail = "Support team" }, +]; +``` + +The card treats steps as values: put a new list in `Steps` to change them. An empty list shows the empty state, which +lists the catalog's triggers to start from. + +The built-in apps are generic and use Lucide glyphs: `Mail`, `Ai` (a model step, drawn in the accent), `Chat`, +`Tracker`, `Docs`, `Sheets`, `Crm`, `Helpdesk`, `Repo`, `Payments`, `Schedule`, `Webhook` and `Branch`. For a real +product pass `new AiWorkflowApp("id", "Name", geometry)` with your own 24 × 24 stroked glyph, and name a model step's +app after the model's vendor (`isAccent: true` draws its mark in the accent). + +### Simulation + +`AiWorkflowSimulation` holds a flow to play while `Steps` is null: a `Title`, a `Description`, the `Steps` and the add +step `Catalog`. The card shows it and, with `AutoPlay`, test-runs it while it is on screen, repeating after a short pause +until someone presses or types inside it. Each step is simulated from its `Output`, `Duration` and `Error`, and nothing +is sent. Your own `Steps`, `Title`, `Description` and `Catalog` win over it. Adding or removing a step puts the edited +flow in `Steps`; setting `Simulation` to null stops the run and shows the empty state. + +```csharp +flow.Simulation = new AiWorkflowSimulation +{ + Title = "Escalate urgent support email", + Description = "Runs on every new email to support@example.com", + Steps = SupportFlow.Steps(), // List with Output, Duration and Error + Catalog = SupportFlow.Catalog(), // List +}; +``` + +The demos on this page build their flow in `AiWorkflowDemoData`; the model steps name real models from the vendors' pages +(checked on 2026-10-29), and every person, company and number is invented. + +### Running steps for real + + + +Set `RunStep` to run each step against your test endpoint. It gets the step and an `AiWorkflowRunStepContext` with a +`Cancellation` token (cancelled on Stop, a new run, or when the card leaves the window), the `Previous` outputs by step id +and the `Attempt` number. Return one of the outputs, or throw to fail the step with the exception's message. A branch +returns an `AiWorkflowRouteOutput` to pick its path. + +```csharp +flow.RunStep = async (step, context) => +{ + using var response = await http.PostAsync($"/api/workflows/test/{step.Id}", null, context.Cancellation); + response.EnsureSuccessStatusCode(); + return new AiWorkflowTextOutput { Text = await response.Content.ReadAsStringAsync(context.Cancellation) }; +}; +``` + +Without `RunStep` every step is simulated from its `Output` and `Duration`; a step with an `Error` fails on its first +attempt, so Retry can be shown. `RunCompleted` reports a passed run, or one that stopped on a failed step, with the +duration, every output and the failed step's id. `StartRun`, `StopRun` and `Retry` drive the run from code. + +### Outputs + +Each output renders inline in small muted text under its step, aligned with the title: + +| Output | Shows | +|---|---| +| `AiWorkflowEmailOutput` | The sender and the subject on one line, then two lines of the body | +| `AiWorkflowLabelOutput` | A hairline label with a dot, the confidence in monospace, and a reason that streams in | +| `AiWorkflowRouteOutput` | Which path the branch took, with an optional note | +| `AiWorkflowMessageOutput` | The channel and author, then the message | +| `AiWorkflowRecordOutput` | A record key in monospace with its title and one more fact | +| `AiWorkflowTextOutput` | Free text, such as a drafted reply, streamed in | + +### Adding and removing steps + +Add step opens a compact command menu: a search field that filters by title, app or detail, and rows with the app's mark +and a 1 to 9 shortcut. A new step lands at the end of the flow, or at the end of the visible path when the flow ends in a +branch. Each step except the trigger has a remove button. Both put a new list in `Steps` (a two-way property) and raise +`StepsChanged` with the list, the kind of change and the step. + +`Catalog` holds the menu's entries (`AiWorkflowCatalogItem`, a step without an id). Triggers appear only in the empty +state. A branch is offered only while the flow has none. An empty catalog hides the menu. + +### Read only + + + +`IsEditable="False"` hides Add step and the remove buttons. Test run still works. + +### Loading + +`IsLoading` shows three skeleton steps while your flow loads. Test run and the menu are unavailable until it is false. + +### Narrow + + + +The card fills its container up to 640px. Below 480px each row grows to 44px with the detail under the title, the +status slot narrows from 72px to 64px, the header wraps to two lines and the empty state's triggers drop their detail. + +### Your own words + +`Labels` holds every word the card shows or says, including the screen reader names and announcements. Replace the object +to localize or reword: + +```csharp +flow.Labels = new AiWorkflowLabels +{ + TestRun = "Probar", + AddStep = "Añadir paso", + Remove = title => "Quitar " + title, + Passed = duration => "Correcto en " + duration.TotalSeconds.ToString("0.0") + " s", +}; +``` + +### Content guidelines + +- Step titles start with a verb ("Create issue", "Post message"); the detail says where ("#support", "Support team"). +- AI steps name the model in the title ("Classify with Claude Sonnet 5.5"). +- Keep a flow short: one trigger, one or two AI steps, one branch at the end, a few actions per path. + +### Best practices + +- Wire `RunStep` to a test endpoint, return the outputs so the previews render, and throw to show a failure with Retry. +- Keep the step ids stable when you replace the list, so rows and run results stay in place. +- Turn `AutoPlay` off outside showcases and marketing pages. + +### Common questions + +**Why did the autoplay stop?** It stops for good after the first press or key inside the card, so it never runs under +someone's hands. It also waits while the card is off screen. + +**Can a flow have two branches?** No. One branch per flow, as its last top-level step. For larger graphs use +`WorkflowCanvas`. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is a group named by its title (or "Loading workflow"); the steps and each branch path are named lists. +- Every step's name carries its title, app and status ("Create issue, Tracker, Done in 700ms"). Status shows as an icon + and a short label, never color alone. +- A polite live region announces when a run starts, each step's result, a failure with its message, and the outcome, + adding "Simulated, nothing was sent." when no `RunStep` is wired. It also announces added and removed steps. +- The add step menu's search field keeps focus; its help text names the highlighted entry. +- Branch paths are a radio group with roving focus: only the selected tab is a tab stop. +- Remove buttons are named per step ("Remove Create issue"). With a mouse they show while their row is hovered or while + they have keyboard focus; on touch screens they always show. +- Every word is in `AiWorkflowLabels`. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through Test run (or Stop), each step's remove button, the branch path tabs, Retry on a failed step, and Add step | +| Enter, Space | Starts or stops a run, removes a step, retries a failed step, or opens the add step menu | +| Arrow keys | Move between paths in the branch tabs (Home and End jump to the ends); in the open menu, Up and Down move the highlight | +| Type | Filters the add step menu by title, app or detail | +| Enter (menu) | Adds the highlighted step and closes the menu | +| 1 to 9 (menu) | With an empty search, adds the step with that shortcut | +| Escape | Closes the menu and returns focus to Add step | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| The run moves to the next step | A 5px accent dot glides down the connector in 240ms while the line behind it fills | The connector fills at once, no dot | At once | +| A step starts or ends | Its status crossfades (100ms out, 160ms in) in a fixed 72px slot; the detail crossfades to a shimmering activity line | Short fades, no shimmer | At once | +| An output, an error, an added or removed step | Opens or closes on its measured height on a 0.22s spring with no bounce, content fades | At once | At once | +| A reason or a draft | Streams five words every 34ms, each chunk fading in; the final layout is reserved from the first frame | Chunks appear without the fade | All at once | +| Switching paths | The lists crossfade (100ms out, 160ms in) while the height follows; the tab highlight slides on the same spring | 120ms crossfade, height at once | At once | +| The add step menu | Fades and settles in 160ms with no overshoot, leaves in 120ms; filtering animates the list to its new height | Fades | At once | +| Autoplay | Waits while the card is off screen and stops for good after the first press or key inside it | The same | The same | + +The step rows keep their height and the status slot its width in every state, so nothing shifts while a run plays. + +## Related + +- `AgentPlan` for the plan an agent writes before it acts. +- `ToolCall` for a single tool call in an agent transcript. +- `AgentStatus` for the live status of a whole agent run. +- `WorkflowCanvas` for large free-form workflow graphs. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/zh-CN.md new file mode 100644 index 00000000..7aedf4eb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/zh-CN.md @@ -0,0 +1,250 @@ +--- +title: AI workflow +summary: 一张自动化卡片:触发器、AI 步骤、分支和动作排在同一条轨道上,测试运行沿轨道逐步滑下并展开每一步的输出,步骤从紧凑的命令菜单中添加。 +source-hash: 31dbad0538c5 +--- + + + +## 何时使用 + +`AiWorkflow` 展示一个简短的自动化流程:应用触发器、模型步骤和应用动作混在一起,并且可以就地测试。步骤是排在同一条细线轨道上的 +32px 紧凑行。测试运行一次只动一件事:一个小小的强调色圆点滑向下一步,该步骤运行时闪烁,然后显示用时和它产出内容的预览。分支会 +选中其中一条路径,并把另一条标为 Skipped。步骤从一个小命令菜单中添加,用所在行上的按钮删除。 + +- **自动化与集成页面**:人们在这里组合一个触发器、一个 AI 步骤和几个动作。 +- **新手引导与产品导览**:展示 AI 步骤在团队已在使用的应用之间处于什么位置。 +- **复查工作流**:它做了什么,以及一次示例运行在每一步产出了什么。 + +## 何时不用 + +- **大型图**:有很多分支、循环或汇合时,使用 Workflow 包中的 `WorkflowCanvas`。 +- **单个工具调用,或智能体的实时步骤**:使用 `ToolCall` 或 `AgentStatus`。 +- **智能体写下的计划**:使用 `AgentPlan`。 +- **开启自动播放的正式产品**:设置 `AutoPlay="False"`;重复播放的运行只用于展示。 + +## 用法 + +把你的流程放进 `Steps`。卡片自己没有步骤时,会播放它的 `Simulation`,也就是由你提供的流程。两者都没有时显示空状态:只有外框, +测试运行不可用,并显示 "No steps yet";如果 `Catalog` 里有触发器,则列出可作为起点的触发器。只要没有接入运行器,标题栏就会显示 +"Simulated"。 + +```xml + +``` + +### 步骤 + +`Steps` 就是流程,按顺序排列。每个 `AiWorkflowStep` 有 `Id`、`Kind`(`Trigger`、`Ai`、`Action` 或 `Branch`)、`App`(内置的 +`AiWorkflowApp` 之一,或你自己的名称与标记)、`Title`,以及可选的 `Detail`、运行时显示的 `Activity`,还有模拟运行使用的 +`Output`、`Duration` 和 `Error`。分支带有 `Paths`(每条是一个 `AiWorkflowBranchPath`,包含 id、标签页文字和它自己的步骤),并 +且必须是最后一个顶层步骤。 + +```csharp +flow.Steps = +[ + new AiWorkflowStep { Id = "mail", Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Mail, Title = "New email", Detail = "support@example.com" }, + new AiWorkflowStep { Id = "label", Kind = AiWorkflowStepKind.Ai, App = AiWorkflowApp.Ai, Title = "Classify with Claude Sonnet 5.5", Activity = "Reading the email" }, + new AiWorkflowStep { Id = "issue", Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Tracker, Title = "Create issue", Detail = "Support team" }, +]; +``` + +卡片把步骤当作值:要修改步骤,就给 `Steps` 一个新列表。空列表显示空状态,其中列出目录里可作为起点的触发器。 + +内置应用都是通用的,使用 Lucide 图标:`Mail`、`Ai`(模型步骤,以强调色绘制)、`Chat`、`Tracker`、`Docs`、`Sheets`、`Crm`、 +`Helpdesk`、`Repo`、`Payments`、`Schedule`、`Webhook` 和 `Branch`。真实产品请传入 `new AiWorkflowApp("id", "Name", geometry)`, +并使用你自己的 24 × 24 描边图形;模型步骤的应用以模型厂商命名(`isAccent: true` 让它的标记以强调色绘制)。 + +### 模拟 + +`AiWorkflowSimulation` 保存一个在 `Steps` 为 null 时播放的流程:`Title`、`Description`、`Steps` 以及添加步骤用的 `Catalog`。 +卡片显示这个流程;开启 `AutoPlay` 时,只要卡片在屏幕上就会测试运行它,短暂停顿后重复,直到有人在卡片内按下鼠标或按键。每一步都按 +它的 `Output`、`Duration` 和 `Error` 模拟,不会发送任何内容。你自己设置的 `Steps`、`Title`、`Description` 和 `Catalog` 优先于 +它。添加或删除步骤会把编辑后的流程放进 `Steps`;把 `Simulation` 设为 null 会停止运行并显示空状态。 + +```csharp +flow.Simulation = new AiWorkflowSimulation +{ + Title = "Escalate urgent support email", + Description = "Runs on every new email to support@example.com", + Steps = SupportFlow.Steps(), // List with Output, Duration and Error + Catalog = SupportFlow.Catalog(), // List +}; +``` + +本页的演示在 `AiWorkflowDemoData` 中构建流程;模型步骤使用厂商官方页面上的真实模型(核对于 2026-10-29),所有人物、公司和数字 +都是虚构的。 + +### 真实运行步骤 + + + +设置 `RunStep`,即可让每一步调用你的测试端点。它会收到步骤和一个 `AiWorkflowRunStepContext`,其中有 `Cancellation` 令牌(在 +Stop、开始新一轮运行或卡片离开窗口时取消)、按步骤 id 索引的 `Previous` 输出,以及 `Attempt` 次数。返回其中一种输出即可;抛出异常 +则以异常消息让该步骤失败。分支返回 `AiWorkflowRouteOutput` 来选择路径。 + +```csharp +flow.RunStep = async (step, context) => +{ + using var response = await http.PostAsync($"/api/workflows/test/{step.Id}", null, context.Cancellation); + response.EnsureSuccessStatusCode(); + return new AiWorkflowTextOutput { Text = await response.Content.ReadAsStringAsync(context.Cancellation) }; +}; +``` + +没有 `RunStep` 时,每一步都按它的 `Output` 和 `Duration` 模拟;带 `Error` 的步骤在第一次尝试时失败,以便展示 Retry。 +`RunCompleted` 报告通过的运行,或停在失败步骤上的运行,并带上用时、所有输出和失败步骤的 id。`StartRun`、`StopRun` 和 `Retry` +可以从代码驱动运行。 + +### 输出 + +每种输出都以小号的弱化文字内嵌在其步骤下方,与标题对齐: + +| 输出 | 显示 | +|---|---| +| `AiWorkflowEmailOutput` | 一行发件人和主题,然后是两行正文 | +| `AiWorkflowLabelOutput` | 一个带圆点的细线标签、等宽字体的置信度,以及流式出现的理由 | +| `AiWorkflowRouteOutput` | 分支走了哪条路径,可附一条说明 | +| `AiWorkflowMessageOutput` | 频道和作者,然后是消息内容 | +| `AiWorkflowRecordOutput` | 等宽字体的记录编号、它的标题和一条补充信息 | +| `AiWorkflowTextOutput` | 自由文本,例如起草的回复,以流式出现 | + +### 添加与删除步骤 + +Add step 会打开一个紧凑的命令菜单:一个按标题、应用或详情筛选的搜索框,以及带应用标记和 1 到 9 快捷键的行。新步骤放在流程末尾; +流程以分支结尾时,放在当前可见路径的末尾。除触发器外,每个步骤都有删除按钮。两者都会给 `Steps`(双向属性)一个新列表,并触发 +`StepsChanged`,参数中有新列表、变化类型和对应的步骤。 + +`Catalog` 存放菜单的条目(`AiWorkflowCatalogItem`,即没有 id 的步骤)。触发器只出现在空状态中。只有当流程中还没有分支时才提供分 +支。目录为空时隐藏菜单。 + +### 只读 + + + +`IsEditable="False"` 会隐藏 Add step 和删除按钮。Test run 仍然可用。 + +### 加载中 + +`IsLoading` 在流程加载时显示三个骨架步骤。在它变为 false 之前,Test run 和菜单都不可用。 + +### 窄布局 + + + +卡片填满容器,最宽 640px。宽度小于 480px 时,每行增高到 44px,详情移到标题下方,状态槽从 72px 收窄到 64px,标题栏折成两行,空状 +态中的触发器不再显示详情。 + +### 自定义文字 + +`Labels` 包含卡片显示或朗读的每一个词,包括屏幕阅读器名称和播报。替换整个对象即可本地化或改写: + +```csharp +flow.Labels = new AiWorkflowLabels +{ + TestRun = "Probar", + AddStep = "Añadir paso", + Remove = title => "Quitar " + title, + Passed = duration => "Correcto en " + duration.TotalSeconds.ToString("0.0") + " s", +}; +``` + +### 内容准则 + +- 步骤标题以动词开头("Create issue"、"Post message");详情说明位置("#support"、"Support team")。 +- AI 步骤在标题中写出模型名("Classify with Claude Sonnet 5.5")。 +- 保持流程简短:一个触发器,一到两个 AI 步骤,末尾一个分支,每条路径几个动作。 + +### 最佳实践 + +- 把 `RunStep` 接到测试端点,返回输出以便渲染预览;抛出异常即可展示带 Retry 的失败。 +- 替换列表时保持步骤 id 不变,这样行和运行结果都会留在原位。 +- 在展示页和营销页之外关闭 `AutoPlay`。 + +### 常见问题 + +**自动播放为什么停了?** 在卡片内第一次按下鼠标或按键后,它会永久停止,以免在别人操作时运行。卡片不在屏幕上时它也会等待。 + +**一个流程可以有两个分支吗?** 不可以。每个流程只有一个分支,并且是最后一个顶层步骤。更大的图请使用 `WorkflowCanvas`。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片是一个以标题命名的分组(加载时为 "Loading workflow");步骤列表和每条分支路径都是有名称的列表。 +- 每个步骤的名称都包含标题、应用和状态("Create issue, Tracker, Done in 700ms")。状态以图标加简短文字呈现,从不只靠颜色。 +- 一个礼貌的实时区域会播报运行开始、每一步的结果、失败及其消息和最终结果;没有接入 `RunStep` 时会补充 + "Simulated, nothing was sent."。添加和删除步骤也会播报。 +- 添加步骤菜单的搜索框始终保持焦点;它的帮助文字说明当前高亮的条目。 +- 分支路径是一个使用游走焦点的单选组:只有选中的标签页是 Tab 停靠点。 +- 删除按钮按步骤命名("Remove Create issue")。使用鼠标时,它在所在行被悬停或自身获得键盘焦点时显示;在触摸屏上始终显示。 +- 所有文字都在 `AiWorkflowLabels` 中。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过 Test run(或 Stop)、每个步骤的删除按钮、分支路径标签页、失败步骤上的 Retry,以及 Add step | +| Enter、Space | 开始或停止运行、删除步骤、重试失败的步骤,或打开添加步骤菜单 | +| 方向键 | 在分支标签页之间切换路径(Home 和 End 跳到两端);菜单打开时,Up 和 Down 移动高亮 | +| 输入文字 | 按标题、应用或详情筛选添加步骤菜单 | +| Enter(菜单) | 添加高亮的步骤并关闭菜单 | +| 1 到 9(菜单) | 搜索框为空时,添加对应快捷键的步骤 | +| Escape | 关闭菜单,并把焦点还给 Add step | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 运行进入下一步 | 一个 5px 的强调色圆点在 240ms 内沿连接线滑下,身后的线随之填满 | 连接线立即填满,没有圆点 | 立即 | +| 步骤开始或结束 | 状态在固定的 72px 槽中交叉淡化(淡出 100ms,淡入 160ms);详情交叉淡化为闪烁的活动行 | 短暂淡化,不闪烁 | 立即 | +| 输出、错误、添加或删除的步骤 | 以 0.22s 无回弹弹簧按测得的高度展开或收起,内容淡入淡出 | 立即 | 立即 | +| 理由或草稿 | 每 34ms 流式出现五个词,每块淡入;最终排版从第一帧起就预留好 | 分块出现,不淡入 | 一次全部显示 | +| 切换路径 | 列表交叉淡化(淡出 100ms,淡入 160ms),高度随之变化;标签页高亮以同一弹簧滑动 | 120ms 交叉淡化,高度立即变化 | 立即 | +| 添加步骤菜单 | 在 160ms 内淡入并就位,无回弹,120ms 内离开;筛选时列表动画到新的高度 | 淡入淡出 | 立即 | +| 自动播放 | 卡片不在屏幕上时等待,在卡片内第一次按下或按键后永久停止 | 相同 | 相同 | + +在所有状态下,步骤行保持高度,状态槽保持宽度,因此运行播放时没有任何东西移位。 + +## 相关组件 + +- `AgentPlan`:智能体行动前写下的计划。 +- `ToolCall`:智能体对话记录中的单个工具调用。 +- `AgentStatus`:整个智能体运行的实时状态。 +- `WorkflowCanvas`:大型自由形式的工作流图。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/zh-TW.md new file mode 100644 index 00000000..88b22c39 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AiWorkflow/zh-TW.md @@ -0,0 +1,250 @@ +--- +title: AI workflow +summary: 一張自動化卡片:觸發條件、AI 步驟、分支與動作排在同一條軌道上,測試執行沿著軌道逐步滑下並展開每一步的輸出,步驟從精簡的指令選單中新增。 +source-hash: 31dbad0538c5 +--- + + + +## 何時使用 + +`AiWorkflow` 呈現一段簡短的自動化流程:應用程式觸發條件、模型步驟與應用程式動作混在一起,而且可以就地測試。步驟是排在同一條細線 +軌道上的 32px 精簡列。測試執行一次只動一件事:一個小小的強調色圓點滑向下一步,該步驟執行時閃爍,接著顯示耗時與它產出內容的預覽。 +分支會選定其中一條路徑,並把另一條標為 Skipped。步驟從一個小指令選單中新增,用所在列上的按鈕移除。 + +- **自動化與整合頁面**:使用者在這裡組合一個觸發條件、一個 AI 步驟和幾個動作。 +- **新手導覽與產品導覽**:說明 AI 步驟在團隊既有的應用程式之間位於何處。 +- **檢閱工作流程**:它做了什麼,以及一次範例執行在每一步產出了什麼。 + +## 何時不用 + +- **大型圖形**:有許多分支、迴圈或匯合時,請使用 Workflow 套件中的 `WorkflowCanvas`。 +- **單一工具呼叫,或代理程式的即時步驟**:請使用 `ToolCall` 或 `AgentStatus`。 +- **代理程式寫下的計畫**:請使用 `AgentPlan`。 +- **開啟自動播放的正式產品**:請設定 `AutoPlay="False"`;重複播放的執行只用於展示。 + +## 用法 + +把你的流程放進 `Steps`。卡片自己沒有步驟時,會播放它的 `Simulation`,也就是由你提供的流程。兩者都沒有時顯示空狀態:只有外框, +測試執行無法使用,並顯示 "No steps yet";如果 `Catalog` 裡有觸發條件,則列出可作為起點的觸發條件。只要沒有接上執行器,標題列就 +會顯示 "Simulated"。 + +```xml + +``` + +### 步驟 + +`Steps` 就是流程,依序排列。每個 `AiWorkflowStep` 都有 `Id`、`Kind`(`Trigger`、`Ai`、`Action` 或 `Branch`)、`App`(內建的 +`AiWorkflowApp` 之一,或你自己的名稱與標記)、`Title`,以及選用的 `Detail`、執行時顯示的 `Activity`,還有模擬執行使用的 +`Output`、`Duration` 與 `Error`。分支帶有 `Paths`(每條都是一個 `AiWorkflowBranchPath`,包含 id、分頁標籤文字與它自己的步驟), +而且必須是最後一個頂層步驟。 + +```csharp +flow.Steps = +[ + new AiWorkflowStep { Id = "mail", Kind = AiWorkflowStepKind.Trigger, App = AiWorkflowApp.Mail, Title = "New email", Detail = "support@example.com" }, + new AiWorkflowStep { Id = "label", Kind = AiWorkflowStepKind.Ai, App = AiWorkflowApp.Ai, Title = "Classify with Claude Sonnet 5.5", Activity = "Reading the email" }, + new AiWorkflowStep { Id = "issue", Kind = AiWorkflowStepKind.Action, App = AiWorkflowApp.Tracker, Title = "Create issue", Detail = "Support team" }, +]; +``` + +卡片把步驟當成值:要修改步驟,就給 `Steps` 一個新清單。空清單會顯示空狀態,其中列出目錄裡可作為起點的觸發條件。 + +內建應用程式都是通用的,使用 Lucide 圖示:`Mail`、`Ai`(模型步驟,以強調色繪製)、`Chat`、`Tracker`、`Docs`、`Sheets`、`Crm`、 +`Helpdesk`、`Repo`、`Payments`、`Schedule`、`Webhook` 與 `Branch`。真實產品請傳入 `new AiWorkflowApp("id", "Name", geometry)`, +並使用你自己的 24 × 24 描邊圖形;模型步驟的應用程式以模型廠商命名(`isAccent: true` 讓它的標記以強調色繪製)。 + +### 模擬 + +`AiWorkflowSimulation` 保存一個在 `Steps` 為 null 時播放的流程:`Title`、`Description`、`Steps` 以及新增步驟用的 `Catalog`。 +卡片會顯示這個流程;開啟 `AutoPlay` 時,只要卡片在螢幕上就會測試執行它,短暫停頓後重複,直到有人在卡片內按下滑鼠或按鍵。每一步 +都依它的 `Output`、`Duration` 與 `Error` 模擬,不會送出任何內容。你自己設定的 `Steps`、`Title`、`Description` 與 `Catalog` +優先於它。新增或移除步驟會把編輯後的流程放進 `Steps`;把 `Simulation` 設為 null 會停止執行並顯示空狀態。 + +```csharp +flow.Simulation = new AiWorkflowSimulation +{ + Title = "Escalate urgent support email", + Description = "Runs on every new email to support@example.com", + Steps = SupportFlow.Steps(), // List with Output, Duration and Error + Catalog = SupportFlow.Catalog(), // List +}; +``` + +本頁的示範在 `AiWorkflowDemoData` 中建立流程;模型步驟使用廠商官方頁面上的真實模型(核對於 2026-10-29),所有人物、公司與數字 +都是虛構的。 + +### 實際執行步驟 + + + +設定 `RunStep`,就能讓每一步呼叫你的測試端點。它會收到步驟與一個 `AiWorkflowRunStepContext`,其中有 `Cancellation` 權杖(在 +Stop、開始新一輪執行或卡片離開視窗時取消)、依步驟 id 索引的 `Previous` 輸出,以及 `Attempt` 次數。傳回其中一種輸出即可;擲回例 +外則以例外訊息讓該步驟失敗。分支傳回 `AiWorkflowRouteOutput` 來選擇路徑。 + +```csharp +flow.RunStep = async (step, context) => +{ + using var response = await http.PostAsync($"/api/workflows/test/{step.Id}", null, context.Cancellation); + response.EnsureSuccessStatusCode(); + return new AiWorkflowTextOutput { Text = await response.Content.ReadAsStringAsync(context.Cancellation) }; +}; +``` + +沒有 `RunStep` 時,每一步都依它的 `Output` 與 `Duration` 模擬;帶有 `Error` 的步驟會在第一次嘗試時失敗,以便展示 Retry。 +`RunCompleted` 會回報通過的執行,或停在失敗步驟上的執行,並附上耗時、所有輸出與失敗步驟的 id。`StartRun`、`StopRun` 與 `Retry` +可以從程式碼驅動執行。 + +### 輸出 + +每種輸出都以小號的淡化文字內嵌在其步驟下方,與標題對齊: + +| 輸出 | 顯示 | +|---|---| +| `AiWorkflowEmailOutput` | 一行寄件者與主旨,接著是兩行內文 | +| `AiWorkflowLabelOutput` | 一個帶圓點的細線標籤、等寬字型的信心值,以及串流出現的理由 | +| `AiWorkflowRouteOutput` | 分支走了哪條路徑,可附一則說明 | +| `AiWorkflowMessageOutput` | 頻道與作者,接著是訊息內容 | +| `AiWorkflowRecordOutput` | 等寬字型的紀錄編號、它的標題與一則補充資訊 | +| `AiWorkflowTextOutput` | 自由文字,例如草擬的回覆,以串流出現 | + +### 新增與移除步驟 + +Add step 會開啟一個精簡的指令選單:一個依標題、應用程式或詳細資訊篩選的搜尋欄位,以及帶有應用程式標記與 1 到 9 快速鍵的列。新步驟 +會放在流程末端;流程以分支結尾時,放在目前可見路徑的末端。除了觸發條件之外,每個步驟都有移除按鈕。兩者都會給 `Steps`(雙向屬性) +一個新清單,並觸發 `StepsChanged`,參數中有新清單、變更類型與對應的步驟。 + +`Catalog` 存放選單的項目(`AiWorkflowCatalogItem`,也就是沒有 id 的步驟)。觸發條件只會出現在空狀態中。只有在流程中還沒有分支時才 +提供分支。目錄為空時會隱藏選單。 + +### 唯讀 + + + +`IsEditable="False"` 會隱藏 Add step 與移除按鈕。Test run 仍然可以使用。 + +### 載入中 + +`IsLoading` 會在流程載入時顯示三個骨架步驟。在它變成 false 之前,Test run 與選單都無法使用。 + +### 窄版面 + + + +卡片會填滿容器,最寬 640px。寬度小於 480px 時,每一列會增高到 44px,詳細資訊移到標題下方,狀態欄位從 72px 縮到 64px,標題列折成 +兩行,空狀態中的觸發條件不再顯示詳細資訊。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或朗讀的每一個字,包括螢幕閱讀器名稱與播報。替換整個物件即可在地化或改寫: + +```csharp +flow.Labels = new AiWorkflowLabels +{ + TestRun = "Probar", + AddStep = "Añadir paso", + Remove = title => "Quitar " + title, + Passed = duration => "Correcto en " + duration.TotalSeconds.ToString("0.0") + " s", +}; +``` + +### 內容指引 + +- 步驟標題以動詞開頭("Create issue"、"Post message");詳細資訊說明位置("#support"、"Support team")。 +- AI 步驟在標題中寫出模型名稱("Classify with Claude Sonnet 5.5")。 +- 讓流程保持簡短:一個觸發條件、一到兩個 AI 步驟、末端一個分支,每條路徑幾個動作。 + +### 最佳實務 + +- 把 `RunStep` 接到測試端點,傳回輸出以便呈現預覽;擲回例外即可展示帶有 Retry 的失敗。 +- 替換清單時保持步驟 id 不變,這樣列與執行結果都會留在原位。 +- 在展示頁與行銷頁之外關閉 `AutoPlay`。 + +### 常見問題 + +**自動播放為什麼停了?** 在卡片內第一次按下滑鼠或按鍵後,它會永久停止,以免在別人操作時執行。卡片不在螢幕上時它也會等待。 + +**一個流程可以有兩個分支嗎?** 不行。每個流程只有一個分支,而且是最後一個頂層步驟。更大的圖形請使用 `WorkflowCanvas`。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片是一個以標題命名的群組(載入時為 "Loading workflow");步驟清單與每條分支路徑都是有名稱的清單。 +- 每個步驟的名稱都包含標題、應用程式與狀態("Create issue, Tracker, Done in 700ms")。狀態以圖示加上簡短文字呈現,從不只靠顏色。 +- 一個禮貌的即時區域會播報執行開始、每一步的結果、失敗及其訊息與最終結果;沒有接上 `RunStep` 時會補上 + "Simulated, nothing was sent."。新增與移除步驟也會播報。 +- 新增步驟選單的搜尋欄位始終保有焦點;它的說明文字會指出目前醒目提示的項目。 +- 分支路徑是一個使用漫遊焦點的單選群組:只有選取的分頁是 Tab 停駐點。 +- 移除按鈕依步驟命名("Remove Create issue")。使用滑鼠時,它會在所在列被懸停或自身取得鍵盤焦點時顯示;在觸控螢幕上一律顯示。 +- 所有文字都在 `AiWorkflowLabels` 中。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過 Test run(或 Stop)、每個步驟的移除按鈕、分支路徑分頁、失敗步驟上的 Retry,以及 Add step | +| Enter、Space | 開始或停止執行、移除步驟、重試失敗的步驟,或開啟新增步驟選單 | +| 方向鍵 | 在分支分頁之間切換路徑(Home 與 End 跳到兩端);選單開啟時,Up 與 Down 移動醒目提示 | +| 輸入文字 | 依標題、應用程式或詳細資訊篩選新增步驟選單 | +| Enter(選單) | 新增醒目提示的步驟並關閉選單 | +| 1 到 9(選單) | 搜尋欄位為空時,新增對應快速鍵的步驟 | +| Escape | 關閉選單,並把焦點還給 Add step | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 執行進入下一步 | 一個 5px 的強調色圓點在 240ms 內沿連接線滑下,身後的線隨之填滿 | 連接線立即填滿,沒有圓點 | 立即 | +| 步驟開始或結束 | 狀態在固定的 72px 欄位中交叉淡化(淡出 100ms,淡入 160ms);詳細資訊交叉淡化為閃爍的活動列 | 短暫淡化,不閃爍 | 立即 | +| 輸出、錯誤、新增或移除的步驟 | 以 0.22 秒無回彈彈簧依量測高度展開或收合,內容淡入淡出 | 立即 | 立即 | +| 理由或草稿 | 每 34ms 串流出現五個字詞,每塊淡入;最終版面從第一個影格起就預留好 | 分塊出現,不淡入 | 一次全部顯示 | +| 切換路徑 | 清單交叉淡化(淡出 100ms,淡入 160ms),高度隨之變化;分頁醒目提示以同一彈簧滑動 | 120ms 交叉淡化,高度立即變化 | 立即 | +| 新增步驟選單 | 在 160ms 內淡入並定位,無回彈,120ms 內離開;篩選時清單以動畫變為新的高度 | 淡入淡出 | 立即 | +| 自動播放 | 卡片不在螢幕上時等待,在卡片內第一次按下或按鍵後永久停止 | 相同 | 相同 | + +在所有狀態下,步驟列保持高度,狀態欄位保持寬度,因此執行播放時沒有任何東西位移。 + +## 相關 + +- `AgentPlan`:代理程式行動前寫下的計畫。 +- `ToolCall`:代理程式對話紀錄中的單一工具呼叫。 +- `AgentStatus`:整個代理程式執行的即時狀態。 +- `WorkflowCanvas`:大型自由形式的工作流程圖。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/en-US.md new file mode 100644 index 00000000..344a966d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/en-US.md @@ -0,0 +1,235 @@ +--- +title: Ask your data +summary: A natural language to chart card: the question goes in, the SQL streams into a collapsible, syntax tinted panel, the query runs with a rolling row count, and the answer builds as stacked bars, lines or a table with a one line insight underneath. +--- + + + +## When to use + +`AskYourData` turns a question in plain words into a chart in one card. The model's SQL streams into a Generated SQL +panel with syntax tints, the query runs while a count of the rows scanned rolls, and the answer builds as stacked bars, +lines or a table, with a legend, a readout for every category and a one line insight streamed underneath. The SQL stays +one click away, so people can check or copy what ran. + +- **Self serve analytics in a product**: a question box on a dashboard, a metrics page or an admin console. +- **An AI assistant's result view** when the answer is a number that is best read as a chart. +- **Teaching or auditing text to SQL**, where showing the generated query builds trust. + +## When not to use + +- **The query is fixed** and there is no question to ask: use `BarChart` or `LineChart`. +- **A database query as one step of a longer agent transcript**: use `ToolCall`. +- **People write and tune queries by hand**: use a SQL editor. + +## Usage + +Wire the card to your model and a read only database role with three delegates. With none of them it plays its +`Simulation`, answers you supply; with neither it waits in its empty state ("Ask a question about your data") and Ask +stays unavailable. + +```xml + +``` + +Your model and database: + +```csharp +card.Source = new AskYourDataSource { Name = "analytics", Dialect = "PostgreSQL" }; + +// The whole SQL at once: the card reveals it four tokens a tick at its final size. +card.GenerateSql = request => model.WriteSqlAsync(request.Question, request.CancellationToken); + +// Run it read only and shape the rows for the chart. Throw to show the database's message. +card.RunQuery = async request => +{ + var rows = await db.QueryAsync(request.Sql, request.CancellationToken); + request.ReportProgress(rows.Count, rows.Count); + return new AskYourDataResult + { + Title = "Weekly signups by plan", + Categories = WeeksOf(rows), + Series = SeriesByPlan(rows), + RowCount = rows.Count, + DurationMs = rows.Milliseconds, + }; +}; + +card.ResultReady += (_, e) => history.Save(e.Result.Title, e.Sql); +``` + +`AutoRun` is on by default only while a simulation writes the SQL; set it to `True` to ask the starting `Question` as +soon as the card is seen. `Ask()` asks the question in the field, `Ask(question)` puts a new one there first, and +`Stop()` cancels a run. + +### Simulation + +`AskYourDataSimulation` plays a model and a database you describe: the `Question` it asks on its own once the card is +35% on screen, the `Suggestions`, the `Source`, and its `Answers`. A question that contains one of an answer's +`Keywords` gets that answer's `Sql`, `RowsScanned` and `Result`; any other gets the `Fallback` (where `{keyword}` in the +SQL becomes the question's longest word), or no rows. The SQL is revealed after a short pause, the count rolls up in ten +steps of 110ms, then the chart builds and the insight streams; the live region adds that nothing was queried. It stands +in only for the delegates you have not wired, and your own `Question`, `Suggestions` and `Source` win over its own. +Setting `Simulation` to null stops its run and returns to the empty state. + +```csharp +card.Simulation = new AskYourDataSimulation +{ + Question = "Weekly signups by plan since August", + Suggestions = ["Weekly signups by plan since August", "Churn rate by plan per month"], + Source = new AskYourDataSource { Name = "analytics", Dialect = "PostgreSQL" }, + Answers = + [ + new AskYourDataSimulationAnswer + { + Keywords = ["signup", "sign up"], + Sql = "SELECT week, plan, count(*) AS signups FROM signups GROUP BY week, plan;", + RowsScanned = 18_342, + Result = weeklySignups, // an AskYourDataResult you build + }, + ], +}; +``` + +The demos on this page take their answers from `AskYourDataDemoData`: an invented product's signups, MRR and churn. + +### Streaming the SQL + + + +Give `StreamSql` an async stream of chunks from your model instead of `GenerateSql` and the panel grows as they arrive, +each chunk fading in. Call `ReportProgress(rowsScanned, totalRows)` from `RunQuery` to roll the count in the running +state; passing the total reserves its width so the line never shifts. Both delegates get a cancellation token: honour it, +so Stop really cancels. + +```csharp +card.StreamSql = request => model.StreamSqlAsync(request.Question, request.CancellationToken); +``` + +### Results + +`AskYourDataResult` is shaped for the chart: `Categories` are the X axis labels in order, and each `AskYourDataSeries` +has one value per category. Series add up by default (`Stacked`): bars stack, and the legend, the readout and the table +show totals. Set `Stacked = false` for rates and averages; bars then group side by side and nothing is totalled. + +| Property | What it does | +|---|---| +| `Title`, `Subtitle` | The header once the chart is built ("Aug 3 to Oct 25 · 1,430 signups") | +| `LegendValue` | `Sum` for flows such as signups, `Last` for levels such as MRR, `None` for nothing | +| `Chart` | The view this result opens on; otherwise `DefaultChart` | +| `FormatValue` | Formats ticks, the legend, the readout and table cells ($258k, 3.9%) | +| `Insight` | One or two sentences that read the chart, streamed under it with its two lines reserved | +| `RowCount`, `DurationMs` | The SQL meta: "PostgreSQL · 48 rows · 214ms" | + +A result with no categories or series shows "No rows matched" with the suggestions. + +### Errors and stopping + + + +Throw from `GenerateSql`, `StreamSql` or `RunQuery` and the card shows the message verbatim with Try again, and raises +`Failed` with the stage that failed (`Sql` or `Query`). While the card works the Ask button stops the run, and so does +Escape in the field; a stopped run reads "Stopped" and raises nothing. + +### The SQL panel + +The panel opens while the SQL is written and folds once the query starts, before the chart builds, so only one region +moves at a time. `KeepSqlOpen` leaves it open. Once the person opens or closes it during a run, their choice wins. The +SQL is tinted by the library's code highlighter and scrolls sideways in its own well; Copy SQL takes the whole query. + +### Chart views + +The switch changes between bars, lines and a table. Leave `Chart` null and each result opens on its own `Chart` or on +`DefaultChart`; bind `Chart` to control the view yourself. `ChartChanged` is raised when the person switches. + +```xml + +``` + +### Narrow cards + + + +Below 520px the source keeps only its icon, the chart switch moves into the result above the legend at full width, and +the stage grows to fit the wrapped legend and insight. Category labels thin out to stay 46px apart, and wide SQL lines +and tables scroll inside their own wells. + +### Your own words + +`Labels` holds every word the card shows or says; `Placeholder` the field's hint and `Suggestions` the questions offered +in the empty state (an empty list hides them). + +```csharp +card.Labels = new AskYourDataLabels +{ + WritingSql = "SQL wird geschrieben", + RunningQuery = "Abfrage läuft", + Rows = rows => $"{rows} Zeilen", +}; +``` + +### Best practices + +- Put the schema in your model's prompt and run the SQL with a read only role and a statement timeout. +- Keep series to four or fewer for the default palette. +- Let the model write the insight from the returned rows, with concrete numbers that are in the result. +- Batch very fast progress reports to a few a second. + +## API reference + + + + + + + + + + + + + + + +## Accessibility + +- A polite live region narrates the run: writing SQL, running the query, then the chart title, the row count and the + insight. Without `GenerateSql`, `StreamSql` and `RunQuery` it adds "Simulated with sample data, nothing was queried." +- Status is never color alone: a spinner, a drawn check, an alert or a stop glyph sits beside a text label in the SQL row + and the stage header. +- The SQL row is a button that reports expanded or collapsed, named with its title and meta, such as "Generated SQL, + PostgreSQL · 48 rows · 214ms". +- The chart is focusable and named after the result; the arrow keys move a readout that is announced, such as + "Aug 10: Free 61, Pro 26, Team 10, Enterprise 2, total 99". +- The field shifts its border to the accent on focus; every other control shows Arc's keyboard ring. + +### Keyboard + +| Keys | Action | +|---|---| +| Enter | In the question field, asks the question | +| Escape | In the question field, stops a run in progress | +| Tab | Moves through the field, Ask, the Generated SQL row, Copy SQL, the SQL well, the chart switch, the chart, and the suggestions or Try again | +| Enter, Space | On the Generated SQL row, opens or closes the SQL | +| Left, Right, Home, End | On the chart, moves the readout between categories; on the chart switch, changes the view | +| Escape (chart) | Clears the readout | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| SQL streams | Four tokens every 34ms, each chunk fading in over 140ms; laid out first so the panel holds its final size | Whole at once | Whole at once | +| SQL panel opens or folds | 300ms bounce free height spring, content fades | At once | At once | +| Bars | Rise from the baseline in 320ms with at most 180ms of stagger | Drawn whole | Drawn whole | +| Lines | Draw from the left in 460ms | Drawn whole | Drawn whole | +| Switching views | Crossfade, 100ms out and 160ms in, in a fixed area | At once | At once | +| Header and SQL meta | Crossfade as single lines with a 2px settle | Short fade | At once | +| Rows scanned | Roll digit by digit in tabular numerals in reserved width | Change in place | Change in place | +| Insight | Four words a tick with its two lines reserved | Whole at once | Whole at once | +| Spinner and skeleton | Turn and breathe; pause off screen | Still | Still | + +## Related + +- `BarChart` and `LineChart` for a fixed query with no question to ask. +- `ToolCall` for a database query as one step of an agent transcript. +- `ReasoningBlock` for the model's thinking above an answer. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/zh-CN.md new file mode 100644 index 00000000..5fea9448 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/zh-CN.md @@ -0,0 +1,206 @@ +--- +title: Ask your data +summary: 自然语言生成图表的卡片:输入问题,SQL 流式写入一个可折叠、带语法着色的面板,查询运行时行数滚动计数,答案以堆叠柱形、折线或表格逐步呈现,下方附一行洞察。 +source-hash: 8e9aa8eaacfc +--- + + + +## 何时使用 + +`AskYourData` 在一张卡片里把一句白话问题变成图表。模型写出的 SQL 带着语法着色流式写入“Generated SQL”面板,查询运行时已扫描行数滚动计数,答案以堆叠柱形、折线或表格逐步呈现,附带图例、每个类别的读数,以及在下方流式出现的一行洞察。SQL 始终只需一次点击即可查看,方便核对或复制实际执行的内容。 + +- **产品内的自助分析**:仪表板、指标页或管理后台里的提问框。 +- **AI 助手的结果视图**:当答案是一个最适合用图表阅读的数字时。 +- **讲解或审计文本转 SQL**:展示生成的查询能建立信任。 + +## 何时不用 + +- **查询是固定的**,没有问题可问:使用 `BarChart` 或 `LineChart`。 +- **数据库查询只是较长智能体记录中的一步**:使用 `ToolCall`。 +- **由人手写并调优查询**:使用 SQL 编辑器。 + +## 用法 + +通过三个委托把卡片接入你的模型和只读数据库角色。一个委托都没有时,它会播放它的 `Simulation`,也就是由你提供的答案;两者都没有时,它停在空状态(“Ask a question about your data”),提问按钮不可用。 + +```xml + +``` + +你的模型和数据库: + +```csharp +card.Source = new AskYourDataSource { Name = "analytics", Dialect = "PostgreSQL" }; + +// The whole SQL at once: the card reveals it four tokens a tick at its final size. +card.GenerateSql = request => model.WriteSqlAsync(request.Question, request.CancellationToken); + +// Run it read only and shape the rows for the chart. Throw to show the database's message. +card.RunQuery = async request => +{ + var rows = await db.QueryAsync(request.Sql, request.CancellationToken); + request.ReportProgress(rows.Count, rows.Count); + return new AskYourDataResult + { + Title = "Weekly signups by plan", + Categories = WeeksOf(rows), + Series = SeriesByPlan(rows), + RowCount = rows.Count, + DurationMs = rows.Milliseconds, + }; +}; + +card.ResultReady += (_, e) => history.Save(e.Result.Title, e.Sql); +``` + +`AutoRun` 只在由模拟编写 SQL 时默认开启;把它设为 `True`,卡片一出现就会提出初始的 `Question`。`Ask()` 提问输入框里的问题,`Ask(question)` 先把新问题放进输入框再提问,`Stop()` 取消正在进行的运行。 + +### 模拟 + +`AskYourDataSimulation` 播放一个由你描述的模型和数据库:卡片有 35% 出现在屏幕上时自行提出的 `Question`、`Suggestions`、`Source`,以及它的 `Answers`。问题中包含某个答案的 `Keywords` 之一时,得到该答案的 `Sql`、`RowsScanned` 和 `Result`;其他问题得到 `Fallback`(SQL 中的 `{keyword}` 会替换为问题中最长的词),或者没有任何行。SQL 在短暂停顿后逐步显示,计数分十步、每步 110ms 滚动上升,然后图表构建、洞察逐字出现;实时区域会补充说明没有查询任何内容。它只替代你没有接入的委托,你自己设置的 `Question`、`Suggestions` 和 `Source` 优先于它。把 `Simulation` 设为 null 会停止它的运行并回到空状态。 + +```csharp +card.Simulation = new AskYourDataSimulation +{ + Question = "Weekly signups by plan since August", + Suggestions = ["Weekly signups by plan since August", "Churn rate by plan per month"], + Source = new AskYourDataSource { Name = "analytics", Dialect = "PostgreSQL" }, + Answers = + [ + new AskYourDataSimulationAnswer + { + Keywords = ["signup", "sign up"], + Sql = "SELECT week, plan, count(*) AS signups FROM signups GROUP BY week, plan;", + RowsScanned = 18_342, + Result = weeklySignups, // an AskYourDataResult you build + }, + ], +}; +``` + +本页的演示从 `AskYourDataDemoData` 获取答案:一个虚构产品的注册数、MRR 和流失率。 + +### 流式输出 SQL + + + +用 `StreamSql` 代替 `GenerateSql`,传入来自模型的异步分块流,面板会随分块到达而增高,每个分块淡入出现。在 `RunQuery` 中调用 `ReportProgress(rowsScanned, totalRows)` 可让运行状态中的计数滚动;传入总数会预留宽度,这一行就不会跳动。两个委托都会收到取消令牌:请遵守它,这样“停止”才能真正取消。 + +```csharp +card.StreamSql = request => model.StreamSqlAsync(request.Question, request.CancellationToken); +``` + +### 结果 + +`AskYourDataResult` 的结构为图表而设计:`Categories` 是按顺序排列的 X 轴标签,每个 `AskYourDataSeries` 为每个类别提供一个值。各系列默认可以相加(`Stacked`):柱形堆叠,图例、读数和表格显示合计。比率和平均值请设 `Stacked = false`;柱形改为并排分组,也不显示合计。 + +| 属性 | 作用 | +|---|---| +| `Title`、`Subtitle` | 图表建成后的标题("Aug 3 to Oct 25 · 1,430 signups") | +| `LegendValue` | 注册数等流量用 `Sum`,MRR 等存量用 `Last`,不显示用 `None` | +| `Chart` | 该结果打开时的视图;否则使用 `DefaultChart` | +| `FormatValue` | 格式化刻度、图例、读数和表格单元格($258k、3.9%) | +| `Insight` | 解读图表的一两句话,在图表下方流式出现,并预留两行高度 | +| `RowCount`、`DurationMs` | SQL 元信息:"PostgreSQL · 48 rows · 214ms" | + +没有类别或系列的结果会显示 "No rows matched" 并给出建议问题。 + +### 错误与停止 + + + +在 `GenerateSql`、`StreamSql` 或 `RunQuery` 中抛出异常,卡片会原样显示消息并提供“重试”,同时引发 `Failed`,并给出失败的阶段(`Sql` 或 `Query`)。卡片工作期间,“提问”按钮会停止运行,在输入框中按 Escape 也会停止;停止的运行显示 "Stopped",不会引发任何事件。 + +### SQL 面板 + +编写 SQL 时面板展开,查询开始后、图表建成之前折叠,这样同一时刻只有一个区域在动。`KeepSqlOpen` 让它保持展开。一旦用户在运行中自己展开或折叠了面板,就以用户的选择为准。SQL 由库自带的代码高亮着色,并在自己的区域内横向滚动;“复制 SQL”会复制整条查询。 + +### 图表视图 + +切换器在柱形、折线和表格之间切换。`Chart` 保持为 null 时,每个结果按自己的 `Chart` 或 `DefaultChart` 打开;绑定 `Chart` 即可自行控制视图。用户切换时会引发 `ChartChanged`。 + +```xml + +``` + +### 窄卡片 + + + +宽度小于 520px 时,数据源只保留图标,图表切换器移到结果中、图例上方并占满整行,舞台区域会增高以容纳换行后的图例和洞察。类别标签会自动稀疏,保持 46px 的间距;过宽的 SQL 行和表格在各自的区域内滚动。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的所有文字;`Placeholder` 是输入框的提示,`Suggestions` 是空状态中提供的问题(空列表会隐藏它们)。 + +```csharp +card.Labels = new AskYourDataLabels +{ + WritingSql = "SQL wird geschrieben", + RunningQuery = "Abfrage läuft", + Rows = rows => $"{rows} Zeilen", +}; +``` + +### 最佳实践 + +- 把数据库结构写进模型的提示词,并用只读角色和语句超时来运行 SQL。 +- 使用默认配色时,系列最多保持四个。 +- 让模型根据返回的行来写洞察,使用结果中确实存在的具体数字。 +- 极快的进度报告请合并为每秒几次。 + +## API 参考 + + + + + + + + + + + + + + + +## 无障碍 + +- 一个礼貌型实时区域会播报运行过程:正在编写 SQL、正在运行查询,然后是图表标题、行数和洞察。没有 `GenerateSql`、`StreamSql` 和 `RunQuery` 时,还会加上 "Simulated with sample data, nothing was queried." +- 状态从不只靠颜色表达:SQL 行和舞台标题中,文字标签旁总有旋转指示器、绘制出的对勾、警告或停止图标。 +- SQL 行是一个会报告展开或折叠状态的按钮,名称由标题和元信息组成,例如 "Generated SQL, PostgreSQL · 48 rows · 214ms"。 +- 图表可获得焦点,以结果命名;方向键移动读数并播报,例如 "Aug 10: Free 61, Pro 26, Team 10, Enterprise 2, total 99"。 +- 输入框获得焦点时边框变为强调色;其他控件显示 Arc 的键盘焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Enter | 在问题输入框中提问 | +| Escape | 在问题输入框中停止正在进行的运行 | +| Tab | 依次经过输入框、提问按钮、Generated SQL 行、复制 SQL、SQL 区域、图表切换器、图表,以及建议问题或“重试” | +| Enter、Space | 在 Generated SQL 行上展开或折叠 SQL | +| Left、Right、Home、End | 在图表上于类别之间移动读数;在图表切换器上切换视图 | +| Escape(图表) | 清除读数 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| SQL 流式出现 | 每 34ms 四个记号,每个分块用 140ms 淡入;先完成排版,面板保持最终尺寸 | 一次显示全部 | 一次显示全部 | +| SQL 面板展开或折叠 | 300ms 无回弹的高度弹簧,内容淡入淡出 | 立即 | 立即 | +| 柱形 | 从基线升起,用时 320ms,错开最多 180ms | 直接画出 | 直接画出 | +| 折线 | 从左向右绘制,用时 460ms | 直接画出 | 直接画出 | +| 切换视图 | 在固定区域内交叉淡化,淡出 100ms、淡入 160ms | 立即 | 立即 | +| 标题与 SQL 元信息 | 以单行交叉淡化,并有 2px 的落定 | 短暂淡化 | 立即 | +| 已扫描行数 | 等宽数字逐位滚动,宽度预留 | 原地更新 | 原地更新 | +| 洞察 | 每个节拍四个词,预留两行高度 | 一次显示全部 | 一次显示全部 | +| 旋转指示器与骨架 | 旋转与呼吸;离开屏幕时暂停 | 静止 | 静止 | + +## 相关组件 + +- `BarChart` 和 `LineChart`:查询固定、没有问题可问时使用。 +- `ToolCall`:数据库查询作为智能体记录中的一步时使用。 +- `ReasoningBlock`:答案上方展示模型的思考过程。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/zh-TW.md new file mode 100644 index 00000000..fb58ed66 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/AskYourData/zh-TW.md @@ -0,0 +1,206 @@ +--- +title: Ask your data +summary: 以自然語言產生圖表的卡片:輸入問題,SQL 串流寫入一個可收合、帶語法著色的面板,查詢執行時列數滾動計數,答案以堆疊長條、折線或表格逐步呈現,下方附一行洞察。 +source-hash: 8e9aa8eaacfc +--- + + + +## 何時使用 + +`AskYourData` 在一張卡片裡把一句白話問題變成圖表。模型寫出的 SQL 帶著語法著色串流寫入「Generated SQL」面板,查詢執行時已掃描列數滾動計數,答案以堆疊長條、折線或表格逐步呈現,附帶圖例、每個類別的讀數,以及在下方串流出現的一行洞察。SQL 隨時只需點一下即可檢視,方便核對或複製實際執行的內容。 + +- **產品內的自助分析**:儀表板、指標頁面或管理後台裡的提問框。 +- **AI 助理的結果檢視**:當答案是一個最適合用圖表閱讀的數字時。 +- **講解或稽核文字轉 SQL**:呈現產生的查詢能建立信任。 + +## 何時不用 + +- **查詢是固定的**,沒有問題可問:使用 `BarChart` 或 `LineChart`。 +- **資料庫查詢只是較長代理程式紀錄中的一步**:使用 `ToolCall`。 +- **由人手寫並調校查詢**:使用 SQL 編輯器。 + +## 用法 + +透過三個委派把卡片接上你的模型與唯讀資料庫角色。一個委派都沒有時,它會播放它的 `Simulation`,也就是由你提供的答案;兩者都沒有時,它會停在空狀態(「Ask a question about your data」),提問按鈕無法使用。 + +```xml + +``` + +你的模型與資料庫: + +```csharp +card.Source = new AskYourDataSource { Name = "analytics", Dialect = "PostgreSQL" }; + +// The whole SQL at once: the card reveals it four tokens a tick at its final size. +card.GenerateSql = request => model.WriteSqlAsync(request.Question, request.CancellationToken); + +// Run it read only and shape the rows for the chart. Throw to show the database's message. +card.RunQuery = async request => +{ + var rows = await db.QueryAsync(request.Sql, request.CancellationToken); + request.ReportProgress(rows.Count, rows.Count); + return new AskYourDataResult + { + Title = "Weekly signups by plan", + Categories = WeeksOf(rows), + Series = SeriesByPlan(rows), + RowCount = rows.Count, + DurationMs = rows.Milliseconds, + }; +}; + +card.ResultReady += (_, e) => history.Save(e.Result.Title, e.Sql); +``` + +`AutoRun` 只在由模擬撰寫 SQL 時預設開啟;把它設為 `True`,卡片一出現就會提出初始的 `Question`。`Ask()` 提問輸入框裡的問題,`Ask(question)` 先把新問題放進輸入框再提問,`Stop()` 取消正在進行的執行。 + +### 模擬 + +`AskYourDataSimulation` 播放一個由你描述的模型與資料庫:卡片有 35% 出現在螢幕上時自行提出的 `Question`、`Suggestions`、`Source`,以及它的 `Answers`。問題中包含某個答案的 `Keywords` 之一時,會得到該答案的 `Sql`、`RowsScanned` 與 `Result`;其他問題得到 `Fallback`(SQL 中的 `{keyword}` 會換成問題中最長的字詞),或者沒有任何資料列。SQL 在短暫停頓後逐步顯示,計數分十步、每步 110ms 滾動上升,接著圖表建立、洞察逐字出現;即時區域會補充說明沒有查詢任何內容。它只替代你沒有接上的委派,你自己設定的 `Question`、`Suggestions` 與 `Source` 優先於它。把 `Simulation` 設為 null 會停止它的執行並回到空狀態。 + +```csharp +card.Simulation = new AskYourDataSimulation +{ + Question = "Weekly signups by plan since August", + Suggestions = ["Weekly signups by plan since August", "Churn rate by plan per month"], + Source = new AskYourDataSource { Name = "analytics", Dialect = "PostgreSQL" }, + Answers = + [ + new AskYourDataSimulationAnswer + { + Keywords = ["signup", "sign up"], + Sql = "SELECT week, plan, count(*) AS signups FROM signups GROUP BY week, plan;", + RowsScanned = 18_342, + Result = weeklySignups, // an AskYourDataResult you build + }, + ], +}; +``` + +本頁的示範從 `AskYourDataDemoData` 取得答案:一個虛構產品的註冊數、MRR 與流失率。 + +### 串流輸出 SQL + + + +用 `StreamSql` 取代 `GenerateSql`,傳入來自模型的非同步分段串流,面板會隨分段抵達而增高,每個分段淡入出現。在 `RunQuery` 中呼叫 `ReportProgress(rowsScanned, totalRows)` 可讓執行狀態中的計數滾動;傳入總數會預留寬度,這一行就不會跳動。兩個委派都會收到取消權杖:請遵守它,這樣「停止」才能真正取消。 + +```csharp +card.StreamSql = request => model.StreamSqlAsync(request.Question, request.CancellationToken); +``` + +### 結果 + +`AskYourDataResult` 的結構為圖表而設計:`Categories` 是依序排列的 X 軸標籤,每個 `AskYourDataSeries` 為每個類別提供一個值。各數列預設可以相加(`Stacked`):長條堆疊,圖例、讀數與表格顯示合計。比率與平均值請設 `Stacked = false`;長條改為並排分組,也不顯示合計。 + +| 屬性 | 作用 | +|---|---| +| `Title`、`Subtitle` | 圖表建立後的標題("Aug 3 to Oct 25 · 1,430 signups") | +| `LegendValue` | 註冊數等流量用 `Sum`,MRR 等存量用 `Last`,不顯示用 `None` | +| `Chart` | 這個結果開啟時的檢視;否則使用 `DefaultChart` | +| `FormatValue` | 格式化刻度、圖例、讀數與表格儲存格($258k、3.9%) | +| `Insight` | 解讀圖表的一兩句話,在圖表下方串流出現,並預留兩行高度 | +| `RowCount`、`DurationMs` | SQL 中繼資訊:"PostgreSQL · 48 rows · 214ms" | + +沒有類別或數列的結果會顯示 "No rows matched" 並提供建議問題。 + +### 錯誤與停止 + + + +在 `GenerateSql`、`StreamSql` 或 `RunQuery` 中擲回例外,卡片會原樣顯示訊息並提供「重試」,同時引發 `Failed`,並帶出失敗的階段(`Sql` 或 `Query`)。卡片運作期間,「提問」按鈕會停止執行,在輸入框中按 Escape 也會停止;停止的執行顯示 "Stopped",不會引發任何事件。 + +### SQL 面板 + +撰寫 SQL 時面板展開,查詢開始後、圖表建立之前收合,這樣同一時間只有一個區域在動。`KeepSqlOpen` 讓它保持展開。一旦使用者在執行中自行展開或收合了面板,就以使用者的選擇為準。SQL 由函式庫內建的程式碼醒目提示著色,並在自己的區域內橫向捲動;「複製 SQL」會複製整段查詢。 + +### 圖表檢視 + +切換器在長條、折線與表格之間切換。`Chart` 保持為 null 時,每個結果依自己的 `Chart` 或 `DefaultChart` 開啟;繫結 `Chart` 即可自行控制檢視。使用者切換時會引發 `ChartChanged`。 + +```xml + +``` + +### 窄卡片 + + + +寬度小於 520px 時,資料來源只保留圖示,圖表切換器移到結果中、圖例上方並佔滿整列,舞台區域會增高以容納換行後的圖例與洞察。類別標籤會自動疏化,保持 46px 的間距;過寬的 SQL 行與表格在各自的區域內捲動。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或朗讀的所有文字;`Placeholder` 是輸入框的提示,`Suggestions` 是空狀態中提供的問題(空清單會隱藏它們)。 + +```csharp +card.Labels = new AskYourDataLabels +{ + WritingSql = "SQL wird geschrieben", + RunningQuery = "Abfrage läuft", + Rows = rows => $"{rows} Zeilen", +}; +``` + +### 最佳實務 + +- 把資料庫結構寫進模型的提示詞,並用唯讀角色與陳述式逾時來執行 SQL。 +- 使用預設配色時,數列最多保持四個。 +- 讓模型根據傳回的資料列撰寫洞察,使用結果中確實存在的具體數字。 +- 極快的進度回報請合併為每秒幾次。 + +## API 參考 + + + + + + + + + + + + + + + +## 無障礙 + +- 一個禮貌型即時區域會播報執行過程:正在撰寫 SQL、正在執行查詢,接著是圖表標題、列數與洞察。沒有 `GenerateSql`、`StreamSql` 與 `RunQuery` 時,還會加上 "Simulated with sample data, nothing was queried." +- 狀態從不只靠顏色表達:SQL 列與舞台標題中,文字標籤旁總有旋轉指示器、繪製出的勾號、警示或停止圖示。 +- SQL 列是一個會回報展開或收合狀態的按鈕,名稱由標題與中繼資訊組成,例如 "Generated SQL, PostgreSQL · 48 rows · 214ms"。 +- 圖表可取得焦點,並以結果命名;方向鍵移動讀數並播報,例如 "Aug 10: Free 61, Pro 26, Team 10, Enterprise 2, total 99"。 +- 輸入框取得焦點時邊框變為強調色;其他控制項顯示 Arc 的鍵盤焦點環。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Enter | 在問題輸入框中提問 | +| Escape | 在問題輸入框中停止正在進行的執行 | +| Tab | 依序經過輸入框、提問按鈕、Generated SQL 列、複製 SQL、SQL 區域、圖表切換器、圖表,以及建議問題或「重試」 | +| Enter、Space | 在 Generated SQL 列上展開或收合 SQL | +| Left、Right、Home、End | 在圖表上於類別之間移動讀數;在圖表切換器上切換檢視 | +| Escape(圖表) | 清除讀數 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| SQL 串流出現 | 每 34ms 四個語彙單元,每個分段以 140ms 淡入;先完成排版,面板維持最終尺寸 | 一次顯示全部 | 一次顯示全部 | +| SQL 面板展開或收合 | 300ms 無回彈的高度彈簧,內容淡入淡出 | 立即 | 立即 | +| 長條 | 從基線升起,耗時 320ms,錯開最多 180ms | 直接畫出 | 直接畫出 | +| 折線 | 由左向右繪製,耗時 460ms | 直接畫出 | 直接畫出 | +| 切換檢視 | 在固定區域內交叉淡化,淡出 100ms、淡入 160ms | 立即 | 立即 | +| 標題與 SQL 中繼資訊 | 以單行交叉淡化,並有 2px 的落定 | 短暫淡化 | 立即 | +| 已掃描列數 | 等寬數字逐位滾動,寬度預留 | 原地更新 | 原地更新 | +| 洞察 | 每個節拍四個字詞,預留兩行高度 | 一次顯示全部 | 一次顯示全部 | +| 旋轉指示器與骨架 | 旋轉與呼吸;離開螢幕時暫停 | 靜止 | 靜止 | + +## 相關 + +- `BarChart` 與 `LineChart`:查詢固定、沒有問題可問時使用。 +- `ToolCall`:資料庫查詢作為代理程式紀錄中的一步時使用。 +- `ReasoningBlock`:在答案上方呈現模型的思考過程。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/en-US.md new file mode 100644 index 00000000..af8ffb52 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/en-US.md @@ -0,0 +1,169 @@ +--- +title: Building blocks +summary: A loading visual for AI app builders: rounded blocks snap into a tiny app layout with no bounce, fill with content and pick up the accent while the label steps from Laying out to Ready. +--- + + + +## When to use + +`BuildingBlocks` shows an app being assembled while an AI generates it. Rounded blocks for a nav bar, a hero card, a +chart, an avatar row, a settings row and a button fly in and snap into a tiny app layout with no bounce. They land as +tinted placeholders, fill with real-looking content, then pick up the tint in a styling pass, while the status line steps +through Laying out, Adding components, Styling and Ready. + +- **Generative UI products** that need a loading state showing an app, a page or a dashboard being built. +- **An assistant reply that will return a UI**, while the code or the layout streams in. +- **An empty state or an onboarding step** that explains an AI builder at a glance. + +## When not to use + +- **Plain text answers**: use a shimmering label or a thinking indicator instead. +- **Image generation**: reveal the image itself. +- **Real progress with known steps and logs**: use `AgentStatus` or `AgentPlan`, so people see what actually happens. +- **Placeholders that mirror the real layout of a page**: use `Skeleton`. + +## Usage + +Map your request to `Status`: `Building` while it runs, `Done` when the result arrives, `Idle` before the first prompt. A +`Done` that arrives mid-build fast forwards what is left and settles on Ready. Leave `Status` unset to play the built-in +simulated build, which loops with a short hold on Ready. + +```csharp +blocks.Status = BuildingBlocksStatus.Building; +var result = await GenerateAsync(prompt); +blocks.Status = BuildingBlocksStatus.Done; +``` + +### Driven by the app + + + +`Duration` is one build, from the first block to the end of styling, split evenly over the three phases (4.8 seconds by +default, never under 2.4). Set it near your typical generation time. While `Status` is `Building` and `IsLooping` is on, +a long generation keeps re-assembling instead of freezing; with `IsLooping` off it holds on Styling. + +`AllowsReplay` shows Replay on Ready. It raises `ReplayRequested`: start the generation again and set `Status` back to +`Building`. `PhaseChanged` reports every phase the status line shows, for analytics or to keep other UI in step, and +`Phase` holds the current one. + +### Anatomy + +1. **Frame**: the mini app on a muted canvas, with a dashed slot for every block. +2. **Blocks**: a nav bar, a hero card with a product photo, a chart, an avatar row, a settings row with a switch and a + button. +3. **Status line**: a mark (four small blocks while building, a check on Ready), the phase label and Replay. Its height is + fixed, so a new label or Replay never moves anything. + +### Sizes + + + +`Size` sets the width the frame grows to: `Small` 280px, `Medium` 400px (the default) and `Large` 560px. `StageWidth` +takes any width in pixels instead. The frame never grows past its container, and the layout, the radii and the strokes +scale with the width, so it stays in proportion from 240px to 880px. Below 320px the status line uses 12px type. + +### Tone and tint + + + +`Tone` picks the tint for the placeholders, the button, the chart, the switch and the status mark: `Accent` (the default) +or `Neutral`, which uses the foreground color. `Tint` takes any color instead, for a brand or a per-product tint. + +### Play once + + + +Without a status, `IsLooping="False"` plays the simulated build once, rests on Ready and offers Replay, which builds it +again. + +### Without the status line + + + +`IsLabelVisible="False"` hides the status line when the surrounding UI already says what is happening. + +### Your own words + +`Labels` holds every word. Name the thing being built, or translate: + +```csharp +blocks.Labels = new BuildingBlocksLabels +{ + Ready = "Your dashboard is ready", + Replay = "Build again", +}; +``` + +### Reduced motion + + + +With reduced motion the placeholders, the content and the styling arrive as plain crossfades, nothing travels and the +build plays once. With motion off every step changes at once. The phases still advance: they are information, not +decoration. + +### Content guidelines + +- Name the result in the Ready label ("Your CRM is ready") rather than repeating "Ready". +- Keep the simulated build for demos and empty states; in a real product, drive `Status` from the request. +- Do not pretend to report progress: the phases are a pace, not a measurement. + +### Best practices + +- Set `Duration` close to your usual generation time so Ready lines up with the result. +- Use one instance per pending result; on a page that shows many, prefer `Small`. +- Put it where the result will appear (a reply bubble, a canvas, an empty state) so the layout hands over to it. + +### Common questions + +**What if the result arrives early?** Set `Status` to `Done`: every block lands, fills and takes the tint at once, with +short transitions, and the label says Ready. + +**Does it run when it is off screen?** No. The schedule and the status mark pause while the frame is scrolled away, hidden +or in a hidden window, and resume where they were. + +## API reference + + + + + + + +## Accessibility + +- The frame is decorative and hidden from assistive technology; the phase is announced from a polite live region, so + screen readers hear Laying out, Adding components, Styling and Ready. +- The simulated build says so ("Simulated build, nothing was generated" as help text), and while it loops its label is not + announced on every loop. +- Ready pairs a check with the label, so the state never relies on color alone. +- Replay is an Arc `Button` with a visible label. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Reaches Replay when it shows on Ready; the frame takes no focus | +| Enter, Space | Presses Replay | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Blocks arrive | Fly in from just outside their slots and a little smaller on a 0.52s critically damped spring (no overshoot), 40ms apart, all within 200ms of the first | Fade in over 240ms in place | At once | +| Placeholders fill | Cross to neutral content over 320ms, 60ms apart; bars grow from their left edge and the chart line draws in over 600ms | 240ms crossfade | At once | +| Styling pass | Tint, photos and portraits fade in over 400ms, 50ms apart; the switch slides once | 240ms crossfade; the switch changes sides without travel | At once | +| Phase label | Old label fades out in 100ms, the new one fades in over 160ms with a 2px rise, in a fixed line | 120ms crossfade | At once | +| Replay | Fades in on Ready | Fades in | At once | +| Status mark | A light runs round four blocks, 300ms each | One block fills per phase | One block fills per phase | +| Loop | The finished layout fades out in place over 320ms, resets out of sight and builds again over the same dashed slots | Plays once | Plays once | + +Every change starts from what is on screen, so a status change mid-build never cuts. The schedule and the mark pause +while the frame is off screen or hidden. + +## Related + +- `AgentStatus` for the live status of an agent run with real steps. +- `AgentPlan` for a plan an agent follows. +- `Skeleton` for placeholders that mirror a page's real layout. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/zh-CN.md new file mode 100644 index 00000000..093edf9a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/zh-CN.md @@ -0,0 +1,160 @@ +--- +title: Building blocks +summary: 为 AI 应用生成器准备的加载画面:圆角积木无回弹地拼进一个小小的应用布局,填上内容并染上强调色,标签则从“正在布局”一步步走到“完成”。 +source-hash: 99c1dbd6aadf +--- + + + +## 何时使用 + +`BuildingBlocks` 在 AI 生成应用时展示应用正被拼装出来的过程。导航栏、主卡片、图表、头像行、设置行和按钮这些圆角积木飞入, +无回弹地吸附进一个小小的应用布局。它们先以着色的占位块落位,接着填上逼真的内容,再在样式阶段染上主题色;状态行则依次显示 +Laying out、Adding components、Styling 和 Ready。 + +- **生成式 UI 产品**:需要一个加载状态,展示应用、页面或仪表板正在被搭建。 +- **将返回界面的助手回复**:在代码或布局流式到达期间显示。 +- **空状态或引导步骤**:一眼说明 AI 生成器在做什么。 + +## 何时不用 + +- **纯文本回答**:改用闪烁的标签或思考指示。 +- **图像生成**:直接逐步显示图像本身。 +- **步骤和日志都已知的真实进度**:使用 `AgentStatus` 或 `AgentPlan`,让人看到实际发生了什么。 +- **与页面真实布局一致的占位**:使用 `Skeleton`。 + +## 用法 + +把请求状态映射到 `Status`:请求进行中设为 `Building`,结果到达设为 `Done`,第一次提示之前设为 `Idle`。构建中途收到的 +`Done` 会快进剩下的阶段并停在 Ready。不设置 `Status` 时播放内置的模拟构建,它在 Ready 上短暂停留后循环。 + +```csharp +blocks.Status = BuildingBlocksStatus.Building; +var result = await GenerateAsync(prompt); +blocks.Status = BuildingBlocksStatus.Done; +``` + +### 由应用驱动 + + + +`Duration` 是一次构建的时长,从第一块积木到样式结束,平均分给三个阶段(默认 4.8 秒,最少 2.4 秒)。把它设得接近你通常的 +生成时间。`Status` 为 `Building` 且 `IsLooping` 开启时,较长的生成会不断重新拼装而不是停住;关闭 `IsLooping` 则停在 +Styling。 + +`AllowsReplay` 在 Ready 时显示 Replay 按钮。按下会触发 `ReplayRequested`:重新开始生成,并把 `Status` 设回 `Building`。 +`PhaseChanged` 报告状态行显示的每个阶段,可用于统计或让其他界面保持同步,`Phase` 保存当前阶段。 + +### 结构 + +1. **框架**:铺在柔和底色上的迷你应用,每块积木都有一个虚线槽位。 +2. **积木**:导航栏、带商品图的主卡片、图表、头像行、带开关的设置行和按钮。 +3. **状态行**:标记(构建时是四个小方块,Ready 时是对勾)、阶段标签和 Replay。它的高度固定,换标签或出现 Replay 都不会 + 挪动任何东西。 + +### 尺寸 + + + +`Size` 决定框架能长到的宽度:`Small` 280px、`Medium` 400px(默认)和 `Large` 560px。`StageWidth` 可以改用任意像素宽度。 +框架永远不会超出容器,布局、圆角和描边都随宽度缩放,因此在 240px 到 880px 之间都保持比例。窄于 320px 时状态行使用 12px +字号。 + +### 色调与着色 + + + +`Tone` 决定占位块、按钮、图表、开关和状态标记的颜色:`Accent`(默认)或使用前景色的 `Neutral`。`Tint` 可以改用任意颜色, +适合品牌色或按产品着色。 + +### 只播放一次 + + + +没有状态时,`IsLooping="False"` 让模拟构建只播放一次,停在 Ready 并提供 Replay,按下会重新构建。 + +### 不显示状态行 + + + +周围界面已经说明正在发生什么时,用 `IsLabelVisible="False"` 隐藏状态行。 + +### 自定义文字 + +`Labels` 保存所有文字。可以写明正在构建的东西,或进行翻译: + +```csharp +blocks.Labels = new BuildingBlocksLabels +{ + Ready = "Your dashboard is ready", + Replay = "Build again", +}; +``` + +### 减少动效 + + + +减少动效时,占位、内容和样式都以简单的交叉淡化出现,没有位移,构建只播放一次。关闭动效时每一步都立即切换。阶段仍会推进: +它们是信息,而不是装饰。 + +### 内容建议 + +- 在 Ready 标签里写明结果(“Your CRM is ready”),而不是只重复“Ready”。 +- 模拟构建留给演示和空状态;真实产品里请由请求驱动 `Status`。 +- 不要假装汇报进度:阶段只是节奏,不是测量。 + +### 最佳实践 + +- 把 `Duration` 设得接近你通常的生成时间,让 Ready 与结果同时到来。 +- 每个待返回的结果用一个实例;页面上要显示很多个时优先使用 `Small`。 +- 把它放在结果将出现的位置(回复气泡、画布、空状态),让布局自然交接给结果。 + +### 常见问题 + +**结果提前到达怎么办?** 把 `Status` 设为 `Done`:所有积木立即落位、填充并染色,过渡很短,标签显示 Ready。 + +**滚出屏幕时还在运行吗?** 不会。框架被滚走、隐藏或位于隐藏的窗口时,日程和状态标记都会暂停,回来后从原处继续。 + +## API 参考 + + + + + + + +## 无障碍 + +- 框架是装饰性的,对辅助技术隐藏;阶段通过礼貌(polite)的实时区域播报,读屏软件会听到 Laying out、Adding components、 + Styling 和 Ready。 +- 模拟构建会说明自己是模拟的(帮助文本“Simulated build, nothing was generated”),循环播放时不会在每一轮都播报标签。 +- Ready 同时显示对勾和标签,状态从不只依赖颜色。 +- Replay 是带可见文字的 Arc `Button`。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | Ready 时出现 Replay 后可聚焦到它;框架本身不获取焦点 | +| Enter、Space | 按下 Replay | + +## 动效 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 积木到达 | 从槽位外侧稍小的位置飞入,0.52 秒临界阻尼弹簧(无过冲),间隔 40ms,全部在第一块之后 200ms 内出发 | 原地淡入 240ms | 立即 | +| 占位填充 | 320ms 内过渡为中性内容,间隔 60ms;条形从左缘长出,图表折线在 600ms 内画出 | 240ms 交叉淡化 | 立即 | +| 样式阶段 | 着色、照片和头像在 400ms 内淡入,间隔 50ms;开关滑动一次 | 240ms 交叉淡化;开关直接换边,不滑动 | 立即 | +| 阶段标签 | 旧标签 100ms 淡出,新标签 160ms 淡入并上升 2px,行位置固定 | 120ms 交叉淡化 | 立即 | +| Replay | Ready 时淡入 | 淡入 | 立即 | +| 状态标记 | 一道光依次绕过四个小方块,每块 300ms | 每个阶段点亮一块 | 每个阶段点亮一块 | +| 循环 | 完成的布局原地在 320ms 内淡出,在看不见时复位,再在同样的虚线槽位上重新构建 | 只播放一次 | 只播放一次 | + +每次变化都从屏幕上的当前值开始,所以构建中途改变状态也不会生硬切换。框架离开屏幕或被隐藏时,日程和标记都会暂停。 + +## 相关组件 + +- `AgentStatus`:展示含真实步骤的智能体运行状态。 +- `AgentPlan`:智能体遵循的计划。 +- `Skeleton`:与页面真实布局一致的占位。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/zh-TW.md new file mode 100644 index 00000000..ef59444d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/BuildingBlocks/zh-TW.md @@ -0,0 +1,160 @@ +--- +title: Building blocks +summary: 為 AI 應用程式產生器準備的載入畫面:圓角積木無回彈地拼進一個小小的應用程式版面,填上內容並染上強調色,標籤則從「正在配置版面」一步步走到「完成」。 +source-hash: 99c1dbd6aadf +--- + + + +## 何時使用 + +`BuildingBlocks` 在 AI 產生應用程式時,呈現應用程式正被組裝起來的過程。導覽列、主卡片、圖表、頭像列、設定列和按鈕這些圓角積木 +飛入,無回彈地吸附進一個小小的應用程式版面。它們先以著色的預留位置落定,接著填上逼真的內容,再於樣式階段染上主題色;狀態列則 +依序顯示 Laying out、Adding components、Styling 和 Ready。 + +- **生成式 UI 產品**:需要一個載入狀態,呈現應用程式、頁面或儀表板正在被搭建。 +- **將回傳介面的助理回覆**:在程式碼或版面串流抵達期間顯示。 +- **空狀態或導覽步驟**:一眼說明 AI 產生器在做什麼。 + +## 何時不用 + +- **純文字回答**:改用閃爍的標籤或思考指示。 +- **影像生成**:直接逐步顯示影像本身。 +- **步驟與日誌都已知的真實進度**:使用 `AgentStatus` 或 `AgentPlan`,讓人看到實際發生了什麼。 +- **與頁面真實版面一致的預留位置**:使用 `Skeleton`。 + +## 用法 + +把請求狀態對應到 `Status`:請求進行中設為 `Building`,結果抵達設為 `Done`,第一次提示之前設為 `Idle`。建置途中收到的 +`Done` 會快轉剩下的階段並停在 Ready。不設定 `Status` 時播放內建的模擬建置,它在 Ready 上短暫停留後循環。 + +```csharp +blocks.Status = BuildingBlocksStatus.Building; +var result = await GenerateAsync(prompt); +blocks.Status = BuildingBlocksStatus.Done; +``` + +### 由應用程式驅動 + + + +`Duration` 是一次建置的時長,從第一塊積木到樣式結束,平均分配給三個階段(預設 4.8 秒,最少 2.4 秒)。把它設得接近你通常的 +產生時間。`Status` 為 `Building` 且 `IsLooping` 開啟時,較長的產生過程會不斷重新組裝而不是停住;關閉 `IsLooping` 則停在 +Styling。 + +`AllowsReplay` 在 Ready 時顯示 Replay 按鈕。按下會觸發 `ReplayRequested`:重新開始產生,並把 `Status` 設回 `Building`。 +`PhaseChanged` 回報狀態列顯示的每個階段,可用於分析或讓其他介面保持同步,`Phase` 保存目前的階段。 + +### 結構 + +1. **框架**:鋪在柔和底色上的迷你應用程式,每塊積木都有一個虛線槽位。 +2. **積木**:導覽列、帶商品照片的主卡片、圖表、頭像列、帶開關的設定列和按鈕。 +3. **狀態列**:標記(建置時是四個小方塊,Ready 時是勾號)、階段標籤和 Replay。它的高度固定,換標籤或出現 Replay 都不會 + 移動任何東西。 + +### 尺寸 + + + +`Size` 決定框架能長到的寬度:`Small` 280px、`Medium` 400px(預設)和 `Large` 560px。`StageWidth` 可以改用任意像素寬度。 +框架永遠不會超出容器,版面、圓角和描邊都隨寬度縮放,因此在 240px 到 880px 之間都維持比例。窄於 320px 時狀態列使用 12px +字級。 + +### 色調與著色 + + + +`Tone` 決定預留位置、按鈕、圖表、開關和狀態標記的顏色:`Accent`(預設)或使用前景色的 `Neutral`。`Tint` 可以改用任意顏色, +適合品牌色或依產品著色。 + +### 只播放一次 + + + +沒有狀態時,`IsLooping="False"` 讓模擬建置只播放一次,停在 Ready 並提供 Replay,按下會重新建置。 + +### 不顯示狀態列 + + + +周圍介面已經說明正在發生什麼時,用 `IsLabelVisible="False"` 隱藏狀態列。 + +### 自訂文字 + +`Labels` 保存所有文字。可以寫明正在建置的東西,或進行翻譯: + +```csharp +blocks.Labels = new BuildingBlocksLabels +{ + Ready = "Your dashboard is ready", + Replay = "Build again", +}; +``` + +### 減少動態效果 + + + +減少動態效果時,預留位置、內容和樣式都以單純的交叉淡化出現,沒有位移,建置只播放一次。關閉動態效果時每一步都立即切換。階段仍會 +推進:它們是資訊,而不是裝飾。 + +### 內容建議 + +- 在 Ready 標籤裡寫明結果(「Your CRM is ready」),而不是只重複「Ready」。 +- 模擬建置留給展示和空狀態;真實產品裡請由請求驅動 `Status`。 +- 不要假裝回報進度:階段只是節奏,不是量測。 + +### 最佳做法 + +- 把 `Duration` 設得接近你通常的產生時間,讓 Ready 與結果同時到來。 +- 每個待回傳的結果用一個執行個體;頁面上要顯示很多個時優先使用 `Small`。 +- 把它放在結果將出現的位置(回覆泡泡、畫布、空狀態),讓版面自然交接給結果。 + +### 常見問題 + +**結果提前抵達怎麼辦?** 把 `Status` 設為 `Done`:所有積木立即落定、填充並著色,轉場很短,標籤顯示 Ready。 + +**捲出畫面時還在執行嗎?** 不會。框架被捲走、隱藏或位於隱藏的視窗時,排程和狀態標記都會暫停,回來後從原處繼續。 + +## API 參考 + + + + + + + +## 無障礙 + +- 框架是裝飾性的,對輔助科技隱藏;階段透過禮貌(polite)的即時區域朗讀,螢幕閱讀器會聽到 Laying out、Adding components、 + Styling 和 Ready。 +- 模擬建置會說明自己是模擬的(說明文字「Simulated build, nothing was generated」),循環播放時不會在每一輪都朗讀標籤。 +- Ready 同時顯示勾號和標籤,狀態從不只依賴顏色。 +- Replay 是帶可見文字的 Arc `Button`。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | Ready 時出現 Replay 後可聚焦到它;框架本身不取得焦點 | +| Enter、Space | 按下 Replay | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 積木抵達 | 從槽位外側稍小的位置飛入,0.52 秒臨界阻尼彈簧(無過衝),間隔 40ms,全部在第一塊之後 200ms 內出發 | 原地淡入 240ms | 立即 | +| 預留位置填充 | 320ms 內轉為中性內容,間隔 60ms;長條從左緣長出,圖表折線在 600ms 內畫出 | 240ms 交叉淡化 | 立即 | +| 樣式階段 | 著色、照片和頭像在 400ms 內淡入,間隔 50ms;開關滑動一次 | 240ms 交叉淡化;開關直接換邊,不滑動 | 立即 | +| 階段標籤 | 舊標籤 100ms 淡出,新標籤 160ms 淡入並上升 2px,行位置固定 | 120ms 交叉淡化 | 立即 | +| Replay | Ready 時淡入 | 淡入 | 立即 | +| 狀態標記 | 一道光依序繞過四個小方塊,每塊 300ms | 每個階段點亮一塊 | 每個階段點亮一塊 | +| 循環 | 完成的版面原地在 320ms 內淡出,在看不見時重設,再於同樣的虛線槽位上重新建置 | 只播放一次 | 只播放一次 | + +每次變化都從畫面上的目前值開始,所以建置途中改變狀態也不會生硬切換。框架離開畫面或被隱藏時,排程和標記都會暫停。 + +## 相關元件 + +- `AgentStatus`:呈現含真實步驟的代理程式執行狀態。 +- `AgentPlan`:代理程式遵循的計畫。 +- `Skeleton`:與頁面真實版面一致的預留位置。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/en-US.md new file mode 100644 index 00000000..a18ed049 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/en-US.md @@ -0,0 +1,233 @@ +--- +title: Code suggestion +summary: An AI code edit suggestion where the proposed changes stream into a small editor panel as inline diff hunks, and the person accepts or rejects each one, or all at once with Cmd or Ctrl + Enter. +--- + + + +## When to use + +`CodeSuggestion` shows an AI's proposed edit to one file as an inline diff in a small editor panel. The header names the +model and what the person asked for; the panel shows the file with each change as a hunk: removed lines tinted red with +a minus, new lines tinted green with a plus. New lines stream in, then every hunk gets Reject and Accept under it, and +the footer offers Reject all and Accept all. Accepted hunks fold into the file with a smooth height change. + +- **Inline edits in an editor**, where the model rewrites part of a file the person has open. +- **Approving an agent's file change** before it is written, with a clear record of what was kept. +- **Config or SQL change previews**, where every line that changes should be read before it lands. +- **Landing pages and docs** that show an AI coding feature with a realistic, self playing edit. + +## When not to use + +- **Large multi file diffs or side by side comparisons**: use a dedicated diff tool; this card is for one file and a + handful of hunks. +- **Code without changes**: use `CodeBlock`. +- **An agent step that is not a file edit**: use `ToolCall`. +- **Approving actions in general** (emails, payments, deploys): use an approval card, not a code review. + +## Usage + +Give the card the file in `Code`, the changes in `Hunks` and set `Status`. Each `CodeSuggestionHunk` replaces `Remove` +lines starting at the zero based line `Start` with its `Insert` lines (remove 0 inserts before `Start`, an empty insert +deletes). Hunks that overlap one before them are ignored. With no code and no hunks the card shows an empty file. + +```xml + + + +``` + +```csharp +// Write the file once every hunk has a decision; the card never writes anything itself. +private async void OnResolved(object? sender, CodeSuggestionResolvedEventArgs e) +{ + await File.WriteAllTextAsync(path, e.Code); +} +``` + +`DecisionMade` fires for every hunk accepted or rejected, with the file as it now stands, if you prefer to write as the +person goes. `GetCode()` returns the same text at any time, and `Accept`, `Reject`, `AcceptAll` and `RejectAll` decide +from code. + +### Anatomy + +1. **Header**: the model's mark in a tinted square, the instruction as the title and the model's name under it. +2. **File tab**: a two letter language mark, the file name, its folder (from a 520px card) and the running +/− count. +3. **Code**: fixed 20px rows with line numbers; a pending hunk's removed lines are red with a minus, its new lines green + with a plus. Pending new lines have no number yet. +4. **Review bar**: under each pending hunk, its summary with Reject and Accept. +5. **Footer**: the status (an icon and a label) and the actions: Stop, Reject all and Accept all, Regenerate or Retry. + +### Streaming + + + +Set `Status` to `Thinking` while the model reads the file, then to `Streaming` and grow `Hunks` as tokens arrive: add a +hunk with no lines, then append to its lines with `AppendLine` (or replace `Insert`). Each new line opens to its row and +fades in. Set `Status` to `Ready` when the answer is complete; every hunk then gets its review bar. Going back to +`Thinking` clears earlier decisions. + +`AllowsStop` shows Stop while the model reads or writes and raises `StopRequested`: abort your request and set the +status. `AllowsRegenerate` shows Regenerate once the review is done and Retry after an error, and raises +`RegenerateRequested`. + +### States + +| Status | Code | Footer | +|---|---|---| +| `Thinking` | The plain file | A spinner and a shimmering "Reading fetch-invoice.ts", Stop | +| `Streaming` | Hunks as they grow | A spinner and "Writing changes", Stop | +| `Ready` | Hunks with review bars | A dot and "3 changes to review", Reject all and Accept all | +| `Ready`, all decided | The file as it now stands | A check and "Applied 2 of 3 changes" or a cross and "Rejected all 3 changes", Regenerate | +| `Error` | The plain file | An alert and `ErrorText`, Retry | + +### Narrow cards + + + +The card fills its container up to 720px. Long lines scroll sideways inside the code area while the line numbers stay +pinned on the left and each review bar stays pinned to the visible width, so Accept is always in reach. The folder hides +below a 520px card. Below 440px the gutter narrows, code drops to 12px, hunk summaries hide (the buttons keep their +names for screen readers) and the footer stacks the status above full width actions. It holds down to 320px. + +### Long files + + + +`MaxCodeHeight` caps the code area; longer files scroll inside it. `AreShortcutsEnabled="False"` turns the keyboard +shortcuts off (Accept all then shows no hint), and `Model="{x:Null}"` hides the model line. + +### The model and the file + +`Model` takes an `AgentModel`: its `Name` goes under the title and its `Mark` (a 24 × 24 stroked glyph) into the header +square, in `MarkBrush` or the text color. `FileName` picks the highlighting language from its extension; any language +the library's code highlighter knows is colored, others show in the plain text color. `FileMark` replaces the two +letter language mark with your own content, and `ShowsFileMark="False"` hides it. + +### Simulation + +For a demo or a prototype without a model, give the card a `CodeSuggestionSimulation` with the file and the hunks to +play, and set the file name, the instruction and the model on the card as usual. While the card has no code and no +hunks of its own it plays the simulation: it reads the file for 0.8s, streams the hunks (three take about 1.6s), then +accepts and rejects them on its own and replays about 2.6s after it finishes. Any pointer, key or focus inside the card +hands control to the person for that run; `Restart()` starts a new run. `AutoPlay="False"` keeps the streaming but +leaves the review to the person. Setting `Code` or `Hunks` ends the simulation. The preview's model is a real one, +credited with a neutral mark; no brand mark is drawn. + +```xml + + + + + +``` + +```csharp +card.Simulation = new CodeSuggestionSimulation +{ + Code = recordedFile, + Hunks = [new CodeSuggestionHunk("greet", 0, 1, "export const greet = (name: string) => `Hello, ${name}`;")], +}; +``` + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +card.Labels = new CodeSuggestionLabels +{ + Accept = "Übernehmen", + Reject = "Verwerfen", + ToReview = count => $"{count} Änderungen offen", +}; +``` + +### Reduced motion + +With reduced motion the card opens on the finished suggestion with no streaming and no autoplay, and folds happen at +once. Decisions still apply and the status still updates. + +### Content guidelines + +- Give each hunk a summary of two to six words ("Add retry limits"); it names the buttons for screen readers. +- Keep hunks sorted and non overlapping. Map a unified diff with `Start` as the zero based line of the first removed + line (or the insert position), `Remove` as the removed line count and `Insert` as the added lines. +- Say in surrounding copy when content is a sample. + +### Best practices + +- Write the file in `Resolved`, or progressively in `DecisionMade`; never assume a suggestion was applied. +- Stream by appending to `Insert`; do not rebuild the hunk list on every token. +- Wire `StopRequested` to abort the request. + +### Common questions + +**Can the person decide while the model is still writing?** No: review bars appear once the status is `Ready`. + +**What happens to overlapping hunks?** A hunk that touches lines an earlier hunk already changes is ignored. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- The card is a labelled group ("Code suggestion: Add retry with backoff") and the code is a focusable region named + after the file. +- In the code's description, each pending removed or added line starts with "Removed:" or "Added:", so the diff never + relies on the red and green tints; the visible minus and plus signs carry it for sighted readers too. +- Accept and Reject name their hunk ("Accept Retry 429 and 5xx with backoff"). Accept all exposes its shortcut. +- A polite live region speaks the state ("3 changes to review"), each decision ("Accepted: Add retry limits. 2 changes + to review") and the result. A simulation adds "Simulated, nothing was written to disk." Streaming is announced once, + not per line. +- The status always pairs an icon with a label: a spinner for reading and writing, a dot for review, a check for + applied, a cross for rejected and an alert for errors. +- Focus never falls back to the page when the pressed control leaves with its hunk or state. Pointer focus draws no + ring; keyboard focus uses Arc's shared focus ring. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the code area, then each hunk's Reject and Accept, then the footer actions | +| Enter, Space | Presses the focused Accept, Reject, Accept all, Reject all, Stop or Regenerate; after a hunk is decided, focus moves to the next pending hunk's Accept, or to the code area when none are left | +| Cmd or Ctrl + Enter | Accepts every pending hunk while focus is inside the card | +| Cmd or Ctrl + Backspace | Rejects every pending hunk while focus is inside the card | +| Arrow left, Arrow right | Scrolls long lines when the code area has focus | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Thinking | About 0.8s with a calm shimmer on the label (simulation) | No shimmer, no simulated streaming | At once | +| A hunk opens | Its removed lines tint red over 240ms | At once | At once | +| A new line streams in | Opens to its 20px row in 120ms while it fades in, one every 38ms (simulation) | At once | At once | +| Review bars appear | All at once, on a critically damped spring (0.24s, no bounce) | At once | At once | +| Accept | Removed lines and the bar fold to zero on the settle spring (0.26s) so the code below glides up; the green tint and plus signs fade over 240ms | At once | At once | +| Reject | New lines and the bar fold the same way; the red tint fades | At once | At once | +| Footer status | Icon, label and actions each crossfade in place (out 100ms, in 160ms, the label rises 2px); the icon only swaps when its kind changes | Short crossfade | At once | + +Rows are a fixed 20px and folds move between known heights, so nothing is measured mid animation. Each line is laid out +once per text, so streaming a new line never re-highlights the others. The simulation runs on single timers, starts only +when the card is in view and waits while it is scrolled away. + +## Related + +- `CodeBlock` for code without changes. +- `ToolCall` for agent steps that are not file edits. +- `AgentPlan` for the plan an agent writes before it edits anything. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/zh-CN.md new file mode 100644 index 00000000..4244eb94 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/zh-CN.md @@ -0,0 +1,197 @@ +--- +title: Code suggestion +summary: AI 代码修改建议:模型提出的改动以行内差异块的形式流式进入一个小型编辑器面板,由人逐个接受或拒绝,也可以用 Cmd 或 Ctrl + Enter 一次全部接受。 +source-hash: 37c31166bfe2 +--- + + + +## 何时使用 + +`CodeSuggestion` 在一个小型编辑器面板中,以行内差异的形式展示 AI 对单个文件提出的修改。标题栏写明模型和用户的要求;面板显示文件,每处改动是一个差异块:删除的行以红色底并带减号,新增的行以绿色底并带加号。新行逐行流入,随后每个差异块下方出现"拒绝"和"接受",底栏提供"全部拒绝"和"全部接受"。被接受的差异块以平滑的高度变化并入文件。 + +- **编辑器中的行内修改**:模型改写用户正在打开的文件的一部分。 +- **批准智能体的文件改动**:在写入之前审阅,并清楚记录保留了哪些内容。 +- **配置或 SQL 改动预览**:每一行改动都应在生效前被读到。 +- **落地页和文档**:用一个逼真、自动播放的修改来展示 AI 编程功能。 + +## 何时不用 + +- **跨多个文件的大型差异或左右对比**:使用专门的差异工具;这张卡片只用于一个文件和少量差异块。 +- **没有改动的代码**:使用 `CodeBlock`。 +- **不是文件修改的智能体步骤**:使用 `ToolCall`。 +- **一般性的操作审批**(邮件、付款、部署):使用审批卡片,而不是代码审阅。 + +## 用法 + +把文件放进 `Code`,把改动放进 `Hunks`,并设置 `Status`。每个 `CodeSuggestionHunk` 从从零开始计数的第 `Start` 行起,用它的 `Insert` 行替换 `Remove` 行(remove 为 0 时在 `Start` 之前插入,insert 为空时表示删除)。与前面某个差异块重叠的差异块会被忽略。既没有代码也没有差异块时,卡片显示一个空文件。 + +```xml + + + +``` + +```csharp +// 每个差异块都有了决定后再写入文件;卡片自己从不写入任何内容。 +private async void OnResolved(object? sender, CodeSuggestionResolvedEventArgs e) +{ + await File.WriteAllTextAsync(path, e.Code); +} +``` + +如果希望随着用户的操作逐步写入,每接受或拒绝一个差异块都会触发 `DecisionMade`,并附带文件当前的内容。`GetCode()` 随时返回同样的文本,`Accept`、`Reject`、`AcceptAll` 和 `RejectAll` 可在代码中做出决定。 + +### 结构 + +1. **标题栏**:着色方块中的模型标记,作为标题的用户要求,以及其下方的模型名称。 +2. **文件标签**:两个字母的语言标记、文件名、所在文件夹(卡片宽度达到 520px 时显示)以及实时的 +/− 计数。 +3. **代码**:固定 20px 高的行,带行号;待定差异块的删除行为红色并带减号,新增行为绿色并带加号。待定的新增行还没有行号。 +4. **审阅栏**:每个待定差异块下方,显示它的摘要以及"拒绝"和"接受"。 +5. **底栏**:状态(一个图标加一段文字)以及操作:停止、全部拒绝与全部接受、重新生成或重试。 + +### 流式 + + + +模型读取文件时把 `Status` 设为 `Thinking`,然后设为 `Streaming`,并随着令牌到达扩充 `Hunks`:先添加一个没有行的差异块,再用 `AppendLine`(或替换 `Insert`)追加行。每个新行展开到自己的行高并淡入。回答完整后把 `Status` 设为 `Ready`,此时每个差异块都会出现审阅栏。回到 `Thinking` 会清除之前的决定。 + +`AllowsStop` 会在模型读取或写入时显示"停止",并引发 `StopRequested`:中止你的请求并设置状态。`AllowsRegenerate` 会在审阅完成后显示"重新生成",出错后显示"重试",并引发 `RegenerateRequested`。 + +### 状态 + +| 状态 | 代码 | 底栏 | +|---|---|---| +| `Thinking` | 原始文件 | 旋转图标和闪烁的 "Reading fetch-invoice.ts",停止 | +| `Streaming` | 正在增长的差异块 | 旋转图标和 "Writing changes",停止 | +| `Ready` | 带审阅栏的差异块 | 圆点和 "3 changes to review",全部拒绝和全部接受 | +| `Ready`,全部已决定 | 文件当前的样子 | 对勾和 "Applied 2 of 3 changes",或叉号和 "Rejected all 3 changes",重新生成 | +| `Error` | 原始文件 | 警示图标和 `ErrorText`,重试 | + +### 窄卡片 + + + +卡片填满容器,最宽 720px。长行在代码区域内横向滚动,而行号固定在左侧,每个审阅栏固定在可见宽度内,因此"接受"始终触手可及。卡片窄于 520px 时隐藏文件夹。窄于 440px 时,行号栏变窄,代码缩小到 12px,差异块摘要隐藏(按钮仍保留供屏幕阅读器使用的名称),底栏把状态叠放在通栏操作按钮上方。最窄可到 320px。 + +### 长文件 + + + +`MaxCodeHeight` 限制代码区域的高度;更长的文件在其中滚动。`AreShortcutsEnabled="False"` 关闭键盘快捷键(此时"全部接受"不显示提示),`Model="{x:Null}"` 隐藏模型那一行。 + +### 模型与文件 + +`Model` 接受一个 `AgentModel`:它的 `Name` 显示在标题下方,它的 `Mark`(24 × 24 的描边图形)显示在标题栏的方块中,颜色为 `MarkBrush` 或文字颜色。`FileName` 根据扩展名选择高亮语言;库中代码高亮器认识的语言都会着色,其他语言以普通文字颜色显示。`FileMark` 用你自己的内容替换两个字母的语言标记,`ShowsFileMark="False"` 则将其隐藏。 + +### 模拟 + +用于没有模型的演示或原型时,给卡片一个 `CodeSuggestionSimulation`,传入要播放的文件和差异块,文件名、指令和模型照常设置在卡片上。卡片没有自己的代码和差异块时就播放这段模拟:读取文件 0.8 秒,流入差异块(三个约需 1.6 秒),然后自行接受和拒绝,结束约 2.6 秒后重播。卡片内的任何指针、按键或焦点都会把这一轮交给用户控制;`Restart()` 会开始新的一轮。`AutoPlay="False"` 保留流式过程,但把审阅留给用户。设置 `Code` 或 `Hunks` 后模拟即结束。预览中的模型是真实的模型,配以中性的标识,不会绘制任何品牌标记。 + +```xml + + + + + +``` + +```csharp +card.Simulation = new CodeSuggestionSimulation +{ + Code = recordedFile, + Hunks = [new CodeSuggestionHunk("greet", 0, 1, "export const greet = (name: string) => `Hello, ${name}`;")], +}; +``` + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的每一个词。替换整个对象即可本地化或改写: + +```csharp +card.Labels = new CodeSuggestionLabels +{ + Accept = "Übernehmen", + Reject = "Verwerfen", + ToReview = count => $"{count} Änderungen offen", +}; +``` + +### 减少动态效果 + +开启减少动态效果时,卡片直接显示已完成的建议,没有流式过程和自动播放,折叠立即完成。决定仍然生效,状态也照常更新。 + +### 内容准则 + +- 为每个差异块写一个两到六个词的摘要("Add retry limits");它会成为屏幕阅读器读出的按钮名称。 +- 让差异块保持有序且互不重叠。从统一差异格式映射时,`Start` 是第一行删除行(或插入位置)从零开始的行号,`Remove` 是删除的行数,`Insert` 是新增的行。 +- 内容是示例时,请在周围的文案中说明。 + +### 最佳实践 + +- 在 `Resolved` 中写入文件,或在 `DecisionMade` 中逐步写入;不要假定建议已经被应用。 +- 通过追加 `Insert` 来流式更新;不要在每个令牌到达时重建差异块列表。 +- 把 `StopRequested` 接到中止请求的逻辑上。 + +### 常见问题 + +**模型还在写入时,用户能做决定吗?** 不能:状态变为 `Ready` 后才会出现审阅栏。 + +**重叠的差异块会怎样?** 触及前面某个差异块已改动的行的差异块会被忽略。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 卡片是一个带标签的组("Code suggestion: Add retry with backoff"),代码是以文件名命名的可聚焦区域。 +- 在代码的描述中,每个待定的删除行或新增行都以 "Removed:" 或 "Added:" 开头,因此差异从不只依赖红色和绿色底色;可见的减号和加号也为视力正常的读者传达了这一信息。 +- "接受"和"拒绝"以所属差异块命名("Accept Retry 429 and 5xx with backoff")。"全部接受"公开了它的快捷键。 +- 一个礼貌型实时区域会朗读状态("3 changes to review")、每个决定("Accepted: Add retry limits. 2 changes to review")以及最终结果。模拟还会加上 "Simulated, nothing was written to disk."。流式过程只朗读一次,而不是每行一次。 +- 状态总是图标加文字:读取和写入时是旋转图标,审阅时是圆点,已应用是对勾,已拒绝是叉号,出错是警示图标。 +- 被按下的控件随其差异块或状态离开时,焦点不会落回页面。指针聚焦不显示焦点环;键盘聚焦使用 Arc 统一的焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 先移到代码区域,再依次移到每个差异块的"拒绝"和"接受",然后是底栏操作 | +| Enter、Space | 按下获得焦点的接受、拒绝、全部接受、全部拒绝、停止或重新生成;一个差异块决定后,焦点移到下一个待定差异块的"接受",没有剩余时移到代码区域 | +| Cmd 或 Ctrl + Enter | 焦点在卡片内时,接受所有待定差异块 | +| Cmd 或 Ctrl + Backspace | 焦点在卡片内时,拒绝所有待定差异块 | +| 左方向键、右方向键 | 代码区域获得焦点时横向滚动长行 | + +## 动效 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 思考 | 约 0.8 秒,标签上有平缓的闪烁(模拟) | 无闪烁,模拟不流式 | 立即 | +| 差异块出现 | 其删除行在 240ms 内染成红色 | 立即 | 立即 | +| 新行流入 | 在 120ms 内展开到 20px 行高并淡入,每 38ms 一行(模拟) | 立即 | 立即 | +| 审阅栏出现 | 同时出现,使用临界阻尼弹簧(0.24 秒,无回弹) | 立即 | 立即 | +| 接受 | 删除行和审阅栏以 settle 弹簧(0.26 秒)折叠为零,下方代码随之上移;绿色底色和加号在 240ms 内淡出 | 立即 | 立即 | +| 拒绝 | 新增行和审阅栏以同样方式折叠;红色底色淡出 | 立即 | 立即 | +| 底栏状态 | 图标、文字和操作各自原地交叉淡化(淡出 100ms,淡入 160ms,文字上移 2px);图标只在种类变化时切换 | 短暂交叉淡化 | 立即 | + +行高固定为 20px,折叠在已知高度之间进行,因此动画过程中不需要测量任何内容。每一行按文本只排版一次,所以流入新行不会重新高亮其他行。模拟只使用单次计时器,卡片进入视口后才开始,滚出视口时暂停等待。 + +## 相关 + +- `CodeBlock`:展示没有改动的代码。 +- `ToolCall`:不是文件修改的智能体步骤。 +- `AgentPlan`:智能体在修改任何内容之前写下的计划。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/zh-TW.md new file mode 100644 index 00000000..cf90fd07 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/CodeSuggestion/zh-TW.md @@ -0,0 +1,197 @@ +--- +title: Code suggestion +summary: AI 程式碼修改建議:模型提出的變更以行內差異區塊的形式串流進一個小型編輯器面板,由使用者逐一接受或拒絕,也可以用 Cmd 或 Ctrl + Enter 一次全部接受。 +source-hash: 37c31166bfe2 +--- + + + +## 何時使用 + +`CodeSuggestion` 在一個小型編輯器面板中,以行內差異的形式呈現 AI 對單一檔案提出的修改。標題列寫明模型與使用者的要求;面板顯示檔案,每處變更是一個差異區塊:刪除的行以紅色底並帶減號,新增的行以綠色底並帶加號。新行逐行串流進來,接著每個差異區塊下方會出現「拒絕」與「接受」,頁尾提供「全部拒絕」與「全部接受」。被接受的差異區塊會以平順的高度變化併入檔案。 + +- **編輯器中的行內修改**:模型改寫使用者正在開啟的檔案的一部分。 +- **核准代理程式的檔案變更**:在寫入之前審閱,並清楚記錄保留了哪些內容。 +- **設定或 SQL 變更預覽**:每一行變更都應在生效前被讀到。 +- **形象頁面與文件**:用一個逼真、自動播放的修改來展示 AI 程式設計功能。 + +## 何時不用 + +- **橫跨多個檔案的大型差異或左右對照**:請使用專門的差異工具;這張卡片只適用於一個檔案和少量差異區塊。 +- **沒有變更的程式碼**:請使用 `CodeBlock`。 +- **不是檔案修改的代理程式步驟**:請使用 `ToolCall`。 +- **一般性的動作核准**(電子郵件、付款、部署):請使用核准卡片,而不是程式碼審閱。 + +## 用法 + +把檔案放進 `Code`,把變更放進 `Hunks`,並設定 `Status`。每個 `CodeSuggestionHunk` 從由零起算的第 `Start` 行開始,用它的 `Insert` 行取代 `Remove` 行(remove 為 0 時在 `Start` 之前插入,insert 為空時表示刪除)。與前面某個差異區塊重疊的差異區塊會被忽略。既沒有程式碼也沒有差異區塊時,卡片顯示一個空白檔案。 + +```xml + + + +``` + +```csharp +// 每個差異區塊都有了決定後再寫入檔案;卡片本身從不寫入任何內容。 +private async void OnResolved(object? sender, CodeSuggestionResolvedEventArgs e) +{ + await File.WriteAllTextAsync(path, e.Code); +} +``` + +若想隨著使用者的操作逐步寫入,每接受或拒絕一個差異區塊都會觸發 `DecisionMade`,並附上檔案目前的內容。`GetCode()` 隨時回傳同樣的文字,`Accept`、`Reject`、`AcceptAll` 和 `RejectAll` 可在程式碼中做出決定。 + +### 結構 + +1. **標題列**:著色方塊中的模型標記、作為標題的使用者要求,以及其下方的模型名稱。 +2. **檔案分頁**:兩個字母的語言標記、檔名、所在資料夾(卡片寬度達 520px 時顯示)以及即時的 +/− 計數。 +3. **程式碼**:固定 20px 高的行,附行號;待定差異區塊的刪除行為紅色並帶減號,新增行為綠色並帶加號。待定的新增行還沒有行號。 +4. **審閱列**:每個待定差異區塊下方,顯示它的摘要以及「拒絕」與「接受」。 +5. **頁尾**:狀態(一個圖示加一段文字)以及動作:停止、全部拒絕與全部接受、重新產生或重試。 + +### 串流 + + + +模型讀取檔案時把 `Status` 設為 `Thinking`,接著設為 `Streaming`,並隨著詞元抵達擴充 `Hunks`:先加入一個沒有行的差異區塊,再用 `AppendLine`(或取代 `Insert`)附加行。每個新行會展開到自己的行高並淡入。回答完整後把 `Status` 設為 `Ready`,此時每個差異區塊都會出現審閱列。回到 `Thinking` 會清除先前的決定。 + +`AllowsStop` 會在模型讀取或寫入時顯示「停止」,並引發 `StopRequested`:中止你的請求並設定狀態。`AllowsRegenerate` 會在審閱完成後顯示「重新產生」,出錯後顯示「重試」,並引發 `RegenerateRequested`。 + +### 狀態 + +| 狀態 | 程式碼 | 頁尾 | +|---|---|---| +| `Thinking` | 原始檔案 | 旋轉圖示和閃爍的 "Reading fetch-invoice.ts",停止 | +| `Streaming` | 正在增長的差異區塊 | 旋轉圖示和 "Writing changes",停止 | +| `Ready` | 附審閱列的差異區塊 | 圓點和 "3 changes to review",全部拒絕和全部接受 | +| `Ready`,全部已決定 | 檔案目前的樣子 | 勾號和 "Applied 2 of 3 changes",或叉號和 "Rejected all 3 changes",重新產生 | +| `Error` | 原始檔案 | 警示圖示和 `ErrorText`,重試 | + +### 窄卡片 + + + +卡片會填滿容器,最寬 720px。長行在程式碼區域內水平捲動,而行號固定在左側,每個審閱列固定在可見寬度內,因此「接受」始終觸手可及。卡片窄於 520px 時隱藏資料夾。窄於 440px 時,行號欄變窄,程式碼縮小到 12px,差異區塊摘要隱藏(按鈕仍保留供螢幕閱讀器使用的名稱),頁尾把狀態疊放在滿版動作按鈕上方。最窄可到 320px。 + +### 長檔案 + + + +`MaxCodeHeight` 限制程式碼區域的高度;更長的檔案會在其中捲動。`AreShortcutsEnabled="False"` 會關閉鍵盤快速鍵(此時「全部接受」不顯示提示),`Model="{x:Null}"` 會隱藏模型那一行。 + +### 模型與檔案 + +`Model` 接受一個 `AgentModel`:它的 `Name` 顯示在標題下方,它的 `Mark`(24 × 24 的描邊圖形)顯示在標題列的方塊中,顏色為 `MarkBrush` 或文字顏色。`FileName` 會依副檔名選擇醒目提示語言;函式庫的程式碼醒目提示器認得的語言都會上色,其他語言則以一般文字顏色顯示。`FileMark` 用你自己的內容取代兩個字母的語言標記,`ShowsFileMark="False"` 則會將其隱藏。 + +### 模擬 + +用於沒有模型的示範或原型時,給卡片一個 `CodeSuggestionSimulation`,傳入要播放的檔案與差異區塊,檔名、指令與模型照常設定在卡片上。卡片沒有自己的程式碼與差異區塊時就播放這段模擬:讀取檔案 0.8 秒,串流進差異區塊(三個約需 1.6 秒),接著自行接受與拒絕,結束約 2.6 秒後重播。卡片內的任何指標、按鍵或焦點都會把這一輪交給使用者控制;`Restart()` 會開始新的一輪。`AutoPlay="False"` 會保留串流過程,但把審閱留給使用者。設定 `Code` 或 `Hunks` 後模擬即結束。預覽中的模型是真實的模型,搭配中性的標誌,不會繪製任何品牌標誌。 + +```xml + + + + + +``` + +```csharp +card.Simulation = new CodeSuggestionSimulation +{ + Code = recordedFile, + Hunks = [new CodeSuggestionHunk("greet", 0, 1, "export const greet = (name: string) => `Hello, ${name}`;")], +}; +``` + +### 自訂文字 + +`Labels` 包含卡片顯示或朗讀的每一個字詞。取代整個物件即可在地化或改寫: + +```csharp +card.Labels = new CodeSuggestionLabels +{ + Accept = "Übernehmen", + Reject = "Verwerfen", + ToReview = count => $"{count} Änderungen offen", +}; +``` + +### 減少動態效果 + +開啟減少動態效果時,卡片會直接顯示已完成的建議,沒有串流過程與自動播放,摺疊也會立即完成。決定仍會生效,狀態也照常更新。 + +### 內容準則 + +- 為每個差異區塊寫一個兩到六個字詞的摘要("Add retry limits");它會成為螢幕閱讀器讀出的按鈕名稱。 +- 讓差異區塊保持排序且互不重疊。從統一差異格式對應時,`Start` 是第一行刪除行(或插入位置)由零起算的行號,`Remove` 是刪除的行數,`Insert` 是新增的行。 +- 內容是範例時,請在周圍的文字中說明。 + +### 最佳做法 + +- 在 `Resolved` 中寫入檔案,或在 `DecisionMade` 中逐步寫入;不要假設建議已經被套用。 +- 以附加 `Insert` 的方式串流更新;不要在每個詞元抵達時重建差異區塊清單。 +- 把 `StopRequested` 接到中止請求的邏輯上。 + +### 常見問題 + +**模型還在寫入時,使用者可以做決定嗎?** 不行:狀態變為 `Ready` 後才會出現審閱列。 + +**重疊的差異區塊會怎樣?** 觸及前面某個差異區塊已變更之行的差異區塊會被忽略。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 卡片是一個附標籤的群組("Code suggestion: Add retry with backoff"),程式碼是以檔名命名的可聚焦區域。 +- 在程式碼的描述中,每個待定的刪除行或新增行都以 "Removed:" 或 "Added:" 開頭,因此差異從不只依賴紅色與綠色底色;可見的減號與加號也為視力正常的讀者傳達了這項資訊。 +- 「接受」與「拒絕」以所屬差異區塊命名("Accept Retry 429 and 5xx with backoff")。「全部接受」公開了它的快速鍵。 +- 一個禮貌型即時區域會朗讀狀態("3 changes to review")、每個決定("Accepted: Add retry limits. 2 changes to review")以及最終結果。模擬還會加上 "Simulated, nothing was written to disk."。串流過程只朗讀一次,而不是每行一次。 +- 狀態一律是圖示加文字:讀取與寫入時是旋轉圖示,審閱時是圓點,已套用是勾號,已拒絕是叉號,出錯是警示圖示。 +- 被按下的控制項隨其差異區塊或狀態離開時,焦點不會落回頁面。指標聚焦不顯示焦點環;鍵盤聚焦使用 Arc 共用的焦點環。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 先移到程式碼區域,再依序移到每個差異區塊的「拒絕」與「接受」,接著是頁尾動作 | +| Enter、Space | 按下取得焦點的接受、拒絕、全部接受、全部拒絕、停止或重新產生;一個差異區塊決定後,焦點移到下一個待定差異區塊的「接受」,沒有剩餘時移到程式碼區域 | +| Cmd 或 Ctrl + Enter | 焦點在卡片內時,接受所有待定差異區塊 | +| Cmd 或 Ctrl + Backspace | 焦點在卡片內時,拒絕所有待定差異區塊 | +| 左方向鍵、右方向鍵 | 程式碼區域取得焦點時水平捲動長行 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 思考 | 約 0.8 秒,標籤上有平緩的閃爍(模擬) | 無閃爍,模擬不串流 | 立即 | +| 差異區塊出現 | 其刪除行在 240ms 內染成紅色 | 立即 | 立即 | +| 新行串流進來 | 在 120ms 內展開到 20px 行高並淡入,每 38ms 一行(模擬) | 立即 | 立即 | +| 審閱列出現 | 同時出現,使用臨界阻尼彈簧(0.24 秒,無回彈) | 立即 | 立即 | +| 接受 | 刪除行與審閱列以 settle 彈簧(0.26 秒)摺疊為零,下方程式碼隨之上移;綠色底色與加號在 240ms 內淡出 | 立即 | 立即 | +| 拒絕 | 新增行與審閱列以同樣方式摺疊;紅色底色淡出 | 立即 | 立即 | +| 頁尾狀態 | 圖示、文字與動作各自原地交叉淡化(淡出 100ms,淡入 160ms,文字上移 2px);圖示只在種類變化時切換 | 短暫交叉淡化 | 立即 | + +行高固定為 20px,摺疊在已知高度之間進行,因此動畫過程中不需要測量任何內容。每一行依文字只排版一次,所以串流進新行不會重新醒目提示其他行。模擬只使用單次計時器,卡片進入可視範圍後才開始,捲出可視範圍時暫停等待。 + +## 相關 + +- `CodeBlock`:呈現沒有變更的程式碼。 +- `ToolCall`:不是檔案修改的代理程式步驟。 +- `AgentPlan`:代理程式在修改任何內容之前寫下的計畫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/en-US.md new file mode 100644 index 00000000..749b7548 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/en-US.md @@ -0,0 +1,215 @@ +--- +title: Connector setup +summary: A "Connect your tools" card for an AI assistant: each app connects inline through redirect, authorize and a first sync with a progress hairline, then lists the scopes it was granted and offers Disconnect with an inline confirmation. +--- + + + +## When to use + +`ConnectorSetup` is the card where people let an assistant into the apps their team uses. Each app is one row with its +mark, its name and one status line. Pressing Connect runs the whole handshake in that row: the sign-in opens, the person +approves, and the first sync counts what it read while a 2px hairline fills along the row. A connected app shows its +account and a Connected button that opens the scopes the assistant holds, with Disconnect and an inline confirmation. + +- **An assistant's settings page** where people connect and review workspace apps. +- **A first-run checklist** that should show the OAuth handshake and the first sync without leaving the page. +- **Reviewing access**: which scopes the assistant holds in each app, and revoking them. + +## When not to use + +- **Browsing dozens of integrations by category**: use the `Integrations` block, a marketplace grid with filters. +- **Approving one action the agent wants to take**: use `ToolCall` with its approval, or a `Dialog`. +- **Showing a tool the agent is calling right now**: use `ToolCall`. + +## Usage + +Give the card your apps in `Connectors` and the ids that are already connected in `ConnectedIds`. Set `ConnectHandler` +to run your OAuth flow: report the steps through the context, return the account, scopes and sync count, and throw an +exception with a short human message ("Needs admin approval") to show it with Retry. The context's token is cancelled +when the person presses Cancel or the card leaves the window. + +```csharp +setup.Connectors = apps; // ConnectorApp: Id, Name, Description, Kind or Glyph, Scopes, SyncCount, SyncNoun +setup.ConnectedIds = savedIds; +setup.ConnectHandler = async (app, context) => +{ + var grant = await OpenOAuthPopupAsync(app.Id, context.CancellationToken); + context.ReportAuthorizing(); + var approved = await grant.ResultAsync; + var total = await StartFirstSyncAsync(app.Id, approved, synced => context.ReportSyncing(synced), context.CancellationToken); + return new ConnectorConnectResult { Account = approved.Workspace, Scopes = approved.Scopes, Synced = total }; +}; +setup.DisconnectHandler = (app, token) => RevokeGrantAsync(app.Id, token); +setup.ConnectedIdsChanged += (_, e) => SaveConnectedApps(e.Ids); +``` + +Without handlers the flow is simulated, nothing is sent, and the live region says so. With no `Connectors` the card +lists its `Simulation`'s apps; with neither it lists no apps. `AssistantName` names the assistant in the texts; without +one they say "your assistant". + +### Simulation + +`ConnectorSetupSimulation` is a set of apps the card plays on its own: the `AssistantName`, the `Apps`, the ids already +`Connected`, and the scripted run's `Steps`. Once the card is on screen each `ConnectorSetupSimulationStep` waits its +`Pause`, then connects its app if it is idle or failed; a step with a `Failure` fails after approval, so the next one can +retry it. The run stops for good at the first pointer or key press inside the card. Your own `Connectors`, +`ConnectedIds` and `AssistantName` win over it, and setting `Simulation` to null stops the run and empties the list. + +```csharp +setup.Simulation = new ConnectorSetupSimulation +{ + AssistantName = "Atlas", + Apps = apps, // the same ConnectorApp records as Connectors + Connected = ["code"], + Steps = + [ + new ConnectorSetupSimulationStep("chat", TimeSpan.FromMilliseconds(900)), + new ConnectorSetupSimulationStep("crm", TimeSpan.FromMilliseconds(1200), "Needs admin approval"), + new ConnectorSetupSimulationStep("crm", TimeSpan.FromMilliseconds(1800)), + ], +}; +``` + +The demos on this page list eight generic kinds of app from `ConnectorSetupDemoData`, with an invented company's +accounts and counts and an invented assistant. + +### Your own flow + + + +The handler reports `ReportAuthorizing()` once the sign-in waits for approval and `ReportSyncing(synced, total)` as the +first sync advances; the report calls may come from any thread. `ConnectedIds` is two-way and is written after each +connect or disconnect finishes, together with `ConnectedIdsChanged`. `AppConnected` carries the account, scopes and sync +count; `AppDisconnected` follows a revoke. + +### Onboarding + + + +`IsSearchable="False"` drops the search field for a short list. `Title`, `Description` and `Footer` replace the heading, +the line under it and the privacy note (`IsFooterVisible="False"` hides the note). `AssistantName` is used in the +description, the scopes heading, the confirmation and the footer. + +### Marks + + + +Apps are shown with neutral Lucide glyphs for their kind (`ConnectorMarkKind`: Chat, Code, Docs, Drive, Issues, Calendar, +Mail, Crm, Database, Analytics), never brand logos. Pass the app's own 24 × 24 stroked mark in `ConnectorApp.Glyph`; with +neither, the first letter of the name is shown. `ConnectorMark` draws the same tile anywhere else in your settings UI. + +### Anatomy + +1. **Header**: the heading, one line under it and the search field (it matches name, description, account and keywords). +2. **Row**: 60px tall. The mark, the name over one status line, and one 32px action: Connect, Cancel, Retry or Connected. +3. **Progress hairline**: 2px along the row's bottom edge while the app connects. +4. **Access details**: under a connected row, aligned with its text: "Atlas can" and the scopes, then Disconnect. +5. **Footer**: a lock and a short privacy note. + +### States + +| Phase | Status line | Action | +|---|---|---| +| `Idle` | The app's description | Connect | +| `Redirecting` | "Opening chat.example.com…" | Cancel | +| `Authorizing` | "Waiting for you to approve in Chat…" | Cancel | +| `Syncing` | "Syncing 1,240 messages", the count rolling | Cancel | +| `Connected` | "tailspin.chat · 1,240 messages synced" | Connected, opening the details | +| `Failed` | An alert and the reason | Retry | +| `Disconnecting` | "Disconnecting…" | Connected (disabled) | + +`GetPhase(id)` reads where an app is; `StartConnect`, `CancelConnect`, `SetAccessOpen`, `Disconnect` and `StopAutoPlay` +drive the card from code. + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +setup.Labels = new ConnectorSetupLabels +{ + Title = "Verbinden Sie Ihre Tools", + Connect = "Verbinden", + Syncing = "Synchronisiere", +}; +``` + +### Content guidelines + +- Write scopes as plain-language capabilities ("Post replies as Atlas"), not raw OAuth scope strings. +- Keep a failure's message short and human ("Needs admin approval"); it shares one line with an alert icon. +- Give `SyncNoun` in the plural ("messages", "files"). + +### Best practices + +- Keep it one card in a settings column; it is fluid up to 520px and holds at 360px. +- Pass the token to every call of your flow so Cancel really stops it. +- For hundreds of apps, filter on your side and pass the result as `Connectors`. + +### Common questions + +**What happens if the person leaves the page while an app connects?** The token is cancelled and the row returns to +rest. + +**Does the card keep the connected list?** It writes `ConnectedIds` after each change; save it from +`ConnectedIdsChanged`. + +## API reference + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is a labelled group with a real heading; the list is a list with one item per app, numbered among the apps + the search shows. +- A polite live region announces each step: connecting, connected with the sync count, failed with the reason, cancelled + and disconnected. With a simulated flow it adds "Simulated, nothing was sent." +- Status is never color alone: a failure pairs an alert icon with its message, and a connected app shows a check with the + word Connected. +- The Connected button reports expanded or collapsed. Disconnect asks first; a press moves focus to the safe answer, + Cancel, and the destructive button is only focused by the person. +- Content that is fading out leaves the accessibility tree and stops taking clicks at once. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the search field, each row's Connect, Cancel, Retry or Connected button, and the open details | +| Enter, Space | Connects, cancels, retries, or opens and closes a connected app's access details | +| Escape | Clears the search field, or backs out of the disconnect confirmation | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Status line or action changes | Old fades out over 100ms, new fades in over 160ms rising 2px, in one cell | At once | At once | +| Progress between steps | The hairline eases over 240ms | At once | At once | +| Progress while syncing | The hairline tracks the count linearly; the count rolls in tabular figures in a width reserved for its final value | Jumps with the count | Jumps with the count | +| Connect finishes | The hairline fills, then fades | At once | At once | +| Details open or close | Height springs (0.24s, no bounce), content fades | At once | At once | +| Chevron | Turns on a spring | At once | At once | +| Scripted run (simulation) | Starts once the card is on screen, stops at the first press | Same | Same | + +Reduced motion keeps every state and announcement. The row is 60px tall in every state, so nothing below it moves while +an app connects. + +## Related + +- `ToolCall` for a tool the agent is calling right now, with approval in place. +- `AgentStatus` for the live status of a whole agent run. +- `ConfirmMorph` for a single destructive button that asks in place. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/zh-CN.md new file mode 100644 index 00000000..37a7d3e1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/zh-CN.md @@ -0,0 +1,204 @@ +--- +title: Connector setup +summary: AI 助手的"连接你的工具"卡片:每个应用都在行内完成跳转、授权和首次同步,并以一条进度细线显示进度,随后列出获得的权限范围,并提供带行内确认的断开连接。 +source-hash: f520864183fa +--- + + + +## 何时使用 + +`ConnectorSetup` 是让人们允许助手进入团队所用应用的卡片。每个应用占一行,包含它的标记、名称和一行状态。按下 Connect +会在这一行里走完整个握手过程:登录页打开、用户批准,首次同步一边计数已读取的内容,一边让一条 2px 细线沿着这一行填满。 +已连接的应用显示其账户和一个 Connected 按钮,按下后展开助手拥有的权限范围,并提供 Disconnect 和行内确认。 + +- **助手的设置页**:人们在这里连接并查看工作区应用。 +- **首次使用清单**:需要在不离开页面的情况下展示 OAuth 握手和首次同步。 +- **复核访问权限**:查看助手在每个应用中拥有哪些权限范围,并撤销它们。 + +## 何时不用 + +- **按类别浏览几十个集成**:使用 `Integrations` 区块,这是一个带筛选的应用市场网格。 +- **批准智能体想执行的某一个操作**:使用带审批的 `ToolCall`,或使用 `Dialog`。 +- **展示智能体此刻正在调用的工具**:使用 `ToolCall`。 + +## 用法 + +在 `Connectors` 中传入你的应用,在 `ConnectedIds` 中传入已经连接的 id。设置 `ConnectHandler` 来运行你的 OAuth +流程:通过上下文报告各个步骤,返回账户、权限范围和同步数量;抛出带有简短可读消息("Needs admin approval")的异常, +即可显示该消息和 Retry。用户按下 Cancel 或卡片离开窗口时,上下文的令牌会被取消。 + +```csharp +setup.Connectors = apps; // ConnectorApp: Id, Name, Description, Kind or Glyph, Scopes, SyncCount, SyncNoun +setup.ConnectedIds = savedIds; +setup.ConnectHandler = async (app, context) => +{ + var grant = await OpenOAuthPopupAsync(app.Id, context.CancellationToken); + context.ReportAuthorizing(); + var approved = await grant.ResultAsync; + var total = await StartFirstSyncAsync(app.Id, approved, synced => context.ReportSyncing(synced), context.CancellationToken); + return new ConnectorConnectResult { Account = approved.Workspace, Scopes = approved.Scopes, Synced = total }; +}; +setup.DisconnectHandler = (app, token) => RevokeGrantAsync(app.Id, token); +setup.ConnectedIdsChanged += (_, e) => SaveConnectedApps(e.Ids); +``` + +没有设置处理程序时,流程是模拟的,不会发送任何内容,实时区域也会说明这一点。没有 `Connectors` 时,卡片列出它的 +`Simulation` 中的应用;两者都没有时不列出任何应用。`AssistantName` 在文字中为助手命名;没有设置时文字会写成“your assistant”。 + +### 模拟 + +`ConnectorSetupSimulation` 是卡片自行播放的一组应用:`AssistantName`、`Apps`、已连接的 `Connected` id,以及脚本演示的 `Steps`。 +卡片出现在屏幕上后,每个 `ConnectorSetupSimulationStep` 先等待它的 `Pause`,然后在其应用处于空闲或失败状态时连接它;带有 `Failure` +的步骤会在批准后失败,以便下一步重试。脚本演示会在卡片内第一次出现指针按下或按键时永久停止。你自己设置的 `Connectors`、 +`ConnectedIds` 和 `AssistantName` 优先于它;把 `Simulation` 设为 null 会停止演示并清空列表。 + +```csharp +setup.Simulation = new ConnectorSetupSimulation +{ + AssistantName = "Atlas", + Apps = apps, // the same ConnectorApp records as Connectors + Connected = ["code"], + Steps = + [ + new ConnectorSetupSimulationStep("chat", TimeSpan.FromMilliseconds(900)), + new ConnectorSetupSimulationStep("crm", TimeSpan.FromMilliseconds(1200), "Needs admin approval"), + new ConnectorSetupSimulationStep("crm", TimeSpan.FromMilliseconds(1800)), + ], +}; +``` + +本页的演示从 `ConnectorSetupDemoData` 列出八种通用应用,账号和数量属于一家虚构的公司,助手也是虚构的。 + +### 你自己的流程 + + + +登录页等待批准时,处理程序调用 `ReportAuthorizing()`;首次同步推进时调用 `ReportSyncing(synced, total)`。这些报告 +调用可以来自任何线程。`ConnectedIds` 是双向的,每次连接或断开完成后都会写回,同时引发 `ConnectedIdsChanged`。 +`AppConnected` 带有账户、权限范围和同步数量;`AppDisconnected` 在撤销之后引发。 + +### 引导流程 + + + +`IsSearchable="False"` 为较短的列表去掉搜索框。`Title`、`Description` 和 `Footer` 替换标题、标题下的一行文字和隐私 +说明(`IsFooterVisible="False"` 隐藏说明)。`AssistantName` 用于描述、权限范围标题、确认问题和页脚。 + +### 标记 + + + +应用以表示其类型的中性 Lucide 图标显示(`ConnectorMarkKind`:Chat、Code、Docs、Drive、Issues、Calendar、Mail、Crm、 +Database、Analytics),绝不使用品牌标志。在 `ConnectorApp.Glyph` 中传入应用自己的 24 × 24 描边标记;两者都没有时, +显示名称的首字母。`ConnectorMark` 可以在设置界面的其他位置绘制同样的图块。 + +### 结构 + +1. **页头**:标题、其下的一行文字和搜索框(匹配名称、描述、账户和关键词)。 +2. **行**:高 60px。标记、名称及其下的一行状态,以及一个 32px 的操作:Connect、Cancel、Retry 或 Connected。 +3. **进度细线**:应用连接期间,沿行底边显示的 2px 细线。 +4. **访问详情**:位于已连接的行下方,与其文字对齐:"Atlas can" 和权限范围,然后是 Disconnect。 +5. **页脚**:一把锁和一段简短的隐私说明。 + +### 状态 + +| 阶段 | 状态行 | 操作 | +|---|---|---| +| `Idle` | 应用的描述 | Connect | +| `Redirecting` | "Opening chat.example.com…" | Cancel | +| `Authorizing` | "Waiting for you to approve in Chat…" | Cancel | +| `Syncing` | "Syncing 1,240 messages",数字滚动 | Cancel | +| `Connected` | "tailspin.chat · 1,240 messages synced" | Connected,展开详情 | +| `Failed` | 警示图标和原因 | Retry | +| `Disconnecting` | "Disconnecting…" | Connected(禁用) | + +`GetPhase(id)` 读取应用所处的阶段;`StartConnect`、`CancelConnect`、`SetAccessOpen`、`Disconnect` 和 +`StopAutoPlay` 用于在代码中驱动卡片。 + +### 自定义文字 + +`Labels` 包含卡片显示或朗读的每一个词。替换整个对象即可本地化或改写: + +```csharp +setup.Labels = new ConnectorSetupLabels +{ + Title = "Verbinden Sie Ihre Tools", + Connect = "Verbinden", + Syncing = "Synchronisiere", +}; +``` + +### 内容准则 + +- 用通俗的能力描述来写权限范围("Post replies as Atlas"),不要写原始的 OAuth scope 字符串。 +- 失败消息要简短、易懂("Needs admin approval");它与警示图标共用一行。 +- `SyncNoun` 使用复数形式("messages"、"files")。 + +### 最佳实践 + +- 在设置栏中只放一张卡片;它在 520px 以内随容器伸缩,最窄可到 360px。 +- 把令牌传给流程中的每一次调用,这样 Cancel 才能真正停止它。 +- 应用有数百个时,在你这边筛选,再把结果作为 `Connectors` 传入。 + +### 常见问题 + +**应用连接期间用户离开页面会怎样?** 令牌被取消,这一行回到静止状态。 + +**卡片会保存已连接列表吗?** 每次变化后它都会写回 `ConnectedIds`;在 `ConnectedIdsChanged` 中保存即可。 + +## API 参考 + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片是一个带标签的分组,有真正的标题;列表中每个应用一项,并按搜索显示出的应用编号。 +- 一个礼貌型实时区域播报每一步:正在连接、已连接及同步数量、失败及原因、已取消和已断开。使用模拟流程时,会追加 + "Simulated, nothing was sent." +- 状态从不只靠颜色表达:失败时警示图标与消息同时出现,已连接的应用显示对勾和 Connected 一词。 +- Connected 按钮会报告展开或折叠状态。Disconnect 会先询问;按下后焦点移到安全的回答 Cancel 上,破坏性按钮只会由用户 + 自己聚焦。 +- 正在淡出的内容会立即离开无障碍树,并且不再接收点击。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过搜索框、每一行的 Connect、Cancel、Retry 或 Connected 按钮,以及已展开的详情 | +| Enter、Space | 连接、取消、重试,或展开、收起已连接应用的访问详情 | +| Escape | 清空搜索框,或退出断开连接的确认 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 状态行或操作变化 | 旧内容在 100ms 内淡出,新内容在 160ms 内淡入并上升 2px,位于同一个单元格 | 立即 | 立即 | +| 步骤之间的进度 | 细线在 240ms 内缓动 | 立即 | 立即 | +| 同步时的进度 | 细线线性跟随计数;数字以等宽数字滚动,宽度按最终值预留 | 随计数跳变 | 随计数跳变 | +| 连接完成 | 细线填满后淡出 | 立即 | 立即 | +| 详情展开或收起 | 高度以弹簧变化(0.24s,无回弹),内容淡入淡出 | 立即 | 立即 | +| 箭头 | 以弹簧转动 | 立即 | 立即 | +| 脚本演示(模拟) | 卡片出现在屏幕上后开始,首次按下时停止 | 相同 | 相同 | + +减弱动效会保留所有状态和播报。每种状态下行高都是 60px,所以应用连接期间,它下方的内容不会移动。 + +## 相关组件 + +- `ToolCall`:智能体此刻正在调用的工具,可就地审批。 +- `AgentStatus`:整个智能体运行的实时状态。 +- `ConfirmMorph`:就地询问的单个破坏性按钮。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/zh-TW.md new file mode 100644 index 00000000..ce84b726 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ConnectorSetup/zh-TW.md @@ -0,0 +1,205 @@ +--- +title: Connector setup +summary: AI 助理的「連結你的工具」卡片:每個應用程式都在行內完成重新導向、授權和首次同步,並以一條進度細線顯示進度,接著列出取得的權限範圍,並提供附帶行內確認的中斷連結。 +source-hash: f520864183fa +--- + + + +## 何時使用 + +`ConnectorSetup` 是讓使用者允許助理進入團隊所用應用程式的卡片。每個應用程式占一列,包含它的標記、名稱和一行狀態。 +按下 Connect 會在這一列裡走完整個交握流程:登入頁面開啟、使用者核准,首次同步一邊計數已讀取的內容,一邊讓一條 2px +細線沿著這一列填滿。已連結的應用程式顯示其帳號和一個 Connected 按鈕,按下後展開助理擁有的權限範圍,並提供 +Disconnect 和行內確認。 + +- **助理的設定頁面**:使用者在這裡連結並檢視工作區應用程式。 +- **首次使用檢查清單**:需要在不離開頁面的情況下展示 OAuth 交握和首次同步。 +- **複查存取權限**:檢視助理在每個應用程式中擁有哪些權限範圍,並撤銷它們。 + +## 何時不用 + +- **依類別瀏覽數十個整合**:使用 `Integrations` 區塊,這是一個附篩選的應用程式市集網格。 +- **核准智慧代理想執行的某一個動作**:使用附核准的 `ToolCall`,或使用 `Dialog`。 +- **展示智慧代理此刻正在呼叫的工具**:使用 `ToolCall`。 + +## 用法 + +在 `Connectors` 中傳入你的應用程式,在 `ConnectedIds` 中傳入已經連結的 id。設定 `ConnectHandler` 來執行你的 OAuth +流程:透過內容物件回報各個步驟,傳回帳號、權限範圍和同步數量;擲回附有簡短易懂訊息("Needs admin approval")的例外, +即可顯示該訊息和 Retry。使用者按下 Cancel 或卡片離開視窗時,內容物件的權杖會被取消。 + +```csharp +setup.Connectors = apps; // ConnectorApp: Id, Name, Description, Kind or Glyph, Scopes, SyncCount, SyncNoun +setup.ConnectedIds = savedIds; +setup.ConnectHandler = async (app, context) => +{ + var grant = await OpenOAuthPopupAsync(app.Id, context.CancellationToken); + context.ReportAuthorizing(); + var approved = await grant.ResultAsync; + var total = await StartFirstSyncAsync(app.Id, approved, synced => context.ReportSyncing(synced), context.CancellationToken); + return new ConnectorConnectResult { Account = approved.Workspace, Scopes = approved.Scopes, Synced = total }; +}; +setup.DisconnectHandler = (app, token) => RevokeGrantAsync(app.Id, token); +setup.ConnectedIdsChanged += (_, e) => SaveConnectedApps(e.Ids); +``` + +沒有設定處理常式時,流程是模擬的,不會傳送任何內容,即時區域也會說明這一點。沒有 `Connectors` 時,卡片列出它的 +`Simulation` 中的應用程式;兩者都沒有時不列出任何應用程式。`AssistantName` 在文字中為助理命名;沒有設定時文字會寫成「your assistant」。 + +### 模擬 + +`ConnectorSetupSimulation` 是卡片自行播放的一組應用程式:`AssistantName`、`Apps`、已連結的 `Connected` id,以及腳本示範的 +`Steps`。卡片出現在螢幕上後,每個 `ConnectorSetupSimulationStep` 先等待它的 `Pause`,然後在其應用程式處於閒置或失敗狀態時連結它; +帶有 `Failure` 的步驟會在核准後失敗,以便下一步重試。腳本示範會在卡片內第一次出現指標按下或按鍵時永久停止。你自己設定的 +`Connectors`、`ConnectedIds` 與 `AssistantName` 優先於它;把 `Simulation` 設為 null 會停止示範並清空清單。 + +```csharp +setup.Simulation = new ConnectorSetupSimulation +{ + AssistantName = "Atlas", + Apps = apps, // the same ConnectorApp records as Connectors + Connected = ["code"], + Steps = + [ + new ConnectorSetupSimulationStep("chat", TimeSpan.FromMilliseconds(900)), + new ConnectorSetupSimulationStep("crm", TimeSpan.FromMilliseconds(1200), "Needs admin approval"), + new ConnectorSetupSimulationStep("crm", TimeSpan.FromMilliseconds(1800)), + ], +}; +``` + +本頁的示範從 `ConnectorSetupDemoData` 列出八種通用應用程式,帳號與數量屬於一家虛構的公司,助理也是虛構的。 + +### 你自己的流程 + + + +登入頁面等待核准時,處理常式呼叫 `ReportAuthorizing()`;首次同步推進時呼叫 `ReportSyncing(synced, total)`。這些回報 +呼叫可以來自任何執行緒。`ConnectedIds` 是雙向的,每次連結或中斷完成後都會寫回,同時引發 `ConnectedIdsChanged`。 +`AppConnected` 附帶帳號、權限範圍和同步數量;`AppDisconnected` 在撤銷之後引發。 + +### 新手引導 + + + +`IsSearchable="False"` 為較短的清單拿掉搜尋欄位。`Title`、`Description` 和 `Footer` 取代標題、標題下的一行文字和 +隱私說明(`IsFooterVisible="False"` 隱藏說明)。`AssistantName` 用於描述、權限範圍標題、確認問題和頁尾。 + +### 標記 + + + +應用程式以代表其類型的中性 Lucide 圖示顯示(`ConnectorMarkKind`:Chat、Code、Docs、Drive、Issues、Calendar、Mail、 +Crm、Database、Analytics),絕不使用品牌標誌。在 `ConnectorApp.Glyph` 中傳入應用程式自己的 24 × 24 描邊標記;兩者 +都沒有時,顯示名稱的首字母。`ConnectorMark` 可以在設定介面的其他位置繪製同樣的圖塊。 + +### 結構 + +1. **頁首**:標題、其下的一行文字和搜尋欄位(比對名稱、描述、帳號和關鍵字)。 +2. **列**:高 60px。標記、名稱及其下的一行狀態,以及一個 32px 的動作:Connect、Cancel、Retry 或 Connected。 +3. **進度細線**:應用程式連結期間,沿列底邊顯示的 2px 細線。 +4. **存取詳細資料**:位於已連結的列下方,與其文字對齊:"Atlas can" 和權限範圍,然後是 Disconnect。 +5. **頁尾**:一把鎖和一段簡短的隱私說明。 + +### 狀態 + +| 階段 | 狀態行 | 動作 | +|---|---|---| +| `Idle` | 應用程式的描述 | Connect | +| `Redirecting` | "Opening chat.example.com…" | Cancel | +| `Authorizing` | "Waiting for you to approve in Chat…" | Cancel | +| `Syncing` | "Syncing 1,240 messages",數字滾動 | Cancel | +| `Connected` | "tailspin.chat · 1,240 messages synced" | Connected,展開詳細資料 | +| `Failed` | 警示圖示和原因 | Retry | +| `Disconnecting` | "Disconnecting…" | Connected(停用) | + +`GetPhase(id)` 讀取應用程式所處的階段;`StartConnect`、`CancelConnect`、`SetAccessOpen`、`Disconnect` 和 +`StopAutoPlay` 用於在程式碼中驅動卡片。 + +### 自訂文字 + +`Labels` 包含卡片顯示或朗讀的每一個詞。取代整個物件即可在地化或改寫: + +```csharp +setup.Labels = new ConnectorSetupLabels +{ + Title = "Verbinden Sie Ihre Tools", + Connect = "Verbinden", + Syncing = "Synchronisiere", +}; +``` + +### 內容準則 + +- 用淺白的能力描述來寫權限範圍("Post replies as Atlas"),不要寫原始的 OAuth scope 字串。 +- 失敗訊息要簡短、易懂("Needs admin approval");它與警示圖示共用一行。 +- `SyncNoun` 使用複數形式("messages"、"files")。 + +### 最佳做法 + +- 在設定欄中只放一張卡片;它在 520px 以內隨容器伸縮,最窄可到 360px。 +- 把權杖傳給流程中的每一次呼叫,這樣 Cancel 才能真正停止它。 +- 應用程式有數百個時,在你這邊篩選,再把結果作為 `Connectors` 傳入。 + +### 常見問題 + +**應用程式連結期間使用者離開頁面會怎樣?** 權杖被取消,這一列回到靜止狀態。 + +**卡片會儲存已連結清單嗎?** 每次變化後它都會寫回 `ConnectedIds`;在 `ConnectedIdsChanged` 中儲存即可。 + +## API 參考 + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片是一個附標籤的群組,有真正的標題;清單中每個應用程式一項,並依搜尋顯示出的應用程式編號。 +- 一個禮貌型即時區域播報每一步:正在連結、已連結及同步數量、失敗及原因、已取消和已中斷。使用模擬流程時,會附加 + "Simulated, nothing was sent." +- 狀態從不只靠顏色表達:失敗時警示圖示與訊息同時出現,已連結的應用程式顯示勾號和 Connected 一詞。 +- Connected 按鈕會回報展開或摺疊狀態。Disconnect 會先詢問;按下後焦點移到安全的回答 Cancel 上,破壞性按鈕只會由 + 使用者自己聚焦。 +- 正在淡出的內容會立即離開無障礙樹狀結構,並且不再接收點擊。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過搜尋欄位、每一列的 Connect、Cancel、Retry 或 Connected 按鈕,以及已展開的詳細資料 | +| Enter、Space | 連結、取消、重試,或展開、收合已連結應用程式的存取詳細資料 | +| Escape | 清除搜尋欄位,或退出中斷連結的確認 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 狀態行或動作變化 | 舊內容在 100ms 內淡出,新內容在 160ms 內淡入並上升 2px,位於同一個儲存格 | 立即 | 立即 | +| 步驟之間的進度 | 細線在 240ms 內緩動 | 立即 | 立即 | +| 同步時的進度 | 細線線性跟隨計數;數字以等寬數字滾動,寬度依最終值預留 | 隨計數跳變 | 隨計數跳變 | +| 連結完成 | 細線填滿後淡出 | 立即 | 立即 | +| 詳細資料展開或收合 | 高度以彈簧變化(0.24s,無回彈),內容淡入淡出 | 立即 | 立即 | +| 箭頭 | 以彈簧轉動 | 立即 | 立即 | +| 腳本示範(模擬) | 卡片出現在螢幕上後開始,首次按下時停止 | 相同 | 相同 | + +減少動態效果會保留所有狀態和播報。每種狀態下列高都是 60px,所以應用程式連結期間,它下方的內容不會移動。 + +## 相關元件 + +- `ToolCall`:智慧代理此刻正在呼叫的工具,可就地核准。 +- `AgentStatus`:整個智慧代理執行的即時狀態。 +- `ConfirmMorph`:就地詢問的單一破壞性按鈕。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/en-US.md index 278c1b0a..e120c6f5 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/en-US.md @@ -20,8 +20,8 @@ message, Attach file and Compact now change the window, and a status line under the cells come back, and the status line says how much was folded. - **Model choice in context**: switching to a model with a smaller window rearranges the grid at once and shows whether the session still fits; when it does not, the card folds the conversation and says so. -- **Documentation and onboarding**: explain context windows, output reserves and compaction with a sample that people - can poke at. With no data the card plays an invented coding session. +- **Documentation and onboarding**: explain context windows, output reserves and compaction with a session that people + can poke at, like the preview on this page. ## When not to use @@ -36,17 +36,24 @@ message, Attach file and Compact now change the window, and a status line under ```xml - + + + + + + + ``` `ContextWindow` is a `TemplatedControl` in the Special package. Register the package with `UseArcSpecial()` next to `UseArcControls()`. The card is at most 744px wide and follows its own width, not the window's. -Given no data at all (no models, items, turns or files) the card shows the invented sample session from -`ContextWindowSamples`: three sample models with 1M, 512K and 400K windows, a coding session of 436K tokens, five turns -for Send message and three files for Attach file. The sample works on its own copy, so `Items` stays empty. As soon as -you give the card any of the four lists it uses yours. +Without data (no models, items, turns or files) the card is empty: no model pill, every cell free, and no Send message +or Attach file. Give it your models and items, and `Turns` and `Files` for the two buttons. The demos on this page use +real models with the context windows their vendors publish (checked 2026-10-29: Claude Sonnet 5.5 1M, Grok 4.7 500K, +Mistral Medium 3.5 256K) and an invented coding session of 436K tokens; the output reserve is what the demo app asks for +in one reply (32K), never more than the model's published output limit. ### Anatomy @@ -100,8 +107,8 @@ demand while there are more messages than the newest two. | A send past the threshold | Added “Why does the nightly sync time out?” (+35K) · past 80% | 0 | | Folding starts | Past 80%: folding 35 earlier messages… | 540ms | | The summary lands | Folded 608K into a 24K summary | 1,240ms | -| A switch to a smaller model | 436K is over 80% of Cirrus M | 0 | -| Folding starts | Too full for Cirrus M: folding 22 earlier messages… | 600ms | +| A switch to a smaller model | 436K is over 80% of Grok 4.7 | 0 | +| Folding starts | Too full for Grok 4.7: folding 22 earlier messages… | 600ms | Files are never folded. When the window is past the threshold and only the newest messages are left, the status line says there is nothing to fold yet; when a compaction leaves the window past the threshold, it says how much was folded @@ -217,7 +224,7 @@ card.Labels = new ContextWindowLabels **Avoid** - Rebuilding `Items` on a timer. Each change animates; update the items that changed instead. -- Presenting the sample session as real numbers. It is made up. +- Presenting an invented session as real usage. - Using the grid for anything but one window. It always has 200 cells; its proportions only make sense for one budget. ### Common questions @@ -231,7 +238,7 @@ Remove or replace the summary item in `Items` when your app rewrites it. **What happens when I switch to a bigger model?** The grid rearranges, the status line names the new model and window, and nothing folds. -**Why are Send message and Attach file missing?** They hide when `Turns` or `Files` is empty outside the sample. +**Why are Send message and Attach file missing?** They hide when `Turns` or `Files` is empty. **Can the window overflow?** Yes. The readout passes 100%, free space reads 0, and the parts that do not fit before the reserve get no cells. @@ -263,15 +270,12 @@ The enums: | `ContextWindowCompactReason` | `Manual`, `Threshold`, `ModelSwitch` | | `ContextWindowChange` | `Sent`, `Attached`, `Added`, `Compacted` | -`ContextWindowSamples` holds the sample session (`CreateModels()`, `CreateItems()`, `CreateTurns()`, -`CreateFiles()`), so a page can start from it and change one part. - ## Accessibility - The card is a group named "Context window". The grid is one image whose name describes it in full: the model, the tokens used of the window, every part with its tokens and the threshold. It updates with every change, not with every frame of a roll. -- The readout is read as one line ("436K of 1M tokens, 44% used, 500K free"). The legend is a list named "Context +- The readout is read as one line ("436K of 1M tokens, 44% used, 532K free"). The legend is a list named "Context breakdown" whose rows read their name, tokens and share, so nothing depends on color. - The pill is named after the model and its window and says that it changes the model. The menu's models are radio items with the selected one checked. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-CN.md index 10111914..d67120b2 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-CN.md @@ -1,7 +1,7 @@ --- title: Context window summary: 模型的上下文窗口呈现为一张由 200 个格子组成的网格,分段填充,并附带压缩阈值、图例,以及说明还剩多少空间的操作按钮。 -source-hash: 10028f4973b8 +source-hash: 9f00beff2aa3 --- @@ -13,7 +13,7 @@ source-hash: 10028f4973b8 - **开发者工具与智能体控制台**:在对话记录旁显示会话的预算,让人看清一次长时间运行为何开始遗忘内容,以及一个大附件会占用多少空间。 - **会压缩长会话的编码智能体**:让压缩可见。较早的消息折叠成一段简短摘要,格子随之释放,状态行说明折叠了多少内容。 - **上下文中的模型选择**:切换到窗口更小的模型时,网格会立即重新排列,并显示会话是否仍能放下;放不下时,卡片会折叠对话并说明这一点。 -- **文档与入门指南**:用一个可以随意操作的示例来解释上下文窗口、输出预留和压缩。没有数据时,卡片会播放一段虚构的编码会话。 +- **文档与入门指南**:用一段可以随意操作的会话来解释上下文窗口、输出预留和压缩,就像本页的预览那样。 ## 何时不用 @@ -27,13 +27,19 @@ source-hash: 10028f4973b8 ```xml - + + + + + + + ``` `ContextWindow` 是 Special 包中的 `TemplatedControl`。请在 `UseArcControls()` 旁调用 `UseArcSpecial()` 以注册该包。卡片最宽为 744px,宽度跟随它自身,而不是窗口的宽度。 -完全没有数据时(没有模型、项目、轮次或文件),卡片会显示来自 `ContextWindowSamples` 的虚构示例会话:三个示例模型,窗口分别为 1M、512K 和 400K;一段用量为 436K token 的编码会话;Send message 对应五个轮次,Attach file 对应三个文件。示例使用自己的副本,因此 `Items` 保持为空。只要你提供了四个列表中的任意一个,卡片就会改用你的数据。 +没有数据时(没有模型、项目、轮次或文件),卡片是空的:没有模型胶囊,所有格子都空闲,也没有 Send message 和 Attach file。请提供你的模型和项目,并为这两个按钮提供 `Turns` 和 `Files`。本页的演示使用真实模型及其厂商公布的上下文窗口(核对于 2026-10-29:Claude Sonnet 5.5 为 1M,Grok 4.7 为 500K,Mistral Medium 3.5 为 256K),以及一段虚构的、用量为 436K token 的编码会话;输出预留是演示应用单次回复所请求的量(32K),绝不超过该模型公布的输出上限。 ### 结构 @@ -65,8 +71,8 @@ Send message 会把 `Turns` 中的下一条作为消息添加到窗口末尾;A | 超过阈值时发送 | Added “Why does the nightly sync time out?” (+35K) · past 80% | 0 | | 开始折叠 | Past 80%: folding 35 earlier messages… | 540ms | | 摘要生成 | Folded 608K into a 24K summary | 1,240ms | -| 切换到较小的模型 | 436K is over 80% of Cirrus M | 0 | -| 开始折叠 | Too full for Cirrus M: folding 22 earlier messages… | 600ms | +| 切换到较小的模型 | 436K is over 80% of Grok 4.7 | 0 | +| 开始折叠 | Too full for Grok 4.7: folding 22 earlier messages… | 600ms | 文件永远不会被折叠。窗口超过阈值且只剩最新的消息时,状态行会说明暂时没有可折叠的内容;压缩后窗口仍超过阈值时,状态行会说明折叠了多少,并建议移除文件。已用 token 可以超过窗口:此时读数会显示超过 100%,且没有空闲 token。 @@ -159,7 +165,7 @@ card.Labels = new ContextWindowLabels **避免** - 用计时器重建 `Items`。每次变化都会产生动画;应只更新发生变化的项目。 -- 把示例会话当作真实数字来展示。它是虚构的。 +- 把虚构的会话当作真实用量来展示。 - 把网格用于一个窗口以外的任何事情。它始终有 200 个格子;它的比例只对一个预算有意义。 ### 常见问题 @@ -170,7 +176,7 @@ card.Labels = new ContextWindowLabels **切换到更大的模型会怎样?** 网格重新排列,状态行写明新模型和窗口,而且不会有任何折叠。 -**为什么没有 Send message 和 Attach file?** 在示例之外,当 `Turns` 或 `Files` 为空时它们会隐藏。 +**为什么没有 Send message 和 Attach file?** 当 `Turns` 或 `Files` 为空时它们会隐藏。 **窗口能溢出吗?** 能。读数会超过 100%,空闲空间显示 0,无法放在预留之前的部分不会获得格子。 @@ -201,12 +207,10 @@ card.Labels = new ContextWindowLabels | `ContextWindowCompactReason` | `Manual`, `Threshold`, `ModelSwitch` | | `ContextWindowChange` | `Sent`, `Attached`, `Added`, `Compacted` | -`ContextWindowSamples` 保存示例会话(`CreateModels()`、`CreateItems()`、`CreateTurns()`、`CreateFiles()`),因此页面可以从它开始,只修改其中一个部分。 - ## 无障碍 - 卡片是一个名为 "Context window" 的分组。网格是一张图片,其名称完整描述了它:模型、已用 token 数与窗口的对比、每个部分及其 token 数,以及阈值。它随每次变化更新,而不是在滚动的每一帧更新。 -- 读数会被朗读为一行("436K of 1M tokens, 44% used, 500K free")。图例是一个名为 "Context breakdown" 的列表,其各行会朗读名称、token 数和占比,因此不依赖颜色。 +- 读数会被朗读为一行("436K of 1M tokens, 44% used, 532K free")。图例是一个名为 "Context breakdown" 的列表,其各行会朗读名称、token 数和占比,因此不依赖颜色。 - 胶囊以模型及其窗口命名,并说明它会切换模型。菜单中的模型是单选项,被选中的那一项带有勾选标记。 - 每次发送、附加、切换模型和压缩都会通过礼貌级别的实时区域播报,并且每种状态都同时带有图标和文字。 - 消息折叠期间,所有操作都不可用。为了不让键盘焦点落回页面,卡片会把焦点保留在一个隐藏的占位元素上,该元素以状态命名,并在压缩完成时把焦点交还:交给原先持有焦点的操作,若该操作已不可用,则交给 Send message。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-TW.md index 45fb1dec..ad71be56 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ContextWindow/zh-TW.md @@ -1,7 +1,7 @@ --- title: Context window summary: 模型的上下文視窗呈現為 200 格的方格,逐段填滿,並帶有壓縮門檻、圖例,以及顯示還剩多少空間的動作。 -source-hash: 10028f4973b8 +source-hash: 9f00beff2aa3 --- @@ -13,7 +13,7 @@ source-hash: 10028f4973b8 - **開發者工具與代理主控台**:在對話紀錄旁顯示工作階段的預算,讓人看出為何長時間執行時會開始遺忘內容,以及一個大型附件會耗用多少空間。 - **會壓縮長工作階段的程式設計代理**:讓壓縮過程可見。較舊的訊息會收合成一段簡短摘要,格子隨之釋放,狀態列會說明收合了多少內容。 - **在上下文中選擇模型**:切換到視窗較小的模型時,方格會立即重新排列,並顯示工作階段是否仍放得下;若放不下,卡片會收合對話並說明此事。 -- **文件與新手導覽**:用一個可以實際操作的範例說明上下文視窗、輸出保留空間與壓縮。沒有資料時,卡片會播放一段虛構的程式設計工作階段。 +- **文件與新手導覽**:用一段可以實際操作的工作階段說明上下文視窗、輸出保留空間與壓縮,就像本頁的預覽那樣。 ## 何時不用 @@ -27,13 +27,19 @@ source-hash: 10028f4973b8 ```xml - + + + + + + + ``` `ContextWindow` 是 Special 套件中的 `TemplatedControl`。請在 `UseArcControls()` 旁呼叫 `UseArcSpecial()` 來註冊該套件。卡片最寬為 744px,並依自身寬度調整,而非視窗的寬度。 -完全沒有資料(沒有模型、項目、回合或檔案)時,卡片會顯示 `ContextWindowSamples` 提供的虛構範例工作階段:三個範例模型,視窗分別為 1M、512K 與 400K;一個 436K token 的程式設計工作階段;Send message 的五個回合,以及 Attach file 的三個檔案。範例使用它自己的副本,因此 `Items` 保持為空。只要你為卡片提供上述四個清單中的任何一個,卡片就會改用你的資料。 +沒有資料(沒有模型、項目、回合或檔案)時,卡片是空的:沒有模型膠囊,所有格子都空著,也沒有 Send message 與 Attach file。請提供你的模型與項目,並為這兩個按鈕提供 `Turns` 與 `Files`。本頁的展示使用真實模型及其廠商公布的上下文視窗(核對於 2026-10-29:Claude Sonnet 5.5 為 1M,Grok 4.7 為 500K,Mistral Medium 3.5 為 256K),以及一個虛構的、436K token 的程式設計工作階段;輸出保留空間是展示應用程式單次回覆所請求的量(32K),絕不超過該模型公布的輸出上限。 ### 結構 @@ -65,8 +71,8 @@ Send message 會加入 `Turns` 中的下一則,放在視窗末端;Attach fil | 超過門檻時傳送 | Added “Why does the nightly sync time out?” (+35K) · past 80% | 0 | | 開始收合 | Past 80%: folding 35 earlier messages… | 540ms | | 摘要出現 | Folded 608K into a 24K summary | 1,240ms | -| 切換到較小的模型 | 436K is over 80% of Cirrus M | 0 | -| 開始收合 | Too full for Cirrus M: folding 22 earlier messages… | 600ms | +| 切換到較小的模型 | 436K is over 80% of Grok 4.7 | 0 | +| 開始收合 | Too full for Grok 4.7: folding 22 earlier messages… | 600ms | 檔案永遠不會被收合。視窗超過門檻且只剩最新的訊息時,狀態列會說明目前還沒有可收合的內容;壓縮後若視窗仍超過門檻,狀態列會說明收合了多少,並建議移除檔案。已使用的 token 可以超過視窗大小:此時讀數會顯示超過 100%,且沒有剩餘 token。 @@ -159,7 +165,7 @@ card.Labels = new ContextWindowLabels **避免** - 用計時器重建 `Items`。每次變化都會有動畫;只要更新有變動的項目即可。 -- 把範例工作階段當成真實數字呈現。它是虛構的。 +- 把虛構的工作階段當成真實用量呈現。 - 把方格用於單一視窗以外的用途。它永遠有 200 格;其比例只對應一種預算才有意義。 ### 常見問題 @@ -170,7 +176,7 @@ card.Labels = new ContextWindowLabels **切換到較大的模型會發生什麼?** 方格會重新排列,狀態列會說明新的模型與視窗,且不會收合任何內容。 -**為什麼沒有 Send message 與 Attach file?** 在範例之外,當 `Turns` 或 `Files` 為空時,它們會隱藏。 +**為什麼沒有 Send message 與 Attach file?** 當 `Turns` 或 `Files` 為空時,它們會隱藏。 **視窗可以溢出嗎?** 可以。讀數會超過 100%,剩餘空間顯示 0,而那些放不進保留空間之前的部分不會分到格子。 @@ -201,12 +207,10 @@ card.Labels = new ContextWindowLabels | `ContextWindowCompactReason` | `Manual`, `Threshold`, `ModelSwitch` | | `ContextWindowChange` | `Sent`, `Attached`, `Added`, `Compacted` | -`ContextWindowSamples` 保存範例工作階段(`CreateModels()`、`CreateItems()`、`CreateTurns()`、`CreateFiles()`),讓頁面能從它開始,並只修改其中一個部分。 - ## 無障礙 - 卡片是一個名為 "Context window" 的群組。方格是一張圖像,其名稱完整描述它:模型、視窗已使用的 token、每個部分及其 token 數與門檻。每次變化都會更新,但不會隨動畫的每個影格更新。 -- 讀數會被朗讀為一行("436K of 1M tokens, 44% used, 500K free")。圖例是一個名為 "Context breakdown" 的清單,每列朗讀其名稱、token 數與占比,因此不依賴顏色。 +- 讀數會被朗讀為一行("436K of 1M tokens, 44% used, 532K free")。圖例是一個名為 "Context breakdown" 的清單,每列朗讀其名稱、token 數與占比,因此不依賴顏色。 - 膠囊以模型及其視窗命名,並說明它會切換模型。選單中的模型是單選項目,目前選取的會打勾。 - 每次傳送、附加、切換模型與壓縮,都會透過禮貌的即時區域播報,每個狀態都同時有圖示與文字。 - 訊息收合期間,所有動作都會關閉。卡片不會讓鍵盤焦點掉到頁面上,而是把它保留在一個以狀態命名的隱藏容器上,並在壓縮完成時交還:交還給原本擁有焦點的動作,若該動作已停用,則交給 Send message。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/en-US.md new file mode 100644 index 00000000..5810f855 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/en-US.md @@ -0,0 +1,147 @@ +--- +title: Cube mascot +summary: An isometric cube mascot whose shaded faces and painted-on face show what the assistant is doing: it turns in eased quarter turns while thinking, prints lines onto the floor while writing, settles with a sparkle when done and shakes once on error. +--- + + + +## When to use + +`CubeMascot` is an assistant character drawn from real 3D math: a cube with a rounded silhouette, three faces shaded by +how they face the light, and a face painted on the front side. Each status changes both its motion and its face. + +- **Show that an assistant is generating** with a recognisable, product-like character: in a reply header, in a + composer, on an image canvas or in an empty state. +- **Brands with a geometric, technical feel**, where a soft blob would feel off. + +## When not to use + +- **A softer, rounder character**, or one that needs a listening state: use a pebble or ghost style mascot instead. +- **Steps, tools and elapsed time of a run**: use `AgentStatus`. +- **A known percentage**: use a progress bar; the cube shows activity, not progress. + +## Usage + +Map your assistant's real phases to `Status`: waiting for the first token is `Thinking`, streaming is `Writing`, +finished is `Done`, failed is `Error`, otherwise `Idle`. Keep thinking short in real use and switch to writing on the +first token. + +```xml + +``` + +```csharp +mascot.Status = CubeMascotStatus.Thinking; // request sent +mascot.Status = CubeMascotStatus.Writing; // first token +mascot.Status = CubeMascotStatus.Done; // last token +``` + +Leave `Status` unset and the cube plays through idle, thinking, writing, done and error on a loop. That is meant for +showcases; `EffectiveStatus` reports the state it shows and `StatusChanged` is raised at each step. + +### Anatomy + +1. **Body**: a cube turned so that its face side looks slightly left, seen from 32 degrees above. Its silhouette is + the convex hull of the eight projected corners with rounded corners. +2. **Faces**: the top, the face side and the shaded side, each shaded by its normal against a light from the upper + left, so a face that turns away darkens continuously. +3. **Painted face**: eyes, brows and a mouth drawn through the face side's projected plane, so they foreshorten with it. +4. **Ground** (56px and up): a contact shadow and, while writing, lines printed onto the floor in front of the face. +5. **Sparkle**: a four-point star that grows and turns in when done. + +### States + +| Status | Motion | Face | +|---|---|---| +| `Idle` | Slow tilt, sway, float and glance on sines that never repeat; blinks every 2.6 to 4.9s | Open eyes, a gentle smile | +| `Thinking` | Holds 0.5s, then turns a quarter turn over 0.8s, rising and leaning into the turn | Eyes up; the face fades in on the side that arrives | +| `Writing` | Tips the face down; a line every 360ms slides out along the ground; a nod per four lines | Eyes down, sweeping slowly | +| `Done` | Dips once and settles; the sparkle grows, turns in, then twinkles | Eyes close into a happy squint | +| `Error` | Shakes 3.5 times in 560ms and comes to rest | Worried brows and a frown | + +### In a reply header + + + +Use 24 to 32px inline in a message header or composer, 40 to 64px in a panel header and 96 to 160px in an empty state +or on a generating canvas. Set `AssistantName` to the assistant's name; it is used in the accessible label. + +### Sizes + + + +`Size` sets the width and height of the square (96 by default). Below 56px a compact drawing takes over: the cube fills +more of the frame, the eyes are bolder and there is no mouth, shadow or printed lines, so it reads at 24px. The square +never changes size between states, so it never moves the layout around it. + +### Colors + + + +The body is the accent by default and the face is the accent foreground, so the cube follows the theme and the accent +in light and dark. `BodyBrush` and `FaceBrush` change them; the top and the shaded side are mixed from the body. +`TopBrush`, `FrontBrush` and `SideBrush` set the three shading anchors directly, and `LineBrush`, `ShadowBrush` and +`SparkleBrush` color the printed lines, the contact shadow and the sparkle. `ShowsGround="False"` drops the shadow and +the lines. + +### On a canvas, and pausing + + + +`IsPaused` freezes the cube on the current frame. The cube also pauses by itself while it is off screen or hidden, and +resumes on the same frame. + +### Best practices + +- Turn `Announce` on for the one mascot that carries status, not for decorative copies. +- Show a text status next to the cube where the state matters; the cube takes no input, so put controls such as Stop + beside it. +- Leave `Status` unset only for showcases. + +## API reference + + + + + + + + + +## Accessibility + +- The cube presents as an image whose name is built from the assistant's name and the status, for example "Atlas, + writing", and updates with every state. `AccessibleName` replaces the whole label; the words are in + `CubeMascotLabels`. +- With `Announce` on, status changes are read in a polite live region. Use it on one mascot per view. +- Status is never motion alone: each state changes the face (eyes up, eyes down, happy squint, worried brows and frown) + and the label. +- Under reduced motion every state is a still pose that switches in place. + +### Keyboard + +| Keys | Action | +|---|---| +| None | The mascot is a status image and takes no focus. Pair it with a real control when people act on what it shows. | + +## Motion + +| Change | Full | Reduced and off | +|---|---|---| +| Idle | Tilt, sway, float and glance as a sum of slow sines with incommensurate periods; a 170ms blink every 2.6 to 4.9s, sometimes twice | Still pose | +| Thinking | Holds 0.5s, turns 90 degrees over 0.8s on a cubic ease in and out, rising slightly and leaning into the turn; the leaving face fades as it turns edge on, the arriving face fades in and opens its eyes | Still, eyes up | +| Writing | A line every 360ms slides out from under the face along the ground for 1.44s and only fades in and out; its length changes while it is invisible; the eyes sweep and the cube nods once per four lines | Still, three printed lines | +| Done | One dip that settles without overshoot; the eyes pass through a closed line into the squint; the sparkle grows over 550ms, turns in and then twinkles slowly | Still squint and sparkle | +| Error | 3.5 shakes in 560ms under a squared envelope that reaches rest with zero velocity | Still brows and frown | +| Status change | Every pose value blends from the frame on screen over 420ms (240ms into error) on an ease in and out; a change caught mid turn keeps turning forward | Switches in place | + +The cube is real 3D: eight corners are rotated (yaw, roll, pitch, then a fixed 32 degree camera) and projected each +frame. One frame loop per cube moves its paths in place and recolors its brushes, so a frame allocates nothing. The +loop runs only while the cube is on screen and visible, and its clock only advances then, so it resumes on the same +frame. A settled error does not loop at all, and under reduced motion nothing loops. + +## Related + +- `AgentStatus` for the steps, tools and elapsed time of a run. +- `ToolCall` for the tool calls an agent makes. +- `ContextWindow` for how much of a model's context is in use. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/zh-CN.md new file mode 100644 index 00000000..e670d66d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/zh-CN.md @@ -0,0 +1,126 @@ +--- +title: 立方体吉祥物 +summary: 一个等轴测立方体吉祥物,用三个明暗不同的面和画在正面上的表情表示助手正在做什么:思考时以缓动的步子转四分之一圈,书写时在地面上印出一行行线条,完成时带着闪光安顿下来,出错时摇晃一次。 +source-hash: 0c6d41adb6c9 +--- + + + +## 何时使用 + +`CubeMascot` 是一个用真实三维数学绘制的助手角色:一个轮廓圆润的立方体,三个面按朝向光线的程度着色,正面画着一张脸。每种状态都会同时改变它的动效和表情。 + +- **表示助手正在生成**,用一个辨识度高、像产品一样的角色:放在回复标题、输入框、图像画布或空状态中。 +- **几何感、技术感强的品牌**,柔软的团状角色会显得不协调时。 + +## 何时不用 + +- **需要更柔和圆润的角色**,或需要“聆听”状态时:改用卵石或幽灵风格的吉祥物。 +- **需要展示一次运行的步骤、工具和耗时**:使用 `AgentStatus`。 +- **已知百分比的进度**:使用进度条;立方体表示的是活动,不是进度。 + +## 用法 + +把助手的真实阶段映射到 `Status`:等待第一个 token 是 `Thinking`,流式输出是 `Writing`,结束是 `Done`,失败是 `Error`,其余为 `Idle`。实际使用时让思考阶段尽量短,收到第一个 token 就切换到书写。 + +```xml + +``` + +```csharp +mascot.Status = CubeMascotStatus.Thinking; // request sent +mascot.Status = CubeMascotStatus.Writing; // first token +mascot.Status = CubeMascotStatus.Done; // last token +``` + +不设置 `Status` 时,立方体会循环播放空闲、思考、书写、完成和出错。这用于展示场景;`EffectiveStatus` 给出当前显示的状态,每一步都会触发 `StatusChanged`。 + +### 结构 + +1. **主体**:一个立方体,转到画着脸的一面略朝左,从上方 32 度俯视。它的轮廓是八个投影顶点的凸包,转角做了圆角。 +2. **各个面**:顶面、脸所在的面和背光的侧面,各自按法线与左上方光源的夹角着色,所以一个面转开时会连续地变暗。 +3. **表情**:眼睛、眉毛和嘴巴通过脸所在面的投影平面绘制,随该面一起透视缩短。 +4. **地面**(56px 及以上):接触阴影,以及书写时印在脸前方地面上的线条。 +5. **闪光**:一颗四角星,完成时长大并旋转到位。 + +### 状态 + +| 状态 | 动效 | 表情 | +|---|---|---| +| `Idle` | 缓慢地倾斜、摇摆、浮动和张望,正弦组合永不重复;每 2.6 到 4.9 秒眨一次眼 | 睁眼,浅浅的微笑 | +| `Thinking` | 停 0.5 秒,然后在 0.8 秒内转四分之一圈,转动时微微升起并向转动方向倾斜 | 眼睛向上;脸在转到正面的那一面上淡入 | +| `Writing` | 脸向下低;每 360ms 一条线沿地面滑出;每四条线点一次头 | 眼睛向下,缓慢地左右扫视 | +| `Done` | 下沉一次后安顿下来;闪光长大、旋转到位,然后闪烁 | 眼睛合拢成开心的眯眼 | +| `Error` | 在 560ms 内摇晃 3.5 次后停稳 | 担忧的眉毛和皱眉的嘴 | + +### 在回复标题中 + + + +在消息标题或输入框中内联使用 24 到 32px,在面板标题中用 40 到 64px,在空状态或正在生成的画布上用 96 到 160px。把 `AssistantName` 设为助手的名字,它会用在无障碍标签中。 + +### 尺寸 + + + +`Size` 设置这个正方形的宽和高(默认 96)。小于 56px 时改用紧凑画法:立方体在画框中占得更多,眼睛更粗,没有嘴巴、阴影和印出的线条,所以在 24px 也能看清。这个正方形在各状态间大小不变,因此不会挤动周围的布局。 + +### 颜色 + + + +主体默认是强调色,表情默认是强调色上的前景色,所以立方体会在亮色和暗色下跟随主题与强调色。`BodyBrush` 和 `FaceBrush` 可以改变它们;顶面和背光侧面由主体颜色混合得到。`TopBrush`、`FrontBrush` 和 `SideBrush` 直接设置三个着色锚点,`LineBrush`、`ShadowBrush` 和 `SparkleBrush` 分别为印出的线条、接触阴影和闪光着色。`ShowsGround="False"` 去掉阴影和线条。 + +### 在画布上与暂停 + + + +`IsPaused` 把立方体冻结在当前帧。立方体在屏幕外或被隐藏时也会自动暂停,并从同一帧继续。 + +### 最佳实践 + +- 只为承载状态的那个吉祥物打开 `Announce`,装饰性的副本不要打开。 +- 在状态重要的地方,在立方体旁边显示文字状态;立方体不接受输入,所以把“停止”等控件放在它旁边。 +- 只在展示场景中不设置 `Status`。 + +## API 参考 + + + + + + + + + +## 无障碍 + +- 立方体呈现为一张图像,名称由助手的名字和状态组成,例如“Atlas, writing”,并随每个状态更新。`AccessibleName` 替换整个标签;所用的词都在 `CubeMascotLabels` 中。 +- 打开 `Announce` 后,状态变化会在礼貌(polite)实时区域中朗读。每个视图只对一个吉祥物使用。 +- 状态从不只靠动效表达:每种状态都会改变表情(眼睛向上、眼睛向下、开心的眯眼、担忧的眉毛和皱眉)以及标签。 +- 在减弱动效下,每种状态都是一个静止的姿态,原地切换。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| 无 | 吉祥物是状态图像,不获取焦点。当用户需要对它显示的内容采取行动时,搭配一个真正的控件。 | + +## 动效 + +| 变化 | 完整 | 减弱与关闭 | +|---|---|---| +| 空闲 | 倾斜、摇摆、浮动和张望由周期互不相称的慢速正弦叠加而成;每 2.6 到 4.9 秒眨一次 170ms 的眼,偶尔连眨两次 | 静止姿态 | +| 思考 | 停 0.5 秒,以三次缓入缓出在 0.8 秒内转 90 度,微微升起并向转动方向倾斜;离开的脸转到侧向时淡出,到来的脸淡入并睁开眼睛 | 静止,眼睛向上 | +| 书写 | 每 360ms 一条线从脸下沿地面滑出,持续 1.44 秒,只做淡入淡出;线条长度在不可见时改变;眼睛扫视,每四条线点一次头 | 静止,三条印出的线 | +| 完成 | 下沉一次后无过冲地停稳;眼睛经过一条闭合的线变成眯眼;闪光在 550ms 内长大、旋转到位,然后缓慢闪烁 | 静止的眯眼和闪光 | +| 出错 | 在平方包络下于 560ms 内摇晃 3.5 次,停下时速度为零 | 静止的眉毛和皱眉 | +| 状态切换 | 每个姿态值都从屏幕上的当前帧以缓入缓出在 420ms 内过渡(进入出错为 240ms);转到一半时切换会继续向前转完 | 原地切换 | + +立方体是真正的三维:每一帧都把八个顶点依次旋转(偏航、横滚、俯仰,然后是固定 32 度的相机)并投影。每个立方体一个帧循环,原地移动路径、重新着色画刷,所以每帧不分配内存。循环只在立方体位于屏幕内且可见时运行,时钟也只在此时前进,所以会从同一帧继续。停稳后的出错状态完全不循环,减弱动效下也没有任何循环。 + +## 相关组件 + +- `AgentStatus`:展示一次运行的步骤、工具和耗时。 +- `ToolCall`:智能体发起的工具调用。 +- `ContextWindow`:模型上下文已使用了多少。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/zh-TW.md new file mode 100644 index 00000000..557d7a0f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/CubeMascot/zh-TW.md @@ -0,0 +1,126 @@ +--- +title: 立方體吉祥物 +summary: 一個等軸測立方體吉祥物,用三個明暗不同的面和畫在正面上的表情表示助理正在做什麼:思考時以緩動的步伐轉四分之一圈,書寫時在地面上印出一行行線條,完成時帶著閃光安頓下來,出錯時搖晃一次。 +source-hash: 0c6d41adb6c9 +--- + + + +## 何時使用 + +`CubeMascot` 是一個用真實三維數學繪製的助理角色:一個輪廓圓潤的立方體,三個面依朝向光線的程度著色,正面畫著一張臉。每種狀態都會同時改變它的動態效果和表情。 + +- **表示助理正在生成**,用一個辨識度高、像產品一樣的角色:放在回覆標題、輸入框、影像畫布或空狀態中。 +- **幾何感、技術感強的品牌**,柔軟的團狀角色會顯得不協調時。 + +## 何時不用 + +- **需要更柔和圓潤的角色**,或需要「聆聽」狀態時:改用卵石或幽靈風格的吉祥物。 +- **需要呈現一次執行的步驟、工具和耗時**:使用 `AgentStatus`。 +- **已知百分比的進度**:使用進度列;立方體表示的是活動,不是進度。 + +## 用法 + +把助理的真實階段對應到 `Status`:等待第一個 token 是 `Thinking`,串流輸出是 `Writing`,結束是 `Done`,失敗是 `Error`,其餘為 `Idle`。實際使用時讓思考階段盡量短,收到第一個 token 就切換到書寫。 + +```xml + +``` + +```csharp +mascot.Status = CubeMascotStatus.Thinking; // request sent +mascot.Status = CubeMascotStatus.Writing; // first token +mascot.Status = CubeMascotStatus.Done; // last token +``` + +不設定 `Status` 時,立方體會循環播放閒置、思考、書寫、完成和出錯。這用於展示情境;`EffectiveStatus` 提供目前顯示的狀態,每一步都會觸發 `StatusChanged`。 + +### 結構 + +1. **主體**:一個立方體,轉到畫著臉的一面略朝左,從上方 32 度俯視。它的輪廓是八個投影頂點的凸包,轉角做了圓角。 +2. **各個面**:頂面、臉所在的面和背光的側面,各自依法線與左上方光源的夾角著色,所以一個面轉開時會連續地變暗。 +3. **表情**:眼睛、眉毛和嘴巴透過臉所在面的投影平面繪製,隨該面一起透視縮短。 +4. **地面**(56px 以上):接觸陰影,以及書寫時印在臉前方地面上的線條。 +5. **閃光**:一顆四角星,完成時長大並旋轉到位。 + +### 狀態 + +| 狀態 | 動態效果 | 表情 | +|---|---|---| +| `Idle` | 緩慢地傾斜、搖擺、浮動和張望,正弦組合永不重複;每 2.6 到 4.9 秒眨一次眼 | 睜眼,淺淺的微笑 | +| `Thinking` | 停 0.5 秒,然後在 0.8 秒內轉四分之一圈,轉動時微微升起並向轉動方向傾斜 | 眼睛向上;臉在轉到正面的那一面上淡入 | +| `Writing` | 臉向下低;每 360ms 一條線沿地面滑出;每四條線點一次頭 | 眼睛向下,緩慢地左右掃視 | +| `Done` | 下沉一次後安頓下來;閃光長大、旋轉到位,然後閃爍 | 眼睛闔成開心的瞇眼 | +| `Error` | 在 560ms 內搖晃 3.5 次後停穩 | 擔憂的眉毛和皺眉的嘴 | + +### 在回覆標題中 + + + +在訊息標題或輸入框中內嵌使用 24 到 32px,在面板標題中用 40 到 64px,在空狀態或正在生成的畫布上用 96 到 160px。把 `AssistantName` 設為助理的名字,它會用在無障礙標籤中。 + +### 尺寸 + + + +`Size` 設定這個正方形的寬和高(預設 96)。小於 56px 時改用精簡畫法:立方體在畫框中佔得更多,眼睛更粗,沒有嘴巴、陰影和印出的線條,所以在 24px 也能看清。這個正方形在各狀態間大小不變,因此不會擠動周圍的版面配置。 + +### 顏色 + + + +主體預設是強調色,表情預設是強調色上的前景色,所以立方體會在淺色和深色下跟隨主題與強調色。`BodyBrush` 和 `FaceBrush` 可以改變它們;頂面和背光側面由主體顏色混合而成。`TopBrush`、`FrontBrush` 和 `SideBrush` 直接設定三個著色錨點,`LineBrush`、`ShadowBrush` 和 `SparkleBrush` 分別為印出的線條、接觸陰影和閃光著色。`ShowsGround="False"` 去掉陰影和線條。 + +### 在畫布上與暫停 + + + +`IsPaused` 把立方體凍結在目前的影格。立方體在螢幕外或被隱藏時也會自動暫停,並從同一影格繼續。 + +### 最佳實務 + +- 只為承載狀態的那個吉祥物開啟 `Announce`,裝飾性的副本不要開啟。 +- 在狀態重要的地方,在立方體旁邊顯示文字狀態;立方體不接受輸入,所以把「停止」等控制項放在它旁邊。 +- 只在展示情境中不設定 `Status`。 + +## API 參考 + + + + + + + + + +## 無障礙 + +- 立方體呈現為一張影像,名稱由助理的名字和狀態組成,例如「Atlas, writing」,並隨每個狀態更新。`AccessibleName` 取代整個標籤;所用的詞都在 `CubeMascotLabels` 中。 +- 開啟 `Announce` 後,狀態變化會在禮貌(polite)即時區域中朗讀。每個檢視只對一個吉祥物使用。 +- 狀態從不只靠動態效果表達:每種狀態都會改變表情(眼睛向上、眼睛向下、開心的瞇眼、擔憂的眉毛和皺眉)以及標籤。 +- 在減少動態效果下,每種狀態都是一個靜止的姿態,原地切換。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| 無 | 吉祥物是狀態影像,不取得焦點。當使用者需要對它顯示的內容採取行動時,搭配一個真正的控制項。 | + +## 動效 + +| 變化 | 完整 | 減少與關閉 | +|---|---|---| +| 閒置 | 傾斜、搖擺、浮動和張望由週期互不相稱的慢速正弦疊加而成;每 2.6 到 4.9 秒眨一次 170ms 的眼,偶爾連眨兩次 | 靜止姿態 | +| 思考 | 停 0.5 秒,以三次緩入緩出在 0.8 秒內轉 90 度,微微升起並向轉動方向傾斜;離開的臉轉到側向時淡出,到來的臉淡入並睜開眼睛 | 靜止,眼睛向上 | +| 書寫 | 每 360ms 一條線從臉下沿地面滑出,持續 1.44 秒,只做淡入淡出;線條長度在不可見時改變;眼睛掃視,每四條線點一次頭 | 靜止,三條印出的線 | +| 完成 | 下沉一次後無過衝地停穩;眼睛經過一條閉合的線變成瞇眼;閃光在 550ms 內長大、旋轉到位,然後緩慢閃爍 | 靜止的瞇眼和閃光 | +| 出錯 | 在平方包絡下於 560ms 內搖晃 3.5 次,停下時速度為零 | 靜止的眉毛和皺眉 | +| 狀態切換 | 每個姿態值都從螢幕上的目前影格以緩入緩出在 420ms 內過渡(進入出錯為 240ms);轉到一半時切換會繼續向前轉完 | 原地切換 | + +立方體是真正的三維:每一影格都把八個頂點依序旋轉(偏航、橫滾、俯仰,然後是固定 32 度的相機)並投影。每個立方體一個影格迴圈,原地移動路徑、重新著色筆刷,所以每影格不配置記憶體。迴圈只在立方體位於螢幕內且可見時執行,時鐘也只在此時前進,所以會從同一影格繼續。停穩後的出錯狀態完全不循環,減少動態效果下也沒有任何迴圈。 + +## 相關 + +- `AgentStatus`:呈現一次執行的步驟、工具和耗時。 +- `ToolCall`:代理發起的工具呼叫。 +- `ContextWindow`:模型內容脈絡已使用了多少。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/en-US.md new file mode 100644 index 00000000..1c011676 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/en-US.md @@ -0,0 +1,316 @@ +--- +title: Design to code +summary: A screenshot-to-code card: a scan line finds the layout regions of a dropped screenshot or design file frame, then code streams in beside it, linked region by region, with a live preview one tab away. +--- + + + +## When to use + +`DesignToCode` is the card of a screenshot-to-code feature. The person drops a UI screenshot on the design side or +imports a frame from a design file. A scan line crosses the screenshot and each layout region it finds appears as a box +with a tag. Then the code streams in beside it, region by region: the region being written is highlighted on the +screenshot and its lines carry a quiet tint. Pointing at a box or a tag lights up its lines, and pointing at a line +lights up its box. One tab away, the Preview shows the result working. + +- **Design to code features**: upload a screenshot or pick a design file frame and get a component back. +- **Showing an AI vision step**: when what the model detected matters, such as layout parsing before generation. +- **Marketing and onboarding**: explain what a code generation product does at a glance. Give the card a + `Simulation` and it plays a design and its code on a loop while it is on screen. + +## When not to use + +- **Plain file uploads with no generation step**: use a file upload. +- **Finished code without the source design**: use `CodeBlock`. +- **A full editor with files, tabs and a terminal**: build a workspace layout instead of stretching this card. + +## Usage + +```xml + + + +``` + +`DesignToCode` is a `TemplatedControl` in the Special package. Register the package with `UseArcSpecial()` next to +`UseArcControls()`. The card fills its container's width; keep it between 360 and 760px wide, in a page or a dialog. + +With no `Generator` and no `Simulation` the card opens on its drop zone: drop a screenshot, choose a file or import a +design file. Wire a `Generator` to turn designs into code, or give the card a `Simulation` to play. + +### Simulation + +`DesignToCodeSimulation` is a design and the code a model could write for it: the `Source` (an image, or a sketch drawn +in code), the `Regions` the scan finds, the code as `Parts`, each `DesignToCodeSimulationPart` tagged with the region it +implements, and optionally the `Model` credited, the `FileName`, the `FrameName` a simulated import brings in, a +`PreviewFactory` and a `PreviewAction` the loop calls on the preview. Without a `Generator` the card shows its design +while `Source` is unset, and Use sample brings it back. The run is simulated: nothing is uploaded or sent anywhere, and +the announcements say so. While the card is on screen it plays on a loop (generate, hold on the code, show the preview, +start again) until the first pointer or key press inside the card. Setting `Simulation` to null stops a simulated run +and returns to the drop zone. + +```csharp +card.Simulation = new DesignToCodeSimulation +{ + Source = new DesignToCodeSource("pricing-card.png", screenshot), + Regions = [new DesignToCodeRegion("price", "Price", 7.5, 42.5, 52, 14.5)], + Parts = + [ + new DesignToCodeSimulationPart("export function PricingCard() {\n return (\n"), + new DesignToCodeSimulationPart("

$24

\n", "price"), + new DesignToCodeSimulationPart(" );\n}\n"), + ], + Model = new AgentModel("Your model"), + FileName = "PricingCard.tsx", +}; +``` + +A design can also be drawn in code instead of shipped as an image: give the source a `Sketch` (a `DesignToCodeSketch` +that draws into the design's rectangle with the card's `DesignToCodeSketchPalette`, so it follows light and dark) and +its `SketchSize`. The demos on this page draw a small pricing card that way and take everything from +`DesignToCodeDemoData`; the model credited is a real one (checked on 2026-10-29), the plan and prices are invented. + +### Anatomy + +1. **Design pane**: a header with the design's name, a "Sample" or "Design file" tag and Remove; under it the + screenshot with its region boxes and tags, or the drop zone when there is no design. +2. **Region boxes and tags**: a 1px accent outline per region and a small tag level with its top, in the margin to its + left (inside the box when there is no room). A tag is a button named "Price: show its code". +3. **Output pane**: a header with the file name, the model's mark and name, the Code and Preview toggle and Copy; under + it the code with line numbers, or the live preview. +4. **Footer**: the status (an icon, a short label, the file and meta text such as "52 lines in 3.4s"), Import design + file and the main action, which reads Generate code, Stop, Regenerate or Try again. + +### The run + +`Phase` follows the run: `Empty` without a design, `Ready` with one, then `Analyzing`, `Writing` and `Done`, or +`Stopped` and `Error`. Generate code (or `Generate()`) starts a run and returns the output to Code. Stop, Escape or +`Stop()` ends a run that is analyzing or writing; the code written so far is kept. `Code` holds the code on screen and +`Completed` reports the finished code with its line count and time. + +| Phase | Status | Main action | +|---|---|---| +| Empty | Add a design to start | Generate code (off) | +| Ready | Ready pricing-card.png | Generate code | +| Analyzing | Analyzing layout pricing-card.png · 3 regions | Stop | +| Writing | Writing PricingCard.tsx · 5 regions | Stop | +| Done | Done PricingCard.tsx · 52 lines in 3.4s | Regenerate | +| Stopped | Stopped PricingCard.tsx · Partial code kept | Regenerate | +| Error | Generation failed: the model timed out. Try again. | Try again | + +### Your model + +Give the card a `Generator`: an async delegate that receives the design and a `DesignToCodeContext`. Call `AddRegion` +for each layout region as it is detected (its box in percent of the design), then `Write` code chunks as they stream, +passing the id of the region a chunk implements so its lines link to the box. Complete the task when done, throw to show +the error, and stop when the context's token is cancelled. Chunks can be any size: the card paces them into whole words. + + + +```csharp +card.Model = new AgentModel("Your model"); +card.Generator = async (source, context) => +{ + await foreach (var e in backend.StreamAsync(source.File, context.CancellationToken)) + { + if (e.Region is { } region) + { + context.AddRegion(new DesignToCodeRegion(region.Id, region.Label, region.X, region.Y, region.Width, region.Height)); + } + else + { + context.Write(e.Text, e.RegionId); + } + } +}; +card.Completed += (_, e) => drafts.Save(e.Code); +``` + +`AddRegion` and `Write` can be called from any thread. With a generator the card opens on the drop zone and does not +loop; set `Source` to start from a design. `Model` credits the model in the code header with its mark (a 24 × 24 stroked +glyph); without one the card shows only the sparkle (or the simulation's model while it plays). + +### Dropping a screenshot + +Drop works anywhere on the design side and shows a "Drop to use this design" cover while a file is over it. Every drop +path also has a button: Choose file, Use sample and Import design file. `AcceptedFileTypes` (as in an input's accept +attribute: MIME types, `image/*` or extensions) and `MaxFileSize` (10 MB) decide what is accepted; any other file shows +an error that names the problem and the fix, and the design shown stays. + + + +Choose file raises `ChooseFileRequested`. Open your picker there, pass the file to `LoadFileAsync` and set `Handled`; +when nobody handles it the card uses the simulation's design, if any, and says that nothing was opened. A dropped or loaded file becomes a +`DesignToCodeSource` with its name, the decoded image and the `IStorageFile`, which your generator can send to your +backend. The card releases images it decoded itself when they are replaced. + +```csharp +card.ChooseFileRequested += async (_, e) => +{ + e.Handled = true; + var files = await TopLevel.GetTopLevel(card)!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()); + if (files.Count > 0) + { + await card.LoadFileAsync(files[0]); + } +}; +``` + +### Design files + +Import design file runs `DesignFileImporter`, your picker or link flow: return the frame as a `DesignToCodeSource` +with its rendered image, or null when the person cancels. Without one the card imports the simulation's design under +its `FrameName`, if there is a simulation, and says that nothing was opened. A source without an image draws its +`Sketch`. + +### The live preview + +The Preview tab shows the finished code working. `PreviewFactory` renders it, for example in a sandboxed web view; +never run generated code inside your app. Without a factory a simulated run uses the simulation's `PreviewFactory` +(the demos render the pricing card, where Monthly and Yearly switch the price), and anything else shows a short note. Code and Preview stay mounted, so switching keeps each scroll position, and every new run returns to +Code so the stream is visible. + +### Narrow containers + +The layout follows the card's own width. From 640px the panes sit side by side (the design 344px, the code the rest); +below that they stack, each 352px tall, so the card never changes size while code streams. Under 520px the status drops +the file name and Import design file shows only its icon, keeping its name for assistive tech. Under 440px the meta text +and the model name hide; the mark stays. Long code lines wrap with a hanging indent, so nothing scrolls sideways. + + + +### Reduced motion + +Under reduced motion there is no scan line and no chunk fade: the regions appear at once, the code arrives a part +(a region) at a time and a simulation plays once instead of looping. + + + +### Content guidelines + +Region labels are tags on a small screenshot: one or two words that name the part, not the component. + +| Instead of | Write | +|---|---| +| div.pricing-header-container | Header | +| Segmented control (2 options) | Billing toggle | +| Generation completed successfully | Done | + +`Labels` takes a `DesignToCodeLabels` object with every word the card shows or announces. Sentences with values are +functions, so the word order can change in other languages: + +```csharp +card.Labels = new DesignToCodeLabels +{ + Generate = "Write the code", + DoneMeta = (lines, time) => $"{lines} lines, {time.TotalSeconds:0.0}s", +}; +``` + +### Best practices + +**Do** + +- Send the regions first, then code tagged with the region it implements, so the boxes and the lines link up. +- Render the preview in a sandbox, and keep the card one per page or dialog. +- Stop your request when the context's token is cancelled. + +**Avoid** + +- Presenting a simulation as a real run. It is made up and says so. +- Running generated code inside your app to preview it. +- Sending regions without ids: a chunk links to its box through the id. + +### Common questions + +**Why does the card open on a design?** Without `Source` and without a `Generator` it shows its simulation's design. +Set `Source` to null to open on the drop zone. + +**Why do my lines not link to a box?** A line belongs to the region of its first character; pass the region's id with +every chunk that implements it. + +**What happens to code that arrived after Stop?** It is dropped. Only the code on screen is kept, and later writes from +the stopped run are ignored. + +## API reference + + + + + + + + + + + + + + + + + + + +The enums: + +| Type | Values | +|---|---| +| `DesignToCodePhase` | `Empty`, `Ready`, `Analyzing`, `Writing`, `Done`, `Stopped`, `Error` | +| `DesignToCodeView` | `Code`, `Preview` | +| `DesignToCodeSourceKind` | `Sample`, `File`, `DesignFile` | + +## Accessibility + +- The card is named "Design to code" and holds two named groups, Design and Output. A polite live region announces each + step: analyzing, the regions found, done with the line count and the time, stopped, and errors. The simulation says + that nothing was uploaded or opened. +- Status is never color alone: each state pairs an icon with a short label, and errors name the problem and the fix. +- Region tags are real buttons named like "Price: show its code". The code area is a focusable, named region whose + status reads "Writing" while code streams; it is not a live region, so screen readers are not flooded with tokens. +- The hidden layer of the Code and Preview pair is invisible and out of the tab order. A sketched design is + decorative; a dropped image is named after its file. +- Every drop path has a button: Choose file, Use sample and Import design file. + +### Keyboard + +| Keys | Action | +|---|---| +| `Tab` | Moves through Remove, the region tags, the view toggle, Copy, Import design file and the main action | +| `Enter` / `Space` | Activates the focused control; on the drop zone, Choose file asks for a file | +| `Left` / `Right` | Switch between Code and Preview on the view toggle | +| `Escape` | Stops a run that is analyzing or writing; the partial code is kept | +| Focus a region tag | Highlights that region's box and its lines, scrolling them into view | + +## Motion + +- Analyzing: a 1px scan line crosses the screenshot in one second, and each region's box and tag fade in as the line + reaches its top (160ms opacity, no scale). +- Writing: code streams in batches of whole words about every 34ms (3 to 8 words at reading pace); each batch only + fades in over 140ms. The region being written is highlighted on the screenshot and its lines carry a quiet tint. +- The status, the placeholders and the Code and Preview layers crossfade: the old one out in 100ms, the new one in over + 160ms. The main button's width follows its label without bounce. +- In the preview the billing thumb glides on a 0.24s spring with no bounce, and the price swaps with a 2px crossfade on + tabular numerals. +- The simulation's loop waits while the card is off screen. + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Scan line | Crosses in 1s | None | None | +| Regions | Fade in as the line reaches them | At once | At once | +| Code | Word batches, each fading in | A part at a time | A part at a time | +| Code and Preview | Crossfade | Short crossfade | At once | +| Simulation loop | Loops | Plays once | Plays once | + +## Related + +- `CodeBlock` for finished code with copy. +- `ToolCall` for one step of an agent's run. +- `AgentStatus` for a compact live status of a running agent. + +| Component | Choose it when | +|---|---| +| `CodeBlock` | The code is finished and the design is not part of the story | +| `ToolCall` | The generation is one step in an agent transcript | +| `AgentStatus` | Only the progress of a run matters | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/zh-CN.md new file mode 100644 index 00000000..e1b6d6bf --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/zh-CN.md @@ -0,0 +1,289 @@ +--- +title: Design to code +summary: 截图转代码卡片:扫描线找出拖入的截图或设计文件画框中的布局区域,随后代码在旁边流式写出,并与区域一一对应,切换一个标签页即可看到实时预览。 +source-hash: 0ad3fab6e8cd +--- + + + +## 何时使用 + +`DesignToCode` 是“截图转代码”功能的卡片。用户把界面截图拖到设计一侧,或者从设计文件导入一个画框。一条扫描线扫过截图, +它找到的每个布局区域都以带标签的方框出现。随后代码在旁边按区域逐段流式写出:正在编写的区域在截图上高亮,对应的代码行带有淡淡的底色。 +指向方框或标签会点亮它的代码行,指向某一行代码也会点亮它的方框。切换一个标签页,“预览”就会展示可以实际操作的结果。 + +- **设计转代码功能**:上传一张截图或选择设计文件中的一个画框,得到一个组件。 +- **展示 AI 视觉步骤**:当模型识别出了什么很重要时,例如生成之前的布局解析。 +- **营销与新手引导**:一眼说明代码生成产品在做什么。给卡片一个 `Simulation`,它就会在可见期间循环播放一个设计及其代码。 + +## 何时不用 + +- **没有生成步骤的普通文件上传**:使用文件上传组件。 +- **只展示完成的代码而不需要源设计**:使用 `CodeBlock`。 +- **带文件、标签页和终端的完整编辑器**:搭建一个工作区布局,而不是把这张卡片撑大。 + +## 用法 + +```xml + + + +``` + +`DesignToCode` 是 Special 包中的 `TemplatedControl`。在 `UseArcControls()` 旁边用 `UseArcSpecial()` 注册这个包。 +卡片填满容器的宽度;请把它放在页面或对话框中,宽度保持在 360 到 760px 之间。 + +既没有 `Generator` 也没有 `Simulation` 时,卡片以拖放区打开:拖入截图、选择文件或导入设计文件。接入 `Generator` 把设计转成代码,或者给卡片一个 `Simulation` 来播放。 + +### 模拟 + +`DesignToCodeSimulation` 是一个设计以及模型可能为它写出的代码:`Source`(一张图像,或用代码绘制的草图)、扫描找到的 `Regions`、 +分段的代码 `Parts`(每个 `DesignToCodeSimulationPart` 标注它实现的区域),以及可选的署名模型 `Model`、`FileName`、模拟导入时带入的 +`FrameName`、`PreviewFactory`,和循环时对预览调用的 `PreviewAction`。没有 `Generator` 时,`Source` 未设置期间卡片显示它的设计, +“Use sample” 会把它带回来。这次运行是模拟的:不会上传或发送任何内容,朗读的提示也会说明这一点。卡片在屏幕上时会循环播放(生成、 +停留在代码上、显示预览、重新开始),直到卡片内第一次出现指针按下或按键。把 `Simulation` 设为 null 会停止模拟运行并回到拖放区。 + +```csharp +card.Simulation = new DesignToCodeSimulation +{ + Source = new DesignToCodeSource("pricing-card.png", screenshot), + Regions = [new DesignToCodeRegion("price", "Price", 7.5, 42.5, 52, 14.5)], + Parts = + [ + new DesignToCodeSimulationPart("export function PricingCard() {\n return (\n"), + new DesignToCodeSimulationPart("

$24

\n", "price"), + new DesignToCodeSimulationPart(" );\n}\n"), + ], + Model = new AgentModel("Your model"), + FileName = "PricingCard.tsx", +}; +``` + +设计也可以用代码绘制,而不是提供图像:给来源设置 `Sketch`(一个 `DesignToCodeSketch`,用卡片的 `DesignToCodeSketchPalette` +在设计的矩形中绘制,因此会跟随浅色和深色主题)以及它的 `SketchSize`。本页的演示就是这样绘制一张小型价格卡片,所有内容都来自 +`DesignToCodeDemoData`;署名的模型是真实的(核对于 2026-10-29),方案和价格是虚构的。 + +### 结构 + +1. **设计面板**:标题栏显示设计的名称、“Sample”或“Design file”标签以及“移除”按钮;下方是带区域方框和标签的截图, + 没有设计时则是拖放区。 +2. **区域方框与标签**:每个区域一个 1px 的强调色轮廓,以及一个与其顶边对齐、位于其左侧留白中的小标签(没有空间时放在方框内)。 + 标签是一个按钮,名称类似“Price: show its code”。 +3. **输出面板**:标题栏显示文件名、模型的标记与名称、“代码 / 预览”切换以及“复制”;下方是带行号的代码,或实时预览。 +4. **底栏**:状态(图标、简短标签、文件以及类似“52 lines in 3.4s”的附加信息)、“导入设计文件”以及主操作, + 主操作依次显示 Generate code、Stop、Regenerate 或 Try again。 + +### 一次运行 + +`Phase` 跟随运行进度:没有设计时为 `Empty`,有设计时为 `Ready`,然后是 `Analyzing`、`Writing` 和 `Done`,或者 +`Stopped` 与 `Error`。Generate code(或 `Generate()`)开始一次运行,并把输出切回“代码”。Stop、Escape 或 `Stop()` +结束正在分析或编写的运行;已经写出的代码会保留。`Code` 保存屏幕上的代码,`Completed` 报告完成的代码以及行数和用时。 + +| 阶段 | 状态 | 主操作 | +|---|---|---| +| Empty | Add a design to start | Generate code(不可用) | +| Ready | Ready pricing-card.png | Generate code | +| Analyzing | Analyzing layout pricing-card.png · 3 regions | Stop | +| Writing | Writing PricingCard.tsx · 5 regions | Stop | +| Done | Done PricingCard.tsx · 52 lines in 3.4s | Regenerate | +| Stopped | Stopped PricingCard.tsx · Partial code kept | Regenerate | +| Error | Generation failed: the model timed out. Try again. | Try again | + +### 接入你的模型 + +为卡片提供 `Generator`:一个接收设计与 `DesignToCodeContext` 的异步委托。每识别出一个布局区域就调用 `AddRegion` +(方框以设计尺寸的百分比表示),然后在代码流式到达时调用 `Write`,并传入该代码块所实现区域的 id,让这些行与方框关联。 +完成时结束任务,抛出异常以显示错误,并在上下文的令牌取消时停止。代码块可以是任意大小:卡片会把它们整理成完整的单词逐批显示。 + + + +```csharp +card.Model = new AgentModel("Your model"); +card.Generator = async (source, context) => +{ + await foreach (var e in backend.StreamAsync(source.File, context.CancellationToken)) + { + if (e.Region is { } region) + { + context.AddRegion(new DesignToCodeRegion(region.Id, region.Label, region.X, region.Y, region.Width, region.Height)); + } + else + { + context.Write(e.Text, e.RegionId); + } + } +}; +card.Completed += (_, e) => drafts.Save(e.Code); +``` + +`AddRegion` 和 `Write` 可以在任意线程调用。设置了生成器的卡片会以拖放区打开,也不会循环播放;设置 `Source` 即可从某个设计开始。 +`Model` 在代码标题栏中注明模型,并显示它的标记(24 × 24 的描边图形);没有设置时卡片只显示闪光图标(播放模拟时显示模拟的模型)。 + +### 拖入截图 + +在设计一侧的任何位置都可以拖放,文件悬停时会显示“Drop to use this design”遮罩。每种拖放方式都有对应的按钮: +Choose file、Use sample 和 Import design file。`AcceptedFileTypes`(写法同输入框的 accept 属性:MIME 类型、`image/*` +或扩展名)与 `MaxFileSize`(10 MB)决定接受哪些文件;其他文件会显示一条说明问题与解决办法的错误,当前显示的设计保持不变。 + + + +Choose file 会引发 `ChooseFileRequested`。在其中打开你的文件选择器,把文件交给 `LoadFileAsync`,并设置 `Handled`; +没有人处理时,卡片会改用模拟的设计(如果有),并说明没有打开任何文件。拖入或加载的文件会变成一个 `DesignToCodeSource`,包含它的名称、 +解码后的图像以及 `IStorageFile`,你的生成器可以把它发送到后端。卡片自己解码的图像在被替换时会释放。 + +```csharp +card.ChooseFileRequested += async (_, e) => +{ + e.Handled = true; + var files = await TopLevel.GetTopLevel(card)!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()); + if (files.Count > 0) + { + await card.LoadFileAsync(files[0]); + } +}; +``` + +### 设计文件 + +Import design file 会运行 `DesignFileImporter`,也就是你的选择器或链接流程:返回带渲染图像的画框(`DesignToCodeSource`), +用户取消时返回 null。没有设置时,如果有模拟,卡片会以它的 `FrameName` 导入模拟的设计,并说明没有打开任何文件。没有图像的来源会绘制它的 `Sketch`。 + +### 实时预览 + +“预览”标签页展示完成的代码实际运行的样子。`PreviewFactory` 负责渲染,例如放在沙箱化的网页视图中;切勿在你的应用内运行生成的代码。 +没有工厂时,模拟运行使用模拟的 `PreviewFactory`(演示渲染价格卡片,Monthly 与 Yearly 会切换价格),其他情况显示一条简短说明。“代码”和“预览”始终保持挂载,因此切换时会保留各自的滚动位置; +每次新的运行都会回到“代码”,让流式输出可见。 + +### 窄容器 + +布局跟随卡片自身的宽度。从 640px 起两个面板并排(设计 344px,代码占其余部分);更窄时上下堆叠,各高 352px, +因此代码流式写出时卡片的尺寸不会变化。低于 520px 时,状态不再显示文件名,“导入设计文件”只显示图标,但保留其名称供辅助技术使用。 +低于 440px 时,附加信息和模型名称隐藏,标记保留。较长的代码行以悬挂缩进换行,因此不会出现横向滚动。 + + + +### 减少动态效果 + +在减少动态效果下没有扫描线,也没有代码块淡入:区域立即出现,代码一次到达一段(一个区域),模拟只播放一次而不再循环。 + + + +### 内容指南 + +区域标签是贴在小截图上的标签:用一两个词说明这个部分,而不是组件的实现。 + +| 不要写 | 应写 | +|---|---| +| div.pricing-header-container | Header | +| Segmented control (2 options) | Billing toggle | +| Generation completed successfully | Done | + +`Labels` 接受一个 `DesignToCodeLabels` 对象,包含卡片显示或朗读的每一个词。带数值的句子是函数,因此其他语言可以调整语序: + +```csharp +card.Labels = new DesignToCodeLabels +{ + Generate = "Write the code", + DoneMeta = (lines, time) => $"{lines} lines, {time.TotalSeconds:0.0}s", +}; +``` + +### 最佳实践 + +**建议** + +- 先发送区域,再发送标注了所实现区域的代码,让方框与代码行相互关联。 +- 在沙箱中渲染预览,并且每个页面或对话框只放一张卡片。 +- 上下文的令牌取消时停止你的请求。 + +**避免** + +- 把模拟当作真实的运行展示。它是虚构的,并且会如实说明。 +- 为了预览而在应用内运行生成的代码。 +- 发送没有 id 的区域:代码块通过 id 与它的方框关联。 + +### 常见问题 + +**卡片为什么以一个设计打开?** 没有设置 `Source` 也没有 `Generator` 时,它显示模拟的设计。把 `Source` 设为 null 即可以拖放区打开。 + +**为什么我的代码行没有关联到方框?** 一行属于它第一个字符所在的区域;请在实现该区域的每个代码块中传入区域的 id。 + +**Stop 之后到达的代码会怎样?** 会被丢弃。只保留屏幕上的代码,已停止的运行之后的写入都会被忽略。 + +## API 参考 + + + + + + + + + + + + + + + + + + + +枚举: + +| 类型 | 值 | +|---|---| +| `DesignToCodePhase` | `Empty`、`Ready`、`Analyzing`、`Writing`、`Done`、`Stopped`、`Error` | +| `DesignToCodeView` | `Code`、`Preview` | +| `DesignToCodeSourceKind` | `Sample`、`File`、`DesignFile` | + +## 无障碍 + +- 卡片名为“Design to code”,包含两个具名分组:Design 与 Output。一个礼貌型实时区域朗读每一步:正在分析、找到的区域、 + 完成时的行数与用时、已停止以及错误。模拟会说明没有上传或打开任何内容。 +- 状态从不只靠颜色表达:每种状态都配有图标和简短标签,错误会说明问题与解决办法。 +- 区域标签是真正的按钮,名称类似“Price: show its code”。代码区是可聚焦的具名区域,代码流式写出时其状态读作“Writing”; + 它不是实时区域,因此屏幕阅读器不会被逐词朗读淹没。 +- “代码 / 预览”中隐藏的那一层不可见,也不在 Tab 顺序中。用代码绘制的设计是装饰性的;拖入的图像以文件名命名。 +- 每种拖放方式都有按钮:Choose file、Use sample 和 Import design file。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| `Tab` | 依次经过“移除”、区域标签、视图切换、“复制”、“导入设计文件”和主操作 | +| `Enter` / `Space` | 激活获得焦点的控件;在拖放区上,Choose file 请求一个文件 | +| `Left` / `Right` | 在视图切换上于“代码”和“预览”之间切换 | +| `Escape` | 停止正在分析或编写的运行;保留已写出的部分代码 | +| 聚焦某个区域标签 | 高亮该区域的方框及其代码行,并把它们滚动到可见位置 | + +## 动效 + +- 分析:一条 1px 的扫描线用一秒扫过截图,扫描线到达某个区域的顶部时,该区域的方框和标签淡入(160ms 透明度,无缩放)。 +- 编写:代码大约每 34ms 以一批完整单词流式写出(按阅读速度每批 3 到 8 个单词);每批只做 140ms 的淡入。 + 正在编写的区域在截图上高亮,它的代码行带有淡淡的底色。 +- 状态、占位提示以及“代码”和“预览”两层交叉淡化:旧内容 100ms 淡出,新内容 160ms 淡入。主按钮的宽度跟随标签变化,没有回弹。 +- 预览中,计费开关的滑块以 0.24s、无回弹的弹簧滑动,价格以等宽数字做 2px 的交叉淡化切换。 +- 卡片不在屏幕上时,模拟的循环会暂停等待。 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 扫描线 | 1s 扫过 | 无 | 无 | +| 区域 | 扫描线到达时淡入 | 立即出现 | 立即出现 | +| 代码 | 按单词分批,逐批淡入 | 一次一段 | 一次一段 | +| 代码与预览 | 交叉淡化 | 短暂交叉淡化 | 立即切换 | +| 模拟循环 | 循环 | 播放一次 | 播放一次 | + +## 相关 + +- `CodeBlock`:带复制功能的完成代码。 +- `ToolCall`:智能体运行中的一个步骤。 +- `AgentStatus`:正在运行的智能体的紧凑实时状态。 + +| 组件 | 适用场景 | +|---|---| +| `CodeBlock` | 代码已经完成,设计不是要讲述的内容 | +| `ToolCall` | 生成只是智能体对话记录中的一步 | +| `AgentStatus` | 只关心一次运行的进度 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/zh-TW.md new file mode 100644 index 00000000..5839f809 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/DesignToCode/zh-TW.md @@ -0,0 +1,290 @@ +--- +title: Design to code +summary: 截圖轉程式碼卡片:掃描線找出拖入的截圖或設計檔畫框中的版面區域,接著程式碼在旁邊串流寫出,並與區域一一對應,切換一個分頁即可看到即時預覽。 +source-hash: 0ad3fab6e8cd +--- + + + +## 何時使用 + +`DesignToCode` 是「截圖轉程式碼」功能的卡片。使用者把介面截圖拖到設計那一側,或從設計檔匯入一個畫框。一條掃描線掃過截圖, +它找到的每個版面區域都以帶標籤的方框出現。接著程式碼在旁邊依區域逐段串流寫出:正在撰寫的區域在截圖上反白,對應的程式碼行帶有淡淡的底色。 +指向方框或標籤會點亮它的程式碼行,指向某一行程式碼也會點亮它的方框。切換一個分頁,「預覽」就會呈現可以實際操作的結果。 + +- **設計轉程式碼功能**:上傳一張截圖或選擇設計檔中的一個畫框,取得一個元件。 +- **展示 AI 視覺步驟**:當模型辨識出了什麼很重要時,例如產生之前的版面解析。 +- **行銷與新手導覽**:一眼說明程式碼產生產品在做什麼。給卡片一個 `Simulation`,它就會在可見期間循環播放一個設計及其程式碼。 + +## 何時不用 + +- **沒有產生步驟的一般檔案上傳**:使用檔案上傳元件。 +- **只呈現完成的程式碼而不需要來源設計**:使用 `CodeBlock`。 +- **具備檔案、分頁與終端機的完整編輯器**:建立一個工作區版面,而不是把這張卡片撐大。 + +## 用法 + +```xml + + + +``` + +`DesignToCode` 是 Special 套件中的 `TemplatedControl`。在 `UseArcControls()` 旁邊以 `UseArcSpecial()` 註冊這個套件。 +卡片會填滿容器的寬度;請把它放在頁面或對話方塊中,寬度保持在 360 到 760px 之間。 + +既沒有 `Generator` 也沒有 `Simulation` 時,卡片以拖放區開啟:拖入截圖、選擇檔案或匯入設計檔。接上 `Generator` 把設計轉成程式碼,或者給卡片一個 `Simulation` 來播放。 + +### 模擬 + +`DesignToCodeSimulation` 是一個設計以及模型可能為它寫出的程式碼:`Source`(一張影像,或以程式碼繪製的草圖)、掃描找到的 +`Regions`、分段的程式碼 `Parts`(每個 `DesignToCodeSimulationPart` 標註它實作的區域),以及可選的署名模型 `Model`、`FileName`、 +模擬匯入時帶入的 `FrameName`、`PreviewFactory`,和循環時對預覽呼叫的 `PreviewAction`。沒有 `Generator` 時,`Source` 未設定期間 +卡片顯示它的設計,「Use sample」會把它帶回來。這次執行是模擬的:不會上傳或傳送任何內容,朗讀的提示也會說明這一點。卡片在畫面上時 +會循環播放(產生、停留在程式碼上、顯示預覽、重新開始),直到卡片內第一次出現指標按下或按鍵。把 `Simulation` 設為 null 會停止模擬 +執行並回到拖放區。 + +```csharp +card.Simulation = new DesignToCodeSimulation +{ + Source = new DesignToCodeSource("pricing-card.png", screenshot), + Regions = [new DesignToCodeRegion("price", "Price", 7.5, 42.5, 52, 14.5)], + Parts = + [ + new DesignToCodeSimulationPart("export function PricingCard() {\n return (\n"), + new DesignToCodeSimulationPart("

$24

\n", "price"), + new DesignToCodeSimulationPart(" );\n}\n"), + ], + Model = new AgentModel("Your model"), + FileName = "PricingCard.tsx", +}; +``` + +設計也可以用程式碼繪製,而不是提供影像:為來源設定 `Sketch`(一個 `DesignToCodeSketch`,以卡片的 `DesignToCodeSketchPalette` +在設計的矩形中繪製,因此會跟隨淺色與深色主題)以及它的 `SketchSize`。本頁的示範就是這樣繪製一張小型價格卡片,所有內容都來自 +`DesignToCodeDemoData`;署名的模型是真實的(核對於 2026-10-29),方案與價格是虛構的。 + +### 結構 + +1. **設計面板**:標題列顯示設計的名稱、「Sample」或「Design file」標籤以及「移除」按鈕;下方是帶區域方框與標籤的截圖, + 沒有設計時則是拖放區。 +2. **區域方框與標籤**:每個區域一個 1px 的強調色外框,以及一個與其頂邊對齊、位於其左側留白中的小標籤(沒有空間時放在方框內)。 + 標籤是一個按鈕,名稱類似「Price: show its code」。 +3. **輸出面板**:標題列顯示檔名、模型的標誌與名稱、「程式碼 / 預覽」切換以及「複製」;下方是帶行號的程式碼,或即時預覽。 +4. **底列**:狀態(圖示、簡短標籤、檔案以及類似「52 lines in 3.4s」的附加資訊)、「匯入設計檔」以及主要操作, + 主要操作依序顯示 Generate code、Stop、Regenerate 或 Try again。 + +### 一次執行 + +`Phase` 跟著執行進度:沒有設計時為 `Empty`,有設計時為 `Ready`,接著是 `Analyzing`、`Writing` 與 `Done`,或是 +`Stopped` 與 `Error`。Generate code(或 `Generate()`)開始一次執行,並把輸出切回「程式碼」。Stop、Escape 或 `Stop()` +結束正在分析或撰寫的執行;已經寫出的程式碼會保留。`Code` 保存畫面上的程式碼,`Completed` 回報完成的程式碼以及行數與耗時。 + +| 階段 | 狀態 | 主要操作 | +|---|---|---| +| Empty | Add a design to start | Generate code(無法使用) | +| Ready | Ready pricing-card.png | Generate code | +| Analyzing | Analyzing layout pricing-card.png · 3 regions | Stop | +| Writing | Writing PricingCard.tsx · 5 regions | Stop | +| Done | Done PricingCard.tsx · 52 lines in 3.4s | Regenerate | +| Stopped | Stopped PricingCard.tsx · Partial code kept | Regenerate | +| Error | Generation failed: the model timed out. Try again. | Try again | + +### 接上你的模型 + +為卡片提供 `Generator`:一個接收設計與 `DesignToCodeContext` 的非同步委派。每辨識出一個版面區域就呼叫 `AddRegion` +(方框以設計尺寸的百分比表示),接著在程式碼串流抵達時呼叫 `Write`,並傳入該程式碼片段所實作區域的 id,讓這些行與方框連結。 +完成時結束工作,擲出例外以顯示錯誤,並在內容的權杖取消時停止。程式碼片段可以是任意大小:卡片會把它們整理成完整的單字逐批顯示。 + + + +```csharp +card.Model = new AgentModel("Your model"); +card.Generator = async (source, context) => +{ + await foreach (var e in backend.StreamAsync(source.File, context.CancellationToken)) + { + if (e.Region is { } region) + { + context.AddRegion(new DesignToCodeRegion(region.Id, region.Label, region.X, region.Y, region.Width, region.Height)); + } + else + { + context.Write(e.Text, e.RegionId); + } + } +}; +card.Completed += (_, e) => drafts.Save(e.Code); +``` + +`AddRegion` 與 `Write` 可以在任何執行緒呼叫。設定了產生器的卡片會以拖放區開啟,也不會循環播放;設定 `Source` 即可從某個設計開始。 +`Model` 在程式碼標題列中註明模型,並顯示它的標誌(24 × 24 的描邊圖形);沒有設定時卡片只顯示閃光圖示(播放模擬時顯示模擬的模型)。 + +### 拖入截圖 + +在設計那一側的任何位置都可以拖放,檔案停留時會顯示「Drop to use this design」遮罩。每種拖放方式都有對應的按鈕: +Choose file、Use sample 與 Import design file。`AcceptedFileTypes`(寫法同輸入框的 accept 屬性:MIME 類型、`image/*` +或副檔名)與 `MaxFileSize`(10 MB)決定接受哪些檔案;其他檔案會顯示一則說明問題與解決方式的錯誤,目前顯示的設計維持不變。 + + + +Choose file 會引發 `ChooseFileRequested`。在其中開啟你的檔案選擇器,把檔案交給 `LoadFileAsync`,並設定 `Handled`; +沒有人處理時,卡片會改用模擬的設計(如果有),並說明沒有開啟任何檔案。拖入或載入的檔案會變成一個 `DesignToCodeSource`,包含它的名稱、 +解碼後的影像以及 `IStorageFile`,你的產生器可以把它傳送到後端。卡片自己解碼的影像在被取代時會釋放。 + +```csharp +card.ChooseFileRequested += async (_, e) => +{ + e.Handled = true; + var files = await TopLevel.GetTopLevel(card)!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()); + if (files.Count > 0) + { + await card.LoadFileAsync(files[0]); + } +}; +``` + +### 設計檔 + +Import design file 會執行 `DesignFileImporter`,也就是你的選擇器或連結流程:回傳帶算繪影像的畫框(`DesignToCodeSource`), +使用者取消時回傳 null。沒有設定時,如果有模擬,卡片會以它的 `FrameName` 匯入模擬的設計,並說明沒有開啟任何檔案。沒有影像的來源會繪製它的 `Sketch`。 + +### 即時預覽 + +「預覽」分頁呈現完成的程式碼實際執行的樣子。`PreviewFactory` 負責算繪,例如放在沙箱化的網頁檢視中;切勿在你的應用程式內執行產生的程式碼。 +沒有工廠時,模擬執行使用模擬的 `PreviewFactory`(示範會算繪價格卡片,Monthly 與 Yearly 會切換價格),其他情況顯示一則簡短說明。「程式碼」與「預覽」始終保持掛載,因此切換時會保留各自的捲動位置; +每次新的執行都會回到「程式碼」,讓串流輸出可見。 + +### 窄容器 + +版面跟著卡片本身的寬度。從 640px 起兩個面板並排(設計 344px,程式碼佔其餘部分);更窄時上下堆疊,各高 352px, +因此程式碼串流寫出時卡片的尺寸不會改變。低於 520px 時,狀態不再顯示檔名,「匯入設計檔」只顯示圖示,但保留其名稱供輔助技術使用。 +低於 440px 時,附加資訊與模型名稱隱藏,標誌保留。較長的程式碼行以凸排縮排換行,因此不會出現水平捲動。 + + + +### 減少動態效果 + +在減少動態效果下沒有掃描線,也沒有程式碼片段淡入:區域立即出現,程式碼一次抵達一段(一個區域),模擬只播放一次而不再循環。 + + + +### 內容指引 + +區域標籤是貼在小截圖上的標籤:用一兩個詞說明這個部分,而不是元件的實作。 + +| 不要寫 | 應寫 | +|---|---| +| div.pricing-header-container | Header | +| Segmented control (2 options) | Billing toggle | +| Generation completed successfully | Done | + +`Labels` 接受一個 `DesignToCodeLabels` 物件,包含卡片顯示或朗讀的每一個詞。帶數值的句子是函式,因此其他語言可以調整語序: + +```csharp +card.Labels = new DesignToCodeLabels +{ + Generate = "Write the code", + DoneMeta = (lines, time) => $"{lines} lines, {time.TotalSeconds:0.0}s", +}; +``` + +### 最佳實務 + +**建議** + +- 先傳送區域,再傳送標註了所實作區域的程式碼,讓方框與程式碼行互相連結。 +- 在沙箱中算繪預覽,並且每個頁面或對話方塊只放一張卡片。 +- 內容的權杖取消時停止你的請求。 + +**避免** + +- 把模擬當成真實的執行來呈現。它是虛構的,也會如實說明。 +- 為了預覽而在應用程式內執行產生的程式碼。 +- 傳送沒有 id 的區域:程式碼片段透過 id 與它的方框連結。 + +### 常見問題 + +**卡片為什麼以一個設計開啟?** 沒有設定 `Source` 也沒有 `Generator` 時,它顯示模擬的設計。把 `Source` 設為 null 即可以拖放區開啟。 + +**為什麼我的程式碼行沒有連結到方框?** 一行屬於它第一個字元所在的區域;請在實作該區域的每個程式碼片段中傳入區域的 id。 + +**Stop 之後抵達的程式碼會怎樣?** 會被捨棄。只保留畫面上的程式碼,已停止的執行之後的寫入都會被忽略。 + +## API 參考 + + + + + + + + + + + + + + + + + + + +列舉: + +| 類型 | 值 | +|---|---| +| `DesignToCodePhase` | `Empty`、`Ready`、`Analyzing`、`Writing`、`Done`、`Stopped`、`Error` | +| `DesignToCodeView` | `Code`、`Preview` | +| `DesignToCodeSourceKind` | `Sample`、`File`、`DesignFile` | + +## 無障礙 + +- 卡片名為「Design to code」,包含兩個具名群組:Design 與 Output。一個禮貌型即時區域朗讀每一步:正在分析、找到的區域、 + 完成時的行數與耗時、已停止以及錯誤。模擬會說明沒有上傳或開啟任何內容。 +- 狀態從不只靠顏色表達:每種狀態都搭配圖示與簡短標籤,錯誤會說明問題與解決方式。 +- 區域標籤是真正的按鈕,名稱類似「Price: show its code」。程式碼區是可聚焦的具名區域,程式碼串流寫出時其狀態讀作「Writing」; + 它不是即時區域,因此螢幕閱讀器不會被逐字朗讀淹沒。 +- 「程式碼 / 預覽」中隱藏的那一層不可見,也不在 Tab 順序中。以程式碼繪製的設計是裝飾性的;拖入的影像以檔名命名。 +- 每種拖放方式都有按鈕:Choose file、Use sample 與 Import design file。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| `Tab` | 依序經過「移除」、區域標籤、檢視切換、「複製」、「匯入設計檔」與主要操作 | +| `Enter` / `Space` | 啟用取得焦點的控制項;在拖放區上,Choose file 會要求一個檔案 | +| `Left` / `Right` | 在檢視切換上於「程式碼」與「預覽」之間切換 | +| `Escape` | 停止正在分析或撰寫的執行;保留已寫出的部分程式碼 | +| 聚焦某個區域標籤 | 反白該區域的方框及其程式碼行,並把它們捲動到可見位置 | + +## 動效 + +- 分析:一條 1px 的掃描線以一秒掃過截圖,掃描線抵達某個區域的頂端時,該區域的方框與標籤淡入(160ms 不透明度,無縮放)。 +- 撰寫:程式碼大約每 34ms 以一批完整單字串流寫出(依閱讀速度每批 3 到 8 個單字);每批只做 140ms 的淡入。 + 正在撰寫的區域在截圖上反白,它的程式碼行帶有淡淡的底色。 +- 狀態、預留位置提示以及「程式碼」與「預覽」兩層交叉淡化:舊內容 100ms 淡出,新內容 160ms 淡入。主按鈕的寬度跟著標籤變化,沒有回彈。 +- 預覽中,計費開關的滑塊以 0.24s、無回彈的彈簧滑動,價格以等寬數字做 2px 的交叉淡化切換。 +- 卡片不在畫面上時,模擬的循環會暫停等候。 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 掃描線 | 1s 掃過 | 無 | 無 | +| 區域 | 掃描線抵達時淡入 | 立即出現 | 立即出現 | +| 程式碼 | 依單字分批,逐批淡入 | 一次一段 | 一次一段 | +| 程式碼與預覽 | 交叉淡化 | 短暫交叉淡化 | 立即切換 | +| 模擬循環 | 循環 | 播放一次 | 播放一次 | + +## 相關 + +- `CodeBlock`:附複製功能的完成程式碼。 +- `ToolCall`:代理程式執行中的一個步驟。 +- `AgentStatus`:正在執行的代理程式的精簡即時狀態。 + +| 元件 | 適用情境 | +|---|---| +| `CodeBlock` | 程式碼已經完成,設計不是要講述的內容 | +| `ToolCall` | 產生只是代理程式對話記錄中的一步 | +| `AgentStatus` | 只在意一次執行的進度 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/en-US.md new file mode 100644 index 00000000..6bd3a8b1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/en-US.md @@ -0,0 +1,228 @@ +--- +title: Dictation button +summary: A chat composer with voice input: the mic springs into a recording pill with a live waveform, a timer, cancel and Insert, and words stream into the field as you speak, muted until the recognizer settles them. +--- + + + +## When to use + +`DictationButton` is a composer for people who would rather talk than type. Pressing the mic springs it leftward into +a recording pill: cancel, a live waveform, the elapsed time and Insert, which takes the mic's place so the next action +sits where the pointer already is. Words stream into the field while the person speaks, the recognizer's guesses muted +until they settle. Insert appends the text and hands the focus back to the field; Enter sends. + +- **Chat and agent composers** on touch devices, for long prompts, or for people who find typing hard. +- **Notes, comments and support replies** where dictating saves time and the person still edits before sending. +- **Any field that already sends** and needs a voice entry point with a clear listening state: use `DictationControl`. + +## When not to use + +- **Full duplex voice conversations** with spoken replies; use a voice agent pattern with call controls instead. +- **Recording audio to keep** (voice memos): dictation inserts text and discards the audio. +- **Command palettes** where one spoken command should run at once; confirm hands-free from `SpeechEnded` only if you + are sure. + +## Usage + +TKCode.Arc records nothing itself. Speech comes from an `IDictationRecognizer` that you write for your platform or +speech service; it reports the input level and the words, and the composer does the rest. Without one (and without a +simulation, below) the composer is a plain field: no model label, no placeholder, and pressing the mic says dictation +is not available (`DictationErrorCode.Unsupported`). + +```xml + +``` + +```csharp +composer.SendRequested += (_, e) => +{ + // The send button spins until the task completes; the field clears when it succeeds. + e.Completion = chat.SendAsync(e.Text); +}; + +composer.Recognizer = new MyPlatformRecognizer(); +``` + +`Text` is two-way: typing, an inserted dictation and the clear after a send all change it. Without a +`SendRequested` handler the send is simulated. `Model` is the label at the start of the toolbar (null, the default, hides +it); keep it honest and pass the model the composer actually sends to. The preview shows a real model, Claude Sonnet 5.5, +as the model its message would go to; the dictated words are invented. + +### Anatomy + +1. **Card**: a Surface card with a 1px border that fills its container. +2. **Field**: 16/24 text, one to six lines, then it scrolls and keeps the newest words in view. Live words follow the + text: settled words in the text color, the recognizer's guess in a muted tone. +3. **Toolbar** (36px): the label (the model, the keyboard hints while listening, or an error), the dictation slot and + the accent send button. +4. **Recording pill**: 216px, 36px high with 4px padding: the 28px cancel, the waveform, the timer and the 28px Insert. + +### States + +| Status | The slot shows | The toolbar label | +|---|---|---| +| `Idle` | The mic | The model | +| `Starting` | The pill, the bars resting as dots | The keyboard hints | +| `Listening` | The pill with the live waveform and the timer | The keyboard hints | +| `Processing` | The pill, Insert spinning ("Transcribing") | The model | +| `Denied` | Try again with a mic-off icon | "Microphone blocked in system settings" | +| `Error` | Try again with an alert icon | A short reason | + +`MaxDuration` (120 seconds by default, 0 for no limit) stops listening and inserts on its own. + +### Your own recognizer + + + +The contract is small. `StartAsync` completes once audio flows, or throws `DictationException` with `NotAllowed` for a +blocked microphone. While it runs, report through the handlers; they may be called from any thread. Cancelling the token +means the person cancelled: release the microphone. `StopAsync` returns the final text. + +```csharp +public sealed class MyPlatformRecognizer : IDictationRecognizer +{ + public async Task StartAsync(DictationHandlers handlers, CancellationToken cancellationToken) + { + var engine = await SpeechEngine.OpenAsync(cancellationToken) + ?? throw new DictationException(DictationErrorCode.NotAllowed); + engine.Result += (final, interim) => handlers.OnTranscript(final, interim); + engine.Level += level => handlers.OnLevel(level); // 0 to 1, as often as you like + engine.UtteranceEnded += () => handlers.OnSpeechEnd(); + engine.Failed += code => handlers.OnError(code); + cancellationToken.Register(engine.Dispose); + return engine; // an IDictationSession: StopAsync returns the text + } +} +``` + +For demos and design reviews, set `Simulation` to a `SimulatedDictationRecognizer` instead: it never touches the +microphone and speaks your own `Scripts`, one per dictation, each a list of `DictationPhrase`s with an optional misheard +`Guess` that it streams first and then corrects. `Deny` rejects every start like a blocked microphone; `StartDelay`, +`FinalizeDelay` and `WordInterval` set its pace. With no scripts the voice stays silent. A `Recognizer` always wins over +the simulation, and setting `Simulation` to null stops it. Give each control its own. + +```csharp +composer.Simulation = new SimulatedDictationRecognizer +{ + Scripts = + [ + [ + new DictationPhrase("Draft a short reply to the design team", "Draft a short reply to the desk team"), + new DictationPhrase("and ask if the empty states can ship on Friday."), + ], + ], +}; +``` + +### Only the control + + + +`DictationControl` is the mic and its pill without a field, for a composer you already have. Render +`TranscriptChanged` yourself with the interim words muted, append the text of `Committed`, and drop the live words on +`Cancelled`. `SpeechEnded` tells you the recognizer heard the end of an utterance. The headless state machine behind +both, `DictationController`, is public too. + +### A blocked microphone + + + +When the recognizer reports `NotAllowed`, the slot springs to Try again with a mic-off icon and the toolbar says where +to fix it. Other errors show an alert icon and a short reason. Try again starts over; Escape dismisses it. + +### Narrow columns + + + +The breakpoints follow the card's own width, not the window: the keyboard hints hide under 460px, error copy shortens +under 400px, and at 360px the toolbar label gives way while recording. The pill keeps its 216px, so nothing reflows +while the person speaks. + +### Reduced motion + + + +With reduced motion the pill and the field change size at once, words appear without fading and the waveform stops +travelling: its bars pulse in place with the voice. With motion off the bars rest as dots. + +### Autoplay + +With a `Simulation` and no `Recognizer` the composer plays its scripts on its own (listen, insert, send) until the first +pointer, key or focus inside the card, and pauses while it is off screen. Set `AutoPlay="False"` to turn it off; it is +for docs and demos, not production. + +### Your own words + +`Labels` holds every word the composer shows or says, including the errors and the announcements: + +```csharp +composer.Labels = new DictationButtonLabels +{ + Dictate = "Dicter", + Insert = "Insérer", + Error = code => code == DictationErrorCode.NotAllowed ? "Micro bloqué dans les réglages" : "La dictée a échoué", +}; +``` + +### Best practices + +- Say in your product copy what to do when the microphone is blocked; the composer already says where to fix it. +- Keep the label honest: pass the model the composer sends to, or null. +- Do not ship autoplay; it is for docs and demos. + +## API reference + + + + + + + + + +## Accessibility + +- A polite live region announces listening (and that the simulated recognizer records nothing), transcribing, the + inserted word count, cancel, send and every error with how to fix it. Streaming words are not announced. +- Status is never color alone: a blocked microphone shows a mic-off icon and its message, other errors an alert icon and + a short reason; Try again keeps the same icon. +- Only the visible face of the slot takes the pointer and Tab. Focus follows the action: the mic moves it to Insert, + Insert back to the field at the end of the text, cancel back to the mic, a failed start to Try again. +- The elapsed time uses tabular numerals in a reserved width; the waveform is decorative and hidden from assistive tech. +- The mic, cancel, Insert and send are icon buttons with names and tooltips (Dictate, Cancel, Insert or Transcribing, + Send). +- Interim words use a tone between TextMuted and TextSecondary and settle to the text color, so the difference reads in + both themes without relying on hue. + +### Keyboard + +| Keys | Action | +|---|---| +| Enter, Space | On the mic: start dictating; the focus moves to Insert | +| Enter | While listening: stop and insert, from anywhere inside the card | +| Escape | While dictating: cancel and discard; on Try again: dismiss the error | +| Enter | In the field: send. Shift+Enter adds a line | +| Tab | Moves through cancel, Insert, then send; hidden faces of the slot are skipped | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Mic to pill | The width springs leftward to 216px (critically damped, 0.24s, no bounce) | At once | At once | +| Pill to Try again | The width springs to the label's measured width | At once | At once | +| Faces of the slot | Crossfade: out 100ms, in 160ms | Crossfade | At once | +| Toolbar label | Old out 100ms, new in 160ms rising 2px | Short fade | At once | +| Insert while transcribing | The check crossfades into a spinner (160ms) | At once | At once | +| A word arrives | Fades in over 140ms; a corrected guess refades only the words that changed | At once | At once | +| The field grows | Springs to each new line | At once | At once | +| Waveform | A sample enters on the right every 60ms and each bar eases toward it every frame; on stop the bars settle to dots | Bars pulse in place with the voice | Dots | + +The waveform's frame loop stops once the bars have settled and pauses while the card is off screen or hidden. Frames +allocate nothing: the levels live in a fixed ring buffer and the bars are drawn by one control. + +## Related + +- `AiComposer` for a full prompt box with attachments, tools and a model menu. +- `ToolCall` and `AgentStatus` for what the agent does with the message. +- `Tooltip` for the names of the icon buttons. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/zh-CN.md new file mode 100644 index 00000000..5b44d077 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/zh-CN.md @@ -0,0 +1,196 @@ +--- +title: Dictation button +summary: 带语音输入的聊天输入框:麦克风弹性展开为录音胶囊,带实时波形、计时器、取消与插入按钮;说话时文字流入输入框,识别器确定之前显示为浅色。 +source-hash: a1eb764b4d8a +--- + + + +## 何时使用 + +`DictationButton` 是为宁愿说话也不想打字的人准备的输入框。按下麦克风,它会向左弹性展开为录音胶囊:取消、实时波形、已录时长和插入按钮;插入按钮占据麦克风原来的位置,下一步操作正好在指针所在之处。说话时文字流入输入框,识别器的猜测以浅色显示,确定后变为正文颜色。插入会把文字追加到输入框并把焦点交还给输入框;按 Enter 发送。 + +- **聊天与智能体输入框**:触屏设备、长提示词,或打字不便的用户。 +- **笔记、评论与客服回复**:口述能节省时间,发送前仍可编辑。 +- **任何已有发送操作的输入框**:需要一个状态清晰的语音入口时,使用 `DictationControl`。 + +## 何时不用 + +- **全双工语音对话**(带语音回复):请改用带通话控件的语音智能体模式。 +- **需要保留的录音**(语音备忘录):听写只插入文字,音频会被丢弃。 +- **命令面板**:一句口令就应立即执行的场景;只有在确有把握时才在 `SpeechEnded` 中免手动确认。 + +## 用法 + +TKCode.Arc 本身不录制任何内容。语音来自你为所在平台或语音服务编写的 `IDictationRecognizer`:它报告输入电平和识别出的文字,其余由输入框完成。没有识别器(也没有下文的模拟)时,输入框只是一个普通字段:没有模型标签、没有占位文字,按下麦克风会提示听写不可用(`DictationErrorCode.Unsupported`)。 + +```xml + +``` + +```csharp +composer.SendRequested += (_, e) => +{ + // The send button spins until the task completes; the field clears when it succeeds. + e.Completion = chat.SendAsync(e.Text); +}; + +composer.Recognizer = new MyPlatformRecognizer(); +``` + +`Text` 是双向的:输入、插入听写内容以及发送后的清空都会改变它。没有 `SendRequested` 处理程序时,发送是模拟的。`Model` 是工具栏开头的标签(为 null,即默认值时隐藏);请如实传入输入框实际发送到的模型。预览显示的是真实模型 Claude Sonnet 5.5,即这条消息将发送到的模型;听写的文字是虚构的。 + +### 结构 + +1. **卡片**:带 1px 边框的 Surface 卡片,填满所在容器。 +2. **输入框**:16/24 的文字,一到六行,超出后滚动,并让最新的文字保持可见。实时文字跟在正文之后:已确定的词使用正文颜色,识别器的猜测使用浅色。 +3. **工具栏**(36px):标签(模型、聆听时的键盘提示,或错误信息)、听写位和强调色的发送按钮。 +4. **录音胶囊**:216px 宽、36px 高、4px 内边距:28px 的取消、波形、计时器和 28px 的插入。 + +### 状态 + +| 状态 | 听写位显示 | 工具栏标签 | +|---|---|---| +| `Idle` | 麦克风 | 模型 | +| `Starting` | 胶囊,柱条静止为圆点 | 键盘提示 | +| `Listening` | 带实时波形和计时器的胶囊 | 键盘提示 | +| `Processing` | 胶囊,插入按钮转圈("Transcribing") | 模型 | +| `Denied` | 带麦克风关闭图标的 Try again | "Microphone blocked in system settings" | +| `Error` | 带警示图标的 Try again | 简短原因 | + +`MaxDuration`(默认 120 秒,0 表示不限制)到时会自动停止聆听并插入。 + +### 你自己的识别器 + + + +约定很简单。`StartAsync` 在音频开始流动后完成;麦克风被阻止时抛出带 `NotAllowed` 的 `DictationException`。运行期间通过处理程序报告,它们可以从任何线程调用。令牌被取消表示用户取消了:此时释放麦克风。`StopAsync` 返回最终文字。 + +```csharp +public sealed class MyPlatformRecognizer : IDictationRecognizer +{ + public async Task StartAsync(DictationHandlers handlers, CancellationToken cancellationToken) + { + var engine = await SpeechEngine.OpenAsync(cancellationToken) + ?? throw new DictationException(DictationErrorCode.NotAllowed); + engine.Result += (final, interim) => handlers.OnTranscript(final, interim); + engine.Level += level => handlers.OnLevel(level); // 0 to 1, as often as you like + engine.UtteranceEnded += () => handlers.OnSpeechEnd(); + engine.Failed += code => handlers.OnError(code); + cancellationToken.Register(engine.Dispose); + return engine; // an IDictationSession: StopAsync returns the text + } +} +``` + +用于演示和设计评审时,改为把 `Simulation` 设为一个 `SimulatedDictationRecognizer`:它从不访问麦克风,朗读你自己的 `Scripts`,每次听写一段,每段由 `DictationPhrase` 组成,每个短语可带一个先流式显示、随后被纠正的听错 `Guess`。`Deny` 会像麦克风被阻止一样拒绝每次启动;`StartDelay`、`FinalizeDelay` 和 `WordInterval` 设置它的节奏。没有脚本时声音保持静默。`Recognizer` 总是优先于模拟,把 `Simulation` 设为 null 会停止它。请给每个控件各自的实例。 + +```csharp +composer.Simulation = new SimulatedDictationRecognizer +{ + Scripts = + [ + [ + new DictationPhrase("Draft a short reply to the design team", "Draft a short reply to the desk team"), + new DictationPhrase("and ask if the empty states can ship on Friday."), + ], + ], +}; +``` + +### 只用控件 + + + +`DictationControl` 是不带输入框的麦克风和胶囊,用于你已有的输入框。自行渲染 `TranscriptChanged`(临时文字用浅色),追加 `Committed` 的文字,并在 `Cancelled` 时丢弃实时文字。`SpeechEnded` 表示识别器听到了一句话的结尾。两者背后的无界面状态机 `DictationController` 同样是公开的。 + +### 麦克风被阻止 + + + +识别器报告 `NotAllowed` 时,听写位弹性变为带麦克风关闭图标的 Try again,工具栏说明去哪里修复。其他错误显示警示图标和简短原因。Try again 重新开始;Escape 关闭它。 + +### 窄栏 + + + +断点跟随卡片自身的宽度,而不是窗口:宽度小于 460px 时隐藏键盘提示,小于 400px 时错误文字变短,360px 时录音期间工具栏标签让出位置。胶囊始终保持 216px,说话时不会有任何重排。 + +### 减少动效 + + + +减少动效时,胶囊与输入框的尺寸直接变化,文字出现时不淡入,波形不再移动:柱条随声音原地起伏。关闭动效时柱条静止为圆点。 + +### 自动播放 + +设置了 `Simulation` 且没有 `Recognizer` 时,输入框会自行播放它的脚本(聆听、插入、发送),直到卡片内第一次出现指针、按键或焦点;卡片不在屏幕上时暂停。设置 `AutoPlay="False"` 可关闭它;它用于文档和演示,不用于生产环境。 + +### 自定义文字 + +`Labels` 包含输入框显示或朗读的所有文字,包括错误信息和播报: + +```csharp +composer.Labels = new DictationButtonLabels +{ + Dictate = "Dicter", + Insert = "Insérer", + Error = code => code == DictationErrorCode.NotAllowed ? "Micro bloqué dans les réglages" : "La dictée a échoué", +}; +``` + +### 最佳实践 + +- 在产品文案中说明麦克风被阻止时该怎么做;输入框已经说明了去哪里修复。 +- 让标签如实:传入输入框实际发送到的模型,或传 null。 +- 不要在生产环境开启自动播放;它用于文档和演示。 + +## API 参考 + + + + + + + + + +## 无障碍 + +- 礼貌型实时区域会播报:开始聆听(以及模拟识别器不录制任何内容)、正在转写、插入的词数、取消、发送,以及每个错误和修复方法。流入的文字不会被播报。 +- 状态从不只靠颜色表达:麦克风被阻止时显示麦克风关闭图标和说明,其他错误显示警示图标和简短原因;Try again 沿用同一个图标。 +- 只有听写位当前可见的那一面接受指针和 Tab。焦点跟随操作:麦克风把焦点移到插入,插入把焦点交还到输入框文字末尾,取消交还给麦克风,启动失败移到 Try again。 +- 已录时长使用等宽数字并预留宽度;波形是装饰性的,对辅助技术隐藏。 +- 麦克风、取消、插入和发送都是带名称和提示的图标按钮(Dictate、Cancel、Insert 或 Transcribing、Send)。 +- 临时文字使用介于 TextMuted 与 TextSecondary 之间的色调,确定后变为正文颜色,因此在两种主题下都能区分,而不依赖色相。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Enter、Space | 在麦克风上:开始听写;焦点移到插入 | +| Enter | 聆听时:停止并插入,在卡片内任何位置都有效 | +| Escape | 听写时:取消并丢弃;在 Try again 上:关闭错误 | +| Enter | 在输入框中:发送。Shift+Enter 换行 | +| Tab | 依次经过取消、插入,然后是发送;听写位隐藏的那几面会被跳过 | + +## 动效 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 麦克风变为胶囊 | 宽度向左弹性展开到 216px(临界阻尼,0.24s,无回弹) | 直接变化 | 直接变化 | +| 胶囊变为 Try again | 宽度弹性变化到标签的实测宽度 | 直接变化 | 直接变化 | +| 听写位的各面 | 交叉淡化:淡出 100ms,淡入 160ms | 交叉淡化 | 直接变化 | +| 工具栏标签 | 旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px | 短暂淡化 | 直接变化 | +| 转写时的插入按钮 | 对勾交叉淡化为转圈(160ms) | 直接变化 | 直接变化 | +| 新词出现 | 淡入 140ms;被纠正的猜测只让改变的词重新淡入 | 直接出现 | 直接出现 | +| 输入框增高 | 弹性变化到每一新行 | 直接变化 | 直接变化 | +| 波形 | 每 60ms 从右侧进入一个采样,每根柱条逐帧缓动靠近它;停止时柱条原地静止为圆点 | 柱条随声音原地起伏 | 圆点 | + +柱条静止后波形的逐帧循环即停止,卡片不在屏幕上或被隐藏时暂停。逐帧零分配:电平存放在固定大小的环形缓冲区中,所有柱条由一个控件绘制。 + +## 相关组件 + +- `AiComposer`:带附件、工具和模型菜单的完整提示词输入框。 +- `ToolCall` 和 `AgentStatus`:智能体如何处理这条消息。 +- `Tooltip`:图标按钮的名称提示。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/zh-TW.md new file mode 100644 index 00000000..81a74258 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/DictationButton/zh-TW.md @@ -0,0 +1,196 @@ +--- +title: Dictation button +summary: 具備語音輸入的聊天輸入框:麥克風以彈簧動態展開成錄音膠囊,附即時波形、計時器、取消與插入按鈕;說話時文字流入輸入框,辨識器確定之前以淡色顯示。 +source-hash: a1eb764b4d8a +--- + + + +## 何時使用 + +`DictationButton` 是為寧可說話也不想打字的人準備的輸入框。按下麥克風,它會向左以彈簧動態展開成錄音膠囊:取消、即時波形、已錄時間與插入按鈕;插入按鈕佔據麥克風原本的位置,下一個動作正好就在指標所在之處。說話時文字流入輸入框,辨識器的猜測以淡色顯示,確定後轉為內文顏色。插入會把文字附加到輸入框,並把焦點交還給輸入框;按 Enter 傳送。 + +- **聊天與代理的輸入框**:觸控裝置、較長的提示詞,或打字不便的使用者。 +- **筆記、留言與客服回覆**:口述能節省時間,傳送前仍可編輯。 +- **任何已有傳送動作的輸入框**:需要一個狀態清楚的語音入口時,請使用 `DictationControl`。 + +## 何時不用 + +- **全雙工語音對話**(含語音回覆):請改用附通話控制項的語音代理模式。 +- **需要保留的錄音**(語音備忘錄):聽寫只會插入文字,音訊會被捨棄。 +- **命令面板**:一句口令就該立即執行的情境;只有在確定的情況下才在 `SpeechEnded` 中免手動確認。 + +## 用法 + +TKCode.Arc 本身不會錄下任何內容。語音來自你為所在平台或語音服務撰寫的 `IDictationRecognizer`:它回報輸入音量與辨識出的文字,其餘由輸入框處理。沒有辨識器(也沒有下文的模擬)時,輸入框只是一個一般欄位:沒有模型標籤、沒有預留位置文字,按下麥克風會提示聽寫無法使用(`DictationErrorCode.Unsupported`)。 + +```xml + +``` + +```csharp +composer.SendRequested += (_, e) => +{ + // The send button spins until the task completes; the field clears when it succeeds. + e.Completion = chat.SendAsync(e.Text); +}; + +composer.Recognizer = new MyPlatformRecognizer(); +``` + +`Text` 是雙向的:輸入、插入的聽寫內容以及傳送後的清空都會改變它。沒有 `SendRequested` 處理常式時,傳送是模擬的。`Model` 是工具列開頭的標籤(為 null,即預設值時隱藏);請如實傳入輸入框實際傳送到的模型。預覽顯示的是真實模型 Claude Sonnet 5.5,即這則訊息將傳送到的模型;聽寫的文字是虛構的。 + +### 結構 + +1. **卡片**:具 1px 邊框的 Surface 卡片,填滿所在的容器。 +2. **輸入框**:16/24 的文字,一到六行,超過後捲動,並讓最新的文字保持可見。即時文字接在內文之後:已確定的字詞使用內文顏色,辨識器的猜測使用淡色。 +3. **工具列**(36px):標籤(模型、聆聽時的鍵盤提示,或錯誤訊息)、聽寫位置與強調色的傳送按鈕。 +4. **錄音膠囊**:寬 216px、高 36px、內距 4px:28px 的取消、波形、計時器與 28px 的插入。 + +### 狀態 + +| 狀態 | 聽寫位置顯示 | 工具列標籤 | +|---|---|---| +| `Idle` | 麥克風 | 模型 | +| `Starting` | 膠囊,長條靜止為圓點 | 鍵盤提示 | +| `Listening` | 附即時波形與計時器的膠囊 | 鍵盤提示 | +| `Processing` | 膠囊,插入按鈕轉圈("Transcribing") | 模型 | +| `Denied` | 附麥克風關閉圖示的 Try again | "Microphone blocked in system settings" | +| `Error` | 附警示圖示的 Try again | 簡短原因 | + +`MaxDuration`(預設 120 秒,0 表示不限制)時間一到會自動停止聆聽並插入。 + +### 自訂辨識器 + + + +約定很簡單。`StartAsync` 在音訊開始流入後完成;麥克風遭封鎖時擲回帶 `NotAllowed` 的 `DictationException`。執行期間透過處理常式回報,它們可以從任何執行緒呼叫。權杖被取消表示使用者取消了:此時請釋放麥克風。`StopAsync` 傳回最終文字。 + +```csharp +public sealed class MyPlatformRecognizer : IDictationRecognizer +{ + public async Task StartAsync(DictationHandlers handlers, CancellationToken cancellationToken) + { + var engine = await SpeechEngine.OpenAsync(cancellationToken) + ?? throw new DictationException(DictationErrorCode.NotAllowed); + engine.Result += (final, interim) => handlers.OnTranscript(final, interim); + engine.Level += level => handlers.OnLevel(level); // 0 to 1, as often as you like + engine.UtteranceEnded += () => handlers.OnSpeechEnd(); + engine.Failed += code => handlers.OnError(code); + cancellationToken.Register(engine.Dispose); + return engine; // an IDictationSession: StopAsync returns the text + } +} +``` + +用於展示和設計審查時,改為把 `Simulation` 設為一個 `SimulatedDictationRecognizer`:它從不存取麥克風,朗讀你自己的 `Scripts`,每次聽寫一段,每段由 `DictationPhrase` 組成,每個片語可附一個先串流顯示、隨後被更正的聽錯 `Guess`。`Deny` 會像麥克風被封鎖一樣拒絕每次啟動;`StartDelay`、`FinalizeDelay` 與 `WordInterval` 設定它的節奏。沒有腳本時聲音保持靜默。`Recognizer` 一律優先於模擬,把 `Simulation` 設為 null 會停止它。請給每個控制項各自的執行個體。 + +```csharp +composer.Simulation = new SimulatedDictationRecognizer +{ + Scripts = + [ + [ + new DictationPhrase("Draft a short reply to the design team", "Draft a short reply to the desk team"), + new DictationPhrase("and ask if the empty states can ship on Friday."), + ], + ], +}; +``` + +### 只用控制項 + + + +`DictationControl` 是不含輸入框的麥克風與膠囊,用於你已有的輸入框。自行呈現 `TranscriptChanged`(暫定文字用淡色),附加 `Committed` 的文字,並在 `Cancelled` 時捨棄即時文字。`SpeechEnded` 表示辨識器聽到了一句話的結尾。兩者背後的無介面狀態機 `DictationController` 同樣是公開的。 + +### 麥克風遭封鎖 + + + +辨識器回報 `NotAllowed` 時,聽寫位置以彈簧動態變成附麥克風關閉圖示的 Try again,工具列說明該到哪裡修正。其他錯誤顯示警示圖示與簡短原因。Try again 會重新開始;Escape 可將它關閉。 + +### 窄欄 + + + +斷點依卡片本身的寬度而定,而不是視窗:寬度小於 460px 時隱藏鍵盤提示,小於 400px 時錯誤文字縮短,360px 時錄音期間工具列標籤會讓出位置。膠囊始終保持 216px,說話時不會有任何重新排版。 + +### 減少動態效果 + + + +減少動態效果時,膠囊與輸入框的尺寸直接改變,文字出現時不淡入,波形不再移動:長條隨聲音在原地起伏。關閉動態效果時長條靜止為圓點。 + +### 自動播放 + +設定了 `Simulation` 且沒有 `Recognizer` 時,輸入框會自行播放它的腳本(聆聽、插入、傳送),直到卡片內第一次出現指標、按鍵或焦點;卡片不在畫面上時暫停。設定 `AutoPlay="False"` 即可關閉;它是供文件與展示使用,不適用於正式環境。 + +### 自訂文字 + +`Labels` 包含輸入框顯示或朗讀的所有文字,包括錯誤訊息與朗讀內容: + +```csharp +composer.Labels = new DictationButtonLabels +{ + Dictate = "Dicter", + Insert = "Insérer", + Error = code => code == DictationErrorCode.NotAllowed ? "Micro bloqué dans les réglages" : "La dictée a échoué", +}; +``` + +### 最佳做法 + +- 在產品文案中說明麥克風遭封鎖時該怎麼做;輸入框已經說明了該到哪裡修正。 +- 讓標籤如實:傳入輸入框實際傳送到的模型,或傳入 null。 +- 不要在正式環境開啟自動播放;它是供文件與展示使用。 + +## API 參考 + + + + + + + + + +## 無障礙 + +- 禮貌型即時區域會朗讀:開始聆聽(以及模擬辨識器不會錄下任何內容)、正在轉寫、插入的字數、取消、傳送,以及每個錯誤與修正方式。流入的文字不會被朗讀。 +- 狀態從不只靠顏色表達:麥克風遭封鎖時顯示麥克風關閉圖示與說明,其他錯誤顯示警示圖示與簡短原因;Try again 沿用同一個圖示。 +- 只有聽寫位置目前可見的那一面接受指標與 Tab。焦點跟著動作走:麥克風把焦點移到插入,插入把焦點交還到輸入框文字末端,取消交還給麥克風,啟動失敗則移到 Try again。 +- 已錄時間使用等寬數字並預留寬度;波形是裝飾性的,對輔助技術隱藏。 +- 麥克風、取消、插入與傳送都是具名稱與提示的圖示按鈕(Dictate、Cancel、Insert 或 Transcribing、Send)。 +- 暫定文字使用介於 TextMuted 與 TextSecondary 之間的色調,確定後轉為內文顏色,因此在兩種主題下都能分辨,而不依賴色相。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Enter、Space | 在麥克風上:開始聽寫;焦點移到插入 | +| Enter | 聆聽時:停止並插入,在卡片內任何位置皆有效 | +| Escape | 聽寫時:取消並捨棄;在 Try again 上:關閉錯誤 | +| Enter | 在輸入框中:傳送。Shift+Enter 換行 | +| Tab | 依序經過取消、插入,然後是傳送;聽寫位置隱藏的那幾面會被略過 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 麥克風變成膠囊 | 寬度向左以彈簧展開到 216px(臨界阻尼,0.24s,無回彈) | 直接改變 | 直接改變 | +| 膠囊變成 Try again | 寬度以彈簧變化到標籤的實測寬度 | 直接改變 | 直接改變 | +| 聽寫位置的各面 | 交叉淡化:淡出 100ms,淡入 160ms | 交叉淡化 | 直接改變 | +| 工具列標籤 | 舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px | 短暫淡化 | 直接改變 | +| 轉寫時的插入按鈕 | 勾號交叉淡化成轉圈(160ms) | 直接改變 | 直接改變 | +| 新字詞出現 | 淡入 140ms;被更正的猜測只讓改變的字詞重新淡入 | 直接出現 | 直接出現 | +| 輸入框變高 | 以彈簧變化到每一新行 | 直接改變 | 直接改變 | +| 波形 | 每 60ms 從右側進入一個取樣,每根長條逐格緩動靠近它;停止時長條在原地靜止為圓點 | 長條隨聲音在原地起伏 | 圓點 | + +長條靜止後波形的逐格迴圈即停止,卡片不在畫面上或被隱藏時暫停。逐格零配置:音量存放在固定大小的環狀緩衝區中,所有長條由一個控制項繪製。 + +## 相關元件 + +- `AiComposer`:附檔案、工具與模型選單的完整提示詞輸入框。 +- `ToolCall` 與 `AgentStatus`:代理如何處理這則訊息。 +- `Tooltip`:圖示按鈕的名稱提示。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/en-US.md new file mode 100644 index 00000000..5bd5cdab --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/en-US.md @@ -0,0 +1,249 @@ +--- +title: Enrich column +summary: An AI enrichment column for a data table, where one prompt runs down every row and each cell shimmers while it works, then settles into a value with its source or a failure you can retry. +--- + + + +## When to use + +`EnrichColumn` is a table card with one AI column. One prompt runs down every row: each cell queues, shimmers while the +agent works on it, then settles into a short value with the source it came from, or a failure with a retry control. The +person can stop the run, re-run the column or a single row, and edit the column's name and prompt in place. + +- **Enriching companies or leads** with facts an agent can look up: headcount, funding, tech stack. +- **Classifying or summarizing rows** with one prompt, with a visible source per answer. +- **Showing per-row AI progress** with stop, re-run and retry in a CRM, a sales tool or an internal admin. + +## When not to use + +- **Large spreadsheets** with many columns and editing: use a data grid and add an AI column there. +- **One question over a whole dataset**: ask it once and show one answer, not a column. +- **A log of one agent's tool calls**: use `ToolCall`. + +## Usage + +Give the column your rows and an `Enrich` delegate that fills one cell. It receives the row and an +`EnrichColumnContext`, and returns the value with its source. Throw to mark the cell failed: the exception's message +becomes the reason next to Retry. + +```csharp +table.Rows = accounts.Select(a => new EnrichColumnRow(a.Id, a.Name, a.Domain)).ToList(); +table.Column = new EnrichColumnDefinition("Headcount and funding", "Find the headcount and latest funding round"); +table.Enrich = async (row, context) => +{ + context.SetActivity($"Searching for {row.Name}"); + var answer = await service.EnrichAsync(row.Domain, context.Prompt, context.CancellationToken); + if (answer is null) + { + throw new InvalidOperationException("No source found"); + } + return new EnrichColumnResult(answer.Value, new EnrichColumnSource(answer.Host, answer.Url)); +}; +table.CellChanged += (_, e) => +{ + if (e.Cell.Status == EnrichColumnCellStatus.Done) + { + store.Save(e.Row.Id, e.Cell); + } +}; +``` + +With no rows of its own the column plays its `Simulation`, a table you supply; with neither it shows the empty frame: +the top bar with "0 rows", an unnamed AI column, no rows and Run column unavailable. Rows of your own with no `Enrich` +show as they are, and nothing runs. + +### Simulation + +`EnrichColumnSimulation` is a table the column plays while `Rows` is null: a `Title`, the `Column`, the `Model` in the +footer, the `Rows`, and `Answers`, one `EnrichColumnSimulationAnswer` per row id with the value and source the cell +settles into. Its `Steps` are the activities the cell shows before that, each for its `Duration`; an answer with an +`Error` fails on its first attempt, so the failed state and Retry show, and the retry settles into the value. The column +runs it on its own when it first comes into view (`AutoRun`), says so in the top bar ("sample data") and to screen +readers, and sends nothing. Your own `Title`, `Column` and `Model` win over the simulation's; setting `Simulation` to +null stops the run and shows the empty frame. + +```csharp +table.Simulation = new EnrichColumnSimulation +{ + Title = "Target accounts", + Column = new EnrichColumnDefinition("Headcount and funding", "Find the headcount and latest funding round"), + Model = new EnrichColumnModel("Your model", "web search"), + Rows = [new EnrichColumnRow("acme", "Acme Robotics", "acme.example")], + Answers = + [ + new EnrichColumnSimulationAnswer("acme", "410 people · Series C", new EnrichColumnSource("acme.example/press", "https://acme.example/press")) + { + Steps = + [ + new EnrichColumnSimulationStep("Searching for Acme Robotics", TimeSpan.FromMilliseconds(700)), + new EnrichColumnSimulationStep("Reading acme.example", TimeSpan.FromMilliseconds(900)), + ], + Error = "No source found", // the first attempt fails, so Retry shows + }, + ], +}; +``` + +The demos on this page take their table from `EnrichColumnDemoData`: the companies and numbers are invented, the model +is a real one (checked on 2026-10-29). + +### Your rows and service + + + +The context carries the prompt and the column as they were when the cell started, a `CancellationToken` that fires when +the person stops the run or re-runs the column, the attempt number (1 on the first run, 2 on the first retry) and +`SetActivity`, which sets the shimmering label of the running cell and is safe from any thread. Pass the token to your +request: a cancelled cell goes back to what it showed before, and an answer that arrives after a stop is ignored. + +### Anatomy + +1. **Top bar**: the table's name (`Title`), the row count with `Note`, and the column button: Run column, Stop while it + runs, Re-run once cells have values. +2. **Header**: the row column's header, then the AI column with a sparkle, its name over its prompt, and an edit control. +3. **Editor**: opens under the header with the column's name and prompt. Save and run applies them and re-runs every + row; Cancel or Escape closes it. +4. **Rows**: the logo (16px; a letter monogram when the row has no `Logo`), the name and the domain, then the cell: a + status icon, its content and the row's run or retry control, which shows under the pointer, on focus and on touch. +5. **Footer**: the model (`Model`, or the simulation's; null hides it) and the progress, with the failure count. + +### States + +| Status | Icon | Content | +|---|---|---| +| `Idle` | Faint dashed ring | A dash | +| `Queued` | Clock | "Queued" | +| `Running` | Spinner | A shimmering activity label | +| `Done` | Check that draws itself | The value (one line, full text on hover) and the source link | +| `Failed` | Alert in red | "Failed" and the reason | + +### Running + +`Concurrency` (2 by default) caps how many cells run at once. Workers start 140ms apart so the column fills top to bottom, +and each takes the next row from a queue as it finishes. Workers read the latest rows and column when they take a row, so +editing the rows mid-run never acts on stale data: a row removed while queued is skipped. + +`Run()`, `Stop()` and `RunRow(id)` do what the buttons do. `IsRunning` is true while any cell is queued or running. +`RunCompleted` fires when a run settles, finished or stopped, with every cell. + +### Saved values + + + +Feed values you already have back through `InitialCells`, keyed by row id. `AutoRun` runs the column once when it first +scrolls into view, but only if no cell has a value yet. It is on while a simulation plays and off with your own +delegate unless you set it. `IsEditable="False"` hides the edit control. + +### Narrow and long tables + + + +The card fills its container and is designed for widths up to 760px. Below 560px the domain, the source text and the +model detail fold away; the source keeps its arrow link. Below 440px the gutters tighten to 16px, values wrap to two lines +in 60px rows and a failure's reason hides behind its label (hover or focus it to read it). On touch screens the row +controls stay visible. + +The rows are a virtualizing list. Give the card a height (or a `MaxHeight`) and only the rows on screen are built; without +one every row shows. + +### Your own words + +`Labels` holds every word the column shows or says. Replace the object to localize or reword: + +```csharp +table.Labels = new EnrichColumnLabels +{ + RowHeader = "Account", + RunColumn = "Enrich", + Rows = n => $"{n} accounts", +}; +``` + +### Content guidelines + +- Keep values to one short line ("240 people · Series B"); the cell truncates with an ellipsis and shows the full value on + hover. +- Give every value a source the person can open; a link they can check is what makes an AI value usable. +- Make failure reasons short and specific ("No source found", "Rate limited"). + +### Best practices + +- Honor the cancellation token and keep `Concurrency` low for rate-limited services. +- Persist values from `CellChanged` and pass them back through `InitialCells` next time. +- For hundreds of rows, give the card a height so the list virtualizes, and keep concurrency low. + +### Common questions + +**What happens to a cell when the person stops?** Its request is cancelled and it shows what it showed before the run: +its previous value, or a dash. + +**Does saving the column keep old values?** Every row re-runs with the new prompt; until a row's turn comes it shows +"Queued". + +**How do I open sources myself?** Handle `SourceRequested` and set `Handled`; otherwise the system opens the address. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The rows form a table: the company is each row's header and the AI column's name and prompt are its column header. +- Every cell state pairs an icon with text (Queued, a shimmering activity, the value, Failed with a reason), never color + alone. +- A polite live region announces runs, stops and the final count, and says when values are simulated and nothing was sent. +- Icon buttons carry labels that name the row ("Run Lumen Freight", "Retry Quarry Labs"); source links say they open a + new tab. +- Every word is in `EnrichColumnLabels`. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the column button, the edit control, each row's run or retry control and source links. Row controls appear on focus | +| Enter, Space | Runs, stops or re-runs the column, runs one row, retries a failed cell or opens a source | +| Ctrl + Enter (⌘ + Enter) | Saves the edited column and re-runs it | +| Escape | Closes the column editor without saving and returns focus to the edit control | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A cell changes state | One crossfade in a shared grid cell: the old content out in 100ms, the new in 160ms rising 2px | 120ms crossfade, no rise | At once | +| Status icon | Crossfades over 120ms with a bounce-free scale (0.6 to 1) | Crossfade only | At once | +| Done tick | Draws in 240ms | Shown whole | Shown whole | +| Running label | A calm shimmer, one pass each 1.8s; a new activity trades places inside it | Still | Still | +| Spinner | One turn each 800ms | One turn each 1.6s | Still | +| Editor opens or closes | A critically damped height spring (0.25s, no bounce) | At once | At once | +| Row control appears | Fades over 160ms | Fades over 120ms | At once | + +Nothing changes size while cells work: rows have a fixed height and a cell's content trades places in the same slot. + +## Related + +- `ToolCall` for the tool calls an agent makes. +- `AgentStatus` for the live status of a whole run. +- `Table` for a sortable, selectable data table without AI. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/zh-CN.md new file mode 100644 index 00000000..3a81f366 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/zh-CN.md @@ -0,0 +1,222 @@ +--- +title: 补全列 +summary: 数据表中的 AI 补全列:同一个提示词逐行运行,每个单元格在处理时显示微光,随后落定为带来源的值,或显示可重试的失败。 +source-hash: 741e6dfbca5e +--- + + + +## 何时使用 + +`EnrichColumn` 是带一列 AI 列的表格卡片。同一个提示词逐行运行:每个单元格先排队,智能体处理时显示微光,随后落定为一个简短的值和它的来源,或者显示失败和重试控件。用户可以停止运行、重新运行整列或单独一行,也可以在原地编辑这一列的名称和提示词。 + +- **补全公司或线索信息**:员工人数、融资、技术栈等智能体能查到的事实。 +- **用一个提示词对各行分类或摘要**,并为每个答案显示来源。 +- **在 CRM、销售工具或内部后台中展示逐行 AI 进度**,支持停止、重新运行和重试。 + +## 何时不用 + +- **列很多且需要编辑的大型表格**:使用数据表格,在其中加一列 AI 列。 +- **针对整个数据集的单个问题**:问一次、显示一个答案,而不是一整列。 +- **单个智能体的工具调用记录**:使用 `ToolCall`。 + +## 用法 + +把行和一个填充单元格的 `Enrich` 委托交给这一列。委托收到行和一个 `EnrichColumnContext`,返回值和它的来源。抛出异常即把单元格标为失败:异常消息会作为原因显示在“重试”旁边。 + +```csharp +table.Rows = accounts.Select(a => new EnrichColumnRow(a.Id, a.Name, a.Domain)).ToList(); +table.Column = new EnrichColumnDefinition("Headcount and funding", "Find the headcount and latest funding round"); +table.Enrich = async (row, context) => +{ + context.SetActivity($"Searching for {row.Name}"); + var answer = await service.EnrichAsync(row.Domain, context.Prompt, context.CancellationToken); + if (answer is null) + { + throw new InvalidOperationException("No source found"); + } + return new EnrichColumnResult(answer.Value, new EnrichColumnSource(answer.Host, answer.Url)); +}; +table.CellChanged += (_, e) => +{ + if (e.Cell.Status == EnrichColumnCellStatus.Done) + { + store.Save(e.Row.Id, e.Cell); + } +}; +``` + +这一列自己没有行时,会播放它的 `Simulation`,也就是由你提供的表格;两者都没有时显示空框架:顶栏显示 “0 rows”,AI 列没有名称, +没有行,“Run column” 不可用。只有你自己的行而没有 `Enrich` 时,行照常显示,但不会运行任何内容。 + +### 模拟 + +`EnrichColumnSimulation` 是 `Rows` 为 null 时这一列播放的表格:`Title`、`Column`、页脚中的 `Model`、`Rows`,以及 +`Answers`:每个行 id 对应一个 `EnrichColumnSimulationAnswer`,包含单元格最终显示的值和来源。它的 `Steps` 是单元格在此之前显示的 +活动,每个持续各自的 `Duration`;带有 `Error` 的答案在第一次尝试时失败,从而显示失败状态和重试,重试后得到该值。这一列首次进入 +视口时会自动运行它(`AutoRun`),在顶栏(“sample data”)和读屏软件中说明这一点,并且不会发送任何内容。你自己设置的 `Title`、 +`Column` 和 `Model` 优先于模拟中的值;把 `Simulation` 设为 null 会停止运行并显示空框架。 + +```csharp +table.Simulation = new EnrichColumnSimulation +{ + Title = "Target accounts", + Column = new EnrichColumnDefinition("Headcount and funding", "Find the headcount and latest funding round"), + Model = new EnrichColumnModel("Your model", "web search"), + Rows = [new EnrichColumnRow("acme", "Acme Robotics", "acme.example")], + Answers = + [ + new EnrichColumnSimulationAnswer("acme", "410 people · Series C", new EnrichColumnSource("acme.example/press", "https://acme.example/press")) + { + Steps = + [ + new EnrichColumnSimulationStep("Searching for Acme Robotics", TimeSpan.FromMilliseconds(700)), + new EnrichColumnSimulationStep("Reading acme.example", TimeSpan.FromMilliseconds(900)), + ], + Error = "No source found", // the first attempt fails, so Retry shows + }, + ], +}; +``` + +本页的演示从 `EnrichColumnDemoData` 获取表格:公司和数字都是虚构的,模型是真实的(核对于 2026-10-29)。 + +### 你的行与服务 + + + +上下文包含单元格开始时的提示词和列定义、一个在用户停止运行或重新运行整列时触发的 `CancellationToken`、尝试次数(首次运行为 1,第一次重试为 2),以及 `SetActivity`:它设置运行中单元格的微光标签,可以在任何线程调用。把令牌传给你的请求:被取消的单元格会恢复为运行前的显示,停止后才到达的答案会被忽略。 + +### 结构 + +1. **顶栏**:表格名称(`Title`)、行数和 `Note`,以及列按钮:未运行时为“Run column”,运行中为“Stop”,有值后为“Re-run”。 +2. **表头**:行列的表头,然后是 AI 列:一个闪光图标、列名及其下方的提示词,以及编辑控件。 +3. **编辑器**:在表头下方展开,包含列名和提示词。“Save and run”应用修改并重新运行每一行;“Cancel”或 Escape 关闭它。 +4. **行**:标志(16px;行没有 `Logo` 时显示首字母)、名称和域名,然后是单元格:状态图标、内容和这一行的运行或重试控件;控件在指针悬停、获得焦点和触屏时显示。 +5. **页脚**:模型(`Model` 或模拟中的模型;为 null 时隐藏)和进度,以及失败数。 + +### 状态 + +| 状态 | 图标 | 内容 | +|---|---|---| +| `Idle` | 浅色虚线圆环 | 一条短横线 | +| `Queued` | 时钟 | “Queued” | +| `Running` | 加载圈 | 显示微光的活动标签 | +| `Done` | 自行描出的对勾 | 值(单行,悬停显示全文)和来源链接 | +| `Failed` | 红色警示图标 | “Failed”和原因 | + +### 运行 + +`Concurrency`(默认 2)限制同时运行的单元格数。各个工作者相隔 140ms 启动,使这一列从上到下依次填充;每个工作者完成后从队列取下一行。工作者在取行时读取最新的行和列定义,因此运行中修改行不会作用于过时的数据:排队期间被移除的行会被跳过。 + +`Run()`、`Stop()` 和 `RunRow(id)` 与对应按钮的作用相同。只要有单元格在排队或运行,`IsRunning` 就为 true。一次运行落定(完成或停止)时触发 `RunCompleted`,并带上所有单元格。 + +### 已保存的值 + + + +把已有的值按行 id 通过 `InitialCells` 传回。`AutoRun` 会在这一列首次滚入视口时运行一次,但仅当还没有任何单元格有值时。播放模拟时默认开启,使用你自己的委托时默认关闭,除非你显式设置。`IsEditable="False"` 隐藏编辑控件。 + +### 窄屏与长表格 + + + +卡片填满容器,设计宽度最大为 760px。宽度小于 560px 时,域名、来源文字和模型说明会收起;来源保留箭头链接。小于 440px 时,左右留白收紧到 16px,值在 60px 高的行中换成两行,失败原因隐藏在标签后面(悬停或聚焦即可查看)。在触屏上,行控件始终可见。 + +行是虚拟化列表。给卡片设置高度(或 `MaxHeight`)后,只会创建屏幕上的行;不设置时显示全部行。 + +### 自定义文案 + +`Labels` 包含这一列显示或朗读的所有文字。替换整个对象即可本地化或改写: + +```csharp +table.Labels = new EnrichColumnLabels +{ + RowHeader = "Account", + RunColumn = "Enrich", + Rows = n => $"{n} accounts", +}; +``` + +### 内容准则 + +- 值保持为简短的一行(“240 people · Series B”);单元格会以省略号截断,悬停时显示完整的值。 +- 为每个值提供用户可以打开的来源;可核实的链接才让 AI 给出的值真正可用。 +- 失败原因要简短具体(“No source found”、“Rate limited”)。 + +### 最佳实践 + +- 遵守取消令牌;对有速率限制的服务,把 `Concurrency` 设低一些。 +- 在 `CellChanged` 中保存值,下次通过 `InitialCells` 传回。 +- 行数达到数百时,给卡片设置高度以启用虚拟化,并保持较低的并发数。 + +### 常见问题 + +**用户停止后单元格会怎样?** 它的请求被取消,并恢复为运行前的显示:之前的值,或一条短横线。 + +**保存列定义会保留旧值吗?** 每一行都会用新的提示词重新运行;轮到之前显示“Queued”。 + +**如何自己打开来源?** 处理 `SourceRequested` 并设置 `Handled`;否则由系统打开该地址。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 各行构成一个表格:公司是每一行的行标题,AI 列的名称和提示词是它的列标题。 +- 每种单元格状态都由图标和文字搭配表示(Queued、微光活动、值、带原因的 Failed),从不只靠颜色。 +- 礼貌模式的实时区域会播报开始运行、停止和最终数量,并在值是模拟数据、没有发送任何内容时说明。 +- 图标按钮的标签包含行名(“Run Lumen Freight”、“Retry Quarry Labs”);来源链接会说明在新标签页中打开。 +- 所有文字都在 `EnrichColumnLabels` 中。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过列按钮、编辑控件、每行的运行或重试控件和来源链接。行控件在获得焦点时出现 | +| Enter、Space | 运行、停止或重新运行整列,运行一行,重试失败的单元格,或打开来源 | +| Ctrl + Enter(⌘ + Enter) | 保存编辑的列并重新运行 | +| Escape | 不保存并关闭列编辑器,焦点回到编辑控件 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 单元格状态变化 | 在同一网格单元内交叉淡化一次:旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px | 120ms 交叉淡化,不上升 | 立即 | +| 状态图标 | 120ms 交叉淡化,配合无回弹的缩放(0.6 到 1) | 仅交叉淡化 | 立即 | +| 完成对勾 | 240ms 描出 | 直接完整显示 | 直接完整显示 | +| 运行标签 | 平缓的微光,每 1.8s 扫过一次;新的活动文字在其中替换 | 静止 | 静止 | +| 加载圈 | 每 800ms 转一圈 | 每 1.6s 转一圈 | 静止 | +| 编辑器展开或收起 | 临界阻尼的高度弹簧(0.25s,无回弹) | 立即 | 立即 | +| 行控件出现 | 160ms 淡入 | 120ms 淡入 | 立即 | + +单元格处理期间尺寸不会变化:行高固定,单元格内容在同一位置替换。 + +## 相关组件 + +- `ToolCall`:智能体发起的工具调用。 +- `AgentStatus`:整个运行的实时状态。 +- `Table`:不带 AI、可排序、可选择的数据表格。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/zh-TW.md new file mode 100644 index 00000000..09c45f32 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/EnrichColumn/zh-TW.md @@ -0,0 +1,222 @@ +--- +title: 補全欄 +summary: 資料表中的 AI 補全欄:同一個提示詞逐列執行,每個儲存格在處理時顯示微光,隨後落定為附來源的值,或顯示可重試的失敗。 +source-hash: 741e6dfbca5e +--- + + + +## 何時使用 + +`EnrichColumn` 是帶一欄 AI 欄的表格卡片。同一個提示詞逐列執行:每個儲存格先排隊,代理處理時顯示微光,隨後落定為一個簡短的值和它的來源,或者顯示失敗和重試控制項。使用者可以停止執行、重新執行整欄或單獨一列,也可以就地編輯這一欄的名稱和提示詞。 + +- **補全公司或潛在客戶資訊**:員工人數、募資、技術堆疊等代理查得到的事實。 +- **用一個提示詞對各列分類或摘要**,並為每個答案顯示來源。 +- **在 CRM、銷售工具或內部後台中呈現逐列 AI 進度**,支援停止、重新執行和重試。 + +## 何時不用 + +- **欄位很多且需要編輯的大型表格**:使用資料表格,在其中加一欄 AI 欄。 +- **針對整個資料集的單一問題**:問一次、顯示一個答案,而不是一整欄。 +- **單一代理的工具呼叫紀錄**:使用 `ToolCall`。 + +## 用法 + +把資料列和一個填入儲存格的 `Enrich` 委派交給這一欄。委派收到資料列和一個 `EnrichColumnContext`,傳回值和它的來源。擲出例外即把儲存格標為失敗:例外訊息會作為原因顯示在「重試」旁邊。 + +```csharp +table.Rows = accounts.Select(a => new EnrichColumnRow(a.Id, a.Name, a.Domain)).ToList(); +table.Column = new EnrichColumnDefinition("Headcount and funding", "Find the headcount and latest funding round"); +table.Enrich = async (row, context) => +{ + context.SetActivity($"Searching for {row.Name}"); + var answer = await service.EnrichAsync(row.Domain, context.Prompt, context.CancellationToken); + if (answer is null) + { + throw new InvalidOperationException("No source found"); + } + return new EnrichColumnResult(answer.Value, new EnrichColumnSource(answer.Host, answer.Url)); +}; +table.CellChanged += (_, e) => +{ + if (e.Cell.Status == EnrichColumnCellStatus.Done) + { + store.Save(e.Row.Id, e.Cell); + } +}; +``` + +這一欄自己沒有資料列時,會播放它的 `Simulation`,也就是由你提供的表格;兩者都沒有時顯示空框架:頂列顯示「0 rows」,AI 欄沒有 +名稱,沒有資料列,「Run column」無法使用。只有你自己的資料列而沒有 `Enrich` 時,資料列照常顯示,但不會執行任何內容。 + +### 模擬 + +`EnrichColumnSimulation` 是 `Rows` 為 null 時這一欄播放的表格:`Title`、`Column`、頁尾中的 `Model`、`Rows`,以及 +`Answers`:每個資料列 id 對應一個 `EnrichColumnSimulationAnswer`,包含儲存格最後顯示的值和來源。它的 `Steps` 是儲存格在此之前 +顯示的活動,每個持續各自的 `Duration`;帶有 `Error` 的答案在第一次嘗試時失敗,因而顯示失敗狀態和重試,重試後得到該值。這一欄 +第一次進入檢視區時會自動執行它(`AutoRun`),在頂列(「sample data」)和螢幕報讀程式中說明這一點,並且不會傳送任何內容。你自己 +設定的 `Title`、`Column` 和 `Model` 優先於模擬中的值;把 `Simulation` 設為 null 會停止執行並顯示空框架。 + +```csharp +table.Simulation = new EnrichColumnSimulation +{ + Title = "Target accounts", + Column = new EnrichColumnDefinition("Headcount and funding", "Find the headcount and latest funding round"), + Model = new EnrichColumnModel("Your model", "web search"), + Rows = [new EnrichColumnRow("acme", "Acme Robotics", "acme.example")], + Answers = + [ + new EnrichColumnSimulationAnswer("acme", "410 people · Series C", new EnrichColumnSource("acme.example/press", "https://acme.example/press")) + { + Steps = + [ + new EnrichColumnSimulationStep("Searching for Acme Robotics", TimeSpan.FromMilliseconds(700)), + new EnrichColumnSimulationStep("Reading acme.example", TimeSpan.FromMilliseconds(900)), + ], + Error = "No source found", // the first attempt fails, so Retry shows + }, + ], +}; +``` + +本頁的示範從 `EnrichColumnDemoData` 取得表格:公司和數字都是虛構的,模型是真實的(核對於 2026-10-29)。 + +### 你的資料列與服務 + + + +內容物件包含儲存格開始時的提示詞和欄位定義、一個在使用者停止執行或重新執行整欄時觸發的 `CancellationToken`、嘗試次數(首次執行為 1,第一次重試為 2),以及 `SetActivity`:它設定執行中儲存格的微光標籤,可以在任何執行緒呼叫。把權杖傳給你的請求:被取消的儲存格會恢復為執行前的顯示,停止後才抵達的答案會被忽略。 + +### 結構 + +1. **頂列**:表格名稱(`Title`)、列數和 `Note`,以及欄位按鈕:未執行時為「Run column」,執行中為「Stop」,有值後為「Re-run」。 +2. **表頭**:資料列欄的表頭,接著是 AI 欄:一個閃光圖示、欄名及其下方的提示詞,以及編輯控制項。 +3. **編輯器**:在表頭下方展開,包含欄名和提示詞。「Save and run」套用修改並重新執行每一列;「Cancel」或 Escape 關閉它。 +4. **資料列**:標誌(16px;資料列沒有 `Logo` 時顯示首字母)、名稱和網域,接著是儲存格:狀態圖示、內容和這一列的執行或重試控制項;控制項在指標停留、取得焦點和觸控螢幕上顯示。 +5. **頁尾**:模型(`Model` 或模擬中的模型;為 null 時隱藏)和進度,以及失敗數。 + +### 狀態 + +| 狀態 | 圖示 | 內容 | +|---|---|---| +| `Idle` | 淺色虛線圓環 | 一條短橫線 | +| `Queued` | 時鐘 | 「Queued」 | +| `Running` | 載入圈 | 顯示微光的活動標籤 | +| `Done` | 自行描出的勾號 | 值(單行,停留時顯示全文)和來源連結 | +| `Failed` | 紅色警示圖示 | 「Failed」和原因 | + +### 執行 + +`Concurrency`(預設 2)限制同時執行的儲存格數。各個工作者相隔 140ms 啟動,讓這一欄由上到下依序填入;每個工作者完成後從佇列取下一列。工作者在取列時讀取最新的資料列和欄位定義,因此執行中修改資料列不會作用於過時的資料:排隊期間被移除的資料列會被略過。 + +`Run()`、`Stop()` 和 `RunRow(id)` 與對應按鈕的作用相同。只要有儲存格在排隊或執行,`IsRunning` 就為 true。一次執行落定(完成或停止)時觸發 `RunCompleted`,並附上所有儲存格。 + +### 已儲存的值 + + + +把已有的值依資料列 id 透過 `InitialCells` 傳回。`AutoRun` 會在這一欄第一次捲入檢視區時執行一次,但僅限尚未有任何儲存格有值時。播放模擬時預設開啟,使用你自己的委派時預設關閉,除非你明確設定。`IsEditable="False"` 會隱藏編輯控制項。 + +### 窄版與長表格 + + + +卡片填滿容器,設計寬度最大為 760px。寬度小於 560px 時,網域、來源文字和模型說明會收起;來源保留箭頭連結。小於 440px 時,左右留白收緊到 16px,值在 60px 高的資料列中換成兩行,失敗原因隱藏在標籤後面(停留或聚焦即可查看)。在觸控螢幕上,資料列控制項一律可見。 + +資料列是虛擬化清單。為卡片設定高度(或 `MaxHeight`)後,只會建立畫面上的資料列;未設定時顯示全部資料列。 + +### 自訂文案 + +`Labels` 包含這一欄顯示或朗讀的所有文字。替換整個物件即可在地化或改寫: + +```csharp +table.Labels = new EnrichColumnLabels +{ + RowHeader = "Account", + RunColumn = "Enrich", + Rows = n => $"{n} accounts", +}; +``` + +### 內容指引 + +- 值保持為簡短的一行(「240 people · Series B」);儲存格會以刪節號截斷,停留時顯示完整的值。 +- 為每個值提供使用者可以開啟的來源;可查證的連結才讓 AI 給出的值真正可用。 +- 失敗原因要簡短具體(「No source found」、「Rate limited」)。 + +### 最佳實務 + +- 遵守取消權杖;對有速率限制的服務,把 `Concurrency` 設低一些。 +- 在 `CellChanged` 中儲存值,下次透過 `InitialCells` 傳回。 +- 資料列達到數百筆時,為卡片設定高度以啟用虛擬化,並維持較低的並行數。 + +### 常見問題 + +**使用者停止後儲存格會怎樣?** 它的請求被取消,並恢復為執行前的顯示:先前的值,或一條短橫線。 + +**儲存欄位定義會保留舊值嗎?** 每一列都會用新的提示詞重新執行;輪到之前顯示「Queued」。 + +**如何自行開啟來源?** 處理 `SourceRequested` 並設定 `Handled`;否則由系統開啟該網址。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 各資料列構成一個表格:公司是每一列的列標題,AI 欄的名稱和提示詞是它的欄標題。 +- 每種儲存格狀態都由圖示和文字搭配表示(Queued、微光活動、值、附原因的 Failed),從不只靠顏色。 +- 禮貌模式的即時區域會播報開始執行、停止和最終數量,並在值是模擬資料、沒有傳送任何內容時說明。 +- 圖示按鈕的標籤包含列名(「Run Lumen Freight」、「Retry Quarry Labs」);來源連結會說明在新分頁中開啟。 +- 所有文字都在 `EnrichColumnLabels` 中。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過欄位按鈕、編輯控制項、每列的執行或重試控制項和來源連結。資料列控制項在取得焦點時出現 | +| Enter、Space | 執行、停止或重新執行整欄,執行一列,重試失敗的儲存格,或開啟來源 | +| Ctrl + Enter(⌘ + Enter) | 儲存編輯的欄位並重新執行 | +| Escape | 不儲存並關閉欄位編輯器,焦點回到編輯控制項 | + +## 動效 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 儲存格狀態變化 | 在同一格線儲存格內交叉淡化一次:舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px | 120ms 交叉淡化,不上升 | 立即 | +| 狀態圖示 | 120ms 交叉淡化,搭配無回彈的縮放(0.6 到 1) | 僅交叉淡化 | 立即 | +| 完成勾號 | 240ms 描出 | 直接完整顯示 | 直接完整顯示 | +| 執行標籤 | 平緩的微光,每 1.8s 掃過一次;新的活動文字在其中替換 | 靜止 | 靜止 | +| 載入圈 | 每 800ms 轉一圈 | 每 1.6s 轉一圈 | 靜止 | +| 編輯器展開或收合 | 臨界阻尼的高度彈簧(0.25s,無回彈) | 立即 | 立即 | +| 資料列控制項出現 | 160ms 淡入 | 120ms 淡入 | 立即 | + +儲存格處理期間尺寸不會變化:列高固定,儲存格內容在同一位置替換。 + +## 相關 + +- `ToolCall`:代理發出的工具呼叫。 +- `AgentStatus`:整個執行的即時狀態。 +- `Table`:不含 AI、可排序、可選取的資料表格。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/en-US.md new file mode 100644 index 00000000..884851a1 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/en-US.md @@ -0,0 +1,224 @@ +--- +title: Generation stats +summary: A live stats strip for a streaming model response: time to first token, speed with a sparkline, output tokens and cost count up, then settle and open into a latency and token cost breakdown. +--- + + + +## When to use + +`GenerationStats` describes one model response while it streams and after it ends. The head shows the model's mark and +name and a status with an icon and a word: Waiting, Streaming, "Done in 4.18s", "Stopped at 1.90s" or Failed. Below it +four cells: time to first token, speed in tokens per second with a small sparkline, output tokens and cost. While the +response streams every number counts up in tabular digits; when it ends they settle on exact values. Details opens a +breakdown with a latency bar and a token table with rates and cost. + +- **Under assistant messages** in a chat product that shows people what a response cost or how fast it was. +- **Playgrounds, eval runners and internal dev panels** that compare models on latency, throughput and cost. +- **Admin or debugging views** that inspect a single request's token usage and cache hit rate. + +## When not to use + +- **How much of the context a conversation uses**: use `ContextWindow`. +- **Spend across many requests or days**: use a chart or a table; this strip describes one response. +- **A long running agent with steps**: use `AgentStatus`; this measures one model call. + +## Usage + +Feed the strip from the streaming handler you already have. Set `StartedAt` when the request leaves and `Status` to +`Waiting`; on the first chunk set `Usage.Ttft` and `Status` to `Streaming`; give it a new `Usage` with the growing +`OutputTokens` on every chunk; on the last one set `Usage.Duration` and `Status` to `Done`. Speed, latency and cost follow. +With no `Status` and no `Simulation` the strip is idle: the head reads Idle and every cell a dash. + +```csharp +// When the request leaves. The provider is your own key for it, written as given into the copied JSON. +stats.Model = new GenerationModel(name: "Claude Sonnet 5.5", provider: "anthropic", id: "claude-sonnet-5-5"); +stats.Pricing = new GenerationPricing(input: rates.Input, output: rates.Output, cachedInput: rates.CachedInput); +stats.StartedAt = DateTimeOffset.Now; +stats.Usage = new GenerationUsage { InputTokens = input + cacheRead, CachedTokens = cacheRead }; +stats.Status = GenerationStatus.Waiting; + +// On every chunk (the first one also sets Ttft and Status = Streaming). +stats.Usage = stats.Usage with { OutputTokens = outputSoFar }; + +// On the last one. +stats.Usage = stats.Usage with { Duration = DateTimeOffset.Now - started }; +stats.Status = GenerationStatus.Done; +``` + + + +### Anatomy + +1. **Head**: the model's mark (a monogram, or your own glyph in `GenerationModel.Mark`), its name (it truncates rather + than wraps), the status icon and word, and Details. +2. **Cells**: First token, Speed (with the sparkline), Output and Cost, split by hairlines. +3. **Error row**: when the request fails, the reason in `ErrorText` and Retry. +4. **Breakdown**: the latency bar (first token, generation, total), the token table (input, cached input, output, + reasoning, total) with rates and cost, a note and a button that copies the stats as JSON. + +### States + +| Status | Head reads | Cells | +|---|---|---| +| `Idle`, or no `Status` and no `Simulation` | Idle | Dashes | +| `Waiting` | A spinner and "Waiting" | First token counts live from `StartedAt` | +| `Streaming` | An accent spinner and "Streaming" | Output, speed and cost count up; the sparkline draws | +| `Done` | A check that draws itself and "Done in 4.18s" | Exact values | +| `Stopped` | A square and "Stopped at 1.90s" | What arrived | +| `Error` | An alert and "Failed" | What arrived; the error row opens | + +### Speed and cost + +Speed is measured when a chunk lands (tokens since the first token over the time since it), so it holds still between +chunks; once the response ends it is the output tokens over the generation time. The sparkline adds a smoothed reading +every 160ms while the output grows and halves its points when it reaches 48, so it always shows the whole run. Pass +`Usage.TokensPerSecond` or `SpeedSamples` when your service reports them. + +Cost comes from `Pricing` (US dollars per million tokens): uncached input at the input rate, cached input at the cached +rate (the input rate when none is given), output and reasoning at the output rate. When your provider reports the cost, +pass `Usage.CostUsd` and leave `Pricing` out; the Rate column then hides. + +Map provider usage like this: Anthropic `input_tokens` plus `cache_read_input_tokens` is `InputTokens` and +`cache_read_input_tokens` is `CachedTokens`; OpenAI `prompt_tokens` and `prompt_tokens_details.cached_tokens` map the same +way, and `completion_tokens_details.reasoning_tokens` is `ReasoningTokens`. `OutputTokens` counts output without +reasoning. + +### Breakdown + + + +Details opens the breakdown; `IsExpanded="True"` opens it on first render. `DetailsToggled` is raised when the person +presses Details. `Note` puts a footnote under it, such as where the rates come from. + +### Inline + + + +`Variant="Inline"` is one quiet line for the space under a chat message ("412ms to first token · 88 tok/s · 284 tokens · +$0.0049"). It wraps onto a second line on phones instead of scrolling, and the breakdown opens as a panel below it. + +### Failures and stops + +When the person stops a response, set `Usage.Duration` and `Status` to `Stopped`. When the request fails, set `ErrorText` +to a short, specific reason and `Status` to `Error`: the error row opens with Retry, which raises `RetryRequested`. +`AllowsRetry="False"` hides it. + +### Simulated runs + +Leave `Status` unset and give the strip a `GenerationStatsSimulation` to play runs of your own, for a landing page, a +prototype or a demo: each `GenerationStatsRun` names the model and its prices, the token counts it ends on, the time to +the first token and the peak speed. The strip waits, streams in chunks with a ramp up and a little waver, settles and moves +on; a run with `FailAtTokens` fails there once with its `ErrorText` and succeeds when retried. The runs loop while the +strip is on screen, hold while the breakdown is open and settle at once under reduced motion. The preview above plays four +runs of real models at their list prices; the tokens and timings are invented. + +```csharp +stats.Simulation = new GenerationStatsSimulation { Runs = MyRuns.All }; +stats.Note = "Simulated runs at list prices."; + +public static class MyRuns +{ + public static IReadOnlyList All { get; } = + [ + new() + { + Model = new GenerationModel("Claude Sonnet 5.5", "anthropic", "claude-sonnet-5-5"), + Pricing = MyPrices.ClaudeSonnet, // your price list, US dollars per million tokens + InputTokens = 3_682, + CachedTokens = 2_816, + OutputTokens = 284, + TimeToFirstToken = TimeSpan.FromMilliseconds(412), + PeakTokensPerSecond = 96, + }, + ]; +} +``` + +In XAML, set `` to a `GenerationStatsSimulation` whose `Runs` come from `{x:Static}`, as the preview's Code tab shows. Give each strip its own simulation (it keeps the playback state); the runs can be shared. Setting `Status` hands the strip +to your data, and setting it back to null starts the simulation again from its first run. + +### Responsive + +The four cells sit in one row from 600px of strip width and in a two by two grid below it, so the speed cell keeps room +for its sparkline. Below 440px Details becomes an icon button (its name stays "Show details" / "Hide details") and the +table's Rate column hides. The strip always fills its container; numbers count in reserved tabular cells, so nothing +beside them moves. + +### Your own words + +`Labels` holds every word the strip shows or says. Replace the object to localize or reword: + +```csharp +stats.Labels = new GenerationStatsLabels +{ + DoneIn = time => $"Terminé en {time}", + FirstToken = "Premier jeton", + Details = "Détails", +}; +``` + +### Best practices + +- Keep the model name as people know it ("Claude Sonnet 5.5") and put the API id in `GenerationModel.Id`. +- Use the inline variant under chat messages and the card in eval views, playgrounds and admin panels. +- Give a new `Usage` per chunk instead of rebuilding the strip; the numbers glide from where they are. + +## API reference + + + + + + + + + + + + + + + +## Accessibility + +- The strip is a group named after the model ("Claude Sonnet 5.5 stats"). Each cell is named by its label and value, so it + reads "First token, 412ms". +- Details is a button that reports expanded or collapsed; at narrow widths it becomes an icon button that keeps the name + "Show details" or "Hide details". +- A polite status region announces settled outcomes ("Claude Sonnet 5.5 done in 4.18s, 284 output tokens at 88 tokens per + second, $0.0049"), failures and stops, never the live counts. A playing simulation says once that its runs are simulated + and stays quiet until someone interacts with the strip. +- Status never relies on color: a spinner, a check, an alert or a square always sits next to a word. The latency bar is + decorative; its values are listed beside it in text. +- Each row of the token table is named with its column headers ("Input: Tokens 866, Rate $2.00 per million tokens, Cost + $0.0017"). +- Pointer focus draws no ring; keyboard focus shows Arc's focus ring. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to Details, then to Retry when a response failed, then to the copy button inside an open breakdown | +| Enter, Space | Opens or closes the breakdown, retries, or copies the stats as JSON | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A number changes | Glides to the new value over about 100ms in tabular digits; a drop cuts straight to it | At once | At once | +| Status changes | Old word and icon fade out in 100ms, the new ones fade in over 160ms rising 2px | Short crossfade | At once | +| Done | The check draws itself once | Drawn whole | Drawn whole | +| Waiting, streaming | The spinner turns once every 800ms | Holds still | Holds still | +| Breakdown or error row | Height on a 0.26s critically damped spring, content fades over 160ms; nothing bounces | At once | At once | +| Details chevron | Turns on the same spring | At once | At once | +| Simulated runs | Play in a loop while on screen; hold while the breakdown is open | Stop on a settled run | Stop on a settled run | + +The live clock runs only while waiting or streaming, and a simulation ticks in virtual time only while the strip is on +screen. + +## Related + +- `ContextWindow` for how much of a model's context a conversation has used. +- `AgentStatus` for the live status of a long running agent. +- `ToolCall` for the tool calls an agent makes. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/zh-CN.md new file mode 100644 index 00000000..959303e2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/zh-CN.md @@ -0,0 +1,188 @@ +--- +title: Generation stats +summary: 流式模型回复的实时统计条:首个 token 时间、带迷你折线的速度、输出 token 与费用逐步累加,结束后定格,并可展开为延迟与 token 费用明细。 +source-hash: 599b25d0c851 +--- + + + +## 何时使用 + +`GenerationStats` 描述一次模型回复在流式输出时和结束后的情况。头部显示模型的标记和名称,以及由图标和文字组成的状态:Waiting、Streaming、"Done in 4.18s"、"Stopped at 1.90s" 或 Failed。下方是四个单元格:首个 token 时间、带迷你折线的每秒 token 速度、输出 token 和费用。回复流式输出时,每个数字都以等宽数字逐步累加;结束后定格在精确值上。Details 会展开明细,其中有一条延迟条和一张带单价与费用的 token 表。 + +- **聊天产品中助手消息的下方**,向用户展示一次回复花了多少钱、有多快。 +- **Playground、评测运行器和内部开发面板**,在延迟、吞吐量和费用上比较模型。 +- **管理或调试视图**,检查单个请求的 token 用量和缓存命中率。 + +## 何时不用 + +- **一段对话用了多少上下文**:使用 `ContextWindow`。 +- **多个请求或多天的花费**:使用图表或表格;这个统计条只描述一次回复。 +- **有多个步骤、长时间运行的智能体**:使用 `AgentStatus`;这里度量的是一次模型调用。 + +## 用法 + +用你已有的流式处理程序为统计条提供数据。请求发出时设置 `StartedAt`,并把 `Status` 设为 `Waiting`;收到第一个数据块时设置 `Usage.Ttft`,并把 `Status` 设为 `Streaming`;每个数据块都传入一个带有不断增长的 `OutputTokens` 的新 `Usage`;最后一块时设置 `Usage.Duration`,并把 `Status` 设为 `Done`。速度、延迟和费用会随之得出。既不设置 `Status` 也不设置 `Simulation` 时,统计条处于空闲状态:头部显示 Idle,每个单元格显示短横线。 + +```csharp +// When the request leaves. The provider is your own key for it, written as given into the copied JSON. +stats.Model = new GenerationModel(name: "Claude Sonnet 5.5", provider: "anthropic", id: "claude-sonnet-5-5"); +stats.Pricing = new GenerationPricing(input: rates.Input, output: rates.Output, cachedInput: rates.CachedInput); +stats.StartedAt = DateTimeOffset.Now; +stats.Usage = new GenerationUsage { InputTokens = input + cacheRead, CachedTokens = cacheRead }; +stats.Status = GenerationStatus.Waiting; + +// On every chunk (the first one also sets Ttft and Status = Streaming). +stats.Usage = stats.Usage with { OutputTokens = outputSoFar }; + +// On the last one. +stats.Usage = stats.Usage with { Duration = DateTimeOffset.Now - started }; +stats.Status = GenerationStatus.Done; +``` + + + +### 结构 + +1. **头部**:模型的标记(字母徽标,或你在 `GenerationModel.Mark` 中提供的图形)、名称(过长时截断而不换行)、状态图标与文字,以及 Details。 +2. **单元格**:First token、Speed(带迷你折线)、Output 和 Cost,以细线分隔。 +3. **错误行**:请求失败时,显示 `ErrorText` 中的原因和 Retry。 +4. **明细**:延迟条(首个 token、生成、总计)、token 表(输入、缓存输入、输出、推理、总计)及其单价与费用、一条备注,以及把统计复制为 JSON 的按钮。 + +### 状态 + +| 状态 | 头部显示 | 单元格 | +|---|---|---| +| `Idle`,或既无 `Status` 也无 `Simulation` | Idle | 短横线 | +| `Waiting` | 旋转图标与 "Waiting" | First token 从 `StartedAt` 起实时计时 | +| `Streaming` | 强调色旋转图标与 "Streaming" | 输出、速度和费用逐步累加;迷你折线逐步绘出 | +| `Done` | 自行绘出的对勾与 "Done in 4.18s" | 精确值 | +| `Stopped` | 方块与 "Stopped at 1.90s" | 已到达的部分 | +| `Error` | 警示图标与 "Failed" | 已到达的部分;错误行展开 | + +### 速度与费用 + +速度在数据块到达时测量(自首个 token 以来的 token 数除以自那时起经过的时间),因此在两个数据块之间保持不动;回复结束后,速度为输出 token 除以生成时间。输出增长期间,迷你折线每 160ms 加入一个平滑后的读数,点数达到 48 时减半,因此始终呈现整个运行过程。如果你的服务会报告这些值,请传入 `Usage.TokensPerSecond` 或 `SpeedSamples`。 + +费用由 `Pricing`(每百万 token 的美元价格)算出:未缓存的输入按输入单价,缓存输入按缓存单价(未提供时按输入单价),输出和推理按输出单价。如果服务商会报告费用,请传入 `Usage.CostUsd` 并省略 `Pricing`;此时 Rate 列会隐藏。 + +服务商的用量字段这样对应:Anthropic 的 `input_tokens` 加上 `cache_read_input_tokens` 是 `InputTokens`,`cache_read_input_tokens` 是 `CachedTokens`;OpenAI 的 `prompt_tokens` 和 `prompt_tokens_details.cached_tokens` 按同样方式对应,`completion_tokens_details.reasoning_tokens` 是 `ReasoningTokens`。`OutputTokens` 统计的是不含推理的输出。 + +### 明细 + + + +Details 用于展开明细;`IsExpanded="True"` 让它在首次渲染时即展开。用户按下 Details 时会引发 `DetailsToggled`。`Note` 会在明细下方放一条脚注,例如单价的来源。 + +### 行内 + + + +`Variant="Inline"` 是一行安静的文字,用于聊天消息下方的空间("412ms to first token · 88 tok/s · 284 tokens · $0.0049")。在手机上它会换到第二行而不是滚动,明细则作为其下方的面板展开。 + +### 失败与停止 + +用户停止回复时,设置 `Usage.Duration`,并把 `Status` 设为 `Stopped`。请求失败时,把 `ErrorText` 设为简短而具体的原因,并把 `Status` 设为 `Error`:错误行会带着 Retry 展开,按下 Retry 会引发 `RetryRequested`。`AllowsRetry="False"` 会隐藏它。 + +### 模拟运行 + +不设置 `Status`,并给统计条一个 `GenerationStatsSimulation`,即可播放你自己的运行,适用于落地页、原型或演示:每个 `GenerationStatsRun` 指定模型及其价格、最终的 token 数、首个 token 时间和峰值速度。统计条会等待、以带爬升和轻微波动的数据块流式输出、定格,然后进入下一个;带有 `FailAtTokens` 的运行会在该处以其 `ErrorText` 失败一次,重试后成功。统计条位于屏幕上时这些运行循环播放,明细展开时停留,减弱动效下立即定格。上面的预览播放四个真实模型按其标价计费的运行;token 数和耗时是虚构的。 + +```csharp +stats.Simulation = new GenerationStatsSimulation { Runs = MyRuns.All }; +stats.Note = "Simulated runs at list prices."; + +public static class MyRuns +{ + public static IReadOnlyList All { get; } = + [ + new() + { + Model = new GenerationModel("Claude Sonnet 5.5", "anthropic", "claude-sonnet-5-5"), + Pricing = MyPrices.ClaudeSonnet, // your price list, US dollars per million tokens + InputTokens = 3_682, + CachedTokens = 2_816, + OutputTokens = 284, + TimeToFirstToken = TimeSpan.FromMilliseconds(412), + PeakTokensPerSecond = 96, + }, + ]; +} +``` + +在 XAML 中,把 `` 设为一个 `GenerationStatsSimulation`,其 `Runs` 来自 `{x:Static}`,如预览的 Code 标签所示。每个统计条使用自己的模拟(它保存播放状态);运行数据可以共享。设置 `Status` 后统计条改为显示你的数据,把它设回 null 会让模拟从第一个运行重新开始。 + +### 响应式 + +统计条宽度达到 600px 时,四个单元格排成一行;低于 600px 时排成二乘二的网格,让速度单元格保留放迷你折线的空间。低于 440px 时,Details 变为图标按钮(名称仍为 "Show details" / "Hide details"),表格的 Rate 列隐藏。统计条始终填满其容器;数字在预留的等宽单元格中累加,因此旁边的内容不会移动。 + +### 自定义文案 + +`Labels` 包含统计条显示或朗读的所有文字。替换整个对象即可本地化或改写: + +```csharp +stats.Labels = new GenerationStatsLabels +{ + DoneIn = time => $"Terminé en {time}", + FirstToken = "Premier jeton", + Details = "Détails", +}; +``` + +### 最佳实践 + +- 模型名称使用人们熟知的写法("Claude Sonnet 5.5"),把 API id 放进 `GenerationModel.Id`。 +- 在聊天消息下方使用行内变体,在评测视图、Playground 和管理面板中使用卡片。 +- 每个数据块传入一个新的 `Usage`,而不是重建统计条;数字会从当前位置平滑过渡。 + +## API 参考 + + + + + + + + + + + + + + + +## 无障碍 + +- 统计条是一个以模型命名的组("Claude Sonnet 5.5 stats")。每个单元格以其标签和值命名,因此读作 "First token, 412ms"。 +- Details 是一个会报告展开或折叠状态的按钮;在较窄的宽度下变为图标按钮,但保留名称 "Show details" 或 "Hide details"。 +- 一个礼貌型状态区域会播报定格的结果("Claude Sonnet 5.5 done in 4.18s, 284 output tokens at 88 tokens per second, $0.0049")、失败和停止,从不播报实时计数。正在播放的模拟只说一次其运行是模拟的,在有人与统计条交互之前保持安静。 +- 状态从不只依赖颜色:旋转图标、对勾、警示图标或方块旁边总有文字。延迟条是装饰性的;它的数值以文字列在旁边。 +- token 表的每一行都带着列标题命名("Input: Tokens 866, Rate $2.00 per million tokens, Cost $0.0017")。 +- 指针聚焦不显示焦点环;键盘聚焦显示 Arc 的焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 移到 Details,回复失败时再移到 Retry,然后移到已展开明细中的复制按钮 | +| Enter、Space | 展开或折叠明细、重试,或把统计复制为 JSON | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 数字变化 | 以等宽数字在约 100ms 内平滑过渡到新值;数值下降时直接跳到新值 | 立即 | 立即 | +| 状态变化 | 旧文字和图标在 100ms 内淡出,新的在 160ms 内淡入并上升 2px | 短暂交叉淡化 | 立即 | +| 完成 | 对勾自行绘出一次 | 完整绘出 | 完整绘出 | +| 等待、流式输出 | 旋转图标每 800ms 转一圈 | 静止 | 静止 | +| 明细或错误行 | 高度以 0.26s 临界阻尼弹簧变化,内容在 160ms 内淡入淡出;没有回弹 | 立即 | 立即 | +| Details 箭头 | 以同一弹簧旋转 | 立即 | 立即 | +| 模拟运行 | 在屏幕上时循环播放;明细展开时停留 | 停在一个已定格的运行上 | 停在一个已定格的运行上 | + +实时时钟只在等待或流式输出时运行,模拟只在统计条位于屏幕上时以虚拟时间推进。 + +## 相关组件 + +- `ContextWindow`:一段对话用了模型多少上下文。 +- `AgentStatus`:长时间运行的智能体的实时状态。 +- `ToolCall`:智能体发起的工具调用。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/zh-TW.md new file mode 100644 index 00000000..bf6f2668 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GenerationStats/zh-TW.md @@ -0,0 +1,188 @@ +--- +title: Generation stats +summary: 串流模型回覆的即時統計列:首個 token 時間、附迷你折線的速度、輸出 token 與費用逐步累加,結束後定格,並可展開為延遲與 token 費用明細。 +source-hash: 599b25d0c851 +--- + + + +## 何時使用 + +`GenerationStats` 描述一次模型回覆在串流輸出時與結束後的情況。頂部顯示模型的標記與名稱,以及由圖示和文字組成的狀態:Waiting、Streaming、"Done in 4.18s"、"Stopped at 1.90s" 或 Failed。下方是四個儲存格:首個 token 時間、附迷你折線的每秒 token 速度、輸出 token 與費用。回覆串流輸出時,每個數字都以等寬數字逐步累加;結束後定格在精確值上。Details 會展開明細,其中有一條延遲條和一張附單價與費用的 token 表格。 + +- **聊天產品中助理訊息的下方**,讓使用者看到一次回覆花了多少錢、有多快。 +- **Playground、評測執行器與內部開發面板**,在延遲、吞吐量與費用上比較模型。 +- **管理或除錯檢視**,檢查單一請求的 token 用量與快取命中率。 + +## 何時不用 + +- **一段對話用了多少上下文**:使用 `ContextWindow`。 +- **多個請求或多天的花費**:使用圖表或表格;這個統計列只描述一次回覆。 +- **有多個步驟、長時間執行的代理**:使用 `AgentStatus`;這裡量測的是一次模型呼叫。 + +## 用法 + +用你既有的串流處理程式為統計列提供資料。請求送出時設定 `StartedAt`,並把 `Status` 設為 `Waiting`;收到第一個資料區塊時設定 `Usage.Ttft`,並把 `Status` 設為 `Streaming`;每個資料區塊都傳入一個帶有持續增加之 `OutputTokens` 的新 `Usage`;最後一個區塊時設定 `Usage.Duration`,並把 `Status` 設為 `Done`。速度、延遲與費用會隨之得出。既未設定 `Status` 也未設定 `Simulation` 時,統計列處於閒置狀態:頭部顯示 Idle,每個儲存格顯示短橫線。 + +```csharp +// When the request leaves. The provider is your own key for it, written as given into the copied JSON. +stats.Model = new GenerationModel(name: "Claude Sonnet 5.5", provider: "anthropic", id: "claude-sonnet-5-5"); +stats.Pricing = new GenerationPricing(input: rates.Input, output: rates.Output, cachedInput: rates.CachedInput); +stats.StartedAt = DateTimeOffset.Now; +stats.Usage = new GenerationUsage { InputTokens = input + cacheRead, CachedTokens = cacheRead }; +stats.Status = GenerationStatus.Waiting; + +// On every chunk (the first one also sets Ttft and Status = Streaming). +stats.Usage = stats.Usage with { OutputTokens = outputSoFar }; + +// On the last one. +stats.Usage = stats.Usage with { Duration = DateTimeOffset.Now - started }; +stats.Status = GenerationStatus.Done; +``` + + + +### 結構 + +1. **頂部**:模型的標記(字母徽標,或你在 `GenerationModel.Mark` 中提供的圖形)、名稱(過長時截斷而不換行)、狀態圖示與文字,以及 Details。 +2. **儲存格**:First token、Speed(附迷你折線)、Output 與 Cost,以細線分隔。 +3. **錯誤列**:請求失敗時,顯示 `ErrorText` 中的原因與 Retry。 +4. **明細**:延遲條(首個 token、生成、總計)、token 表格(輸入、快取輸入、輸出、推理、總計)及其單價與費用、一則備註,以及把統計複製為 JSON 的按鈕。 + +### 狀態 + +| 狀態 | 頂部顯示 | 儲存格 | +|---|---|---| +| `Idle`,或既無 `Status` 也無 `Simulation` | Idle | 短橫線 | +| `Waiting` | 旋轉圖示與 "Waiting" | First token 從 `StartedAt` 起即時計時 | +| `Streaming` | 強調色旋轉圖示與 "Streaming" | 輸出、速度與費用逐步累加;迷你折線逐步繪出 | +| `Done` | 自行繪出的勾號與 "Done in 4.18s" | 精確值 | +| `Stopped` | 方塊與 "Stopped at 1.90s" | 已抵達的部分 | +| `Error` | 警示圖示與 "Failed" | 已抵達的部分;錯誤列展開 | + +### 速度與費用 + +速度在資料區塊抵達時量測(自首個 token 以來的 token 數除以自那時起經過的時間),因此在兩個資料區塊之間保持不動;回覆結束後,速度為輸出 token 除以生成時間。輸出增加期間,迷你折線每 160ms 加入一個平滑後的讀數,點數達到 48 時減半,因此始終呈現整個執行過程。若你的服務會回報這些值,請傳入 `Usage.TokensPerSecond` 或 `SpeedSamples`。 + +費用由 `Pricing`(每百萬 token 的美元價格)算出:未快取的輸入按輸入單價,快取輸入按快取單價(未提供時按輸入單價),輸出與推理按輸出單價。若服務供應商會回報費用,請傳入 `Usage.CostUsd` 並省略 `Pricing`;此時 Rate 欄會隱藏。 + +服務供應商的用量欄位這樣對應:Anthropic 的 `input_tokens` 加上 `cache_read_input_tokens` 是 `InputTokens`,`cache_read_input_tokens` 是 `CachedTokens`;OpenAI 的 `prompt_tokens` 與 `prompt_tokens_details.cached_tokens` 以同樣方式對應,`completion_tokens_details.reasoning_tokens` 是 `ReasoningTokens`。`OutputTokens` 計算的是不含推理的輸出。 + +### 明細 + + + +Details 用來展開明細;`IsExpanded="True"` 讓它在首次轉譯時即展開。使用者按下 Details 時會引發 `DetailsToggled`。`Note` 會在明細下方放一則註腳,例如單價的來源。 + +### 行內 + + + +`Variant="Inline"` 是一行安靜的文字,用於聊天訊息下方的空間("412ms to first token · 88 tok/s · 284 tokens · $0.0049")。在手機上它會換到第二行而不是捲動,明細則以其下方的面板展開。 + +### 失敗與停止 + +使用者停止回覆時,設定 `Usage.Duration`,並把 `Status` 設為 `Stopped`。請求失敗時,把 `ErrorText` 設為簡短而具體的原因,並把 `Status` 設為 `Error`:錯誤列會帶著 Retry 展開,按下 Retry 會引發 `RetryRequested`。`AllowsRetry="False"` 會隱藏它。 + +### 模擬執行 + +不設定 `Status`,並給統計列一個 `GenerationStatsSimulation`,即可播放你自己的執行,適用於到達頁、原型或展示:每個 `GenerationStatsRun` 指定模型及其價格、最終的 token 數、首個 token 時間與峰值速度。統計列會等待、以帶爬升與輕微波動的資料區塊串流輸出、定格,然後進入下一個;帶有 `FailAtTokens` 的執行會在該處以其 `ErrorText` 失敗一次,重試後成功。統計列位於畫面上時這些執行循環播放,明細展開時停留,減弱動態效果下立即定格。上面的預覽播放四個真實模型依其牌價計費的執行;token 數與耗時是虛構的。 + +```csharp +stats.Simulation = new GenerationStatsSimulation { Runs = MyRuns.All }; +stats.Note = "Simulated runs at list prices."; + +public static class MyRuns +{ + public static IReadOnlyList All { get; } = + [ + new() + { + Model = new GenerationModel("Claude Sonnet 5.5", "anthropic", "claude-sonnet-5-5"), + Pricing = MyPrices.ClaudeSonnet, // your price list, US dollars per million tokens + InputTokens = 3_682, + CachedTokens = 2_816, + OutputTokens = 284, + TimeToFirstToken = TimeSpan.FromMilliseconds(412), + PeakTokensPerSecond = 96, + }, + ]; +} +``` + +在 XAML 中,把 `` 設為一個 `GenerationStatsSimulation`,其 `Runs` 來自 `{x:Static}`,如預覽的 Code 分頁所示。每個統計列使用自己的模擬(它保存播放狀態);執行資料可以共用。設定 `Status` 後統計列改為顯示你的資料,把它設回 null 會讓模擬從第一個執行重新開始。 + +### 回應式 + +統計列寬度達到 600px 時,四個儲存格排成一列;低於 600px 時排成二乘二的格線,讓速度儲存格保有放迷你折線的空間。低於 440px 時,Details 變為圖示按鈕(名稱仍為 "Show details" / "Hide details"),表格的 Rate 欄隱藏。統計列始終填滿其容器;數字在預留的等寬儲存格中累加,因此旁邊的內容不會移動。 + +### 自訂文案 + +`Labels` 包含統計列顯示或朗讀的所有文字。替換整個物件即可在地化或改寫: + +```csharp +stats.Labels = new GenerationStatsLabels +{ + DoneIn = time => $"Terminé en {time}", + FirstToken = "Premier jeton", + Details = "Détails", +}; +``` + +### 最佳做法 + +- 模型名稱使用大家熟知的寫法("Claude Sonnet 5.5"),把 API id 放進 `GenerationModel.Id`。 +- 在聊天訊息下方使用行內變體,在評測檢視、Playground 與管理面板中使用卡片。 +- 每個資料區塊傳入一個新的 `Usage`,而不是重建統計列;數字會從目前位置平滑過渡。 + +## API 參考 + + + + + + + + + + + + + + + +## 無障礙 + +- 統計列是一個以模型命名的群組("Claude Sonnet 5.5 stats")。每個儲存格以其標籤與值命名,因此讀作 "First token, 412ms"。 +- Details 是一個會回報展開或收合狀態的按鈕;在較窄的寬度下變為圖示按鈕,但保留名稱 "Show details" 或 "Hide details"。 +- 一個禮貌型狀態區域會播報定格的結果("Claude Sonnet 5.5 done in 4.18s, 284 output tokens at 88 tokens per second, $0.0049")、失敗與停止,從不播報即時計數。正在播放的模擬只說一次其執行是模擬的,在有人與統計列互動之前保持安靜。 +- 狀態從不只依賴顏色:旋轉圖示、勾號、警示圖示或方塊旁邊總有文字。延遲條是裝飾性的;它的數值以文字列在旁邊。 +- token 表格的每一列都帶著欄標題命名("Input: Tokens 866, Rate $2.00 per million tokens, Cost $0.0017")。 +- 指標聚焦不顯示焦點環;鍵盤聚焦顯示 Arc 的焦點環。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 移到 Details,回覆失敗時再移到 Retry,然後移到已展開明細中的複製按鈕 | +| Enter、Space | 展開或收合明細、重試,或把統計複製為 JSON | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 數字變化 | 以等寬數字在約 100ms 內平滑過渡到新值;數值下降時直接跳到新值 | 立即 | 立即 | +| 狀態變化 | 舊文字與圖示在 100ms 內淡出,新的在 160ms 內淡入並上升 2px | 短暫交叉淡化 | 立即 | +| 完成 | 勾號自行繪出一次 | 完整繪出 | 完整繪出 | +| 等待、串流輸出 | 旋轉圖示每 800ms 轉一圈 | 靜止 | 靜止 | +| 明細或錯誤列 | 高度以 0.26s 臨界阻尼彈簧變化,內容在 160ms 內淡入淡出;沒有回彈 | 立即 | 立即 | +| Details 箭頭 | 以同一彈簧旋轉 | 立即 | 立即 | +| 模擬執行 | 在畫面上時循環播放;明細展開時停留 | 停在一個已定格的執行上 | 停在一個已定格的執行上 | + +即時時鐘只在等待或串流輸出時執行,模擬只在統計列位於畫面上時以虛擬時間推進。 + +## 相關元件 + +- `ContextWindow`:一段對話用了模型多少上下文。 +- `AgentStatus`:長時間執行之代理的即時狀態。 +- `ToolCall`:代理發出的工具呼叫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/en-US.md new file mode 100644 index 00000000..28158ff2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/en-US.md @@ -0,0 +1,167 @@ +--- +title: Ghost mascot +summary: A floating ghost mascot whose hem ripples while it hovers: it blinks when idle, listens, thinks under a swirl, writes with a tiny pen, sparkles when done and wobbles on an error. +--- + + + +## When to use + +`GhostMascot` gives an assistant a light, friendly face that shows what it is doing. A soft sheet floats with a +rippling hem, a quiet form shade and two simple eyes, and every state changes the face and its marks, so it still reads +at 24px. + +- **In a reply header or a composer**, next to the assistant's name, while it thinks and writes. +- **Over an image canvas** while a picture generates. +- **In an empty state**, where a 96 to 160px ghost gives the AI feature an identity. +- **Instead of a spinner**, when generation should feel alive. + +## When not to use + +- **Steps, tools and elapsed time** of a run: use `AgentStatus`, not just a mood. +- **Dense interfaces** where a character would distract: a plain shimmering "Thinking" label is enough. +- **Known progress** with a percentage: use a progress bar. + +## Usage + +Map the assistant's real phases to `Status`: microphone open is `Listening`, waiting for the first token is +`Thinking`, streaming is `Writing`, finished is `Done`, failed is `Error`, otherwise `Idle`. Keep thinking short in real +use and switch to writing on the first token. + +```xml + +``` + +```csharp +ghost.Status = GhostMascotStatus.Thinking; // request sent +ghost.Status = GhostMascotStatus.Writing; // first token +ghost.Status = GhostMascotStatus.Done; // last token +``` + +Leave `Status` unset and the ghost plays through every state on a loop. That is meant for showcases; +`EffectiveStatus` reports the state it shows and `StatusChanged` is raised at each step, so a caption or a control can +follow it. + +### Anatomy + +1. **Sheet**: a domed head, slightly flared sides and a hem of two traveling waves with rounded corners. +2. **Form shade**: a darker, translucent tone of the sheet on its lower right, clipped to the sheet. +3. **Eyes**: two ovals that glance, blink, widen, look up, follow the pen, turn into happy arcs or droop. +4. **Marks**: the thinking swirl, the writing pen and its line, the done sparkle (and a small smile). +5. **Ground**: a soft floating shadow, from 56px up. + +### States + + + +| Status | Pose and face | Marks | +|---|---|---| +| `Idle` | Floats, blinks every few seconds and glances around | None | +| `Listening` | Comes forward, wide eyes looking at you, a small head tilt | None | +| `Thinking` | Drifts up and turns translucent, eyes drift up | A swirl that draws in from its outer end and erases behind itself | +| `Writing` | Leans toward the line, eyes on the pen tip | A tiny pen writes a line word by word, then glides back as it erases | +| `Done` | Lifts, happy eyes and a small smile | A sparkle grows with a quarter turn, then twinkles | +| `Error` | Wobbles once, sinks, droopy eyes | None | + +### In a reply header + + + +Use 24 to 32px inline in a reply header or a composer. Below 56px the compact drawing takes over: the ghost is framed +tighter, the eyes are bigger and the swirl, pen and line are heavier, and there is no shadow, so every state still reads. + +### Sizes + + + +`Size` sets the width and height (default 96): 24 to 32 inline, 40 to 64 in a panel or on an image canvas, 96 to 160 in +an empty state. The ghost is a fixed square, so it never reflows its parent, and it is vector art that stays crisp at +any scale. + +### Color + + + +`Tone` picks the sheet color from the theme: `Accent` (default) with accent-foreground eyes and follows the theme +accent, including Arc's near-neutral grey; `Sheet` is the classic pale ghost, a raised surface with a hairline and +foreground eyes whose marks keep the accent; `Ink` uses the foreground with background-colored eyes. `SheetBrush` sets +any color and the eyes become a dark mix of it unless `EyeBrush` is set. `MarkBrush`, `ShadeBrush` and `GroundBrush` +restyle the marks, the form shade and the ground shadow. + +### Empty state + + + +A large idle ghost in the `Sheet` tone gives an empty screen a calm, friendly face. + +### On an image canvas + + + +A 64px ghost in the corner of a canvas shows that a picture is generating. Turn `ShowGround` off where the ghost does +not stand on a surface. `IsPaused` freezes it on the current frame; resuming continues from the same frame. + +### Content guidelines + +- Set `AssistantName` to the assistant's name so the label reads well ("Atlas, thinking"). +- Show a text status next to the ghost wherever the state matters; the ghost adds character, the words carry meaning. + +### Best practices + +- Turn `Announce` on for the one ghost that carries status, not for decorative copies. +- Pair the ghost with real controls such as Stop when people act on what it shows; the ghost takes no input. +- Leave `Status` unset only for showcases. + +### Common questions + +**Does a ghost off screen cost anything?** No. Its frame loop pauses while it is off screen or hidden, and its clock only +runs while it is visible, so it resumes on the same frame instead of jumping. + +**Can I restyle it?** Yes: `SheetBrush`, `EyeBrush`, `MarkBrush`, `ShadeBrush` and `GroundBrush` take any solid color. + +## API reference + + + + + + + +## Accessibility + +- The ghost presents as an image with a label built from `AssistantName` and the status, for example "Atlas, + thinking", which updates with every state. `AccessibleName` replaces the whole label; every word is in + `GhostMascotLabels`. +- With `Announce` on, the ghost is also a polite live region, so status changes are read once. Use it on one ghost per + view so screen readers do not hear duplicates. +- Status is never carried by color or motion alone: each state changes the face and its marks (wide eyes, eyes up with + a swirl, a pen and a line, happy eyes with a sparkle, droopy eyes) and the label. + +### Keyboard + +| Keys | Action | +|---|---| +| None | The ghost is a status image and takes no focus. Pair it with a real control when people act on what it shows. | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Float and hem | A slow float; two traveling waves ripple the hem, which trails the float: the sheet stretches on the way up and the hem lifts and flares on the way down | Still | Still | +| Idle | Blinks every 2.4 to 5.2s (160ms) and glances around with quick saccades | Still, eyes centered | Still | +| Thinking | The swirl draws in from its outer end and erases behind itself, empty when its cycle wraps; the eyes drift up slowly | A half drawn swirl | A half drawn swirl | +| Writing | The pen writes the line word by word, lifting between words, then glides back while the line erases; the eyes track the tip | Half a written line | Half a written line | +| Done | The sparkle grows in 320ms with an ease out and a quarter turn, then twinkles slowly | The sparkle | The sparkle | +| Error | One wobble that decays to rest in about 1.2s, softened at the start so it never kicks | The droop | The droop | +| Status change | Every pose value blends from the frame on screen over 400ms with an ease in and out (keeping its velocity); the eyes ride a critically damped spring | Switches in place | Switches in place | + +Every loop is a function of a phase that only grows: the float, the two hem waves, the swirl and the writing cycle. Rates +blend by integrating into those phases, so speed changes never jump and nothing restarts. One frame loop per ghost moves +the sheet's path points in place with cached brushes and pens; it pauses off screen and when hidden. Under reduced motion +the ghost repaints only when the status changes. + +## Related + +- `AgentStatus` for the steps, tools and elapsed time of an agent run. +- `ToolCall` for the tool calls an agent makes. +- `AgentPlan` for the plan an agent writes before it acts. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/zh-CN.md new file mode 100644 index 00000000..357b7e5e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/zh-CN.md @@ -0,0 +1,146 @@ +--- +title: 幽灵吉祥物 +summary: 一只漂浮的幽灵吉祥物,悬停时下摆轻轻起伏:空闲时眨眼,会倾听,在旋涡下思考,用小笔书写,完成时闪光,出错时摇晃一下。 +source-hash: 9402af949836 +--- + + + +## 何时使用 + +`GhostMascot` 给助手一张轻松友好的脸,表明它正在做什么。一张柔软的布单带着起伏的下摆漂浮着,配有低调的形体阴影和两只简单的眼睛;每种状态都会改变脸和它的标记,所以在 24px 下依然清晰可辨。 + +- **在回复标题或输入框中**,放在助手名字旁边,表示它正在思考和书写。 +- **在图像画布之上**,表示图片正在生成。 +- **在空状态中**,用 96 到 160px 的幽灵给 AI 功能一个形象。 +- **代替加载圈**,让生成过程显得生动。 + +## 何时不用 + +- **一次运行的步骤、工具与耗时**:使用 `AgentStatus`,而不只是一种情绪。 +- **信息密集的界面**,角色会分散注意力时:一行闪烁的“Thinking”文字就够了。 +- **已知进度**,有百分比时:使用进度条。 + +## 用法 + +把助手的真实阶段对应到 `Status`:麦克风打开为 `Listening`,等待第一个词元为 `Thinking`,流式输出为 `Writing`,完成为 `Done`,失败为 `Error`,其余为 `Idle`。实际使用中思考要短,收到第一个词元就切换到书写。 + +```xml + +``` + +```csharp +ghost.Status = GhostMascotStatus.Thinking; // request sent +ghost.Status = GhostMascotStatus.Writing; // first token +ghost.Status = GhostMascotStatus.Done; // last token +``` + +不设置 `Status` 时,幽灵会循环演示每一种状态。这用于展示;`EffectiveStatus` 报告它正在显示的状态,每一步都会引发 `StatusChanged`,说明文字或控件可以随之更新。 + +### 结构 + +1. **布单**:圆顶的头部、略微外展的两侧,以及由两道行进波组成、带圆角的下摆。 +2. **形体阴影**:布单右下方一块更深、半透明的同色调,裁剪在布单之内。 +3. **眼睛**:两个椭圆,会张望、眨眼、睁大、向上看、跟随笔尖,变成开心的弧线或耷拉下来。 +4. **标记**:思考的旋涡、书写的笔和它写出的线、完成时的闪光(以及一个小小的微笑)。 +5. **地面**:柔和的漂浮阴影,56px 及以上才显示。 + +### 状态 + + + +| 状态 | 姿态与表情 | 标记 | +|---|---|---| +| `Idle` | 漂浮,每隔几秒眨一次眼,四处张望 | 无 | +| `Listening` | 向前靠近,睁大眼睛看着你,头微微歪向一侧 | 无 | +| `Thinking` | 向上飘起并变得半透明,眼睛慢慢向上看 | 一个旋涡,从外端画入,并在身后擦去 | +| `Writing` | 倾向那条线,眼睛盯着笔尖 | 小笔逐词写出一行,然后在擦除时滑回 | +| `Done` | 升起,眼睛变成开心的弧线,带一个小微笑 | 闪光旋转四分之一圈长出,然后轻轻闪烁 | +| `Error` | 摇晃一次,下沉,眼睛耷拉 | 无 | + +### 在回复标题中 + + + +在回复标题或输入框中内联使用 24 到 32px。小于 56px 时改用紧凑画法:幽灵取景更紧,眼睛更大,旋涡、笔和线更粗,并且没有阴影,因此每种状态依然清晰可辨。 + +### 尺寸 + + + +`Size` 设置宽度和高度(默认 96):内联用 24 到 32,面板或图像画布上用 40 到 64,空状态用 96 到 160。幽灵是固定的正方形,从不让父级重新排版;它是矢量图形,在任何缩放下都保持清晰。 + +### 颜色 + + + +`Tone` 从主题中选取布单颜色:`Accent`(默认)配强调前景色的眼睛,并跟随主题强调色,包括 Arc 近乎中性的灰色;`Sheet` 是经典的浅色幽灵,凸起的表面加一条细线和前景色的眼睛,标记保持强调色;`Ink` 使用前景色,眼睛为背景色。`SheetBrush` 可设置任意颜色,除非设置了 `EyeBrush`,眼睛会变成它的深色混合。`MarkBrush`、`ShadeBrush` 和 `GroundBrush` 用于重设标记、形体阴影和地面阴影的颜色。 + +### 空状态 + + + +一只 `Sheet` 色调的大号空闲幽灵,给空屏幕一张平静友好的脸。 + +### 在图像画布上 + + + +画布角落里一只 64px 的幽灵表示图片正在生成。幽灵不是立在某个表面上时,关闭 `ShowGround`。`IsPaused` 把它冻结在当前帧;恢复后从同一帧继续。 + +### 内容准则 + +- 把 `AssistantName` 设为助手的名字,让标签读起来自然(“Atlas, thinking”)。 +- 在状态重要的地方,于幽灵旁边显示文字状态;幽灵增添个性,文字传达含义。 + +### 最佳实践 + +- 只为承载状态的那一只幽灵打开 `Announce`,装饰用的副本不要打开。 +- 当用户要根据它显示的内容采取行动时,把幽灵和真正的控件(例如 Stop)放在一起;幽灵本身不接受输入。 +- 只在展示时不设置 `Status`。 + +### 常见问题 + +**屏幕外的幽灵有开销吗?** 没有。它的帧循环在屏幕外或隐藏时暂停,时钟只在可见时运行,所以恢复时从同一帧继续,而不会跳变。 + +**可以重设样式吗?** 可以:`SheetBrush`、`EyeBrush`、`MarkBrush`、`ShadeBrush` 和 `GroundBrush` 接受任意纯色。 + +## API 参考 + + + + + + + +## 无障碍 + +- 幽灵呈现为一张图像,其标签由 `AssistantName` 和状态组成,例如“Atlas, thinking”,并随每种状态更新。`AccessibleName` 替换整个标签;所有文字都在 `GhostMascotLabels` 中。 +- 打开 `Announce` 时,幽灵同时也是一个礼貌的实时区域,状态变化只朗读一次。每个视图只在一只幽灵上使用它,以免屏幕阅读器重复朗读。 +- 状态从不只靠颜色或动效传达:每种状态都会改变表情和标记(睁大的眼睛、向上看的眼睛配旋涡、笔和线、开心的眼睛配闪光、耷拉的眼睛)以及标签。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| 无 | 幽灵是一张状态图像,不获取焦点。当用户要根据它显示的内容采取行动时,把它和真正的控件放在一起。 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 漂浮与下摆 | 缓慢漂浮;两道行进波让下摆起伏,下摆跟随漂浮滞后:上升时布单被拉长,下降时下摆抬起并张开 | 静止 | 静止 | +| 空闲 | 每 2.4 到 5.2 秒眨一次眼(160ms),以快速的扫视四处张望 | 静止,眼睛居中 | 静止 | +| 思考 | 旋涡从外端画入并在身后擦去,循环衔接处为空;眼睛慢慢向上看 | 画了一半的旋涡 | 画了一半的旋涡 | +| 书写 | 笔逐词写出这一行,词与词之间抬笔,然后在线条擦除时滑回;眼睛跟随笔尖 | 写了一半的线 | 写了一半的线 | +| 完成 | 闪光在 320ms 内以缓出旋转四分之一圈长出,然后缓慢闪烁 | 闪光 | 闪光 | +| 出错 | 摇晃一次,约 1.2 秒内衰减到静止,开头经过柔化,从不突然一抖 | 耷拉的姿态 | 耷拉的姿态 | +| 状态切换 | 每个姿态值从屏幕上的当前帧开始,以缓入缓出在 400ms 内过渡(保留其速度);眼睛跟随一个临界阻尼弹簧 | 原地切换 | 原地切换 | + +每个循环都是一个只增不减的相位的函数:漂浮、两道下摆波、旋涡和书写循环。速率通过积分进入这些相位来过渡,所以速度变化从不跳变,也没有任何东西重新开始。每只幽灵一个帧循环,用缓存的画刷和画笔就地移动布单路径的各点;它在屏幕外或隐藏时暂停。在减弱动效下,幽灵只在状态变化时重绘。 + +## 相关组件 + +- `AgentStatus`:智能体运行的步骤、工具与耗时。 +- `ToolCall`:智能体发起的工具调用。 +- `AgentPlan`:智能体行动前写下的计划。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/zh-TW.md new file mode 100644 index 00000000..735ebc02 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostMascot/zh-TW.md @@ -0,0 +1,146 @@ +--- +title: 幽靈吉祥物 +summary: 一隻漂浮的幽靈吉祥物,懸停時下襬輕輕起伏:閒置時眨眼,會傾聽,在漩渦下思考,用小筆書寫,完成時閃光,出錯時搖晃一下。 +source-hash: 9402af949836 +--- + + + +## 何時使用 + +`GhostMascot` 給助理一張輕鬆友善的臉,表明它正在做什麼。一張柔軟的布單帶著起伏的下襬漂浮著,搭配低調的形體陰影和兩隻簡單的眼睛;每種狀態都會改變臉和它的標記,所以在 24px 下依然清楚可辨。 + +- **在回覆標頭或輸入框中**,放在助理名字旁邊,表示它正在思考和書寫。 +- **在影像畫布之上**,表示圖片正在產生。 +- **在空狀態中**,用 96 到 160px 的幽靈給 AI 功能一個形象。 +- **取代載入圈**,讓產生過程顯得生動。 + +## 何時不用 + +- **一次執行的步驟、工具與耗時**:使用 `AgentStatus`,而不只是一種情緒。 +- **資訊密集的介面**,角色會分散注意力時:一行閃爍的「Thinking」文字就夠了。 +- **已知進度**,有百分比時:使用進度條。 + +## 用法 + +把助理的實際階段對應到 `Status`:麥克風開啟為 `Listening`,等待第一個詞元為 `Thinking`,串流輸出為 `Writing`,完成為 `Done`,失敗為 `Error`,其餘為 `Idle`。實際使用時思考要短,收到第一個詞元就切換到書寫。 + +```xml + +``` + +```csharp +ghost.Status = GhostMascotStatus.Thinking; // request sent +ghost.Status = GhostMascotStatus.Writing; // first token +ghost.Status = GhostMascotStatus.Done; // last token +``` + +不設定 `Status` 時,幽靈會循環示範每一種狀態。這用於展示;`EffectiveStatus` 回報它正在顯示的狀態,每一步都會引發 `StatusChanged`,說明文字或控制項可以隨之更新。 + +### 結構 + +1. **布單**:圓頂的頭部、略微外展的兩側,以及由兩道行進波組成、帶圓角的下襬。 +2. **形體陰影**:布單右下方一塊更深、半透明的同色調,裁切在布單之內。 +3. **眼睛**:兩個橢圓,會張望、眨眼、睜大、向上看、跟隨筆尖,變成開心的弧線或垂下來。 +4. **標記**:思考的漩渦、書寫的筆和它寫出的線、完成時的閃光(以及一個小小的微笑)。 +5. **地面**:柔和的漂浮陰影,56px 以上才顯示。 + +### 狀態 + + + +| 狀態 | 姿態與表情 | 標記 | +|---|---|---| +| `Idle` | 漂浮,每隔幾秒眨一次眼,四處張望 | 無 | +| `Listening` | 向前靠近,睜大眼睛看著你,頭微微歪向一側 | 無 | +| `Thinking` | 向上飄起並變得半透明,眼睛慢慢向上看 | 一個漩渦,從外端畫入,並在身後擦去 | +| `Writing` | 傾向那條線,眼睛盯著筆尖 | 小筆逐詞寫出一行,然後在擦除時滑回 | +| `Done` | 升起,眼睛變成開心的弧線,帶一個小微笑 | 閃光旋轉四分之一圈長出,然後輕輕閃爍 | +| `Error` | 搖晃一次,下沉,眼睛垂下 | 無 | + +### 在回覆標頭中 + + + +在回覆標頭或輸入框中內嵌使用 24 到 32px。小於 56px 時改用精簡畫法:幽靈取景更緊,眼睛更大,漩渦、筆和線更粗,而且沒有陰影,因此每種狀態依然清楚可辨。 + +### 尺寸 + + + +`Size` 設定寬度和高度(預設 96):內嵌用 24 到 32,面板或影像畫布上用 40 到 64,空狀態用 96 到 160。幽靈是固定的正方形,從不讓父層重新排版;它是向量圖形,在任何縮放下都保持清晰。 + +### 顏色 + + + +`Tone` 從主題中選取布單顏色:`Accent`(預設)搭配強調前景色的眼睛,並跟隨主題強調色,包括 Arc 近乎中性的灰色;`Sheet` 是經典的淺色幽靈,凸起的表面加一條細線和前景色的眼睛,標記保持強調色;`Ink` 使用前景色,眼睛為背景色。`SheetBrush` 可設定任意顏色,除非設定了 `EyeBrush`,眼睛會變成它的深色混合。`MarkBrush`、`ShadeBrush` 和 `GroundBrush` 用於重設標記、形體陰影和地面陰影的顏色。 + +### 空狀態 + + + +一隻 `Sheet` 色調的大型閒置幽靈,給空白畫面一張平靜友善的臉。 + +### 在影像畫布上 + + + +畫布角落裡一隻 64px 的幽靈表示圖片正在產生。幽靈不是立在某個表面上時,關閉 `ShowGround`。`IsPaused` 把它凍結在目前影格;恢復後從同一影格繼續。 + +### 內容準則 + +- 把 `AssistantName` 設為助理的名字,讓標籤讀起來自然(「Atlas, thinking」)。 +- 在狀態重要的地方,於幽靈旁邊顯示文字狀態;幽靈增添個性,文字傳達含義。 + +### 最佳實務 + +- 只為承載狀態的那一隻幽靈開啟 `Announce`,裝飾用的副本不要開啟。 +- 當使用者要根據它顯示的內容採取行動時,把幽靈和真正的控制項(例如 Stop)放在一起;幽靈本身不接受輸入。 +- 只在展示時不設定 `Status`。 + +### 常見問題 + +**畫面外的幽靈有負擔嗎?** 沒有。它的影格迴圈在畫面外或隱藏時暫停,時鐘只在可見時執行,所以恢復時從同一影格繼續,而不會跳動。 + +**可以重設樣式嗎?** 可以:`SheetBrush`、`EyeBrush`、`MarkBrush`、`ShadeBrush` 和 `GroundBrush` 接受任意純色。 + +## API 參考 + + + + + + + +## 無障礙 + +- 幽靈呈現為一張影像,其標籤由 `AssistantName` 和狀態組成,例如「Atlas, thinking」,並隨每種狀態更新。`AccessibleName` 取代整個標籤;所有文字都在 `GhostMascotLabels` 中。 +- 開啟 `Announce` 時,幽靈同時也是一個禮貌的即時區域,狀態變化只朗讀一次。每個檢視只在一隻幽靈上使用它,以免螢幕閱讀器重複朗讀。 +- 狀態從不只靠顏色或動態效果傳達:每種狀態都會改變表情和標記(睜大的眼睛、向上看的眼睛搭配漩渦、筆和線、開心的眼睛搭配閃光、垂下的眼睛)以及標籤。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| 無 | 幽靈是一張狀態影像,不取得焦點。當使用者要根據它顯示的內容採取行動時,把它和真正的控制項放在一起。 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 漂浮與下襬 | 緩慢漂浮;兩道行進波讓下襬起伏,下襬跟隨漂浮延遲:上升時布單被拉長,下降時下襬抬起並張開 | 靜止 | 靜止 | +| 閒置 | 每 2.4 到 5.2 秒眨一次眼(160ms),以快速的掃視四處張望 | 靜止,眼睛置中 | 靜止 | +| 思考 | 漩渦從外端畫入並在身後擦去,循環銜接處為空;眼睛慢慢向上看 | 畫了一半的漩渦 | 畫了一半的漩渦 | +| 書寫 | 筆逐詞寫出這一行,詞與詞之間提筆,然後在線條擦除時滑回;眼睛跟隨筆尖 | 寫了一半的線 | 寫了一半的線 | +| 完成 | 閃光在 320ms 內以緩出旋轉四分之一圈長出,然後緩慢閃爍 | 閃光 | 閃光 | +| 出錯 | 搖晃一次,約 1.2 秒內衰減到靜止,開頭經過柔化,從不突然一抖 | 垂下的姿態 | 垂下的姿態 | +| 狀態切換 | 每個姿態值從畫面上的目前影格開始,以緩入緩出在 400ms 內過渡(保留其速度);眼睛跟隨一個臨界阻尼彈簧 | 原地切換 | 原地切換 | + +每個循環都是一個只增不減的相位的函數:漂浮、兩道下襬波、漩渦和書寫循環。速率透過積分進入這些相位來過渡,所以速度變化從不跳動,也沒有任何東西重新開始。每隻幽靈一個影格迴圈,用快取的筆刷和畫筆就地移動布單路徑的各點;它在畫面外或隱藏時暫停。在減弱動態效果下,幽靈只在狀態變化時重繪。 + +## 相關元件 + +- `AgentStatus`:代理程式執行的步驟、工具與耗時。 +- `ToolCall`:代理程式發出的工具呼叫。 +- `AgentPlan`:代理程式行動前寫下的計畫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/en-US.md new file mode 100644 index 00000000..ba524731 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/en-US.md @@ -0,0 +1,202 @@ +--- +title: Ghost text +summary: An inline AI autocomplete where muted ghost text continues the sentence at the caret: Tab accepts it in place, Cmd+Right takes one word, Escape dismisses it, and a calm chip shows the model behind it. +--- + + + +## When to use + +`GhostText` is a text field with AI help that stays out of the way. When the person pauses, muted ghost text continues +their sentence exactly at the caret. Tab turns it solid in place, Cmd+Right takes one word at a time, Escape fades it +away, and the text never changes without an explicit key press. A small chip at the end shows the model behind it. + +- **Long-form fields people write in your product**: pull request descriptions, emails, support replies, issue bodies, + release notes, CRM notes. +- **Short single-line fields** where a likely completion saves typing, such as an email subject or a commit message. +- **Anywhere AI help should wait for a pause** and only act on a key press. + +## When not to use + +- **A chat prompt box** that sends a message to an assistant: use a composer. +- **Code edits** with accept and reject per hunk: use a code suggestion view. +- **Picking from known options**: use a combobox or a command menu. +- **Sensitive fields** (passwords, card numbers, health data), unless your model provider and data policy allow it. + +## Usage + +Give the field a `Suggest` function. It receives the whole text (the caret is always at the end when it is called) and a +cancellation token, and returns a `GhostTextResponse`: a string, a `Task`, or a stream of chunks wrapped with +`GhostTextResponse.FromStream`. Return `null` to skip a text without showing Thinking. + +```csharp +field.Suggest = (text, cancellationToken) => + GhostTextResponse.FromStream(StreamCompletion(text, cancellationToken)); + +async IAsyncEnumerable StreamCompletion(string text, + [EnumeratorCancellation] CancellationToken cancellationToken) +{ + using var response = await http.PostAsJsonAsync("/api/autocomplete", new { text }, cancellationToken); + response.EnsureSuccessStatusCode(); + using var reader = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken)); + var buffer = new char[256]; + int read; + while ((read = await reader.ReadAsync(buffer, cancellationToken)) > 0) + { + yield return new string(buffer, 0, read); + } +} +``` + +`Text` is two-way. The field asks after `Delay` (450ms) of quiet, once the trimmed text has `MinLength` (3) characters. +Every keystroke restarts the pause and cancels the request in flight, so a fast typist triggers nothing until they stop. +With no `Suggest` and no text, the field is a plain text field, or plays the simulation you give it. + +### Streaming suggestions + + + +Each streamed chunk appends to the ghost and fades in on its own. `Label` adds a quiet row above the text and `Footer` +content at the start of the footer row. `Rows` (3) is the minimum height in lines and `MaxRows` (10) the height at which +the field stops growing and scrolls. `Model` names the model and gives its 12px mark; pass your own glyph and color with +`AgentModel`. + +For a demo or a prototype without a model, give the field a `GhostTextSimulation` with a `Script` of lines: it types +each line, lets its `Reply` stream in 3 to 6 word chunks after a short think and accepts it, alternating Tab with a word +at a time, then starts over. Clicking the field hands it to the person, who gets the scripted replies and, for anything +else, the simulation's own `Suggest` (the preview passes a small rule based stand-in that completes words and fails on +"timeout"). Label, placeholder, footer and model stay on the field. Setting `Suggest` or `Text` ends the simulation. + +```csharp +field.Label = "Commit message"; +field.Simulation = new GhostTextSimulation +{ + Script = [new GhostTextSimulationLine("Bump the parser", " to fix the date bug.")], +}; +``` + +### Single line + + + +`IsMultiline="False"` makes a single-line field that scrolls sideways with the caret and keeps the chip inline at its +end. Line breaks typed or pasted into it become spaces. + +### Plain + + + +`Variant="Plain"` drops the border, background and focus ring so the text sits flush inside a card you already have. +Give that card its own focus style. + +### Events and methods + + + +`Accepted` reports the accepted text and whether it was the whole suggestion or one word; `Dismissed` reports what was +dismissed with Escape. Log both to measure acceptance per model and prompt. `Status`, `Suggestion` and `IsStreaming` +describe the suggestion at any moment, and `StatusChanged` reports each change. + +`AcceptAll()`, `AcceptWord()`, `Dismiss()`, `Request()` (ask now, without waiting for a pause) and `FocusField()` drive +the field from your own buttons or tests. + +### States + +| Status | Chip reads | The text | +|---|---|---| +| `Idle` | The model's mark and name | Your text only | +| `Loading` | A shimmering "Thinking" | Your text only | +| `Ready` | A Tab key cap and "to accept" ("Tap to accept" on touch screens) | Your text, then the ghost | +| `Error` | An alert, "No suggestion" and "Retry" | Your text only | + +After Tab the chip reads "Accepted" with a check for a moment, then the model's name again. + +### Writing suggestions + +- Prompt for a continuation only: no preamble and no repetition of the input. +- Start with a space when the text ends mid-sentence without one; the field strips a leading space when the text already + ends with whitespace. +- Keep it to a sentence or a short line, and use a fast model: people expect ghost text within about half a second. +- Return `null` for contexts you never want to complete (inside a code block, after a URL), so Thinking never flashes. + +### Your own words + +`Labels` holds every word the field shows or says. Replace the object to localize or reword: + +```csharp +field.Labels = new GhostTextLabels +{ + Thinking = "Réflexion", + ToAccept = "pour accepter", + SuggestionAnnouncement = suggestion => $"Suggestion : {suggestion}. Tab pour accepter.", +}; +``` + +### Common questions + +**Does typing over the ghost ask again?** No. Characters that match the start of the ghost consume it in place; +anything else clears it and a new suggestion is asked for after the next pause. + +**Why does the ghost disappear when I click?** Suggestions only continue from the end of the text. Moving the caret away +clears the suggestion; it comes back after the next pause at the end. + +## API reference + + + + + + + + + + + +## Accessibility + +- The field is a native text box, named by `AutomationProperties.Name` or a string `Label`, and described by a sentence + that explains Tab, Command or Control plus Right Arrow, and Escape (`IsHintVisible` turns it off with the chip). +- The ghost is part of how the text is drawn, never part of the text, so it is not read as content. A polite live region + reads the finished suggestion ("Suggestion: date the plan changed. Press Tab to accept."), then Suggestion accepted, + Suggestion dismissed or Couldn't get a suggestion. `Notice` adds a one-off message to the same region. +- The chip is a button with a clear name in each state (Accept suggestion, Retry suggestion). Tab reaches it only in the + error state, where Retry is its action; in the ready state Tab itself accepts. Pressing it never takes focus from the + field. +- State is never shown by color alone: Thinking, the Tab key cap with "to accept", Accepted with a check and No + suggestion with an alert icon all carry words. +- Keyboard focus draws Arc's focus ring around the whole field; pointer focus does not. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Accepts the whole suggestion and keeps the caret at the end. With no suggestion showing, Tab moves focus as usual | +| Cmd+Right, Ctrl+Right | Accepts the next word; the rest stays. Option+Right and Alt+Right work too. With no suggestion showing, the keys move the caret as usual | +| Escape | Dismisses the suggestion. With no suggestion showing, Escape is left alone, so a surrounding dialog can still close | +| Typing | Characters that match the start of the ghost consume it in place; anything else clears it | +| Arrow keys, click | Moving the caret away from the end clears the suggestion | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A chunk streams in | Fades in over 140ms, opacity only | At once | At once | +| Escape, leaving the field | The ghost fades out over 110ms | At once | At once | +| Typing a different character | The ghost goes at once | At once | At once | +| Tab, Cmd+Right | The accepted text eases from ghost ink to text ink over 200ms in place; the rest of the ghost never fades again | At once | At once | +| The text grows or shrinks | The height follows on a 180ms tween, clipped while it moves, never bouncing | At once | At once | +| Chip state | Old content out in 100ms, new in 160ms, while the width follows on a 200ms tween anchored to its right edge | Short crossfade | At once | +| Key caps | Press for 160ms when their key or method fires | At once | At once | +| Thinking | A calm shimmer | Still | Still | + +The simulation loops with full motion; with reduced motion or none it plays once and stops. It pauses while off screen. + +Below 520px of width the word and Escape hints step aside and only the chip remains, and a footer note truncates with an +ellipsis. + +## Related + +- `Textarea` and `Input` for plain text fields. +- `MentionInput` for @ mentions inside a message. +- `TextShimmer` for the calm light of the Thinking state. +- `ToolCall` and `AgentStatus` for the rest of an AI reply. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/zh-CN.md new file mode 100644 index 00000000..4b93e3c8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/zh-CN.md @@ -0,0 +1,174 @@ +--- +title: Ghost text +summary: 行内 AI 自动补全:浅色的幽灵文字在光标处接着把句子写下去,Tab 原地接受,Cmd+Right 接受一个词,Escape 将其关闭,一个安静的标签显示背后的模型。 +source-hash: 90d90ae2b1e5 +--- + + + +## 何时使用 + +`GhostText` 是一个带 AI 辅助、却不打扰人的文本框。当用户停下来时,浅色的幽灵文字会恰好在光标处接着他们的句子写下去。Tab 让它原地变为正文,Cmd+Right 一次接受一个词,Escape 让它淡出;没有明确的按键,文字永远不会被改动。末尾的小标签显示背后的模型。 + +- **产品中需要长篇书写的字段**:拉取请求描述、邮件、客服回复、问题正文、发布说明、CRM 备注。 +- **简短的单行字段**,可能的补全能省下打字,例如邮件主题或提交信息。 +- **任何希望 AI 辅助等到停顿再出现**、且只在按键后才生效的地方。 + +## 何时不用 + +- **向助手发送消息的聊天输入框**:使用编写器(composer)。 +- **按块接受或拒绝的代码修改**:使用代码建议视图。 +- **从已知选项中挑选**:使用组合框或命令菜单。 +- **敏感字段**(密码、卡号、健康数据),除非你的模型提供方与数据政策允许。 + +## 用法 + +给字段一个 `Suggest` 函数。它收到完整文本(调用时光标总在末尾)和一个取消令牌,返回一个 `GhostTextResponse`:字符串、`Task`,或用 `GhostTextResponse.FromStream` 包装的分块流。返回 `null` 表示跳过这段文本,且不显示 Thinking。 + +```csharp +field.Suggest = (text, cancellationToken) => + GhostTextResponse.FromStream(StreamCompletion(text, cancellationToken)); + +async IAsyncEnumerable StreamCompletion(string text, + [EnumeratorCancellation] CancellationToken cancellationToken) +{ + using var response = await http.PostAsJsonAsync("/api/autocomplete", new { text }, cancellationToken); + response.EnsureSuccessStatusCode(); + using var reader = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken)); + var buffer = new char[256]; + int read; + while ((read = await reader.ReadAsync(buffer, cancellationToken)) > 0) + { + yield return new string(buffer, 0, read); + } +} +``` + +`Text` 是双向的。去掉首尾空白后的文本达到 `MinLength`(3)个字符,并安静 `Delay`(450ms)之后,字段才会请求。每次按键都会重新计时并取消进行中的请求,所以打字很快的人在停下之前不会触发任何请求。没有 `Suggest` 也没有文本时,字段就是一个普通的文本框,或者播放你传入的模拟。 + +### 流式建议 + + + +每个流入的分块都追加到幽灵文字末尾,并各自淡入。`Label` 在文本上方加一行安静的标签,`Footer` 是底栏开头的内容。`Rows`(3)是以行计的最小高度,`MaxRows`(10)是字段停止增高、开始滚动的高度。`Model` 给出模型名称与 12px 的标记;用 `AgentModel` 传入你自己的图形和颜色。 + +用于没有模型的演示或原型时,给字段一个 `GhostTextSimulation`,在 `Script` 中写好各行:它会逐行输入,短暂思考后以 3 到 6 个词的分块流出该行的 `Reply` 并接受,接受方式在 Tab 与逐词之间交替,然后从头开始。点击字段即可把它交给用户,用户会得到剧本中的回复,其他内容则由模拟自己的 `Suggest` 回答(预览传入了一个基于规则的小替身,会补全单词,遇到 "timeout" 时失败)。标签、占位文本、页脚和模型仍设置在字段上。设置 `Suggest` 或 `Text` 后模拟即结束。 + +```csharp +field.Label = "Commit message"; +field.Simulation = new GhostTextSimulation +{ + Script = [new GhostTextSimulationLine("Bump the parser", " to fix the date bug.")], +}; +``` + +### 单行 + + + +`IsMultiline="False"` 生成单行字段:它随光标横向滚动,标签保持在行尾。输入或粘贴进来的换行会变成空格。 + +### 精简样式 + + + +`Variant="Plain"` 去掉边框、背景和焦点环,让文字平贴在你已有的卡片里。请给那张卡片自己的焦点样式。 + +### 事件与方法 + + + +`Accepted` 报告被接受的文字,以及接受的是整条建议还是一个词;`Dismissed` 报告用 Escape 关闭的内容。把两者都记录下来,就能按模型和提示词衡量接受率。`Status`、`Suggestion` 和 `IsStreaming` 随时描述建议的状况,`StatusChanged` 报告每一次变化。 + +`AcceptAll()`、`AcceptWord()`、`Dismiss()`、`Request()`(立即请求,不等停顿)和 `FocusField()` 可以从你自己的按钮或测试中驱动字段。 + +### 状态 + +| 状态 | 标签显示 | 文本 | +|---|---|---| +| `Idle` | 模型的标记与名称 | 只有你的文字 | +| `Loading` | 闪烁的 "Thinking" | 只有你的文字 | +| `Ready` | Tab 键帽与 "to accept"(触摸屏上为 "Tap to accept") | 你的文字,后接幽灵文字 | +| `Error` | 警示图标、"No suggestion" 和 "Retry" | 只有你的文字 | + +按下 Tab 后,标签会短暂显示带对勾的 "Accepted",然后回到模型名称。 + +### 编写建议 + +- 只请求续写:不要开场白,也不要重复输入。 +- 文本在句中结束且末尾没有空格时,以空格开头;文本末尾已有空白时,字段会去掉开头的空格。 +- 控制在一句话或一短行以内,并使用快速的模型:人们期望停顿后约半秒内看到幽灵文字。 +- 对永远不想补全的上下文(代码块内、URL 之后)返回 `null`,这样 Thinking 不会一闪而过。 + +### 你自己的文字 + +`Labels` 保存字段显示或朗读的每一个词。替换整个对象即可本地化或改写: + +```csharp +field.Labels = new GhostTextLabels +{ + Thinking = "Réflexion", + ToAccept = "pour accepter", + SuggestionAnnouncement = suggestion => $"Suggestion : {suggestion}. Tab pour accepter.", +}; +``` + +### 常见问题 + +**在幽灵文字上继续打字会重新请求吗?** 不会。与幽灵文字开头相同的字符会原地消耗它;其他任何输入都会清除它,并在下一次停顿后请求新的建议。 + +**为什么点击后幽灵文字消失了?** 建议只从文本末尾续写。把光标移离末尾会清除建议;在末尾再次停顿后它会回来。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 字段是原生文本框,由 `AutomationProperties.Name` 或字符串形式的 `Label` 命名,并由一句话描述 Tab、Command 或 Control 加右方向键以及 Escape 的作用(`IsHintVisible` 会连同标签一起关闭它)。 +- 幽灵文字只是文本的绘制方式,从不属于文本本身,因此不会被当作内容朗读。一个礼貌的实时区域会朗读完成的建议("Suggestion: date the plan changed. Press Tab to accept."),之后是 Suggestion accepted、Suggestion dismissed 或 Couldn't get a suggestion。`Notice` 向同一区域添加一次性的消息。 +- 标签是一个按钮,每种状态都有清晰的名称(Accept suggestion、Retry suggestion)。只有在错误状态下 Tab 才能到达它,此时 Retry 就是它的操作;就绪状态下 Tab 本身就是接受。按下它永远不会夺走字段的焦点。 +- 状态从不只靠颜色表达:Thinking、带 "to accept" 的 Tab 键帽、带对勾的 Accepted 和带警示图标的 No suggestion 都有文字。 +- 键盘焦点会在整个字段周围画出 Arc 的焦点环;指针焦点不会。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 接受整条建议,光标保持在末尾。没有建议时,Tab 照常移动焦点 | +| Cmd+Right、Ctrl+Right | 接受下一个词,其余保留。Option+Right 和 Alt+Right 同样可用。没有建议时,这些键照常移动光标 | +| Escape | 关闭建议。没有建议时不处理 Escape,外层对话框仍可关闭 | +| 打字 | 与幽灵文字开头相同的字符会原地消耗它;其他输入会清除它 | +| 方向键、点击 | 把光标移离末尾会清除建议 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 分块流入 | 在 140ms 内淡入,只改透明度 | 立即 | 立即 | +| Escape、离开字段 | 幽灵文字在 110ms 内淡出 | 立即 | 立即 | +| 输入不同的字符 | 幽灵文字立即消失 | 立即 | 立即 | +| Tab、Cmd+Right | 被接受的文字在 200ms 内原地从幽灵墨色过渡到正文墨色;其余幽灵文字不会再次淡入 | 立即 | 立即 | +| 文本变多或变少 | 高度以 180ms 补间跟随,移动时裁剪,从不回弹 | 立即 | 立即 | +| 标签状态 | 旧内容 100ms 淡出、新内容 160ms 淡入,宽度以锚定右边缘的 200ms 补间跟随 | 短暂交叉淡化 | 立即 | +| 键帽 | 对应按键或方法触发时按下 160ms | 立即 | 立即 | +| Thinking | 安静的闪光 | 静止 | 静止 | + +完整动效下模拟会循环播放;减弱或关闭动效时只播放一次就停止。它在屏幕外时会暂停。 + +宽度小于 520px 时,单词与 Escape 提示会让开,只保留标签,底栏备注以省略号截断。 + +## 相关组件 + +- `Textarea` 与 `Input`:普通文本框。 +- `MentionInput`:消息中的 @ 提及。 +- `TextShimmer`:Thinking 状态的安静闪光。 +- `ToolCall` 与 `AgentStatus`:AI 回复的其余部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/zh-TW.md new file mode 100644 index 00000000..b3dd61bd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/GhostText/zh-TW.md @@ -0,0 +1,174 @@ +--- +title: Ghost text +summary: 行內 AI 自動完成:淡色的幽靈文字在游標處接著把句子寫下去,Tab 原地接受,Cmd+Right 接受一個字詞,Escape 將其關閉,一個安靜的標籤顯示背後的模型。 +source-hash: 90d90ae2b1e5 +--- + + + +## 何時使用 + +`GhostText` 是一個帶有 AI 輔助、卻不打擾人的文字欄位。使用者停下來時,淡色的幽靈文字會剛好在游標處接著他們的句子寫下去。Tab 讓它原地變成正文,Cmd+Right 一次接受一個字詞,Escape 讓它淡出;沒有明確的按鍵,文字永遠不會被更動。末端的小標籤顯示背後的模型。 + +- **產品中需要長篇撰寫的欄位**:拉取請求說明、電子郵件、客服回覆、問題內文、版本說明、CRM 備註。 +- **簡短的單行欄位**,可能的完成內容能省下打字,例如郵件主旨或提交訊息。 +- **任何希望 AI 輔助等到停頓才出現**、且只在按鍵後才生效的地方。 + +## 何時不用 + +- **向助理傳送訊息的聊天輸入框**:請使用撰寫器(composer)。 +- **逐段接受或拒絕的程式碼修改**:請使用程式碼建議檢視。 +- **從已知選項中挑選**:請使用下拉式方塊或命令選單。 +- **敏感欄位**(密碼、卡號、健康資料),除非你的模型供應商與資料政策允許。 + +## 用法 + +給欄位一個 `Suggest` 函式。它會收到完整文字(呼叫時游標一定在末端)與一個取消權杖,並回傳一個 `GhostTextResponse`:字串、`Task`,或以 `GhostTextResponse.FromStream` 包裝的分塊串流。回傳 `null` 表示略過這段文字,而且不顯示 Thinking。 + +```csharp +field.Suggest = (text, cancellationToken) => + GhostTextResponse.FromStream(StreamCompletion(text, cancellationToken)); + +async IAsyncEnumerable StreamCompletion(string text, + [EnumeratorCancellation] CancellationToken cancellationToken) +{ + using var response = await http.PostAsJsonAsync("/api/autocomplete", new { text }, cancellationToken); + response.EnsureSuccessStatusCode(); + using var reader = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken)); + var buffer = new char[256]; + int read; + while ((read = await reader.ReadAsync(buffer, cancellationToken)) > 0) + { + yield return new string(buffer, 0, read); + } +} +``` + +`Text` 是雙向的。去除前後空白的文字達到 `MinLength`(3)個字元,並安靜 `Delay`(450ms)之後,欄位才會發出請求。每次按鍵都會重新計時並取消進行中的請求,所以打字很快的人在停下之前不會觸發任何請求。沒有 `Suggest` 也沒有文字時,欄位就是一般的文字欄位,或是播放你傳入的模擬。 + +### 串流建議 + + + +每個串流進來的分塊都會接在幽靈文字後面,並各自淡入。`Label` 在文字上方加一列安靜的標籤,`Footer` 是頁尾列開頭的內容。`Rows`(3)是以行計的最小高度,`MaxRows`(10)是欄位停止長高、開始捲動的高度。`Model` 提供模型名稱與 12px 的標誌;用 `AgentModel` 傳入你自己的圖形與顏色。 + +用於沒有模型的示範或原型時,給欄位一個 `GhostTextSimulation`,在 `Script` 中寫好各行:它會逐行輸入,短暫思考後以 3 到 6 個字詞的分塊串流出該行的 `Reply` 並接受,接受方式在 Tab 與逐字詞之間交替,然後從頭開始。點一下欄位即可把它交給使用者,使用者會得到劇本中的回覆,其他內容則由模擬自己的 `Suggest` 回答(預覽傳入了一個以規則運作的小替身,會補完字詞,遇到 "timeout" 時失敗)。標籤、預留位置文字、頁尾與模型仍設定在欄位上。設定 `Suggest` 或 `Text` 後模擬即結束。 + +```csharp +field.Label = "Commit message"; +field.Simulation = new GhostTextSimulation +{ + Script = [new GhostTextSimulationLine("Bump the parser", " to fix the date bug.")], +}; +``` + +### 單行 + + + +`IsMultiline="False"` 產生單行欄位:它會隨游標水平捲動,標籤保持在行尾。輸入或貼上的換行會變成空格。 + +### 精簡樣式 + + + +`Variant="Plain"` 拿掉邊框、背景與焦點環,讓文字平貼在你既有的卡片裡。請給那張卡片它自己的焦點樣式。 + +### 事件與方法 + + + +`Accepted` 回報被接受的文字,以及接受的是整段建議還是一個字詞;`Dismissed` 回報以 Escape 關閉的內容。兩者都記錄下來,就能依模型與提示詞衡量接受率。`Status`、`Suggestion` 與 `IsStreaming` 隨時描述建議的狀況,`StatusChanged` 回報每一次變化。 + +`AcceptAll()`、`AcceptWord()`、`Dismiss()`、`Request()`(立即請求,不等停頓)與 `FocusField()` 可以從你自己的按鈕或測試中驅動欄位。 + +### 狀態 + +| 狀態 | 標籤顯示 | 文字 | +|---|---|---| +| `Idle` | 模型的標誌與名稱 | 只有你的文字 | +| `Loading` | 閃爍的 "Thinking" | 只有你的文字 | +| `Ready` | Tab 鍵帽與 "to accept"(觸控螢幕上為 "Tap to accept") | 你的文字,後接幽靈文字 | +| `Error` | 警示圖示、"No suggestion" 與 "Retry" | 只有你的文字 | + +按下 Tab 後,標籤會短暫顯示帶勾號的 "Accepted",接著回到模型名稱。 + +### 撰寫建議 + +- 只請求接續的內容:不要開場白,也不要重複輸入。 +- 文字在句子中間結束且末端沒有空格時,以空格開頭;文字末端已有空白時,欄位會拿掉開頭的空格。 +- 控制在一句話或一短行以內,並使用快速的模型:大家期望停頓後約半秒內就看到幽靈文字。 +- 對永遠不想補完的情境(程式碼區塊內、網址之後)回傳 `null`,Thinking 就不會一閃而過。 + +### 你自己的文字 + +`Labels` 存放欄位顯示或朗讀的每一個字。替換整個物件即可在地化或改寫: + +```csharp +field.Labels = new GhostTextLabels +{ + Thinking = "Réflexion", + ToAccept = "pour accepter", + SuggestionAnnouncement = suggestion => $"Suggestion : {suggestion}. Tab pour accepter.", +}; +``` + +### 常見問題 + +**在幽靈文字上繼續打字會重新請求嗎?** 不會。與幽靈文字開頭相同的字元會原地消耗它;其他任何輸入都會清除它,並在下一次停頓後請求新的建議。 + +**為什麼點一下幽靈文字就不見了?** 建議只從文字末端接續。把游標移離末端會清除建議;在末端再次停頓後它就會回來。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 欄位是原生文字方塊,由 `AutomationProperties.Name` 或字串形式的 `Label` 命名,並以一句話描述 Tab、Command 或 Control 加向右鍵以及 Escape 的作用(`IsHintVisible` 會連同標籤一起關閉它)。 +- 幽靈文字只是文字的繪製方式,從不屬於文字本身,因此不會被當成內容朗讀。一個禮貌的即時區域會朗讀完成的建議("Suggestion: date the plan changed. Press Tab to accept."),之後是 Suggestion accepted、Suggestion dismissed 或 Couldn't get a suggestion。`Notice` 會在同一個區域加入一次性的訊息。 +- 標籤是一個按鈕,每種狀態都有清楚的名稱(Accept suggestion、Retry suggestion)。只有在錯誤狀態下 Tab 才會到達它,這時 Retry 就是它的動作;就緒狀態下 Tab 本身就是接受。按下它永遠不會搶走欄位的焦點。 +- 狀態從不只靠顏色表達:Thinking、帶 "to accept" 的 Tab 鍵帽、帶勾號的 Accepted 與帶警示圖示的 No suggestion 都有文字。 +- 鍵盤焦點會在整個欄位外圍畫出 Arc 的焦點環;指標焦點不會。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 接受整段建議,游標留在末端。沒有建議時,Tab 照常移動焦點 | +| Cmd+Right、Ctrl+Right | 接受下一個字詞,其餘保留。Option+Right 與 Alt+Right 也可以。沒有建議時,這些鍵照常移動游標 | +| Escape | 關閉建議。沒有建議時不處理 Escape,外層對話方塊仍可關閉 | +| 打字 | 與幽靈文字開頭相同的字元會原地消耗它;其他輸入會清除它 | +| 方向鍵、點選 | 把游標移離末端會清除建議 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 分塊串流進來 | 在 140ms 內淡入,只改透明度 | 立即 | 立即 | +| Escape、離開欄位 | 幽靈文字在 110ms 內淡出 | 立即 | 立即 | +| 輸入不同的字元 | 幽靈文字立即消失 | 立即 | 立即 | +| Tab、Cmd+Right | 被接受的文字在 200ms 內原地從幽靈墨色過渡到正文墨色;其餘幽靈文字不會再次淡入 | 立即 | 立即 | +| 文字變多或變少 | 高度以 180ms 補間跟隨,移動時裁切,從不回彈 | 立即 | 立即 | +| 標籤狀態 | 舊內容 100ms 淡出、新內容 160ms 淡入,寬度以錨定右緣的 200ms 補間跟隨 | 短暫交叉淡化 | 立即 | +| 鍵帽 | 對應按鍵或方法觸發時按下 160ms | 立即 | 立即 | +| Thinking | 安靜的閃光 | 靜止 | 靜止 | + +完整動效下模擬會循環播放;減少或關閉動效時只播放一次就停止。它在螢幕外時會暫停。 + +寬度小於 520px 時,字詞與 Escape 提示會讓開,只保留標籤,頁尾備註以刪節號截斷。 + +## 相關 + +- `Textarea` 與 `Input`:一般的文字欄位。 +- `MentionInput`:訊息中的 @ 提及。 +- `TextShimmer`:Thinking 狀態的安靜閃光。 +- `ToolCall` 與 `AgentStatus`:AI 回覆的其餘部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/en-US.md new file mode 100644 index 00000000..db04eca8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/en-US.md @@ -0,0 +1,234 @@ +--- +title: Image generation +summary: An image generation card where four images resolve as a pixel mosaic, from coarse colour blocks to full detail, each with its own progress ring; pick one to upscale, vary or download. +--- + + + +## When to use + +`ImageGeneration` is a text to image card. The person writes a prompt, picks a model and a shape, and four images +resolve side by side as pixel mosaics: coarse colour blocks first, then finer and finer blocks under a soft light band, +then the full image, each with its own progress ring. They pick one and continue with it: Upscale grows it to fill the +grid and sweeps a band of crisp pixels across it, Vary makes four new images from it and Download hands it to your app. + +- **A text to image feature inside a product**: marketing assets, listing photos, illustrations for slides or documents. +- **Any flow where people compare a few candidates** and continue with one (upscale, vary, download). +- **Prototyping an image model integration** before the backend exists, with a simulation. + +## When not to use + +- **A full canvas editor** with layers, masks and inpainting: build on a drawing surface instead. +- **One image with a loading state** and no generation controls: show the image with its own placeholder. +- **A gallery of past results**: the card holds only the current batch; pair it with your own asset list. + +## Usage + +With no generator the card plays its `Simulation`, and with neither Generate stays unavailable. An +`ImageGenerationSimulation` holds the `Prompt` the card starts with, the `Models` and `Suggestions` on offer and a +`Painter` (an `ImageGenerationPainter`) that paints a picture for a prompt; the card reports progress frame by frame over +two to three seconds, resolves each tile from the painted preview, and generates once when it first scrolls into view. +Nothing is downloaded or sent anywhere. Your own `Prompt`, `Models` and `Suggestions` win over the simulation's, and +setting `Simulation` to null stops its run. + +```csharp +card.Simulation = new ImageGenerationSimulation +{ + Prompt = "A quiet mountain lake at golden hour", + Models = [new ImageGenerationModel { Id = "your-model", Name = "Your image model", Mark = ArcIcons.Sparkles }], + Suggestions = ["A neon city street after rain at night"], + // Opaque BGRA bytes, width × height × 4; the same prompt and seed should paint the same picture. + Painter = (prompt, seed, width, height) => MyPainter.Paint(prompt, seed, width, height), +}; +``` + +The demos on this page paint a lake at golden hour, a city street after rain, a cup on a sunlit sill or a pastel room +from `ImageGenerationDemoData`. The model catalog the gallery's other AI demos use lists no image generation models, so +these image models keep invented names. + +Connect it to your image service with `Generator`. It is called once per tile, in parallel, for Generate, Vary and Retry: + +```csharp +card.Generator = async (request, context) => +{ + var job = await images.CreateAsync(request.Prompt, request.Model.Id, request.Aspect.Width, request.Aspect.Height, + seedFrom: request.VariationOf?.Id, context.CancellationToken); + await foreach (var step in job.ProgressAsync(context.CancellationToken)) + { + context.ReportProgress(step.Fraction, step.Preview); + } + return new ImageGenerationImage + { + Id = job.Id, + Source = await job.DownloadBitmapAsync(context.CancellationToken), + Description = request.Prompt, + Width = request.Aspect.Width, + Height = request.Aspect.Height, + }; +}; +``` + +Report progress from 0 to 1. A preview bitmap lets the mosaic resolve from your image; without one a quiet placeholder +grid shimmers until the image arrives and fades in, and without any reports the ring estimates. Throw to mark a tile +failed; Stop cancels `context.CancellationToken`. `Upscaler` works the same way for the picked image. + +### Anatomy + +1. **Prompt**: a text field; Cmd/Ctrl + Enter generates. +2. **Chips**: the models (the chosen one shows its name, the others collapse to their 16px mark with a tooltip), the + shapes (each with a small outline) and Generate, which reads Stop while a batch runs. +3. **Grid**: four tiles in the batch's shape, never taller than 420px. Each running tile has a glass pill with a ring + and its percentage; a picked tile has an accent ring and a check badge. +4. **Footer**: a status line, then Upscale, Vary and Download. + +### Picking, upscaling, varying and downloading + +Press a finished image (or focus it and press Enter or Space) to pick it; press it again to clear the pick. Upscale +grows the picked image from its cell to the whole grid and sweeps a band of crisp pixels from left to right: soft ahead +of it, sharp behind it. Escape or the button in its corner returns to the grid. Vary makes four new images with the +picked one as `VariationOf`. Download raises `DownloadRequested` with the image and a file name built from the prompt, +the image's number and its size; save it from there. + +A failed tile shows an alert and "Failed", a stopped one a square and "Stopped"; pressing either tries that image again. + +### Portrait shapes + + + +Each batch keeps its own shape: picking another aspect chip applies to the next generation. The grid springs to the new +shape and caps its height at 420px, so portrait batches stay compact. + +### Your own generator + + + +Here the card's simulation stands in for an image service (`Card.Simulation.Generate`), and image 3 fails on its first try. `AutoGenerate` is off by default +when you set a generator; set it to `True` to generate once when the card scrolls into view. + +### Listening to the card + + + +`SelectionChanged` reports the picked image (and the upscaled one once an upscale finishes), `SelectedImage` holds it, +and `DownloadRequested` carries the image and a file name. + +```csharp +card.DownloadRequested += async (_, e) => +{ + var file = await TopLevel.GetTopLevel(card)!.StorageProvider.SaveFilePickerAsync(new() { SuggestedFileName = e.FileName }); + if (file is not null && e.Image.Source is { } bitmap) + { + await using var stream = await file.OpenWriteAsync(); + bitmap.Save(stream); + } +}; +``` + +### Narrow containers + + + +The card fills its container up to 640px. Below 420px the aspect chips move to a second row; below 520px the footer +stacks, the status on top and three equal buttons below. + +### Your own words and models + +`Models` and `AspectRatios` replace the chips (`ImageGenerationModel` has an id, a name and an optional mark; +`ImageGenerationAspect` an id, a label and the pixel size a generation returns). `Suggestions` replaces the prompts the +empty grid offers. `Labels` holds every word the card shows or announces: + +```csharp +card.Labels = new ImageGenerationLabels +{ + Generate = "Créer", + Stop = "Arrêter", + Generating = (count, model) => $"Création de {count} images avec {model}", +}; +``` + +### Reduced motion + +With reduced motion the ring, the stepped mosaic (it follows the progress, not time) and the final states stay; the +light band, the upscale sweep, the hover zoom and the expand travel go. With motion off everything changes at once. + +### Content guidelines + +- Keep model names to what your service actually runs; the demos' names are invented. +- Return the real width and height so the footer and the file name show the true size. +- Describe each image in `Description` (usually the prompt): screen readers read it on the tile. + +### Best practices + +- Pass `context.CancellationToken` to every call so Stop really stops the work. +- Report a preview as early as your service can: the mosaic then shows real colours from the first step. +- A service that returns four images per call can be wrapped by caching one request and resolving each index from it. + +### Common questions + +**Can the person change the prompt while a batch runs?** Yes; the change applies to the next Generate. + +**Does the card keep earlier batches?** No. It holds the current batch; listen to `SelectionChanged` to keep results. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- Model and aspect chips are radio groups with one tab stop each; a collapsed model chip keeps its name as its accessible + name and in a tooltip. +- Each tile is a toggle button that reports whether it is picked; its name reads the image's number and description, or + that it is generating, failed or stopped. +- A polite live region reports starts, finishes, failures, upscales and downloads, and says when results are simulated. +- Status is never colour alone: a failure pairs a danger icon with "Failed", a stop a square with "Stopped", a pick an + accent ring with a check badge. +- While an image is expanded the other tiles take no pointer or focus. + +### Keyboard + +| Keys | Action | +|---|---| +| Cmd/Ctrl + Enter | Generates from the prompt field | +| Arrow keys | Move between model chips or aspect chips (and choose), and between images in the grid | +| Enter, Space | Pick or clear the focused image; retry a failed or stopped one | +| Escape | Close the expanded image and return to the grid | +| Tab | Prompt, model, aspect, Generate, the picked image, then Upscale, Vary and Download | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| An image resolves | 4 column blocks fade in from the centre in a seeded order, then 8, 16, 32 and 64 columns land on them, then the image crossfades in; one progress value drives the mosaic, ring and percentage | The same steps, driven by progress | The same steps | +| Running tile | A soft light band sweeps diagonally over the blocks on the resolving grid, fading as detail arrives, 1.6s a pass, paused off screen | None | None | +| Done tile | Picture zooms 2% on hover; the tile presses to 98.5% | None | None | +| Upscale | The tile grows from its cell to the grid on a bounce-free 240ms spring, the others fade; a band of crisp pixels sweeps left to right | At once, no sweep | At once | +| New batch shape | The grid springs to the new shape; a container resize cuts straight to the new size | At once | At once | +| Button labels | Stacked in one cell and crossfaded (100ms out, 160ms in), so no button changes width | Crossfade | At once | +| Footer status | Old line fades out over 100ms, the new one fades in over 160ms and rises 2px | Crossfade | At once | +| Model chip | The chosen chip springs its name open by its measured width | At once | At once | + +## Related + +- `ContextWindow` for how much of a model's window a session uses. +- `ToolCall` for the tool calls an agent makes. +- `AgentStatus` for the live status of a running agent. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/zh-CN.md new file mode 100644 index 00000000..adbe42a3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/zh-CN.md @@ -0,0 +1,207 @@ +--- +title: Image generation +summary: 一张图像生成卡片:四张图像以像素马赛克的方式逐步成形,从粗糙的色块到完整细节,每张都有自己的进度环;选中其中一张即可放大、生成变体或下载。 +source-hash: 11e47e45be40 +--- + + + +## 何时使用 + +`ImageGeneration` 是一张文生图卡片。用户写下提示词,选择模型和画幅,四张图像便并排以像素马赛克的方式成形:先是粗糙的色块,然后在一道柔和的光带下变成越来越细的色块,最后呈现完整图像,每张都有自己的进度环。用户选中其中一张继续操作:Upscale 把它放大到铺满整个网格,并让一道清晰像素带扫过画面;Vary 以它为基础生成四张新图;Download 把它交给你的应用。 + +- **产品内的文生图功能**:营销素材、商品图片、幻灯片或文档的插图。 +- **任何需要比较几个候选再继续其中一个的流程**(放大、变体、下载)。 +- **在后端就绪之前为图像模型集成做原型**,使用一个模拟。 + +## 何时不用 + +- **完整的画布编辑器**,带图层、蒙版和局部重绘:请基于绘图表面来构建。 +- **只显示一张带加载状态的图像**,没有生成控件:用图像自己的占位符即可。 +- **历史结果的图库**:卡片只保存当前这一批;请搭配你自己的素材列表。 + +## 用法 + +没有生成器时,卡片播放它的 `Simulation`;两者都没有时,“生成”不可用。`ImageGenerationSimulation` 保存卡片起始的 `Prompt`、可选的 `Models` 和 `Suggestions`,以及一个为提示词绘制图片的 `Painter`(`ImageGenerationPainter`);卡片在两到三秒内逐帧报告进度,每个图块从绘制的预览中逐步呈现,并在第一次滚动进入视口时自动生成一次。不会下载或发送任何内容。你自己设置的 `Prompt`、`Models` 和 `Suggestions` 优先于模拟中的值,把 `Simulation` 设为 null 会停止它的运行。 + +```csharp +card.Simulation = new ImageGenerationSimulation +{ + Prompt = "A quiet mountain lake at golden hour", + Models = [new ImageGenerationModel { Id = "your-model", Name = "Your image model", Mark = ArcIcons.Sparkles }], + Suggestions = ["A neon city street after rain at night"], + // Opaque BGRA bytes, width × height × 4; the same prompt and seed should paint the same picture. + Painter = (prompt, seed, width, height) => MyPainter.Paint(prompt, seed, width, height), +}; +``` + +本页的演示从 `ImageGenerationDemoData` 绘制黄金时刻的湖泊、雨后的城市街道、阳光窗台上的杯子或粉彩色的房间。图库其他 AI 演示所用的模型目录里没有图像生成模型,因此这些图像模型保留虚构的名称。 + +用 `Generator` 把它连接到你的图像服务。每个格子调用一次,并行执行,用于 Generate、Vary 和 Retry: + +```csharp +card.Generator = async (request, context) => +{ + var job = await images.CreateAsync(request.Prompt, request.Model.Id, request.Aspect.Width, request.Aspect.Height, + seedFrom: request.VariationOf?.Id, context.CancellationToken); + await foreach (var step in job.ProgressAsync(context.CancellationToken)) + { + context.ReportProgress(step.Fraction, step.Preview); + } + return new ImageGenerationImage + { + Id = job.Id, + Source = await job.DownloadBitmapAsync(context.CancellationToken), + Description = request.Prompt, + Width = request.Aspect.Width, + Height = request.Aspect.Height, + }; +}; +``` + +以 0 到 1 报告进度。提供预览位图时,马赛克会从你的图像中成形;没有预览时,一个安静的占位网格会闪烁,直到图像到达后淡入;完全不报告时,进度环会自行估算。抛出异常即把该格子标记为失败;Stop 会取消 `context.CancellationToken`。`Upscaler` 对选中的图像以同样的方式工作。 + +### 结构 + +1. **提示词**:一个文本框;Cmd/Ctrl + Enter 开始生成。 +2. **标签**:模型(选中的显示名称,其余收起为带提示的 16px 标志)、画幅(各带一个小轮廓)以及 Generate,批次运行时它显示为 Stop。 +3. **网格**:四个格子保持本批次的画幅,高度不超过 420px。每个运行中的格子有一个玻璃质感的小胶囊,内含进度环和百分比;选中的格子有强调色描边和一个对勾徽章。 +4. **底栏**:一行状态,然后是 Upscale、Vary 和 Download。 + +### 选中、放大、变体与下载 + +点按一张完成的图像(或聚焦后按 Enter 或 Space)即可选中;再按一次取消选中。Upscale 会把选中的图像从它的格子放大到整个网格,并让一道清晰像素带从左向右扫过:前方柔和,后方清晰。按 Escape 或角落里的按钮回到网格。Vary 以选中的图像作为 `VariationOf` 生成四张新图。Download 触发 `DownloadRequested`,附带图像和一个由提示词、图像编号与尺寸组成的文件名;请在那里保存它。 + +失败的格子显示警示图标和 "Failed",被停止的格子显示方块和 "Stopped";点按任意一个都会重新生成该图像。 + +### 竖向画幅 + + + +每一批都保持自己的画幅:选择另一个画幅标签只对下一次生成生效。网格以弹簧动画过渡到新画幅,并把高度限制在 420px,因此竖向批次依然紧凑。 + +### 你自己的生成器 + + + +这里用卡片的模拟代替图像服务(`Card.Simulation.Generate`),第 3 张图在第一次尝试时失败。设置了生成器后 `AutoGenerate` 默认关闭;把它设为 `True`,卡片会在滚动进入视口时自动生成一次。 + +### 监听卡片 + + + +`SelectionChanged` 报告选中的图像(放大完成后报告放大后的图像),`SelectedImage` 保存它,`DownloadRequested` 携带图像和文件名。 + +```csharp +card.DownloadRequested += async (_, e) => +{ + var file = await TopLevel.GetTopLevel(card)!.StorageProvider.SaveFilePickerAsync(new() { SuggestedFileName = e.FileName }); + if (file is not null && e.Image.Source is { } bitmap) + { + await using var stream = await file.OpenWriteAsync(); + bitmap.Save(stream); + } +}; +``` + +### 窄容器 + + + +卡片填满容器,最宽 640px。宽度小于 420px 时,画幅标签移到第二行;小于 520px 时,底栏改为上下排列:状态在上,下面是三个等宽按钮。 + +### 你自己的文案与模型 + +`Models` 和 `AspectRatios` 替换标签(`ImageGenerationModel` 有 id、名称和可选的标志;`ImageGenerationAspect` 有 id、显示文字和一次生成返回的像素尺寸)。`Suggestions` 替换空网格中提供的提示词。`Labels` 保存卡片显示或播报的每一个词: + +```csharp +card.Labels = new ImageGenerationLabels +{ + Generate = "Créer", + Stop = "Arrêter", + Generating = (count, model) => $"Création de {count} images avec {model}", +}; +``` + +### 减弱动效 + +减弱动效时,进度环、分步的马赛克(它跟随进度而非时间)以及最终状态保留;光带、放大扫描、悬停缩放和展开位移被去掉。关闭动效时一切立即变化。 + +### 内容准则 + +- 模型名称只写你的服务实际提供的;演示中的名称是虚构的。 +- 返回真实的宽高,让底栏和文件名显示真实尺寸。 +- 在 `Description` 中描述每张图像(通常就是提示词):屏幕阅读器会在格子上朗读它。 + +### 最佳实践 + +- 把 `context.CancellationToken` 传给每一次调用,这样 Stop 才会真正停止工作。 +- 尽早报告预览:马赛克从第一步起就会显示真实的颜色。 +- 一次调用返回四张图的服务,可以缓存同一个请求,再按序号分别取出每一张。 + +### 常见问题 + +**批次运行时用户能修改提示词吗?** 可以;修改会在下一次 Generate 时生效。 + +**卡片会保留之前的批次吗?** 不会。它只保存当前批次;监听 `SelectionChanged` 来保留结果。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 模型标签和画幅标签各是一个单选组,每组只有一个 Tab 停靠点;收起的模型标签仍以名称作为无障碍名称,并显示在提示中。 +- 每个格子都是一个切换按钮,会报告是否被选中;它的名称朗读图像编号和描述,或说明正在生成、已失败或已停止。 +- 一个礼貌的实时区域会播报开始、完成、失败、放大和下载,并说明结果是否为模拟。 +- 状态从不只靠颜色表达:失败配有危险图标和 "Failed",停止配有方块和 "Stopped",选中配有强调色描边和对勾徽章。 +- 图像展开时,其余格子不接收指针或焦点。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Cmd/Ctrl + Enter | 从提示词输入框开始生成 | +| 方向键 | 在模型标签或画幅标签之间移动(并选择),以及在网格中的图像之间移动 | +| Enter、Space | 选中或取消选中当前聚焦的图像;重试失败或已停止的图像 | +| Escape | 关闭展开的图像并回到网格 | +| Tab | 提示词、模型、画幅、Generate、选中的图像,然后是 Upscale、Vary 和 Download | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 图像成形 | 4 列色块按带种子的顺序从中心淡入,然后 8、16、32、64 列依次落在其上,最后交叉淡入完整图像;同一个进度值驱动马赛克、进度环和百分比 | 同样的步骤,由进度驱动 | 同样的步骤 | +| 运行中的格子 | 一道柔和的光带沿对角线扫过成形中网格的色块,随着细节出现而淡去,每遍 1.6s,离开屏幕时暂停 | 无 | 无 | +| 完成的格子 | 悬停时画面放大 2%;按下时格子缩到 98.5% | 无 | 无 | +| 放大 | 格子以无回弹的 240ms 弹簧从它的格位长到整个网格,其余格子淡出;一道清晰像素带从左向右扫过 | 立即,无扫描 | 立即 | +| 新批次的画幅 | 网格以弹簧动画过渡到新画幅;容器尺寸变化则直接切换到新尺寸 | 立即 | 立即 | +| 按钮文字 | 叠放在同一格中交叉淡化(淡出 100ms,淡入 160ms),按钮宽度不变 | 交叉淡化 | 立即 | +| 底栏状态 | 旧的一行在 100ms 内淡出,新的一行在 160ms 内淡入并上移 2px | 交叉淡化 | 立即 | +| 模型标签 | 选中的标签按测得的宽度以弹簧展开名称 | 立即 | 立即 | + +## 相关组件 + +- `ContextWindow`:一次会话用了模型窗口的多少。 +- `ToolCall`:智能体发起的工具调用。 +- `AgentStatus`:运行中智能体的实时状态。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/zh-TW.md new file mode 100644 index 00000000..5ca39f55 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageGeneration/zh-TW.md @@ -0,0 +1,207 @@ +--- +title: Image generation +summary: 一張圖像生成卡片:四張圖像以像素馬賽克的方式逐步成形,從粗略的色塊到完整細節,每張都有自己的進度環;選取其中一張即可放大、產生變體或下載。 +source-hash: 11e47e45be40 +--- + + + +## 何時使用 + +`ImageGeneration` 是一張文字轉圖像的卡片。使用者寫下提示詞,選擇模型和畫面比例,四張圖像便並排以像素馬賽克的方式成形:先是粗略的色塊,接著在一道柔和的光帶下變成越來越細的色塊,最後呈現完整圖像,每張都有自己的進度環。使用者選取其中一張繼續操作:Upscale 把它放大到鋪滿整個網格,並讓一道清晰像素帶掃過畫面;Vary 以它為基礎產生四張新圖;Download 把它交給你的應用程式。 + +- **產品內的文字轉圖像功能**:行銷素材、商品照片、簡報或文件的插圖。 +- **任何需要比較幾個候選再繼續其中一個的流程**(放大、變體、下載)。 +- **在後端就緒之前為圖像模型整合製作原型**,使用一個模擬。 + +## 何時不用 + +- **完整的畫布編輯器**,帶圖層、遮罩和局部重繪:請以繪圖表面為基礎來建構。 +- **只顯示一張帶載入狀態的圖像**,沒有生成控制項:用圖像自己的預留位置即可。 +- **歷史結果的圖庫**:卡片只保存目前這一批;請搭配你自己的素材清單。 + +## 用法 + +沒有產生器時,卡片會播放它的 `Simulation`;兩者都沒有時,「生成」無法使用。`ImageGenerationSimulation` 保存卡片起始的 `Prompt`、可選的 `Models` 與 `Suggestions`,以及一個為提示詞繪製圖片的 `Painter`(`ImageGenerationPainter`);卡片在兩到三秒內逐影格回報進度,每個圖塊從繪製的預覽中逐步呈現,並在第一次捲動進入可視範圍時自動生成一次。不會下載或傳送任何內容。你自己設定的 `Prompt`、`Models` 與 `Suggestions` 優先於模擬中的值,把 `Simulation` 設為 null 會停止它的執行。 + +```csharp +card.Simulation = new ImageGenerationSimulation +{ + Prompt = "A quiet mountain lake at golden hour", + Models = [new ImageGenerationModel { Id = "your-model", Name = "Your image model", Mark = ArcIcons.Sparkles }], + Suggestions = ["A neon city street after rain at night"], + // Opaque BGRA bytes, width × height × 4; the same prompt and seed should paint the same picture. + Painter = (prompt, seed, width, height) => MyPainter.Paint(prompt, seed, width, height), +}; +``` + +本頁的示範從 `ImageGenerationDemoData` 繪製黃金時刻的湖泊、雨後的城市街道、陽光窗台上的杯子或粉彩色的房間。圖庫其他 AI 示範所用的模型目錄裡沒有圖像生成模型,因此這些圖像模型保留虛構的名稱。 + +用 `Generator` 把它連接到你的圖像服務。每個格子呼叫一次,平行執行,用於 Generate、Vary 和 Retry: + +```csharp +card.Generator = async (request, context) => +{ + var job = await images.CreateAsync(request.Prompt, request.Model.Id, request.Aspect.Width, request.Aspect.Height, + seedFrom: request.VariationOf?.Id, context.CancellationToken); + await foreach (var step in job.ProgressAsync(context.CancellationToken)) + { + context.ReportProgress(step.Fraction, step.Preview); + } + return new ImageGenerationImage + { + Id = job.Id, + Source = await job.DownloadBitmapAsync(context.CancellationToken), + Description = request.Prompt, + Width = request.Aspect.Width, + Height = request.Aspect.Height, + }; +}; +``` + +以 0 到 1 回報進度。提供預覽點陣圖時,馬賽克會從你的圖像中成形;沒有預覽時,一個安靜的預留網格會閃爍,直到圖像到達後淡入;完全不回報時,進度環會自行估算。擲出例外即把該格子標記為失敗;Stop 會取消 `context.CancellationToken`。`Upscaler` 對選取的圖像以同樣的方式運作。 + +### 結構 + +1. **提示詞**:一個文字欄位;Cmd/Ctrl + Enter 開始生成。 +2. **標籤**:模型(選取的顯示名稱,其餘收合為帶提示的 16px 標誌)、畫面比例(各帶一個小輪廓)以及 Generate,批次執行時它顯示為 Stop。 +3. **網格**:四個格子維持本批次的畫面比例,高度不超過 420px。每個執行中的格子有一個玻璃質感的小膠囊,內含進度環和百分比;選取的格子有強調色外框和一個勾號徽章。 +4. **頁尾**:一行狀態,接著是 Upscale、Vary 和 Download。 + +### 選取、放大、變體與下載 + +點按一張完成的圖像(或聚焦後按 Enter 或 Space)即可選取;再按一次取消選取。Upscale 會把選取的圖像從它的格子放大到整個網格,並讓一道清晰像素帶由左向右掃過:前方柔和,後方清晰。按 Escape 或角落的按鈕回到網格。Vary 以選取的圖像作為 `VariationOf` 產生四張新圖。Download 觸發 `DownloadRequested`,附帶圖像和一個由提示詞、圖像編號與尺寸組成的檔名;請在那裡儲存它。 + +失敗的格子顯示警示圖示和 "Failed",被停止的格子顯示方塊和 "Stopped";點按任一個都會重新生成該圖像。 + +### 直式畫面比例 + + + +每一批都維持自己的畫面比例:選擇另一個比例標籤只對下一次生成生效。網格以彈簧動畫過渡到新比例,並把高度限制在 420px,因此直式批次依然精簡。 + +### 你自己的產生器 + + + +這裡用卡片的模擬代替圖像服務(`Card.Simulation.Generate`),第 3 張圖在第一次嘗試時失敗。設定了產生器後 `AutoGenerate` 預設關閉;把它設為 `True`,卡片會在捲動進入可視範圍時自動生成一次。 + +### 監聽卡片 + + + +`SelectionChanged` 回報選取的圖像(放大完成後回報放大後的圖像),`SelectedImage` 保存它,`DownloadRequested` 攜帶圖像和檔名。 + +```csharp +card.DownloadRequested += async (_, e) => +{ + var file = await TopLevel.GetTopLevel(card)!.StorageProvider.SaveFilePickerAsync(new() { SuggestedFileName = e.FileName }); + if (file is not null && e.Image.Source is { } bitmap) + { + await using var stream = await file.OpenWriteAsync(); + bitmap.Save(stream); + } +}; +``` + +### 窄容器 + + + +卡片填滿容器,最寬 640px。寬度小於 420px 時,比例標籤移到第二列;小於 520px 時,頁尾改為上下排列:狀態在上,下面是三個等寬按鈕。 + +### 你自己的文案與模型 + +`Models` 和 `AspectRatios` 取代標籤(`ImageGenerationModel` 有 id、名稱和選用的標誌;`ImageGenerationAspect` 有 id、顯示文字和一次生成傳回的像素尺寸)。`Suggestions` 取代空網格中提供的提示詞。`Labels` 保存卡片顯示或播報的每一個詞: + +```csharp +card.Labels = new ImageGenerationLabels +{ + Generate = "Créer", + Stop = "Arrêter", + Generating = (count, model) => $"Création de {count} images avec {model}", +}; +``` + +### 減少動態效果 + +減少動態效果時,進度環、分步的馬賽克(它跟隨進度而非時間)以及最終狀態保留;光帶、放大掃描、懸停縮放和展開位移會被移除。關閉動態效果時一切立即變化。 + +### 文案規範 + +- 模型名稱只寫你的服務實際提供的;示範中的名稱是虛構的。 +- 傳回真實的寬高,讓頁尾和檔名顯示真實尺寸。 +- 在 `Description` 中描述每張圖像(通常就是提示詞):螢幕閱讀器會在格子上朗讀它。 + +### 最佳做法 + +- 把 `context.CancellationToken` 傳給每一次呼叫,這樣 Stop 才會真正停止工作。 +- 盡早回報預覽:馬賽克從第一步起就會顯示真實的顏色。 +- 一次呼叫傳回四張圖的服務,可以快取同一個請求,再依序號分別取出每一張。 + +### 常見問題 + +**批次執行時使用者能修改提示詞嗎?** 可以;修改會在下一次 Generate 時生效。 + +**卡片會保留之前的批次嗎?** 不會。它只保存目前批次;監聽 `SelectionChanged` 來保留結果。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 模型標籤和比例標籤各是一個單選群組,每組只有一個 Tab 停駐點;收合的模型標籤仍以名稱作為無障礙名稱,並顯示在提示中。 +- 每個格子都是一個切換按鈕,會回報是否被選取;它的名稱朗讀圖像編號和描述,或說明正在生成、已失敗或已停止。 +- 一個禮貌的即時區域會播報開始、完成、失敗、放大和下載,並說明結果是否為模擬。 +- 狀態從不只靠顏色表達:失敗配有危險圖示和 "Failed",停止配有方塊和 "Stopped",選取配有強調色外框和勾號徽章。 +- 圖像展開時,其餘格子不接收指標或焦點。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Cmd/Ctrl + Enter | 從提示詞欄位開始生成 | +| 方向鍵 | 在模型標籤或比例標籤之間移動(並選擇),以及在網格中的圖像之間移動 | +| Enter、Space | 選取或取消選取目前聚焦的圖像;重試失敗或已停止的圖像 | +| Escape | 關閉展開的圖像並回到網格 | +| Tab | 提示詞、模型、比例、Generate、選取的圖像,接著是 Upscale、Vary 和 Download | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 圖像成形 | 4 欄色塊依帶種子的順序從中心淡入,接著 8、16、32、64 欄依序落在其上,最後交叉淡入完整圖像;同一個進度值驅動馬賽克、進度環和百分比 | 同樣的步驟,由進度驅動 | 同樣的步驟 | +| 執行中的格子 | 一道柔和的光帶沿對角線掃過成形中網格的色塊,隨著細節出現而淡去,每趟 1.6s,離開螢幕時暫停 | 無 | 無 | +| 完成的格子 | 懸停時畫面放大 2%;按下時格子縮到 98.5% | 無 | 無 | +| 放大 | 格子以無回彈的 240ms 彈簧從它的格位長到整個網格,其餘格子淡出;一道清晰像素帶由左向右掃過 | 立即,無掃描 | 立即 | +| 新批次的比例 | 網格以彈簧動畫過渡到新比例;容器尺寸變化則直接切換到新尺寸 | 立即 | 立即 | +| 按鈕文字 | 疊放在同一格中交叉淡化(淡出 100ms,淡入 160ms),按鈕寬度不變 | 交叉淡化 | 立即 | +| 頁尾狀態 | 舊的一行在 100ms 內淡出,新的一行在 160ms 內淡入並上移 2px | 交叉淡化 | 立即 | +| 模型標籤 | 選取的標籤依測得的寬度以彈簧展開名稱 | 立即 | 立即 | + +## 相關元件 + +- `ContextWindow`:一次工作階段用了模型視窗的多少。 +- `ToolCall`:代理程式發出的工具呼叫。 +- `AgentStatus`:執行中代理程式的即時狀態。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/en-US.md new file mode 100644 index 00000000..868a99bd --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/en-US.md @@ -0,0 +1,215 @@ +--- +title: Image reveal +summary: An image generation canvas where a soft, blurred preview made from the image's own colors sharpens top to bottom behind a soft-edged band, with fine film grain, a translucent progress pill and calm queued and error states. +--- + + + +## When to use + +`ImageReveal` shows an image model at work the way current image tools do. A soft, heavily blurred preview made from the +picture's own colors fades in with fine film grain, then a soft-edged band travels top to bottom: above it the picture is +sharp, inside it a near-sharp copy blends in, below it the preview stays soft while it slowly comes into focus. A +translucent pill shows a progress ring and the step count. Queued jobs get a calm surface with one broad shimmer, and a +failed job an alert, its message and Retry. + +- **An image canvas** or an inline image in a chat reply while the model renders it. +- **A grid of variations** that resolve one after another. +- **A queued job** that should feel alive before it starts. +- **An empty state** while a first image renders. + +## When not to use + +- **Plain image loading** from a server or disk: use a blur-up placeholder or a skeleton. +- **Text generation or tool calls**: use a text shimmer, `ToolCall` or `AgentStatus`. +- **Determinate file uploads**: use a progress bar. + +## Usage + +Pass the job's `Status` and `Progress` (step over total) and the image in `Source` as soon as a preview or the final +image exists: the soft preview is sampled from it, and until it is there the canvas keeps the calm queued look. Coarse +progress updates, one per model step, are smoothed into one continuous reveal. + +```xml + +``` + +With no status and no generator, an image in `Source` is simply shown, finished. With nothing at all the canvas rests on +its plain surface: no pill, no shimmer, and it is named "No image" (the Empty tab above). For demos, a simulation plays a +looping generation over pictures you give it (below). + +### Anatomy + +1. **Canvas**: fills its container up to 560px wide and keeps `AspectRatio` (width over height); the image is + cover-cropped to it. Corners follow `Size`. +2. **Soft preview**: copies of the image 6 to 48 pixels wide, smoothly upscaled and blurred, crossfaded along that chain + by progress, with fine monochrome grain and a very slow drift. +3. **Band**: a soft-edged strip about a quarter of the canvas high that sharpens the picture as it travels down. +4. **Pill**: at the top left, a spinner, ring or check with a short label and the step count ("18 / 30", tabular + numerals in a reserved width). It fades out shortly after the image resolves. +5. **Error state**: an alert, the message and a Retry button in the middle of the canvas. + +### States + +| `ActualStatus` | Canvas | Pill | +|---|---|---| +| `Queued` | The surface tone with one broad, soft shimmer | A spinner and `QueueLabel` ("Queued, 2 ahead") | +| `Generating` | The soft preview, sharpening top to bottom behind the band | A progress ring, "Generating" and "18 / 30" | +| `Done` | The sharp image; one soft sheen sweeps across | A check and "Done", then the pill fades out | +| `Error` | The sharp layers blur back out over a dimmed preview | Hidden; the alert, `ErrorMessage` and Retry show | + +Every change starts from the current pose: restarting, failing or swapping the picture blurs the sharp layers back out +before anything resets underneath, and a new picture crossfades in from a snapshot of the old frame. + +### A generator + + + +`Generator` takes an async delegate that runs one job: report `ImageRevealProgress` values (the progress and, as soon as +you have it, the image) and return the final image. Throwing shows the error state, and Retry runs the generator again. +`Restart()` (or a new `RunKey`) cancels the running job and starts another. + +```csharp +reveal.Generator = async (progress, cancellationToken) => +{ + var job = await images.StartAsync(prompt, cancellationToken); + await foreach (var update in job.UpdatesAsync(cancellationToken)) + { + progress.Report(new ImageRevealProgress(update.Step / 30.0, update.Preview)); + } + return job.Image; +}; +``` + +### No progress events + + + +When the service reports no progress, set only `Status` and an estimated `Duration`. The pill eases towards the end and +waits there; the band sweeps the image in once you set `Source` and `Done`. + +### A simulation + +For a demo or a prototype, leave `Status`, `Generator` and `Source` unset and give the canvas an `ImageRevealSimulation` +with your own pictures (each an `ImageRevealSimulationPicture`: the image and its alt text). Each run waits in the queue +for `QueueDuration` (900ms), generates over `Duration` (5.2s) and reveals the next picture; with `IsLooping` it holds the +finished image for `HoldDuration` (2.6s), blurs back out and runs again. `Seed` picks the first picture, and the +announcements add "simulated". Nothing is generated or downloaded. Setting `Simulation` to null stops it and empties the +canvas. + +```csharp +reveal.Simulation = new ImageRevealSimulation +{ + Pictures = + [ + new ImageRevealSimulationPicture { Image = lakeBitmap, AltText = "A calm alpine lake at golden hour" }, + new ImageRevealSimulationPicture { Image = dunesBitmap, AltText = "Desert dunes at dusk" }, + ], +}; +``` + +Give each control its own simulation: it keeps its place in the pictures. The preview above reveals three scenes the +Gallery paints in code (an alpine lake, desert dunes, a lighthouse); they are demo data, not part of the library. + +### A grid of variations + + + +Use `Size="Small"` in grids: the pill shows only the ring and the count. Give each tile its own `Seed` so the grain and +the drift differ (here each tile also plays its own simulation over the same pictures), stagger the runs with +`StartDelay`, turn off `IsLooping`, and set `MaxWidth="Infinity"` so the tiles fill their cells. Changing `RunKey` (or +calling `Restart()`) blurs every tile back out and runs again. + +### Your own words + +`Labels` holds every word the control shows or says; `QueueLabel` and `ErrorMessage` change the chip and the message of +one job, and `AltText` names the finished image. + +```csharp +reveal.Labels = new ImageRevealLabels +{ + Generating = "Génération", + Done = "Terminé", + Count = (step, total) => $"{step} sur {total}", +}; +``` + +`RingBrush` colors the progress ring (the accent by default), `ShowsProgress` hides the pill and `ShowsSheen` the +completion sheen. + +### Content guidelines + +- `AltText` describes the picture ("A calm alpine lake at golden hour"), not the job. +- Keep `QueueLabel` short: "Queued", "Queued, 2 ahead". +- `ErrorMessage` says what happened and what to do, in one sentence. + +### Best practices + +- Pass the image as soon as any preview exists; the reveal is sampled from it. +- Report every model step: the 200ms follow turns them into one smooth reveal. +- In grids, use Small tiles, different seeds and a stagger of about 400ms. + +### Common questions + +**Does it read the image's pixels?** No. The copies are drawn, never read back, so any `IImage` works. + +**Why does it stay in the queued look while generating?** There is no image yet; pass `Source` (or report one from the +generator) and the preview fades in. + +## API reference + + + + + + + + + + + + + + + +## Accessibility + +- The canvas reports itself as an image; its name says the state while it works ("Generating image") and becomes + `AltText` once done. An empty canvas is named "No image" and announces nothing. +- A polite live region announces Queued, Generating image, Image ready and the error message, not every step. The + simulation appends "simulated". +- While generating, the pill is a read-only progress bar whose value is the step, named "Step 18 of 30". +- Status is never color alone: the pill pairs a spinner, ring or check with a short label, and the error state pairs an + alert icon with its message. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the Retry button in the error state. The canvas itself is not focusable. | +| Enter, Space | Activates Retry. | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Queued | One broad, soft shimmer, off canvas to off canvas every 2.6s | The plain surface tone | The plain surface tone | +| Preview appears | Fades in over 420ms from the 6px copy, crossfading along the blur chain by progress | The same fade, no drift | At once | +| Generating | A soft-edged band travels top to bottom; grain refreshes 24 times a second; a very slow drift settles as it resolves | No band: the near-sharp copy and then the image crossfade in by progress; still grain | The same crossfade, following progress at once | +| Progress updates | A 200ms exponential follow; the simulation eases out so the composition resolves early | The same | At once | +| Resolves | One soft sheen sweeps across (1.1s), then the pill fades out (240ms) | No sheen | At once | +| Restart, failure, new picture | The sharp layers blur back out (500ms) before the reveal resets; a new picture crossfades from a snapshot (450ms) | The same | At once | +| Pill label | Old text out over 100ms, new text in over 160ms after a 60ms gap; the pill's width springs with no bounce | 120ms crossfade, width at once | At once | + +The frame loop pauses off screen and when the window is hidden, and draws nothing once the image has resolved. + +## Related + +- `AgentStatus` for the live status of an agent run. +- `ToolCall` for the tool calls an agent makes. +- `ReasoningBlock` for the thinking part of an AI reply. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/zh-CN.md new file mode 100644 index 00000000..7f5593b4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/zh-CN.md @@ -0,0 +1,186 @@ +--- +title: Image reveal +summary: 图像生成画布:由图像自身颜色生成的柔和模糊预览,在一条边缘柔和的光带后方自上而下逐渐变清晰,带有细腻的胶片颗粒、半透明的进度胶囊,以及安静的排队与错误状态。 +source-hash: 11bcd64820da +--- + + + +## 何时使用 + +`ImageReveal` 以当下图像工具的方式展示图像模型的工作过程。一张由图片自身颜色生成、高度模糊的柔和预览带着细腻的胶片颗粒淡入,随后一条边缘柔和的光带自上而下移动:光带上方的图片已经清晰,光带内部混入一份接近清晰的副本,光带下方的预览保持柔和,并慢慢对焦。一个半透明的胶囊显示进度环与步数。排队中的任务是一块安静的表面,上面掠过一道宽阔的微光;失败的任务显示警示图标、错误信息与 Retry。 + +- **图像画布**,或模型渲染图片时聊天回复中的内联图片。 +- **一组变体图片**,一张接一张地生成完成。 +- **排队中的任务**,在开始之前也应当显得有生气。 +- **空状态**,第一张图片仍在渲染时。 + +## 何时不用 + +- **普通的图片加载**(来自服务器或磁盘):使用模糊占位或骨架屏。 +- **文本生成或工具调用**:使用文字微光、`ToolCall` 或 `AgentStatus`。 +- **进度确定的文件上传**:使用进度条。 + +## 用法 + +传入任务的 `Status` 与 `Progress`(当前步数除以总步数),并在预览图或最终图片一出现时就把它交给 `Source`:柔和预览从它取样,在它出现之前画布保持安静的排队外观。粗粒度的进度更新(每个模型步骤一次)会被平滑成一段连续的显现。 + +```xml + +``` + +没有状态也没有生成器时,`Source` 中的图片会直接以完成状态显示。什么都没有设置时,画布停在它的素色表面上:没有胶囊、没有微光,名称为 "No image"(上方的 Empty 选项卡)。用于演示时,可以让模拟在你提供的图片上循环播放生成过程(见下文)。 + +### 结构 + +1. **画布**:填满容器,最宽 560px,并保持 `AspectRatio`(宽度除以高度);图片按覆盖方式裁剪。圆角随 `Size` 变化。 +2. **柔和预览**:图片宽 6 到 48 像素的副本,平滑放大并模糊,按进度沿这条模糊链交叉淡化,带有细腻的单色颗粒与极其缓慢的漂移。 +3. **光带**:一条约为画布四分之一高、边缘柔和的条带,向下移动时让图片变清晰。 +4. **胶囊**:位于左上角,一个加载圈、进度环或对勾,配一个简短标签与步数("18 / 30",等宽数字,宽度预留)。图片显现完成后不久淡出。 +5. **错误状态**:画布中央的警示图标、错误信息与 Retry 按钮。 + +### 状态 + +| `ActualStatus` | 画布 | 胶囊 | +|---|---|---| +| `Queued` | 表面底色,掠过一道宽阔柔和的微光 | 加载圈与 `QueueLabel`("Queued, 2 ahead") | +| `Generating` | 柔和预览在光带后方自上而下变清晰 | 进度环、"Generating" 与 "18 / 30" | +| `Done` | 清晰的图片;一道柔和的光泽横扫而过 | 对勾与 "Done",随后胶囊淡出 | +| `Error` | 清晰的图层在变暗的预览上重新模糊淡出 | 隐藏;显示警示图标、`ErrorMessage` 与 Retry | + +每次变化都从当前姿态出发:重新开始、失败或更换图片时,清晰图层会先重新模糊淡出,底下的状态才会重置;新图片从旧画面的快照交叉淡入。 + +### 生成器 + + + +`Generator` 接受一个运行一次任务的异步委托:报告 `ImageRevealProgress` 值(进度,以及一旦拿到就附上的图片),并返回最终图片。抛出异常会显示错误状态,Retry 会再次运行生成器。`Restart()`(或新的 `RunKey`)会取消正在运行的任务并开始新的任务。 + +```csharp +reveal.Generator = async (progress, cancellationToken) => +{ + var job = await images.StartAsync(prompt, cancellationToken); + await foreach (var update in job.UpdatesAsync(cancellationToken)) + { + progress.Report(new ImageRevealProgress(update.Step / 30.0, update.Preview)); + } + return job.Image; +}; +``` + +### 没有进度事件 + + + +服务不报告进度时,只设置 `Status` 和预估的 `Duration`。胶囊会缓缓走向终点并在那里等待;当你设置 `Source` 与 `Done` 后,光带会把图片扫入。 + +### 模拟 + +用于演示或原型时,不要设置 `Status`、`Generator` 与 `Source`,而是给画布一个 `ImageRevealSimulation`,并传入你自己的图片(每张是一个 `ImageRevealSimulationPicture`:图片及其替代文本)。每次运行先在队列中等待 `QueueDuration`(900ms),再在 `Duration`(5.2s)内生成并揭示下一张图片;开启 `IsLooping` 时,完成的图片会停留 `HoldDuration`(2.6s),然后重新模糊淡出并再次运行。`Seed` 决定第一张图片,播报会附加 "simulated"。不会生成或下载任何内容。把 `Simulation` 设为 null 会停止模拟并清空画布。 + +```csharp +reveal.Simulation = new ImageRevealSimulation +{ + Pictures = + [ + new ImageRevealSimulationPicture { Image = lakeBitmap, AltText = "A calm alpine lake at golden hour" }, + new ImageRevealSimulationPicture { Image = dunesBitmap, AltText = "Desert dunes at dusk" }, + ], +}; +``` + +给每个控件各自的模拟实例:它会记住自己播放到哪张图片。上方预览揭示的是 Gallery 在代码中绘制的三幅场景(高山湖泊、沙漠沙丘、灯塔),它们是演示数据,不属于组件库。 + +### 一组变体 + + + +在网格中使用 `Size="Small"`:胶囊只显示进度环与步数。给每个图块不同的 `Seed`,让颗粒与漂移各不相同(这里每个图块还在同一组图片上播放各自的模拟);用 `StartDelay` 错开各次运行,关闭 `IsLooping`,并设置 `MaxWidth="Infinity"` 让图块填满各自的单元格。修改 `RunKey`(或调用 `Restart()`)会让每个图块重新模糊淡出并再次运行。 + +### 自定义文字 + +`Labels` 包含控件显示或朗读的每一个词;`QueueLabel` 与 `ErrorMessage` 修改单个任务的标签与错误信息,`AltText` 为完成的图片命名。 + +```csharp +reveal.Labels = new ImageRevealLabels +{ + Generating = "Génération", + Done = "Terminé", + Count = (step, total) => $"{step} sur {total}", +}; +``` + +`RingBrush` 设置进度环的颜色(默认为强调色),`ShowsProgress` 隐藏胶囊,`ShowsSheen` 隐藏完成时的光泽。 + +### 内容准则 + +- `AltText` 描述图片内容("A calm alpine lake at golden hour"),而不是任务本身。 +- `QueueLabel` 保持简短:"Queued"、"Queued, 2 ahead"。 +- `ErrorMessage` 用一句话说明发生了什么、该怎么做。 + +### 最佳实践 + +- 只要有任何预览图就立即传入;显现效果从它取样。 +- 报告每一个模型步骤:200ms 的跟随会把它们变成一段平滑的显现。 +- 在网格中使用 Small 图块、不同的种子,并错开约 400ms。 + +### 常见问题 + +**它会读取图片的像素吗?** 不会。副本只被绘制,从不回读,因此任何 `IImage` 都可以使用。 + +**为什么生成中仍保持排队外观?** 还没有图片;传入 `Source`(或由生成器报告一张图片),预览就会淡入。 + +## API 参考 + + + + + + + + + + + + + + + +## 无障碍 + +- 画布报告为图像;工作期间它的名称说明当前状态("Generating image"),完成后变为 `AltText`。空画布的名称为 "No image",不播报任何内容。 +- 一个礼貌型实时区域会播报 Queued、Generating image、Image ready 与错误信息,而不是每一步。模拟模式会附加 "simulated"。 +- 生成期间,胶囊是一个只读进度条,其值为当前步数,名称为 "Step 18 of 30"。 +- 状态从不只靠颜色表达:胶囊把加载圈、进度环或对勾与简短标签配对,错误状态把警示图标与错误信息配对。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 在错误状态下移到 Retry 按钮。画布本身不可聚焦。 | +| Enter、Space | 激活 Retry。 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 排队 | 一道宽阔柔和的微光,每 2.6s 从画布外掠到画布外 | 只有表面底色 | 只有表面底色 | +| 预览出现 | 从 6px 副本开始在 420ms 内淡入,按进度沿模糊链交叉淡化 | 同样的淡入,没有漂移 | 立即 | +| 生成中 | 边缘柔和的光带自上而下移动;颗粒每秒刷新 24 次;极其缓慢的漂移随显现逐渐停下 | 没有光带:接近清晰的副本与图片按进度依次交叉淡入;颗粒静止 | 同样的交叉淡化,立即跟随进度 | +| 进度更新 | 200ms 的指数跟随;模拟模式使用缓出曲线,让构图更早成形 | 相同 | 立即 | +| 显现完成 | 一道柔和的光泽横扫而过(1.1s),随后胶囊淡出(240ms) | 没有光泽 | 立即 | +| 重新开始、失败、新图片 | 清晰图层先重新模糊淡出(500ms),显现才会重置;新图片从快照交叉淡入(450ms) | 相同 | 立即 | +| 胶囊标签 | 旧文字在 100ms 内淡出,间隔 60ms 后新文字在 160ms 内淡入;胶囊宽度以无回弹的弹簧变化 | 120ms 交叉淡化,宽度立即变化 | 立即 | + +帧循环在画布滚出屏幕或窗口隐藏时暂停,图片显现完成后不再绘制。 + +## 相关组件 + +- `AgentStatus`:智能体运行的实时状态。 +- `ToolCall`:智能体发起的工具调用。 +- `ReasoningBlock`:AI 回复中的思考部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/zh-TW.md new file mode 100644 index 00000000..11802077 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ImageReveal/zh-TW.md @@ -0,0 +1,186 @@ +--- +title: Image reveal +summary: 影像生成畫布:由影像本身顏色產生的柔和模糊預覽,在一條邊緣柔和的光帶後方由上而下逐漸變清晰,帶有細緻的底片顆粒、半透明的進度膠囊,以及安靜的排隊與錯誤狀態。 +source-hash: 11bcd64820da +--- + + + +## 何時使用 + +`ImageReveal` 以當今影像工具的方式呈現影像模型的工作過程。一張由圖片本身顏色產生、高度模糊的柔和預覽帶著細緻的底片顆粒淡入,接著一條邊緣柔和的光帶由上而下移動:光帶上方的圖片已經清晰,光帶內部混入一份接近清晰的副本,光帶下方的預覽維持柔和,並慢慢對焦。一個半透明的膠囊顯示進度環與步數。排隊中的工作是一塊安靜的表面,上面掠過一道寬闊的微光;失敗的工作則顯示警示圖示、錯誤訊息與 Retry。 + +- **影像畫布**,或模型算繪圖片時聊天回覆中的內嵌圖片。 +- **一組變體圖片**,一張接一張地生成完成。 +- **排隊中的工作**,在開始之前也應該顯得有生氣。 +- **空狀態**,第一張圖片仍在算繪時。 + +## 何時不用 + +- **一般的圖片載入**(來自伺服器或磁碟):使用模糊預留位置或骨架畫面。 +- **文字生成或工具呼叫**:使用文字微光、`ToolCall` 或 `AgentStatus`。 +- **進度確定的檔案上傳**:使用進度列。 + +## 用法 + +傳入工作的 `Status` 與 `Progress`(目前步數除以總步數),並在預覽圖或最終圖片一出現時就把它交給 `Source`:柔和預覽從它取樣,在它出現之前畫布維持安靜的排隊外觀。粗略的進度更新(每個模型步驟一次)會被平滑成一段連續的顯現。 + +```xml + +``` + +沒有狀態也沒有產生器時,`Source` 中的圖片會直接以完成狀態顯示。什麼都沒有設定時,畫布停在它的素色表面上:沒有膠囊、沒有微光,名稱為 "No image"(上方的 Empty 分頁)。用於展示時,可以讓模擬在你提供的圖片上循環播放生成過程(見下文)。 + +### 結構 + +1. **畫布**:填滿容器,最寬 560px,並維持 `AspectRatio`(寬度除以高度);圖片以覆蓋方式裁切。圓角隨 `Size` 變化。 +2. **柔和預覽**:圖片寬 6 到 48 像素的副本,平滑放大並模糊,依進度沿這條模糊鏈交叉淡化,帶有細緻的單色顆粒與極其緩慢的漂移。 +3. **光帶**:一條約為畫布四分之一高、邊緣柔和的條帶,向下移動時讓圖片變清晰。 +4. **膠囊**:位於左上角,一個載入圈、進度環或勾號,搭配一個簡短標籤與步數("18 / 30",等寬數字,寬度預留)。圖片顯現完成後不久淡出。 +5. **錯誤狀態**:畫布中央的警示圖示、錯誤訊息與 Retry 按鈕。 + +### 狀態 + +| `ActualStatus` | 畫布 | 膠囊 | +|---|---|---| +| `Queued` | 表面底色,掠過一道寬闊柔和的微光 | 載入圈與 `QueueLabel`("Queued, 2 ahead") | +| `Generating` | 柔和預覽在光帶後方由上而下變清晰 | 進度環、"Generating" 與 "18 / 30" | +| `Done` | 清晰的圖片;一道柔和的光澤橫掃而過 | 勾號與 "Done",接著膠囊淡出 | +| `Error` | 清晰的圖層在變暗的預覽上重新模糊淡出 | 隱藏;顯示警示圖示、`ErrorMessage` 與 Retry | + +每次變化都從目前的姿態出發:重新開始、失敗或更換圖片時,清晰圖層會先重新模糊淡出,底下的狀態才會重設;新圖片從舊畫面的快照交叉淡入。 + +### 產生器 + + + +`Generator` 接受一個執行一次工作的非同步委派:回報 `ImageRevealProgress` 值(進度,以及一拿到就附上的圖片),並傳回最終圖片。擲回例外會顯示錯誤狀態,Retry 會再次執行產生器。`Restart()`(或新的 `RunKey`)會取消正在執行的工作並開始新的工作。 + +```csharp +reveal.Generator = async (progress, cancellationToken) => +{ + var job = await images.StartAsync(prompt, cancellationToken); + await foreach (var update in job.UpdatesAsync(cancellationToken)) + { + progress.Report(new ImageRevealProgress(update.Step / 30.0, update.Preview)); + } + return job.Image; +}; +``` + +### 沒有進度事件 + + + +服務不回報進度時,只設定 `Status` 與預估的 `Duration`。膠囊會緩緩走向終點並在那裡等待;當你設定 `Source` 與 `Done` 後,光帶會把圖片掃入。 + +### 模擬 + +用於展示或原型時,不要設定 `Status`、`Generator` 與 `Source`,而是給畫布一個 `ImageRevealSimulation`,並傳入你自己的圖片(每張是一個 `ImageRevealSimulationPicture`:圖片及其替代文字)。每次執行先在佇列中等待 `QueueDuration`(900ms),再於 `Duration`(5.2s)內生成並揭示下一張圖片;開啟 `IsLooping` 時,完成的圖片會停留 `HoldDuration`(2.6s),然後重新模糊淡出並再次執行。`Seed` 決定第一張圖片,播報會附加 "simulated"。不會生成或下載任何內容。把 `Simulation` 設為 null 會停止模擬並清空畫布。 + +```csharp +reveal.Simulation = new ImageRevealSimulation +{ + Pictures = + [ + new ImageRevealSimulationPicture { Image = lakeBitmap, AltText = "A calm alpine lake at golden hour" }, + new ImageRevealSimulationPicture { Image = dunesBitmap, AltText = "Desert dunes at dusk" }, + ], +}; +``` + +給每個控制項各自的模擬執行個體:它會記住自己播放到哪張圖片。上方預覽揭示的是 Gallery 在程式碼中繪製的三幅場景(高山湖泊、沙漠沙丘、燈塔),它們是展示資料,不屬於元件庫。 + +### 一組變體 + + + +在格線中使用 `Size="Small"`:膠囊只顯示進度環與步數。給每個圖塊不同的 `Seed`,讓顆粒與漂移各不相同(這裡每個圖塊也在同一組圖片上播放各自的模擬);用 `StartDelay` 錯開各次執行,關閉 `IsLooping`,並設定 `MaxWidth="Infinity"` 讓圖塊填滿各自的儲存格。變更 `RunKey`(或呼叫 `Restart()`)會讓每個圖塊重新模糊淡出並再次執行。 + +### 自訂文字 + +`Labels` 包含控制項顯示或朗讀的每一個詞;`QueueLabel` 與 `ErrorMessage` 修改單一工作的標籤與錯誤訊息,`AltText` 為完成的圖片命名。 + +```csharp +reveal.Labels = new ImageRevealLabels +{ + Generating = "Génération", + Done = "Terminé", + Count = (step, total) => $"{step} sur {total}", +}; +``` + +`RingBrush` 設定進度環的顏色(預設為強調色),`ShowsProgress` 隱藏膠囊,`ShowsSheen` 隱藏完成時的光澤。 + +### 內容指引 + +- `AltText` 描述圖片內容("A calm alpine lake at golden hour"),而不是工作本身。 +- `QueueLabel` 保持簡短:"Queued"、"Queued, 2 ahead"。 +- `ErrorMessage` 用一句話說明發生了什麼、該怎麼做。 + +### 最佳實務 + +- 只要有任何預覽圖就立即傳入;顯現效果從它取樣。 +- 回報每一個模型步驟:200ms 的跟隨會把它們變成一段平滑的顯現。 +- 在格線中使用 Small 圖塊、不同的種子,並錯開約 400ms。 + +### 常見問題 + +**它會讀取圖片的像素嗎?** 不會。副本只被繪製,從不回讀,因此任何 `IImage` 都可以使用。 + +**為什麼生成中仍維持排隊外觀?** 還沒有圖片;傳入 `Source`(或由產生器回報一張圖片),預覽就會淡入。 + +## API 參考 + + + + + + + + + + + + + + + +## 無障礙 + +- 畫布回報為影像;工作期間它的名稱說明目前狀態("Generating image"),完成後變為 `AltText`。空白畫布的名稱為 "No image",不播報任何內容。 +- 一個禮貌型即時區域會播報 Queued、Generating image、Image ready 與錯誤訊息,而不是每一步。模擬模式會附加 "simulated"。 +- 生成期間,膠囊是一個唯讀進度列,其值為目前步數,名稱為 "Step 18 of 30"。 +- 狀態從不只靠顏色表達:膠囊把載入圈、進度環或勾號與簡短標籤搭配,錯誤狀態把警示圖示與錯誤訊息搭配。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 在錯誤狀態下移到 Retry 按鈕。畫布本身無法取得焦點。 | +| Enter、Space | 啟用 Retry。 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 排隊 | 一道寬闊柔和的微光,每 2.6s 從畫布外掠到畫布外 | 只有表面底色 | 只有表面底色 | +| 預覽出現 | 從 6px 副本開始在 420ms 內淡入,依進度沿模糊鏈交叉淡化 | 同樣的淡入,沒有漂移 | 立即 | +| 生成中 | 邊緣柔和的光帶由上而下移動;顆粒每秒更新 24 次;極其緩慢的漂移隨顯現逐漸停下 | 沒有光帶:接近清晰的副本與圖片依進度依序交叉淡入;顆粒靜止 | 同樣的交叉淡化,立即跟隨進度 | +| 進度更新 | 200ms 的指數跟隨;模擬模式使用緩出曲線,讓構圖更早成形 | 相同 | 立即 | +| 顯現完成 | 一道柔和的光澤橫掃而過(1.1s),接著膠囊淡出(240ms) | 沒有光澤 | 立即 | +| 重新開始、失敗、新圖片 | 清晰圖層先重新模糊淡出(500ms),顯現才會重設;新圖片從快照交叉淡入(450ms) | 相同 | 立即 | +| 膠囊標籤 | 舊文字在 100ms 內淡出,間隔 60ms 後新文字在 160ms 內淡入;膠囊寬度以無回彈的彈簧變化 | 120ms 交叉淡化,寬度立即變化 | 立即 | + +畫框迴圈在畫布捲出螢幕或視窗隱藏時暫停,圖片顯現完成後不再繪製。 + +## 相關 + +- `AgentStatus`:代理程式執行的即時狀態。 +- `ToolCall`:代理程式發出的工具呼叫。 +- `ReasoningBlock`:AI 回覆中的思考部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/en-US.md new file mode 100644 index 00000000..944f1174 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/en-US.md @@ -0,0 +1,198 @@ +--- +title: Install snippet +summary: A quickstart card where framework tabs, three numbered steps and highlighted snippets take a developer from install to the first event, and Generate key wipes a fresh key into place. +--- + + + +## When to use + +`InstallSnippet` is a "get started in 30 seconds" card. Framework tabs pick the reader's stack, and three numbered steps +(Install, Add your key, Ship) show a highlighted snippet each, with a copy button. Step 1 offers npm, pnpm, yarn and bun +when the framework installs packages. Generate key creates a key and wipes it into the placeholder in place, in every +snippet that uses it. Switching frameworks crossfades the snippets in space reserved for the longest of them, so the +card never changes size. + +- **A quickstart for several frameworks** without a page of docs for each. +- **Onboarding where creating an API key is part of getting started**: the first screen after sign up, or an empty + dashboard waiting for its first event. +- **Marketing pages for developer tools** that show how little setup it takes. + +## When not to use + +- **A single code sample with no steps**: use `CodeBlock`. +- **Trying requests against a live API**: an API playground fits better. +- **Long setup guides with many branches**: write docs pages and link to them. +- **Showing existing secret keys in full**: mask them in an API keys page instead (see `ProviderKeys`). + +## Usage + +With no frameworks and no simulation (below) the card shows its three steps over empty snippets (the Empty tab above). +With no key generator, Generate key invents a demo key and says so: nothing is sent. Give it your own frameworks and your +key endpoint: + +```csharp +snippet.KeyGenerator = async token => +{ + var response = await http.PostAsync("/api/keys", null, token); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync(token))!.Key; +}; +snippet.Copied += (_, e) => analytics.Track("onboarding_copy", e.Step.ToString()); +``` + +Return the key and it wipes into step 2. Throw and step 2 shows the exception's message (or "Could not create a key.") +with an alert icon, and the button reads Try again. The token is cancelled when the card leaves the window or the reader +asks for another key. + +### Anatomy + +1. **Header**: the title, one line under it and a muted version label at the top right (`Version`; empty hides it). +2. **Framework tabs**: a mark and a name per framework; a raised highlight slides to the selected tab. +3. **Steps**: a numbered marker on a rail, the title, a line that says what to do, and a code block with the file + name, a copy button and the snippet. A step whose snippet was copied swaps its number for a check. +4. **Step 1** shows the package manager tabs at the right of its title when the framework installs packages. +5. **Step 2** has Generate key at the right of its title and a status line under its code. + +### Your frameworks + + + +Each `InstallSnippetFramework` has an `Id`, a `Label` and three snippets. Set `Packages` (package names separated by +spaces) for an install through npm, pnpm, yarn or bun, or `Install` for a command or a script tag. Write `{{key}}` where +the key goes; until a key exists the placeholder (`KeyPlaceholder`) shows there, tinted, and copies as written: + +```xml + + + + + + + + + + +``` + +The ids `node`, `python`, `go` and `html` get a mark of their own; any other id shows a text-only tab unless you set +`Mark` to a 24 × 24 stroked glyph. The language for highlighting follows the file name (`Terminal` and `.env` read as +shell) unless you set `Language`. Keep snippets under ten lines so the card stays easy to scan. + +### Your key service + + + +`SelectedFramework` and `PackageManager` are two-way: bind them to remember the reader's choice across pages. +`FrameworkChanged`, `PackageManagerChanged`, `KeyGenerated` and `Copied` report what the reader did, with the exact text +a copy put on the clipboard. Pass `ApiKey` for a signed-in reader who already has a key: every snippet shows it and +Generate key is hidden. + +### A simulated SDK + +For a demo, leave `Frameworks` empty and give the card an `InstallSnippetSimulation`: its framework tabs and the +`KeyPrefix` of the keys its Generate key invents. Your own `Frameworks` set it aside, and setting `Simulation` to null +empties the card. The previews here show this library's own install steps that way: `TKCode.Arc.Special` added with the +.NET CLI, a PackageReference or central package management, a model provider key kept in user secrets, development +settings or an environment variable (for your app's own model calls; Arc's components never see it), and the +registration in `Program.cs`. The keys are invented demo keys. + +```csharp +snippet.Simulation = new InstallSnippetSimulation +{ + Frameworks = + [ + new InstallSnippetFramework("dotnet", ".NET CLI") + { + Install = new InstallSnippetFile("Terminal", "dotnet add package TKCode.Arc.Special --version 0.3.2-preview.1"), + Key = new InstallSnippetFile("Terminal", "dotnet user-secrets set \"Chat:ApiKey\" \"{{key}}\""), + }, + ], + KeyPrefix = "demo_", +}; +``` + +### Narrow + + + +The card fills its container up to 640px. Below 460px the padding tightens and the package manager tabs show their marks +only (their names stay for screen readers and in a tooltip). Below 380px the framework tabs drop their marks and the +version label hides. Long lines scroll sideways inside their code block, never the page. + +### Your own words + +`Labels` holds every word the card shows or says, apart from `Title`, `Description` and `Version`: + +```csharp +snippet.Labels = new InstallSnippetLabels +{ + InstallTitle = "Installer", + KeyTitle = "Ajouter votre clé", + CodeTitle = "Livrer", + GenerateKey = "Générer une clé", +}; +``` + +### Content guidelines + +- Step lines say what to do with the snippet in a few words ("Put your key in .env. Never commit it."). +- File names are the reader's real files (`.env`, `app.py`); a command shows as Terminal. +- Keep each snippet short and runnable as is, with `{{key}}` exactly where the key goes. + +### Best practices + +- Wire `KeyGenerator` to your key endpoint, and pass `ApiKey` for readers who already have one. +- Track `Copied` to see where onboarding stalls. +- Keep simulated keys out of production: without `KeyGenerator` the card invents keys on purpose. + +## API reference + + + + + + + + + + + +## Accessibility + +- The framework and package manager tabs are tab lists with roving focus: only the selected tab is a Tab stop, and each + tab reports whether it is selected. The steps are the framework tabs' panel. +- The steps are a list of three items; each title is a heading. A finished step reads ", done" and shows a check + instead of its number, so progress is never shown by color alone. +- A polite live region announces framework changes, new keys and errors, and says when a key is simulated. Copy buttons + announce Copied on their own. +- An error shows an alert icon and text, and the button changes to Try again. +- Marks are decorative; every tab has a name, and the package manager tabs keep theirs when the narrow layout hides it. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves between the framework tabs, the package manager tabs, the copy buttons and Generate key | +| Left Arrow, Right Arrow | In a tab list, moves to and selects the previous or next tab (wrapping) | +| Home, End | In a tab list, selects the first or last tab | +| Enter, Space | Copies a snippet or generates a key | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Tab highlight | Slides to the measured tab on a bounce-free spring (0.24s) | Moves at once | At once | +| Snippets, file names, step lines | Old fades out over 100ms, new fades in over 160ms rising 2px, in one shared cell | Crossfade over 120ms, no rise | At once | +| A new key | The slot eases from the placeholder's width to the key's (exact, in characters) while the key wipes in from the left over 340ms, then its tint fades over 480ms | The key appears at once; the tint still fades | At once | +| A finished step | The number fades to a check as the marker fills with the accent | The check fades in over 120ms | At once | + +Every code block reserves the height of its step's longest snippet and every step line the height of its longest line, +so switching frameworks moves nothing. Only opacity, clips and one measured width animate, and nothing runs when idle. + +## Related + +- `CodeBlock` for a single code sample with copy. +- `CopyButton` for copying one value. +- `ProviderKeys` for showing and rotating keys a person already has. +- `Tabs` for switching related content in one place. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/zh-CN.md new file mode 100644 index 00000000..e12b314e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/zh-CN.md @@ -0,0 +1,171 @@ +--- +title: 安装片段 +summary: 一张快速上手卡片:框架标签、三个编号步骤和高亮代码片段带开发者从安装走到第一个事件,"生成密钥"会把新密钥就地擦入占位符。 +source-hash: 8834ae45e0b1 +--- + + + +## 何时使用 + +`InstallSnippet` 是一张"30 秒上手"卡片。框架标签选择读者的技术栈,三个编号步骤(安装、添加密钥、上线)各显示一段高亮代码,并带有复制按钮。当框架通过包安装时,步骤 1 提供 npm、pnpm、yarn 和 bun 可选。"生成密钥"会创建一个密钥,并把它就地擦入占位符,所有用到它的片段都会更新。切换框架时,片段在为最长内容预留的空间里交叉淡化,因此卡片尺寸始终不变。 + +- **面向多个框架的快速上手**,无需为每个框架各写一页文档。 +- **创建 API 密钥是入门一部分的引导流程**:注册后的第一屏,或等待第一个事件的空仪表板。 +- **开发者工具的营销页面**,展示接入只需多少配置。 + +## 何时不用 + +- **只有一段代码、没有步骤**:使用 `CodeBlock`。 +- **对线上 API 试发请求**:API 调试台更合适。 +- **分支众多的长篇安装指南**:编写文档页面并链接过去。 +- **完整显示已有的密钥**:请在 API 密钥页面中遮蔽显示(参见 `ProviderKeys`)。 + +## 用法 + +没有框架也没有模拟(见下文)时,卡片显示三个步骤和空的代码片段(上方的 Empty 选项卡)。没有设置密钥生成器时,"生成密钥"会编造一个演示密钥并如实说明:不会发送任何内容。传入你自己的框架和密钥接口: + +```csharp +snippet.KeyGenerator = async token => +{ + var response = await http.PostAsync("/api/keys", null, token); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync(token))!.Key; +}; +snippet.Copied += (_, e) => analytics.Track("onboarding_copy", e.Step.ToString()); +``` + +返回密钥后,它会擦入步骤 2。抛出异常时,步骤 2 会以警示图标显示异常的消息(或"Could not create a key."),按钮变为"重试"(Try again)。当卡片离开窗口或读者再次请求密钥时,令牌会被取消。 + +### 结构 + +1. **标题区**:标题、其下的一行说明,以及右上角柔和的版本标签(`Version`;为空则隐藏)。 +2. **框架标签**:每个框架一个标记和名称;凸起的高亮会滑到所选标签下。 +3. **步骤**:轨道上的编号标记、标题、一行说明要做什么,以及带文件名、复制按钮和代码片段的代码块。片段被复制过的步骤会把编号换成对勾。 +4. **步骤 1** 在框架通过包安装时,于标题右侧显示包管理器标签。 +5. **步骤 2** 在标题右侧有"生成密钥",代码下方有一行状态。 + +### 你的框架 + + + +每个 `InstallSnippetFramework` 都有 `Id`、`Label` 和三段片段。通过 npm、pnpm、yarn 或 bun 安装时设置 `Packages`(以空格分隔的包名),命令或脚本标签则设置 `Install`。在密钥所在位置写 `{{key}}`;在密钥出现之前,那里会显示带底色的占位符(`KeyPlaceholder`),并按原样复制: + +```xml + + + + + + + + + + +``` + +id 为 `node`、`python`、`go` 和 `html` 时会有各自的标记;其他 id 显示纯文字标签,除非你把 `Mark` 设为 24 × 24 的描边图形。高亮所用的语言跟随文件名(`Terminal` 和 `.env` 按 shell 处理),也可以设置 `Language`。每段片段保持在十行以内,卡片才便于快速浏览。 + +### 你的密钥服务 + + + +`SelectedFramework` 和 `PackageManager` 是双向绑定的:绑定它们即可在不同页面间记住读者的选择。`FrameworkChanged`、`PackageManagerChanged`、`KeyGenerated` 和 `Copied` 报告读者的操作,复制事件会带上放入剪贴板的确切文本。对已登录且已有密钥的读者传入 `ApiKey`:每段片段都会显示它,"生成密钥"随之隐藏。 + +### 模拟的 SDK + +用于演示时,让 `Frameworks` 保持为空,并给卡片一个 `InstallSnippetSimulation`:它的框架标签,以及"生成密钥"编造的密钥所用的前缀 `KeyPrefix`。你自己的 `Frameworks` 会让它让位,把 `Simulation` 设为 null 会清空卡片。这里的预览就用这种方式展示本库自己的安装步骤:通过 .NET CLI、PackageReference 或中央包管理添加 `TKCode.Arc.Special`,把模型服务商的密钥保存在用户机密、开发环境设置或环境变量中(供你的应用自己调用模型;Arc 的组件永远接触不到它),以及在 `Program.cs` 中注册。这些密钥都是编造的演示密钥。 + +```csharp +snippet.Simulation = new InstallSnippetSimulation +{ + Frameworks = + [ + new InstallSnippetFramework("dotnet", ".NET CLI") + { + Install = new InstallSnippetFile("Terminal", "dotnet add package TKCode.Arc.Special --version 0.3.2-preview.1"), + Key = new InstallSnippetFile("Terminal", "dotnet user-secrets set \"Chat:ApiKey\" \"{{key}}\""), + }, + ], + KeyPrefix = "demo_", +}; +``` + +### 窄布局 + + + +卡片填满容器,最宽 640px。宽度低于 460px 时内边距收紧,包管理器标签只显示标记(名称保留给屏幕阅读器,并显示在工具提示中)。低于 380px 时框架标签去掉标记,版本标签隐藏。过长的行在各自的代码块内横向滚动,而不是整个页面滚动。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的每一个词,`Title`、`Description` 和 `Version` 除外: + +```csharp +snippet.Labels = new InstallSnippetLabels +{ + InstallTitle = "Installer", + KeyTitle = "Ajouter votre clé", + CodeTitle = "Livrer", + GenerateKey = "Générer une clé", +}; +``` + +### 内容准则 + +- 步骤说明用几个词讲清片段该怎么用("把密钥放进 .env,切勿提交。")。 +- 文件名使用读者真实的文件(`.env`、`app.py`);命令显示为 Terminal。 +- 每段片段保持简短、可直接运行,`{{key}}` 准确放在密钥所在位置。 + +### 最佳实践 + +- 把 `KeyGenerator` 接到你的密钥接口,并为已有密钥的读者传入 `ApiKey`。 +- 跟踪 `Copied`,了解引导流程卡在哪一步。 +- 不要把模拟密钥带进生产:没有 `KeyGenerator` 时,卡片会有意编造密钥。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 框架标签和包管理器标签都是采用游走焦点的标签列表:只有所选标签是 Tab 停靠点,每个标签都会报告自己是否被选中。步骤是框架标签的面板。 +- 步骤是一个含三项的列表,每个标题都是标题元素。完成的步骤会朗读",done",并以对勾代替编号,因此进度从不只靠颜色表达。 +- 一个礼貌型实时区域会播报框架切换、新密钥和错误,并在密钥为模拟时加以说明。复制按钮会自行播报"已复制"。 +- 出错时显示警示图标和文字,按钮变为"重试"。 +- 标记仅作装饰;每个标签都有名称,窄布局隐藏包管理器标签的名称时仍会保留它。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 在框架标签、包管理器标签、复制按钮和"生成密钥"之间移动 | +| 左箭头、右箭头 | 在标签列表中移到并选中上一个或下一个标签(循环) | +| Home、End | 在标签列表中选中第一个或最后一个标签 | +| Enter、Space | 复制片段或生成密钥 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 标签高亮 | 以无回弹的弹簧(0.24s)滑到测量出的标签 | 立即移动 | 立即 | +| 片段、文件名、步骤说明 | 旧内容在 100ms 内淡出,新内容在 160ms 内淡入并上升 2px,二者共用同一个单元格 | 120ms 交叉淡化,不上升 | 立即 | +| 新密钥 | 槽位从占位符的宽度缓动到密钥的宽度(按字符精确计算),同时密钥在 340ms 内从左侧擦入,随后底色在 480ms 内淡去 | 密钥立即出现;底色仍会淡去 | 立即 | +| 完成的步骤 | 标记填充强调色的同时,编号淡出、对勾淡入 | 对勾在 120ms 内淡入 | 立即 | + +每个代码块都预留其步骤中最长片段的高度,每行步骤说明都预留最长说明的高度,因此切换框架不会移动任何东西。只有不透明度、裁剪和一个测量出的宽度参与动画,空闲时不运行任何东西。 + +## 相关组件 + +- `CodeBlock`:带复制按钮的单段代码。 +- `CopyButton`:复制单个值。 +- `ProviderKeys`:显示和轮换用户已有的密钥。 +- `Tabs`:在同一处切换相关内容。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/zh-TW.md new file mode 100644 index 00000000..a2224b09 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/InstallSnippet/zh-TW.md @@ -0,0 +1,171 @@ +--- +title: 安裝片段 +summary: 一張快速上手卡片:框架分頁、三個編號步驟和醒目標示的程式碼片段帶開發者從安裝走到第一個事件,「產生金鑰」會把新金鑰就地擦入預留位置。 +source-hash: 8834ae45e0b1 +--- + + + +## 何時使用 + +`InstallSnippet` 是一張「30 秒上手」卡片。框架分頁選擇讀者的技術堆疊,三個編號步驟(安裝、新增金鑰、上線)各顯示一段語法標示的程式碼,並附有複製按鈕。當框架透過套件安裝時,步驟 1 提供 npm、pnpm、yarn 和 bun 可選。「產生金鑰」會建立一把金鑰,並把它就地擦入預留位置,所有用到它的片段都會更新。切換框架時,片段在為最長內容預留的空間裡交叉淡化,因此卡片尺寸始終不變。 + +- **適用多個框架的快速上手**,不必為每個框架各寫一頁文件。 +- **建立 API 金鑰屬於入門一環的引導流程**:註冊後的第一個畫面,或等待第一個事件的空白儀表板。 +- **開發者工具的行銷頁面**,展示接入只需要多少設定。 + +## 何時不用 + +- **只有一段程式碼、沒有步驟**:使用 `CodeBlock`。 +- **對線上 API 試送請求**:API 測試台更合適。 +- **分支眾多的長篇安裝指南**:撰寫文件頁面並連結過去。 +- **完整顯示既有的金鑰**:請在 API 金鑰頁面中遮蔽顯示(參見 `ProviderKeys`)。 + +## 用法 + +沒有框架也沒有模擬(見下文)時,卡片顯示三個步驟與空白的程式碼片段(上方的 Empty 分頁)。沒有設定金鑰產生器時,「產生金鑰」會編造一把展示金鑰並據實說明:不會傳送任何內容。傳入你自己的框架和金鑰端點: + +```csharp +snippet.KeyGenerator = async token => +{ + var response = await http.PostAsync("/api/keys", null, token); + response.EnsureSuccessStatusCode(); + return (await response.Content.ReadFromJsonAsync(token))!.Key; +}; +snippet.Copied += (_, e) => analytics.Track("onboarding_copy", e.Step.ToString()); +``` + +傳回金鑰後,它會擦入步驟 2。擲出例外時,步驟 2 會以警示圖示顯示例外的訊息(或「Could not create a key.」),按鈕變為「重試」(Try again)。當卡片離開視窗或讀者再次要求金鑰時,權杖會被取消。 + +### 結構 + +1. **標題區**:標題、其下的一行說明,以及右上角低調的版本標籤(`Version`;為空則隱藏)。 +2. **框架分頁**:每個框架一個標記和名稱;浮起的醒目標示會滑到所選分頁下。 +3. **步驟**:軌道上的編號標記、標題、一行說明要做什麼,以及含檔名、複製按鈕和程式碼片段的程式碼區塊。片段被複製過的步驟會把編號換成勾號。 +4. **步驟 1** 在框架透過套件安裝時,於標題右側顯示套件管理工具分頁。 +5. **步驟 2** 在標題右側有「產生金鑰」,程式碼下方有一行狀態。 + +### 你的框架 + + + +每個 `InstallSnippetFramework` 都有 `Id`、`Label` 和三段片段。透過 npm、pnpm、yarn 或 bun 安裝時設定 `Packages`(以空格分隔的套件名稱),指令或指令碼標籤則設定 `Install`。在金鑰所在位置寫 `{{key}}`;在金鑰出現之前,那裡會顯示帶底色的預留位置(`KeyPlaceholder`),並依原樣複製: + +```xml + + + + + + + + + + +``` + +id 為 `node`、`python`、`go` 和 `html` 時會有各自的標記;其他 id 顯示純文字分頁,除非你把 `Mark` 設為 24 × 24 的描邊圖形。語法標示所用的語言依檔名而定(`Terminal` 和 `.env` 視為 shell),也可以設定 `Language`。每段片段保持在十行以內,卡片才方便快速瀏覽。 + +### 你的金鑰服務 + + + +`SelectedFramework` 和 `PackageManager` 是雙向繫結的:繫結它們即可在不同頁面間記住讀者的選擇。`FrameworkChanged`、`PackageManagerChanged`、`KeyGenerated` 和 `Copied` 回報讀者的操作,複製事件會帶上放入剪貼簿的確切文字。對已登入且已有金鑰的讀者傳入 `ApiKey`:每段片段都會顯示它,「產生金鑰」隨之隱藏。 + +### 模擬的 SDK + +用於展示時,讓 `Frameworks` 維持空白,並給卡片一個 `InstallSnippetSimulation`:它的框架分頁,以及「產生金鑰」編造的金鑰所用的前置字串 `KeyPrefix`。你自己的 `Frameworks` 會讓它讓位,把 `Simulation` 設為 null 會清空卡片。這裡的預覽就以這種方式呈現本程式庫自己的安裝步驟:透過 .NET CLI、PackageReference 或中央套件管理加入 `TKCode.Arc.Special`,把模型服務供應商的金鑰存放在 user secrets、開發環境設定或環境變數中(供你的應用程式自己呼叫模型;Arc 的元件永遠接觸不到它),以及在 `Program.cs` 中註冊。這些金鑰都是編造的展示金鑰。 + +```csharp +snippet.Simulation = new InstallSnippetSimulation +{ + Frameworks = + [ + new InstallSnippetFramework("dotnet", ".NET CLI") + { + Install = new InstallSnippetFile("Terminal", "dotnet add package TKCode.Arc.Special --version 0.3.2-preview.1"), + Key = new InstallSnippetFile("Terminal", "dotnet user-secrets set \"Chat:ApiKey\" \"{{key}}\""), + }, + ], + KeyPrefix = "demo_", +}; +``` + +### 窄版面 + + + +卡片填滿容器,最寬 640px。寬度低於 460px 時內距收緊,套件管理工具分頁只顯示標記(名稱保留給螢幕閱讀器,並顯示在工具提示中)。低於 380px 時框架分頁拿掉標記,版本標籤隱藏。過長的行在各自的程式碼區塊內橫向捲動,而不是整個頁面捲動。 + +### 自訂文字 + +`Labels` 包含卡片顯示或朗讀的每一個詞,`Title`、`Description` 和 `Version` 除外: + +```csharp +snippet.Labels = new InstallSnippetLabels +{ + InstallTitle = "Installer", + KeyTitle = "Ajouter votre clé", + CodeTitle = "Livrer", + GenerateKey = "Générer une clé", +}; +``` + +### 內容準則 + +- 步驟說明用幾個詞講清楚片段該怎麼用(「把金鑰放進 .env,切勿提交。」)。 +- 檔名使用讀者真實的檔案(`.env`、`app.py`);指令顯示為 Terminal。 +- 每段片段保持簡短、可直接執行,`{{key}}` 準確放在金鑰所在位置。 + +### 最佳做法 + +- 把 `KeyGenerator` 接到你的金鑰端點,並為已有金鑰的讀者傳入 `ApiKey`。 +- 追蹤 `Copied`,了解引導流程卡在哪一步。 +- 別把模擬金鑰帶進正式環境:沒有 `KeyGenerator` 時,卡片會刻意編造金鑰。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 框架分頁和套件管理工具分頁都是採用遊走焦點的分頁清單:只有所選分頁是 Tab 停駐點,每個分頁都會回報自己是否被選取。步驟是框架分頁的面板。 +- 步驟是一個含三項的清單,每個標題都是標題元素。完成的步驟會朗讀「,done」,並以勾號取代編號,因此進度從不只靠顏色表達。 +- 一個禮貌型即時區域會播報框架切換、新金鑰和錯誤,並在金鑰為模擬時加以說明。複製按鈕會自行播報「已複製」。 +- 出錯時顯示警示圖示和文字,按鈕變為「重試」。 +- 標記僅作裝飾;每個分頁都有名稱,窄版面隱藏套件管理工具分頁的名稱時仍會保留它。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 在框架分頁、套件管理工具分頁、複製按鈕和「產生金鑰」之間移動 | +| 左方向鍵、右方向鍵 | 在分頁清單中移到並選取上一個或下一個分頁(循環) | +| Home、End | 在分頁清單中選取第一個或最後一個分頁 | +| Enter、Space | 複製片段或產生金鑰 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 分頁醒目標示 | 以無回彈的彈簧(0.24s)滑到量測出的分頁 | 立即移動 | 立即 | +| 片段、檔名、步驟說明 | 舊內容在 100ms 內淡出,新內容在 160ms 內淡入並上升 2px,兩者共用同一個儲存格 | 120ms 交叉淡化,不上升 | 立即 | +| 新金鑰 | 欄位從預留位置的寬度緩動到金鑰的寬度(依字元精確計算),同時金鑰在 340ms 內從左側擦入,接著底色在 480ms 內淡去 | 金鑰立即出現;底色仍會淡去 | 立即 | +| 完成的步驟 | 標記填入強調色的同時,編號淡出、勾號淡入 | 勾號在 120ms 內淡入 | 立即 | + +每個程式碼區塊都預留其步驟中最長片段的高度,每行步驟說明都預留最長說明的高度,因此切換框架不會移動任何東西。只有不透明度、裁切和一個量測出的寬度參與動畫,閒置時不執行任何東西。 + +## 相關元件 + +- `CodeBlock`:附複製按鈕的單段程式碼。 +- `CopyButton`:複製單一值。 +- `ProviderKeys`:顯示和輪替使用者既有的金鑰。 +- `Tabs`:在同一處切換相關內容。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/en-US.md new file mode 100644 index 00000000..530845fe --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/en-US.md @@ -0,0 +1,205 @@ +--- +title: Live visitors +summary: A realtime visitors card where the live count rolls digit by digit, new arrivals ping in on a dotted world map, the top pages reorder with bars, and signups and upgrades tick in. +--- + + + +## When to use + +`LiveVisitors` answers "who is here right now" in one tile: a pulsing live status, the number of people on the site +rolling digit by digit, a dotted world map where each new arrival pings in and fades, the busiest pages with bars, and a +ticker of signups and upgrades with the city, the country and how long ago. + +- **Realtime visitors** on a marketing site or in an app, next to the signups and upgrades as they happen. +- **Launch days and campaigns** where a team watches traffic and conversions live. +- **Admin and growth dashboards** that need one glanceable tile for right now. + +## When not to use + +- **Trends over days or weeks**: use a chart with axes. +- **Precise geography or per-country breakdowns**: use a table or a proper map; the dotted map is decoration. +- **A single number with no context**: use a stat card. + +## Usage + +With no data and no simulation (below) the card holds its placeholders and its status says No data. As soon as you set +`Count`, it shows only the data you pass. Feed it from your realtime source (a websocket, server-sent events or a two second poll of your +analytics API) by setting new snapshots; the card works out what moved from the ids: + +```csharp +card.Count = snapshot.Online; +card.Pages = snapshot.Pages.Select(p => new LiveVisitorsPage(p.Path, p.Visitors)).ToArray(); +card.Visitors = snapshot.Arrivals // the last 5 to 8 seconds of arrivals + .Select(a => new LiveVisitorsPing(a.Id, a.Latitude, a.Longitude)).ToArray(); +card.Events = snapshot.Conversions // newest first + .Select(c => new LiveVisitorsEvent(c.Id, c.Label, c.City, c.CountryCode, c.At)).ToArray(); +card.Connection = socket.IsOpen ? LiveVisitorsConnection.Live : LiveVisitorsConnection.Reconnecting; +``` + +### Push events as they arrive + + + +When your stream sends single events rather than snapshots, push them. `PushVisitor` adds one arrival that pings in now +and fades out after six seconds; `PushEvent` adds a conversion at the top of the ticker and, with a latitude and +longitude, pings it on the map with a stronger ring and labels its city for two seconds. Pushed items join whatever is +in `Visitors` and `Events`. + +```csharp +socket.OnVisit += v => card.PushVisitor(v.Latitude, v.Longitude); +socket.OnConversion += c => card.PushEvent(new LiveVisitorsEvent(c.Id, "Signed up", c.City, c.Country, c.At) +{ + Latitude = c.Latitude, + Longitude = c.Longitude, +}); +``` + +### A simulated stream + +For a prototype or a demo, leave `Count` unset and give the card a `LiveVisitorsSimulation` of a site you describe: +about `TypicalCount` people spread over `SitePages`, an arrival from one of `Cities` every 600ms, page counts that drift, +and one of `Conversions` every 2.4s, each picked by its weight. The card advances it from its own clock while it is on +screen, live and not paused, and says once that the stream is simulated. Your own `Count` sets it aside, and setting +`Simulation` to null empties the card. The preview's site, cities, pages and conversions are invented demo data. + +```csharp +card.Simulation = new LiveVisitorsSimulation +{ + TypicalCount = 128, + Cities = [new LiveVisitorsSimulationCity("Berlin", "DE", 52.52, 13.40, 4), new LiveVisitorsSimulationCity("Tokyo", "JP", 35.68, 139.69, 3)], + SitePages = [new LiveVisitorsSimulationPage("/", 34), new LiveVisitorsSimulationPage("/pricing", 22)], + Conversions = [new LiveVisitorsSimulationConversion("Signed up", 62), new LiveVisitorsSimulationConversion("Upgraded to Pro", 28)], +}; +``` + +### Anatomy + +1. **Status**: a dot and a word: Live (the dot pulses), Paused, Connecting or Reconnecting (a warning dot). +2. **Pause**: a toggle that holds the last snapshot. `IsPauseVisible="False"` hides it. +3. **Count**: the number in tabular digits that roll on their own strips, then the words from `Labels.People`. +4. **Map**: a dotted world map from 80°N to 56°S. Arrivals are accent dots with a ring; conversions have a larger dot + and a stronger ring. +5. **Top pages**: the busiest `MaxPages` pages (4 by default) with bars scaled to the busiest page. +6. **Signups and upgrades**: the newest `MaxEvents` conversions (4 by default) with a country code chip, what happened, + the city and the time. + +### Connection states + +`Connection` reflects your socket instead of hiding the card. `Connecting` shows placeholder bars in the same space as +the number and the lists. `Reconnecting` keeps the last data and switches the status to a warning dot with the word +Reconnecting. + +### Pausing + +The pause button sets `IsPaused` (two-way) and raises `PausedChanged`; use it to pause or close your own subscription. +While paused the card holds its last snapshot whatever you keep passing, and its clock stops. Resuming shows the latest +data. Setting `IsPaused` from code does the same without raising the event. + +### Narrow containers + + + +The card is at most 640px wide and fills narrower containers. Below 520px the two lists stack and the padding drops to +16px; the map scales with the card. Paths and cities end in an ellipsis, while counts and times keep fixed right-aligned +columns. + +### Your own words + +`Labels` holds every word the card shows or says, including the plural of the count, the short and spoken times and +the country names. Replace the object to localize or reword: + +```csharp +card.Labels = new LiveVisitorsLabels +{ + People = n => n == 1 ? "person watching the stream" : "people watching the stream", + EventsHeading = "New subscribers", +}; +``` + +### Flags + +The card shows a country as its ISO alpha-2 code in a small chip and spells it out for screen readers. Pass your own +image in `LiveVisitorsEvent.Flag` to show it instead; it stays decorative. + +### Content guidelines + +- Keep event labels short and in the past tense: "Signed up", "Upgraded to Pro". +- Never put personal data such as an email address in the ticker; a city and a country are enough. +- Send arrivals as a rolling window of five to eight seconds; the card animates arrivals and departures from the ids. + +### Best practices + +- Keep ids stable across snapshots so rows update in place and only real changes move. +- Use `Connection` for socket state instead of hiding or emptying the card. +- Pause your own subscription in `PausedChanged` so a paused card costs nothing. + +### Common questions + +**Can I use the simulated stream elsewhere?** Yes. Give a `LiveVisitorsSimulation` its data, call `Advance` with the time +that passed and read `Count`, `Pages`, `Visitors` and `Events`; `Restart` starts it over. Nothing is fetched or tracked. + +**Why does the number not change width while it counts?** The digits are tabular and roll on their own strips; only a new +leading digit (99 to 100) grows in. + +## API reference + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is a labelled group named by `Labels.Title` ("Live visitors"). +- The count is read once as plain text ("128 people on the site now"); the rolling digits are hidden from screen readers, + and count changes are not announced, so screen readers are not flooded. +- The pause button is a toggle that reports whether it is pressed; its name and tooltip name the action ("Pause live + updates", "Resume live updates"). +- The status is never color alone: the dot always sits next to Live, Paused, Connecting or Reconnecting. +- Each event row reads as one sentence with the country spelled out and the time in words ("Signed up, Berlin, Germany, + 12 seconds ago"); the code chip is decorative. Each page row reads as "/pricing, 24 people". +- The map is decorative and hidden; the same information is in the lists. +- A polite live region reports pause and resume, and says when the stream is simulated. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Reaches the pause button. The map and the lists are read-only. | +| Enter, Space | Pauses or resumes live updates. | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| The count changes | Each digit rolls on its own strip (280ms, ease-out); a new leading digit grows in | Changes at once | Changes at once | +| An arrival | The dot fades in and one ring expands and fades over 900ms | The dot fades in | Appears at once | +| A conversion with a position | A larger dot with a stronger ring, and the city labelled for 2s | The dot and the label fade in | At once; the label still leaves after 2s | +| An arrival expires | Fades out over 400ms | Fades out over 400ms | Leaves at once | +| Rows reorder or a new one arrives | Rows slide by a row height (a bounce-free 0.28s spring), the new row fades in over 160ms, the last one slides out below the clip and fades | Rows fade, nothing slides | At once | +| Page bars | Ease to their new width over 280ms | Ease over 280ms | At once | +| Live dot | A ring leaves it every 1.6s while live and on screen | Still | Still | +| Status word | Crossfades (out 100ms, in 160ms) in a reserved width | Short crossfade | At once | + +The card never changes height: the lists hold their rows' space and the map keeps its aspect ratio. The pulse, the +clock and the simulation stop while the card is off screen or paused. + +## Related + +- `AgentStatus` for the live status of a running agent. +- `ContextWindow` for another live, self-playing data card. +- `AgentTimeline` for a replay of a run with counters that roll as it plays. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/zh-CN.md new file mode 100644 index 00000000..97f32b11 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/zh-CN.md @@ -0,0 +1,187 @@ +--- +title: Live visitors +summary: 实时访客卡片:在线人数逐位滚动,新访客在点阵世界地图上闪现,热门页面带着条形重新排序,注册与升级依次出现。 +source-hash: 57663a6874b8 +--- + + + +## 何时使用 + +`LiveVisitors` 用一张卡片回答"此刻谁在这里":一个脉动的实时状态、逐位滚动的站内人数、一张点阵世界地图(每位新访客在上面闪现后淡去)、 +带条形的最热门页面,以及一条注册与升级动态,显示城市、国家和多久之前。 + +- **网站或应用的实时访客**,与刚刚发生的注册和升级放在一起。 +- **发布日和推广活动**,团队实时关注流量与转化。 +- **管理与增长仪表板**,需要一块一眼看清"此刻"的卡片。 + +## 何时不用 + +- **几天或几周的趋势**:使用带坐标轴的图表。 +- **精确的地理位置或按国家细分**:使用表格或真正的地图;点阵地图只是装饰。 +- **没有上下文的单个数字**:使用统计卡片。 + +## 用法 + +没有数据也没有模拟(见下文)时,卡片保持占位状态,状态显示 No data。一旦设置了 `Count`,它就只显示你传入的数据。从你的实时数据源(websocket、服务器推送事件,或每两秒轮询一次分析接口) +设置新的快照即可,卡片会根据 id 判断哪些发生了变化: + +```csharp +card.Count = snapshot.Online; +card.Pages = snapshot.Pages.Select(p => new LiveVisitorsPage(p.Path, p.Visitors)).ToArray(); +card.Visitors = snapshot.Arrivals // the last 5 to 8 seconds of arrivals + .Select(a => new LiveVisitorsPing(a.Id, a.Latitude, a.Longitude)).ToArray(); +card.Events = snapshot.Conversions // newest first + .Select(c => new LiveVisitorsEvent(c.Id, c.Label, c.City, c.CountryCode, c.At)).ToArray(); +card.Connection = socket.IsOpen ? LiveVisitorsConnection.Live : LiveVisitorsConnection.Reconnecting; +``` + +### 事件到达时推送 + + + +数据流逐条发送事件而不是快照时,直接推送。`PushVisitor` 添加一位访客,它立即闪现并在六秒后淡出;`PushEvent` 在动态顶部添加一次转化, +带有经纬度时还会在地图上以更明显的光环闪现,并标出城市两秒。推送的内容与 `Visitors`、`Events` 中的数据合并显示。 + +```csharp +socket.OnVisit += v => card.PushVisitor(v.Latitude, v.Longitude); +socket.OnConversion += c => card.PushEvent(new LiveVisitorsEvent(c.Id, "Signed up", c.City, c.Country, c.At) +{ + Latitude = c.Latitude, + Longitude = c.Longitude, +}); +``` + +### 模拟数据流 + +用于原型或演示时,不要设置 `Count`,而是给卡片一个 `LiveVisitorsSimulation`,描述一个网站:约 `TypicalCount` 人分布在 `SitePages` 上,每 600ms 有一位来自 `Cities` 之一的新访客,页面人数会漂移,每 2.4s 发生一次 `Conversions` 之一,每项都按权重选取。卡片在屏幕上、处于 Live 且未暂停时由自己的时钟推进它,并会说明一次数据流是模拟的。你自己的 `Count` 会让它让位,把 `Simulation` 设为 null 会清空卡片。预览中的网站、城市、页面和转化都是虚构的演示数据。 + +```csharp +card.Simulation = new LiveVisitorsSimulation +{ + TypicalCount = 128, + Cities = [new LiveVisitorsSimulationCity("Berlin", "DE", 52.52, 13.40, 4), new LiveVisitorsSimulationCity("Tokyo", "JP", 35.68, 139.69, 3)], + SitePages = [new LiveVisitorsSimulationPage("/", 34), new LiveVisitorsSimulationPage("/pricing", 22)], + Conversions = [new LiveVisitorsSimulationConversion("Signed up", 62), new LiveVisitorsSimulationConversion("Upgraded to Pro", 28)], +}; +``` + +### 结构 + +1. **状态**:一个圆点和一个词:Live(圆点脉动)、Paused、Connecting 或 Reconnecting(警告色圆点)。 +2. **暂停**:保留最后一份快照的切换按钮。`IsPauseVisible="False"` 可隐藏它。 +3. **人数**:等宽数字,每一位在自己的滚轮上滚动,后面是 `Labels.People` 给出的文字。 +4. **地图**:北纬 80° 到南纬 56° 的点阵世界地图。访客是强调色圆点加一圈光环;转化的圆点更大,光环更明显。 +5. **热门页面**:最热门的 `MaxPages` 个页面(默认 4 个),条形按最热门页面缩放。 +6. **注册与升级**:最新的 `MaxEvents` 次转化(默认 4 次),带国家代码标签、发生了什么、城市和时间。 + +### 连接状态 + +`Connection` 反映你的连接状态,而不是把卡片藏起来。`Connecting` 在人数和列表的位置显示占位条。`Reconnecting` 保留最后的数据, +状态切换为警告色圆点和 Reconnecting 字样。 + +### 暂停 + +暂停按钮设置 `IsPaused`(双向)并引发 `PausedChanged`;在其中暂停或关闭你自己的订阅。暂停期间卡片保留最后一份快照, +不管你继续传入什么,它的时钟也会停止。恢复后显示最新的数据。从代码设置 `IsPaused` 效果相同,但不会引发该事件。 + +### 窄容器 + + + +卡片最宽 640px,在更窄的容器中填满宽度。卡片宽度小于 520px 时两个列表上下堆叠,内边距降为 16px;地图随卡片缩放。 +路径和城市过长时以省略号结尾,人数和时间保持固定的右对齐列。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的每个词,包括人数的单复数、简短与朗读用的时间,以及国家名称。替换整个对象即可本地化或改写: + +```csharp +card.Labels = new LiveVisitorsLabels +{ + People = n => n == 1 ? "person watching the stream" : "people watching the stream", + EventsHeading = "New subscribers", +}; +``` + +### 国旗 + +卡片以小标签显示国家的 ISO 两位字母代码,并为屏幕阅读器读出完整国名。可在 `LiveVisitorsEvent.Flag` 中传入你自己的图片来代替;它仍然只是装饰。 + +### 内容准则 + +- 事件标签简短,使用过去时:"Signed up"、"Upgraded to Pro"。 +- 绝不在动态里放入个人数据,例如电子邮件地址;城市和国家就足够了。 +- 以五到八秒的滚动窗口发送访客;卡片根据 id 为到达和离开播放动画。 + +### 最佳实践 + +- 在快照之间保持 id 稳定,这样行会就地更新,只有真正的变化才会移动。 +- 用 `Connection` 表示连接状态,而不是隐藏或清空卡片。 +- 在 `PausedChanged` 中暂停你自己的订阅,让暂停的卡片不产生开销。 + +### 常见问题 + +**模拟数据流能在别处使用吗?** 可以。给 `LiveVisitorsSimulation` 提供数据,用经过的时间调用 `Advance`,然后读取 `Count`、`Pages`、 +`Visitors` 和 `Events`;`Restart` 会让它从头开始。不会获取或跟踪任何内容。 + +**为什么数字在计数时宽度不变?** 数字是等宽的,每一位在自己的滚轮上滚动;只有新增的最高位(99 到 100)会展开出现。 + +## API 参考 + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片是一个带名称的分组,名称来自 `Labels.Title`("Live visitors")。 +- 人数以纯文本读出一次("128 people on the site now");滚动的数字对屏幕阅读器隐藏,人数变化也不会播报,以免信息泛滥。 +- 暂停按钮是一个会报告是否按下的切换按钮;它的名称和提示说明操作("Pause live updates"、"Resume live updates")。 +- 状态从不只靠颜色表达:圆点旁边总有 Live、Paused、Connecting 或 Reconnecting。 +- 每个事件行读作一句话,国家完整读出,时间用文字表达("Signed up, Berlin, Germany, 12 seconds ago");代码标签只是装饰。 + 每个页面行读作"/pricing, 24 people"。 +- 地图是装饰且被隐藏;同样的信息在列表中。 +- 一个礼貌的实时区域会报告暂停与恢复,并说明数据流是模拟的。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 移到暂停按钮。地图和列表只读。 | +| Enter、Space | 暂停或恢复实时更新。 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 人数变化 | 每一位在自己的滚轮上滚动(280ms,ease-out);新增的最高位展开出现 | 立即变化 | 立即变化 | +| 一位访客 | 圆点淡入,一圈光环在 900ms 内扩散并淡去 | 圆点淡入 | 立即出现 | +| 带位置的一次转化 | 更大的圆点和更明显的光环,城市标注 2s | 圆点和标注淡入 | 立即出现;标注仍在 2s 后离开 | +| 访客过期 | 在 400ms 内淡出 | 在 400ms 内淡出 | 立即离开 | +| 行重新排序或新行到来 | 各行滑动一行高度(0.28s 无回弹弹簧),新行在 160ms 内淡入,最后一行滑出裁剪区并淡去 | 各行淡入淡出,不滑动 | 立即 | +| 页面条形 | 在 280ms 内缓动到新宽度 | 在 280ms 内缓动 | 立即 | +| 实时圆点 | 实时且在屏幕上时每 1.6s 发出一圈光环 | 静止 | 静止 | +| 状态文字 | 在预留宽度内交叉淡化(淡出 100ms,淡入 160ms) | 短暂交叉淡化 | 立即 | + +卡片的高度从不改变:列表保留各行的空间,地图保持宽高比。卡片离开屏幕或暂停时,脉动、时钟和模拟都会停止。 + +## 相关组件 + +- `AgentStatus`:运行中智能体的实时状态。 +- `ContextWindow`:另一张实时、可自动播放的数据卡片。 +- `AgentTimeline`:一次运行的回放,计数随播放滚动。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/zh-TW.md new file mode 100644 index 00000000..6e724da8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/LiveVisitors/zh-TW.md @@ -0,0 +1,187 @@ +--- +title: Live visitors +summary: 即時訪客卡片:線上人數逐位滾動,新訪客在點陣世界地圖上閃現,熱門頁面帶著長條重新排序,註冊與升級依序出現。 +source-hash: 57663a6874b8 +--- + + + +## 何時使用 + +`LiveVisitors` 用一張卡片回答「此刻誰在這裡」:一個脈動的即時狀態、逐位滾動的站內人數、一張點陣世界地圖(每位新訪客在上面閃現後淡去)、 +帶長條的最熱門頁面,以及一條註冊與升級動態,顯示城市、國家和多久之前。 + +- **網站或應用程式的即時訪客**,與剛剛發生的註冊和升級放在一起。 +- **發表日和行銷活動**,團隊即時關注流量與轉換。 +- **管理與成長儀表板**,需要一塊一眼看清「此刻」的卡片。 + +## 何時不用 + +- **幾天或幾週的趨勢**:使用帶座標軸的圖表。 +- **精確的地理位置或依國家細分**:使用表格或真正的地圖;點陣地圖只是裝飾。 +- **沒有脈絡的單一數字**:使用統計卡片。 + +## 用法 + +沒有資料也沒有模擬(見下文)時,卡片維持預留位置狀態,狀態顯示 No data。一旦設定了 `Count`,它就只顯示你傳入的資料。從你的即時資料來源(websocket、伺服器推送事件,或每兩秒輪詢一次分析 API) +設定新的快照即可,卡片會根據 id 判斷哪些發生了變化: + +```csharp +card.Count = snapshot.Online; +card.Pages = snapshot.Pages.Select(p => new LiveVisitorsPage(p.Path, p.Visitors)).ToArray(); +card.Visitors = snapshot.Arrivals // the last 5 to 8 seconds of arrivals + .Select(a => new LiveVisitorsPing(a.Id, a.Latitude, a.Longitude)).ToArray(); +card.Events = snapshot.Conversions // newest first + .Select(c => new LiveVisitorsEvent(c.Id, c.Label, c.City, c.CountryCode, c.At)).ToArray(); +card.Connection = socket.IsOpen ? LiveVisitorsConnection.Live : LiveVisitorsConnection.Reconnecting; +``` + +### 事件抵達時推送 + + + +資料串流逐筆傳送事件而不是快照時,直接推送。`PushVisitor` 新增一位訪客,它立即閃現並在六秒後淡出;`PushEvent` 在動態頂端新增一次轉換, +帶有經緯度時還會在地圖上以更明顯的光環閃現,並標出城市兩秒。推送的內容會與 `Visitors`、`Events` 中的資料合併顯示。 + +```csharp +socket.OnVisit += v => card.PushVisitor(v.Latitude, v.Longitude); +socket.OnConversion += c => card.PushEvent(new LiveVisitorsEvent(c.Id, "Signed up", c.City, c.Country, c.At) +{ + Latitude = c.Latitude, + Longitude = c.Longitude, +}); +``` + +### 模擬資料串流 + +用於原型或展示時,不要設定 `Count`,而是給卡片一個 `LiveVisitorsSimulation`,描述一個網站:約 `TypicalCount` 人分布在 `SitePages` 上,每 600ms 有一位來自 `Cities` 之一的新訪客,頁面人數會漂移,每 2.4s 發生一次 `Conversions` 之一,每項都依權重選取。卡片在畫面上、處於 Live 且未暫停時由自己的時鐘推進它,並會說明一次資料串流是模擬的。你自己的 `Count` 會讓它讓位,把 `Simulation` 設為 null 會清空卡片。預覽中的網站、城市、頁面與轉換都是虛構的展示資料。 + +```csharp +card.Simulation = new LiveVisitorsSimulation +{ + TypicalCount = 128, + Cities = [new LiveVisitorsSimulationCity("Berlin", "DE", 52.52, 13.40, 4), new LiveVisitorsSimulationCity("Tokyo", "JP", 35.68, 139.69, 3)], + SitePages = [new LiveVisitorsSimulationPage("/", 34), new LiveVisitorsSimulationPage("/pricing", 22)], + Conversions = [new LiveVisitorsSimulationConversion("Signed up", 62), new LiveVisitorsSimulationConversion("Upgraded to Pro", 28)], +}; +``` + +### 結構 + +1. **狀態**:一個圓點和一個詞:Live(圓點脈動)、Paused、Connecting 或 Reconnecting(警告色圓點)。 +2. **暫停**:保留最後一份快照的切換按鈕。`IsPauseVisible="False"` 可隱藏它。 +3. **人數**:等寬數字,每一位在自己的滾輪上滾動,後面是 `Labels.People` 提供的文字。 +4. **地圖**:北緯 80° 到南緯 56° 的點陣世界地圖。訪客是強調色圓點加一圈光環;轉換的圓點更大,光環更明顯。 +5. **熱門頁面**:最熱門的 `MaxPages` 個頁面(預設 4 個),長條依最熱門頁面縮放。 +6. **註冊與升級**:最新的 `MaxEvents` 次轉換(預設 4 次),帶國家代碼標籤、發生了什麼、城市和時間。 + +### 連線狀態 + +`Connection` 反映你的連線狀態,而不是把卡片藏起來。`Connecting` 在人數和清單的位置顯示預留位置長條。`Reconnecting` 保留最後的資料, +狀態切換為警告色圓點和 Reconnecting 字樣。 + +### 暫停 + +暫停按鈕設定 `IsPaused`(雙向)並引發 `PausedChanged`;在其中暫停或關閉你自己的訂閱。暫停期間卡片保留最後一份快照, +不論你繼續傳入什麼,它的時鐘也會停止。恢復後顯示最新的資料。從程式碼設定 `IsPaused` 效果相同,但不會引發該事件。 + +### 窄容器 + + + +卡片最寬 640px,在更窄的容器中填滿寬度。卡片寬度小於 520px 時兩份清單上下堆疊,內距降為 16px;地圖隨卡片縮放。 +路徑和城市過長時以刪節號結尾,人數和時間保持固定的靠右對齊欄位。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或朗讀的每個詞,包括人數的單複數、簡短與朗讀用的時間,以及國家名稱。替換整個物件即可在地化或改寫: + +```csharp +card.Labels = new LiveVisitorsLabels +{ + People = n => n == 1 ? "person watching the stream" : "people watching the stream", + EventsHeading = "New subscribers", +}; +``` + +### 國旗 + +卡片以小標籤顯示國家的 ISO 兩位字母代碼,並為螢幕閱讀器唸出完整國名。可在 `LiveVisitorsEvent.Flag` 中傳入你自己的圖片來取代;它仍然只是裝飾。 + +### 內容指引 + +- 事件標籤簡短,使用過去式:"Signed up"、"Upgraded to Pro"。 +- 絕不在動態中放入個人資料,例如電子郵件地址;城市和國家就足夠了。 +- 以五到八秒的滾動視窗傳送訪客;卡片根據 id 為抵達和離開播放動畫。 + +### 最佳實務 + +- 在快照之間保持 id 穩定,這樣列會就地更新,只有真正的變化才會移動。 +- 用 `Connection` 表示連線狀態,而不是隱藏或清空卡片。 +- 在 `PausedChanged` 中暫停你自己的訂閱,讓暫停的卡片不產生開銷。 + +### 常見問題 + +**模擬資料串流能在別處使用嗎?** 可以。給 `LiveVisitorsSimulation` 提供資料,用經過的時間呼叫 `Advance`,然後讀取 `Count`、`Pages`、 +`Visitors` 和 `Events`;`Restart` 會讓它從頭開始。不會擷取或追蹤任何內容。 + +**為什麼數字在計數時寬度不變?** 數字是等寬的,每一位在自己的滾輪上滾動;只有新增的最高位(99 到 100)會展開出現。 + +## API 參考 + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片是一個帶名稱的群組,名稱來自 `Labels.Title`("Live visitors")。 +- 人數以純文字唸出一次("128 people on the site now");滾動的數字對螢幕閱讀器隱藏,人數變化也不會播報,以免資訊氾濫。 +- 暫停按鈕是一個會回報是否按下的切換按鈕;它的名稱和提示說明操作("Pause live updates"、"Resume live updates")。 +- 狀態從不只靠顏色表達:圓點旁邊總有 Live、Paused、Connecting 或 Reconnecting。 +- 每個事件列唸作一句話,國家完整唸出,時間用文字表達("Signed up, Berlin, Germany, 12 seconds ago");代碼標籤只是裝飾。 + 每個頁面列唸作「/pricing, 24 people」。 +- 地圖是裝飾且被隱藏;同樣的資訊在清單中。 +- 一個禮貌的即時區域會回報暫停與恢復,並說明資料串流是模擬的。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 移到暫停按鈕。地圖和清單為唯讀。 | +| Enter、Space | 暫停或恢復即時更新。 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 人數變化 | 每一位在自己的滾輪上滾動(280ms,ease-out);新增的最高位展開出現 | 立即變化 | 立即變化 | +| 一位訪客 | 圓點淡入,一圈光環在 900ms 內擴散並淡去 | 圓點淡入 | 立即出現 | +| 帶位置的一次轉換 | 更大的圓點和更明顯的光環,城市標註 2 秒 | 圓點和標註淡入 | 立即出現;標註仍在 2 秒後離開 | +| 訪客過期 | 在 400ms 內淡出 | 在 400ms 內淡出 | 立即離開 | +| 列重新排序或新列到來 | 各列滑動一列高度(0.28 秒無回彈彈簧),新列在 160ms 內淡入,最後一列滑出裁切區並淡去 | 各列淡入淡出,不滑動 | 立即 | +| 頁面長條 | 在 280ms 內緩動到新寬度 | 在 280ms 內緩動 | 立即 | +| 即時圓點 | 即時且在螢幕上時每 1.6 秒發出一圈光環 | 靜止 | 靜止 | +| 狀態文字 | 在預留寬度內交叉淡化(淡出 100ms,淡入 160ms) | 短暫交叉淡化 | 立即 | + +卡片的高度從不改變:清單保留各列的空間,地圖保持長寬比。卡片離開螢幕或暫停時,脈動、時鐘和模擬都會停止。 + +## 相關 + +- `AgentStatus`:執行中代理程式的即時狀態。 +- `ContextWindow`:另一張即時、可自動播放的資料卡片。 +- `AgentTimeline`:一次執行的回放,計數隨播放滾動。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/en-US.md new file mode 100644 index 00000000..dbbcf81b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/en-US.md @@ -0,0 +1,184 @@ +--- +title: LLM cost calculator +summary: A pricing calculator for LLM workloads: set tokens per request, requests per day and prompt caching, and the models re-rank by monthly cost as bars glide and prices roll. +--- + + + +## When to use + +`LlmCostCalculator` puts an honest monthly estimate for an LLM workload in one panel. The header shows the selected +model's monthly total in a large rolling figure. Presets, three sliders and a Cached input switch set the workload, and +every model sits in a list ranked by monthly cost, lowest first, with a bar relative to the most expensive model. Rows +glide to their new rank as values change; picking one selects that model for the total, the breakdown and the price line. + +- **A pricing or settings page** that should show what a workload costs across several models. +- **Choosing a model for a feature**, when people should see how cost moves with prompt size, output length, traffic + and caching. +- **An internal tool** that compares provider prices before a migration or a budget review. + +## When not to use + +- **Choosing a model in a chat or composer** without a cost comparison: use a model menu. +- **The live token usage of one conversation**: use `ContextWindow`. +- **Actual metered spend against a plan**: this estimates, it does not report. + +## Usage + +Pass the models to compare with their list prices in US dollars per 1M tokens, and say where the prices come from and +when they were checked in `Description`; without `Models` the panel shows its empty state ("No models to compare"): + +```csharp +calculator.Models = priceList + .Select(p => new LlmModelPrice(p.ApiId, p.Name, p.Provider, input: p.Input, output: p.Output, cachedInput: p.CachedInput)) + .ToList(); +calculator.Description = $"List prices per 1M tokens, checked {priceList.CheckedOn:yyyy-MM-dd}"; +calculator.Note = "List prices, not a quote. Calculated on this device, nothing was sent."; +``` + +The demos on this page compare real models at their vendors' list prices (standard tier, checked 2026-10-29). These are +the prices of short prompts: Claude Haiku 5.5 charges more for prompts over 100K tokens, the GPT-6 models over 272K and +Grok 4.7 from 200K, for the whole request; DeepSeek's prices are its peak rates (off-peak is half), and Gemini 3.8 Flash +is at its introductory price until the end of 2026. The workload presets are invented. + +`CachedInput` is the cache read price; without it cached tokens bill at the input price. `Mark` takes the model's mark +as a 24 × 24 stroked glyph drawn at 16px; without one the row shows the provider's first letter in a tinted square. + +### Your prices and workload + + + +`Usage` is the workload and is two-way: input and output tokens per request, requests a day and whether caching is on. +Every slider step, preset and cache toggle writes it and raises `UsageChanged`. Values between slider stops still +compute exactly; the slider shows the nearest stop. `SelectedId` picks the model (null picks the second one; an unknown +id falls back to the cheapest), and `SelectedModelChanged` reports a row picked by pointer or arrow keys with its cost. +`SelectedModel` and `SelectedCost` always hold the current pick. + +`CacheHitRate` is the share of input billed at the cached rate while Cached input is on (0.6 by default, shown beside +the switch). `DaysPerMonth` sets the billing month (30). + +### The same numbers on the server + +`LlmCostCalculator.ComputeCost` is the arithmetic the panel uses, so you can store the same estimate with a plan or put +it on an invoice: + +```csharp +var cost = LlmCostCalculator.ComputeCost(model, new LlmUsage(16_000, 500, 5_000, Cached: true), cacheHitRate: 0.6); +// cost.Input, cost.CachedInput, cost.Output, cost.Total (dollars a month) and cost.PerRequest +``` + +### Presets + + + +`Presets` are the workload shortcuts in the segmented row (the demos pass Chat, RAG, Agent and Batch). A Custom segment +lights up on its own when the sliders match no preset. Without presets, or with an empty list, the row hides. + +### Loading, errors and no models + +`IsLoading` shows placeholder rows with the list's exact geometry and disables the controls. `ErrorText` shows "Prices +did not load" with your reason in place of the list; set `AllowsRetry` for a Try again button that raises +`RetryRequested`. No `Models`, or an empty list, shows the empty state. The list area keeps one height in every state, and the +figures read as dashes while there is no list. + +```csharp +calculator.IsLoading = true; +try +{ + calculator.Models = await prices.FetchAsync(); + calculator.ErrorText = null; +} +catch (HttpRequestException ex) +{ + calculator.ErrorText = ex.Message; +} +finally +{ + calculator.IsLoading = false; +} +``` + +### Narrow layouts + + + +The panel reads its own width, not the window's. Below 600px the sliders stack. Below 520px the total moves under the +heading, each bar takes its own line under the model name (rows grow from 44 to 58px) and the breakdown wraps into two +columns. Monthly costs switch to compact figures ($420K, $4.2M) from $100,000, so the cost column never overflows; the +total keeps every digit. The panel fills its container and caps at 720px. + +### Your own words + +`Labels` holds every word the panel shows or says; `Title`, `Description` and `Note` override the three texts most apps +change: + +```csharp +calculator.Labels = new LlmCostCalculatorLabels +{ + Title = "Coût mensuel estimé", + PerMonth = "/mois", + Status = (name, cost, rank, count) => $"{name} coûte {cost} par mois, rang {rank} sur {count}.", +}; +``` + +### Content guidelines + +- Prices are inputs. Pass current list prices and say in `Description` where they came from and when. +- Name the tier the prices are for in `Note` when a vendor charges more for long prompts or less off-peak. +- Keep preset labels to one short word. + +## API reference + + + + + + + + + +## Accessibility + +- The panel is a group named by its heading. The model list is a list named "Models ranked by monthly cost, lowest + first", whose rows are radio buttons with a single tab stop on the selected model. +- The accessibility tree lists the rows by rank, so a screen reader reads them in the order shown. Each row's name says + the model, the provider, its rank and its monthly cost. +- The sliders are Arc sliders whose values are spoken as token and request counts, not stop numbers. +- One polite status region announces the selected model's monthly cost and rank once the numbers have been still for + 450ms (not on every drag step). +- Selection is shown by a tinted row and an accent bar, and reported as selected, never by color alone. The error state + pairs an icon with the words "Prices did not load". +- The large total and the rolling figures are hidden from assistive technology where the status region and the row + names already carry the value. +- Pointer focus draws no ring; keyboard focus uses Arc's shared focus ring. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the presets, the three sliders, the Cached input switch, the selected model row, then Try again in the error state | +| Arrow left, Arrow right | On the presets, moves between workloads; on a slider, moves one stop | +| Page up, Page down, Shift + Arrow | On a slider, moves two stops | +| Home, End | On a slider, the smallest or largest stop; in the model list, selects the cheapest or most expensive model | +| Arrow up, Arrow down | In the model list, selects the previous or next model by rank and moves focus with it | +| Space | Toggles Cached input | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A row's rank changes | Glides to its new place by transform on a critically damped spring (about 240ms, no bounce); no reflow | Jumps | Jumps | +| A bar's share changes | Follows the same spring | Jumps | Jumps | +| A price changes | Its digits roll to the new value over 240ms on an ease-out, in tabular figures | Jumps | Jumps | +| The selection changes | The model under the total and the price line crossfade (100ms out, 160ms in); the row tint and accent bar fade over 160ms | Short fade | At once | +| Loading, list, error and empty | The list area crossfades over 160ms at a fixed height | Short fade | At once | +| Loading | The placeholder rows pulse | Still | Still | + +Rows, bars and numbers are drawn, not laid out, while they move, so a drag that re-ranks eight rows runs no layout pass. +Nothing runs while the panel is idle. + +## Related + +- `ContextWindow` for the live token usage of one conversation. +- `AgentTimeline` for the time, tokens and cost of one agent run. +- `Slider` and `SegmentedControl`, which the panel uses for its inputs. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/zh-CN.md new file mode 100644 index 00000000..7f97f208 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/zh-CN.md @@ -0,0 +1,156 @@ +--- +title: LLM cost calculator +summary: 面向 LLM 工作负载的价格计算器:设置每次请求的 token 数、每天的请求数与提示缓存,各模型按月度费用重新排名,条形平滑滑动,价格滚动变化。 +source-hash: 6b4c782fba53 +--- + + + +## 何时使用 + +`LlmCostCalculator` 在一个面板里给出某个 LLM 工作负载可信的月度估算。头部以大号滚动数字显示所选模型的月度总额。预设、三个滑块和一个 Cached input 开关设定工作负载,所有模型按月度费用从低到高排在一个列表中,每行有一根相对于最贵模型的条形。数值变化时,各行滑动到新的名次;选中某一行,总额、明细与价格行就切换为该模型。 + +- **价格页或设置页**:需要展示一个工作负载在多个模型上的费用。 +- **为某项功能挑选模型**:让人看到费用如何随提示长度、输出长度、流量与缓存变化。 +- **内部工具**:在迁移或预算评审前比较各家服务商的价格。 + +## 何时不用 + +- **在聊天或输入框里选择模型**,且不需要比较费用:使用模型菜单。 +- **单个会话的实时 token 用量**:使用 `ContextWindow`。 +- **按套餐计量的实际花费**:本组件只做估算,不做报告。 + +## 用法 + +传入要比较的模型及其标价(每 1M token 的美元价格),并在 `Description` 中说明价格来源与核对日期;没有 `Models` 时,面板显示空状态("No models to compare"): + +```csharp +calculator.Models = priceList + .Select(p => new LlmModelPrice(p.ApiId, p.Name, p.Provider, input: p.Input, output: p.Output, cachedInput: p.CachedInput)) + .ToList(); +calculator.Description = $"List prices per 1M tokens, checked {priceList.CheckedOn:yyyy-MM-dd}"; +calculator.Note = "List prices, not a quote. Calculated on this device, nothing was sent."; +``` + +本页的演示以各厂商的标价(标准档,核对于 2026-10-29)比较真实模型。这些是短提示的价格:Claude Haiku 5.5 对超过 100K token 的提示收取更高费用,GPT-6 系列为超过 272K,Grok 4.7 为达到 200K 起,且按整个请求计费;DeepSeek 的价格是其高峰时段费率(非高峰时段为一半),Gemini 3.8 Flash 的推广价持续到 2026 年底。工作负载预设是虚构的。 + +`CachedInput` 是缓存读取价格;不设置时,缓存的 token 按输入价格计费。`Mark` 接受模型的标志,即 24 × 24 的描边图形,以 16px 绘制;没有标志时,该行在浅色方块中显示服务商名称的首字母。 + +### 你的价格与工作负载 + + + +`Usage` 是工作负载,双向绑定:每次请求的输入与输出 token 数、每天的请求数,以及是否开启缓存。滑块的每一步、每个预设和每次切换缓存都会写入它并触发 `UsageChanged`。介于滑块刻度之间的值仍按精确值计算,滑块显示最近的刻度。`SelectedId` 选择模型(为 null 时选第二个模型;未知的 id 回退到最便宜的模型),通过指针或方向键选中某一行时,`SelectedModelChanged` 会带上该模型及其费用。`SelectedModel` 与 `SelectedCost` 始终是当前的选择。 + +`CacheHitRate` 是开启 Cached input 时按缓存价格计费的输入占比(默认 0.6,显示在开关旁边)。`DaysPerMonth` 设定一个计费月的天数(30)。 + +### 在服务器上得到相同的数字 + +`LlmCostCalculator.ComputeCost` 就是面板使用的算法,因此你可以把同样的估算与套餐一起保存,或写进发票: + +```csharp +var cost = LlmCostCalculator.ComputeCost(model, new LlmUsage(16_000, 500, 5_000, Cached: true), cacheHitRate: 0.6); +// cost.Input, cost.CachedInput, cost.Output, cost.Total (dollars a month) and cost.PerRequest +``` + +### 预设 + + + +`Presets` 是分段控件那一行里的工作负载快捷方式(演示传入 Chat、RAG、Agent 和 Batch)。滑块与任何预设都不匹配时,Custom 分段会自动亮起。不设置预设或传入空列表时,这一行隐藏。 + +### 加载、错误与没有模型 + +`IsLoading` 显示与列表几何完全一致的占位行,并禁用各个控件。`ErrorText` 在列表的位置显示 "Prices did not load" 和你给出的原因;设置 `AllowsRetry` 会出现一个 Try again 按钮,它触发 `RetryRequested`。没有 `Models` 或其为空列表时显示空状态。列表区域在所有状态下保持同一高度,没有列表时各个数字显示为短横线。 + +```csharp +calculator.IsLoading = true; +try +{ + calculator.Models = await prices.FetchAsync(); + calculator.ErrorText = null; +} +catch (HttpRequestException ex) +{ + calculator.ErrorText = ex.Message; +} +finally +{ + calculator.IsLoading = false; +} +``` + +### 窄布局 + + + +面板读取自身的宽度,而不是窗口的宽度。宽度低于 600px 时,滑块纵向堆叠。低于 520px 时,总额移到标题下方,每根条形单独占一行、位于模型名称下方(行高从 44px 增加到 58px),明细折成两列。月度费用从 $100,000 起改用紧凑数字($420K、$4.2M),所以费用列不会溢出;总额保留全部位数。面板填满其容器,最宽 720px。 + +### 自定义文字 + +`Labels` 包含面板显示或朗读的每一个词;`Title`、`Description` 和 `Note` 可以覆盖大多数应用会修改的三段文字: + +```csharp +calculator.Labels = new LlmCostCalculatorLabels +{ + Title = "Coût mensuel estimé", + PerMonth = "/mois", + Status = (name, cost, rank, count) => $"{name} coûte {cost} par mois, rang {rank} sur {count}.", +}; +``` + +### 内容指南 + +- 价格是输入。传入当前的标价,并在 `Description` 中说明来源与日期。 +- 厂商对长提示加价或在非高峰时段降价时,在 `Note` 中写明价格对应的档位。 +- 预设标签保持为一个简短的词。 + +## API 参考 + + + + + + + + + +## 无障碍 + +- 面板是一个以其标题命名的分组。模型列表是一个名为 "Models ranked by monthly cost, lowest first" 的列表,各行是单选按钮,只有所选模型是一个 Tab 停靠点。 +- 无障碍树按名次列出各行,所以屏幕阅读器按显示的顺序朗读。每行的名称包含模型、服务商、名次和月度费用。 +- 滑块是 Arc 滑块,朗读的是 token 数和请求数,而不是刻度序号。 +- 一个礼貌型状态区域在数字静止 450ms 后(而不是拖动的每一步)播报所选模型的月度费用与名次。 +- 选中状态以浅色底和强调色竖条显示,并报告为已选中,从不只依靠颜色。错误状态把图标与 "Prices did not load" 文字放在一起。 +- 大号总额和滚动数字对辅助技术隐藏,因为状态区域和行名称已经包含这些数值。 +- 指针获得焦点时不绘制焦点环;键盘焦点使用 Arc 共用的焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过预设、三个滑块、Cached input 开关、所选模型行,错误状态下再到 Try again | +| 左方向键、右方向键 | 在预设上切换工作负载;在滑块上移动一个刻度 | +| Page Up、Page Down、Shift + 方向键 | 在滑块上移动两个刻度 | +| Home、End | 在滑块上跳到最小或最大的刻度;在模型列表中选中最便宜或最贵的模型 | +| 上方向键、下方向键 | 在模型列表中按名次选中上一个或下一个模型,焦点随之移动 | +| 空格 | 切换 Cached input | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 某行的名次变化 | 通过变换在临界阻尼弹簧上滑到新位置(约 240ms,无回弹),不重排 | 直接跳到 | 直接跳到 | +| 条形的占比变化 | 跟随同一弹簧 | 直接跳到 | 直接跳到 | +| 价格变化 | 各位数字在 240ms 内以 ease-out 曲线滚动到新值,使用等宽数字 | 直接跳到 | 直接跳到 | +| 选择变化 | 总额下方的模型名与价格行交叉淡化(淡出 100ms,淡入 160ms);行的浅色底与强调色竖条在 160ms 内淡入淡出 | 短暂淡化 | 立即 | +| 加载、列表、错误与空状态 | 列表区域以固定高度在 160ms 内交叉淡化 | 短暂淡化 | 立即 | +| 加载 | 占位行呼吸闪烁 | 静止 | 静止 | + +行、条形和数字在运动时是绘制出来的,不参与布局,所以一次让八行重新排名的拖动不会触发布局。面板空闲时不运行任何东西。 + +## 相关组件 + +- `ContextWindow`:单个会话的实时 token 用量。 +- `AgentTimeline`:一次智能体运行的时间、token 与费用。 +- `Slider` 与 `SegmentedControl`:面板的输入控件。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/zh-TW.md new file mode 100644 index 00000000..83bdc992 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/LlmCostCalculator/zh-TW.md @@ -0,0 +1,156 @@ +--- +title: LLM cost calculator +summary: 針對 LLM 工作負載的價格試算器:設定每次請求的 token 數、每天的請求數與提示快取,各模型依月費重新排名,長條平滑滑動,價格滾動變化。 +source-hash: 6b4c782fba53 +--- + + + +## 何時使用 + +`LlmCostCalculator` 在一個面板裡提供某個 LLM 工作負載可信的每月估算。頁首以大型滾動數字顯示所選模型的每月總額。預設、三個滑桿和一個 Cached input 開關設定工作負載,所有模型依月費由低到高排在一份清單中,每列有一根相對於最貴模型的長條。數值改變時,各列滑動到新的名次;選取某一列,總額、明細與價格列就切換為該模型。 + +- **價格頁或設定頁**:需要呈現一個工作負載在多個模型上的費用。 +- **為某項功能挑選模型**:讓使用者看到費用如何隨提示長度、輸出長度、流量與快取變化。 +- **內部工具**:在遷移或預算審查前比較各家供應商的價格。 + +## 何時不用 + +- **在聊天或輸入框裡選擇模型**,且不需要比較費用:請使用模型選單。 +- **單一對話的即時 token 用量**:請使用 `ContextWindow`。 +- **依方案計量的實際花費**:本元件只做估算,不做報告。 + +## 用法 + +傳入要比較的模型及其牌價(每 1M token 的美元價格),並在 `Description` 中說明價格來源與核對日期;沒有 `Models` 時,面板顯示空狀態("No models to compare"): + +```csharp +calculator.Models = priceList + .Select(p => new LlmModelPrice(p.ApiId, p.Name, p.Provider, input: p.Input, output: p.Output, cachedInput: p.CachedInput)) + .ToList(); +calculator.Description = $"List prices per 1M tokens, checked {priceList.CheckedOn:yyyy-MM-dd}"; +calculator.Note = "List prices, not a quote. Calculated on this device, nothing was sent."; +``` + +本頁的展示以各廠商的牌價(標準級距,核對於 2026-10-29)比較真實模型。這些是短提示的價格:Claude Haiku 5.5 對超過 100K token 的提示收取較高費用,GPT-6 系列為超過 272K,Grok 4.7 為達到 200K 起,且依整個請求計費;DeepSeek 的價格是其尖峰時段費率(離峰時段為一半),Gemini 3.8 Flash 的推廣價持續到 2026 年底。工作負載預設是虛構的。 + +`CachedInput` 是快取讀取價格;未設定時,快取的 token 依輸入價格計費。`Mark` 接受模型的標誌,也就是 24 × 24 的描邊圖形,以 16px 繪製;沒有標誌時,該列會在淺色方塊中顯示供應商名稱的第一個字母。 + +### 你的價格與工作負載 + + + +`Usage` 是工作負載,採雙向繫結:每次請求的輸入與輸出 token 數、每天的請求數,以及是否開啟快取。滑桿的每一步、每個預設和每次切換快取都會寫入它並觸發 `UsageChanged`。介於滑桿刻度之間的值仍依精確值計算,滑桿顯示最接近的刻度。`SelectedId` 選擇模型(為 null 時選第二個模型;未知的 id 會退回最便宜的模型),透過指標或方向鍵選取某一列時,`SelectedModelChanged` 會帶上該模型及其費用。`SelectedModel` 與 `SelectedCost` 永遠是目前的選擇。 + +`CacheHitRate` 是開啟 Cached input 時依快取價格計費的輸入比例(預設 0.6,顯示在開關旁邊)。`DaysPerMonth` 設定一個計費月的天數(30)。 + +### 在伺服器上得到相同的數字 + +`LlmCostCalculator.ComputeCost` 就是面板使用的演算法,因此你可以把同樣的估算與方案一起儲存,或寫進發票: + +```csharp +var cost = LlmCostCalculator.ComputeCost(model, new LlmUsage(16_000, 500, 5_000, Cached: true), cacheHitRate: 0.6); +// cost.Input, cost.CachedInput, cost.Output, cost.Total (dollars a month) and cost.PerRequest +``` + +### 預設 + + + +`Presets` 是分段控制項那一列裡的工作負載捷徑(展示傳入 Chat、RAG、Agent 和 Batch)。滑桿與任何預設都不相符時,Custom 分段會自動亮起。未設定預設或傳入空清單時,這一列隱藏。 + +### 載入、錯誤與沒有模型 + +`IsLoading` 顯示與清單幾何完全一致的佔位列,並停用各個控制項。`ErrorText` 在清單的位置顯示 "Prices did not load" 和你提供的原因;設定 `AllowsRetry` 會出現一個 Try again 按鈕,它會觸發 `RetryRequested`。沒有 `Models` 或其為空清單時顯示空狀態。清單區域在所有狀態下維持同一高度,沒有清單時各個數字顯示為短橫線。 + +```csharp +calculator.IsLoading = true; +try +{ + calculator.Models = await prices.FetchAsync(); + calculator.ErrorText = null; +} +catch (HttpRequestException ex) +{ + calculator.ErrorText = ex.Message; +} +finally +{ + calculator.IsLoading = false; +} +``` + +### 窄版面 + + + +面板讀取自身的寬度,而不是視窗的寬度。寬度低於 600px 時,滑桿改為垂直堆疊。低於 520px 時,總額移到標題下方,每根長條各占一行、位於模型名稱下方(列高從 44px 增加到 58px),明細折成兩欄。每月費用從 $100,000 起改用精簡數字($420K、$4.2M),所以費用欄不會溢出;總額保留全部位數。面板會填滿其容器,最寬 720px。 + +### 你自己的文字 + +`Labels` 包含面板顯示或朗讀的每一個詞;`Title`、`Description` 和 `Note` 可以覆寫大多數應用程式會修改的三段文字: + +```csharp +calculator.Labels = new LlmCostCalculatorLabels +{ + Title = "Coût mensuel estimé", + PerMonth = "/mois", + Status = (name, cost, rank, count) => $"{name} coûte {cost} par mois, rang {rank} sur {count}.", +}; +``` + +### 內容指引 + +- 價格是輸入。傳入目前的牌價,並在 `Description` 中說明來源與日期。 +- 廠商對長提示加價或在離峰時段降價時,在 `Note` 中寫明價格對應的級距。 +- 預設標籤維持一個簡短的詞。 + +## API 參考 + + + + + + + + + +## 無障礙 + +- 面板是一個以其標題命名的群組。模型清單是一份名為 "Models ranked by monthly cost, lowest first" 的清單,各列是選項按鈕,只有所選模型是一個 Tab 停駐點。 +- 無障礙樹依名次列出各列,所以螢幕閱讀器會依顯示的順序朗讀。每列的名稱包含模型、供應商、名次和每月費用。 +- 滑桿是 Arc 滑桿,朗讀的是 token 數和請求數,而不是刻度序號。 +- 一個禮貌型狀態區域在數字靜止 450ms 後(而不是拖曳的每一步)播報所選模型的每月費用與名次。 +- 選取狀態以淺色底和強調色直條呈現,並回報為已選取,從不只依靠顏色。錯誤狀態把圖示與 "Prices did not load" 文字放在一起。 +- 大型總額和滾動數字對輔助科技隱藏,因為狀態區域和列名稱已經包含這些數值。 +- 指標取得焦點時不繪製焦點環;鍵盤焦點使用 Arc 共用的焦點環。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過預設、三個滑桿、Cached input 開關、所選模型列,錯誤狀態下再到 Try again | +| 左方向鍵、右方向鍵 | 在預設上切換工作負載;在滑桿上移動一個刻度 | +| Page Up、Page Down、Shift + 方向鍵 | 在滑桿上移動兩個刻度 | +| Home、End | 在滑桿上跳到最小或最大的刻度;在模型清單中選取最便宜或最貴的模型 | +| 上方向鍵、下方向鍵 | 在模型清單中依名次選取上一個或下一個模型,焦點隨之移動 | +| 空白鍵 | 切換 Cached input | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 某列的名次變化 | 透過變換在臨界阻尼彈簧上滑到新位置(約 240ms,無回彈),不重新排版 | 直接跳到 | 直接跳到 | +| 長條的比例變化 | 跟隨同一個彈簧 | 直接跳到 | 直接跳到 | +| 價格變化 | 各位數字在 240ms 內以 ease-out 曲線滾動到新值,使用等寬數字 | 直接跳到 | 直接跳到 | +| 選取變化 | 總額下方的模型名稱與價格列交叉淡化(淡出 100ms,淡入 160ms);列的淺色底與強調色直條在 160ms 內淡入淡出 | 短暫淡化 | 立即 | +| 載入、清單、錯誤與空狀態 | 清單區域以固定高度在 160ms 內交叉淡化 | 短暫淡化 | 立即 | +| 載入 | 佔位列呼吸閃爍 | 靜止 | 靜止 | + +列、長條和數字在移動時是繪製出來的,不參與排版,所以一次讓八列重新排名的拖曳不會觸發排版。面板閒置時不執行任何動作。 + +## 相關 + +- `ContextWindow`:單一對話的即時 token 用量。 +- `AgentTimeline`:一次代理程式執行的時間、token 與費用。 +- `Slider` 與 `SegmentedControl`:面板的輸入控制項。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/en-US.md new file mode 100644 index 00000000..2e2cb7a8 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/en-US.md @@ -0,0 +1,244 @@ +--- +title: Meeting summary +summary: AI notes for a call: the platform, attendees and length, then Summary, Decisions and Action items that stream in, each line linked to its transcript quote and each task ready to check off or send to a tracker. +--- + + + +## When to use + +`MeetingSummary` is the card an AI notetaker leaves after a call. The header names the platform, the date, the length and +the people on the call. A short summarizing pass gives way to three sections that stream in a few words at a time: +Summary, Decisions and Action items. Every line keeps a timestamp, and hovering the line (or focusing its timestamp) +shows the transcript quote it came from, so readers can check a point against what was actually said. Action items can +be checked off and sent to an issue tracker or a docs app. + +- **Notes after a call** in a CRM, a recruiting tool, a meeting recorder or a team workspace. +- **Turning a transcript into decisions and owned tasks** that people push to their tracker. +- **Any summary that should be checkable** against its source, line by line. + +## When not to use + +- **Live captions or a running transcript** during the call: show the transcript itself as it arrives. +- **A general task list** with no meeting behind it: use rows of `Checkbox` in your own list. +- **Long documents with many sections**: render them as a document page instead of a card. + +## Usage + +Give the card the notes in `Meeting` and drive `Status` yourself: `Summarizing` while your request runs, `Ready` when it +resolves (the notes then stream in), `Error` with `ErrorText` when it fails. Leave `Status` unset and the card plays its +own short summarizing pass when it scrolls into view. With no `Meeting`, no `Summarizer` and no `Simulation` the card +starts on its no transcript state ("No transcript to summarize") with no header details. + +```csharp +card.Meeting = new MeetingSummaryData +{ + Title = "Onboarding flow review", + Platform = "Video call", + StartsAt = new DateTime(2026, 10, 27, 10, 30, 0), + DurationMinutes = 48, + Attendees = [new("ines", "Ines Duarte", "Design"), new("kofi", "Kofi Mensah")], + Summary = [new("s1", "Most new teams stall on the invite step.", + new MeetingSummaryQuote("ines", TimeSpan.FromSeconds(252), "Seven of the ten teams got stuck on invites."))], + ActionItems = [new MeetingActionItem("a1", "Move the invite step after project creation", "kofi", + new DateOnly(2026, 11, 3))], +}; +card.Status = MeetingSummaryStatus.Summarizing; +// ... when your request resolves: +card.Status = MeetingSummaryStatus.Ready; +``` + +`Quote.At` is the time from the start of the recording; the card shows it as `4:12`. `StartsAt` is printed as given, so +convert time zones before you pass it. + +### Simulation + +To show the card without a model (a demo, a design review, an onboarding tour), give it a `MeetingSummarySimulation`: +the notes, the model to credit and how simulated sends are numbered. When the card scrolls into view it plays its +summarizing pass, streams the notes in and, unless someone uses the card first, ticks off the second action item and +sends the first one; sends are simulated and say that nothing was sent. Your own `Meeting` or `Summarizer` wins over the +simulation, and setting `Simulation` to null stops it and shows the no transcript state. + +```csharp +card.Simulation = new MeetingSummarySimulation +{ + Meeting = notes, + Model = new AgentModel("Qwen3.8-Max", ArcIcons.Sparkles), + ReferenceFormats = new Dictionary { ["issues"] = "ISS-{0}", ["docs"] = "Notes {0}" }, + FirstReferenceNumber = 142, +}; +``` + +The preview above plays a simulation whose call is the gallery's own demo data. + +### Your meeting + + + +This card's data, status and sends belong to the app. Regenerate in the footer and Try again in the error state raise +`RegenerateRequested`; a card that drives its own status also replays its pass. + +### Streaming from your model + + + +`Summarizer` is an async streaming delegate. When the card drives its own status it calls it on first view and on +Regenerate, shows the summarizing pass until the first snapshot, then streams every snapshot you yield (each one is the +notes so far). Text that still matches keeps streaming, and an exception shows the error state with its message. Set +`Meeting` to the call's header so the title and attendees show while it summarizes. + +```csharp +card.Meeting = header; // title, date and attendees, no notes yet +card.Summarizer = SummarizeAsync; + +async IAsyncEnumerable SummarizeAsync([EnumeratorCancellation] CancellationToken token) +{ + await foreach (var notes in myModel.StreamNotesAsync(transcript, token)) + { + yield return notes; + } +} +``` + +### Sending action items + +Each action item has one send button per entry in `SendTargets`: a generic `Issues` and `Docs` by default (pass your own +`MeetingSummarySendTarget` with your product's mark, or an empty list to hide the buttons). Pressing one raises +`SendRequested`; set `Completion` to your request and pass the token on. The button shows a spinner, then a chip with the +reference you return ("ENG-31") and a check, or an alert and Retry when the task faults. Without a handler the send is +simulated and the live region says that nothing was sent; its chip reads "Sent", or the reference the simulation's +`ReferenceFormats` gives. + +```csharp +card.SendRequested += (_, e) => e.Completion = CreateIssueAsync(e.Item, e.Target, e.CancellationToken); + +async Task CreateIssueAsync(MeetingActionItem item, MeetingSummarySendTarget target, + CancellationToken token) +{ + var issue = await tracker.CreateAsync(item.Text, token); + return new MeetingSummarySendResult(issue.Key, issue.Url); +} +``` + +`ActionItemSent` follows a successful send. The token is cancelled when the card is removed or regenerated, and the +button returns to its mark. A sent chip with a `Url` opens it. + +### Checking items off + +Checking an item raises `ActionItemChanged` with the item and its new state; the card keeps the checked state itself. +An item whose `IsDone` is true starts checked. + +### Transcript quotes + +Hover any line for a short moment and its quote opens under it (it flips above when the window runs out of room). +While a quote is open, moving to another line switches at once. Focusing a timestamp with the keyboard shows its quote; +Enter, Space or a tap pins it, and Escape or a press elsewhere closes it. + +### States + +| Status | Body | +|---|---| +| `Summarizing` | A shimmering "Summarizing transcript" over placeholder lines | +| `Ready` | Summary, Decisions and Action items (sections with no lines are left out) | +| `Ready`, empty lists | "No transcript to summarize" | +| `Error` | "Couldn't summarize this meeting", `ErrorText` and Try again | + +### Narrow cards + + + +The card fills its container up to 640px and holds down to about 360px. Below 460px the attendee stack moves under the +date; below 520px owners show their first name so the meta under each item stays on one line (screen readers still +hear the full name). `AutoPlay="False"` starts on the finished notes. + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +card.Labels = new MeetingSummaryLabels +{ + Summary = "Résumé", + ActionItems = "Actions", + Due = due => "Pour le " + due.ToString("d MMM"), +}; +``` + +`Model` sets the model credited in the footer (an `AgentModel` with a name and a mark); null hides the credit. +`MeetingSummaryData.PlatformMark` replaces the video glyph with your platform's mark. + +### Reduced motion + +With reduced motion the notes appear whole instead of streaming, the body and send slots change size at once, and the +checkbox, strikethrough and quote show their end states without drawing or sliding. Every announcement still happens. +The summarizing pass waits until the card is on screen. + +### Content guidelines + +- A summary line is one sentence about what happened; a decision says what was decided, not who argued. +- Action items start with a verb and name one owner. +- Keep each quote to the sentence or two that backs the line. + +### Common questions + +**Can I show notes that are already done?** Set `Status` to `Ready` (or `AutoPlay` to false) and the notes show whole. + +**Who keeps the checked state?** The card. `ActionItemChanged` tells you about each change; `IsDone` seeds the first state. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- The card is a group named after the meeting, with the title as a level 2 heading and each section as a level 3 heading. +- Timestamps are buttons that report expanded while their quote is open; the quote's text is their help text. +- Each checkbox is a button with the checkbox role and its checked state, and each checkbox and send button is named with + the action item's text, so a list of them still reads clearly. +- The 16px checkbox has a 32px hit area, and the keyboard focus ring hugs the square. +- A polite live region announces summarizing, the finished summary, checked items and sends, and says "Simulated, + nothing was sent" when no handler sent it. +- Status never relies on color: a failed send shows an alert icon with Retry, a sent item shows the target's mark, its + reference and a check. +- The platform name is read before the date, and owners shown by first name keep their full name for screen readers. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the attendees, each timestamp, checkbox and send button, then copy and regenerate | +| Focus on a timestamp | Shows the transcript quote for that line | +| Enter, Space | On a timestamp, pins or closes its quote; on a checkbox, toggles it; on a send button, sends the item | +| Escape | Closes an open quote | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Summarizing | About one second, the label shimmers | Same, no shimmer | Same, no shimmer | +| Notes arrive | 3 to 8 words every 34ms, each chunk fades in (about 1.5s in all) | Whole at once | Whole at once | +| Body between states | Height springs (0.24s, no bounce), the new state fades in | At once | At once | +| Owner, date, buttons | Keep their space while the text streams, fade in once it is whole | At once | At once | +| Check an item | Fill grows in (160ms), check draws on (200ms ease-out), a 1px strikethrough sweeps across every wrapped line, text eases to muted | End state at once | End state at once | +| Uncheck | The same in reverse, faster (120ms) | At once | At once | +| Press a checkbox | The square scales to 0.9, never the row | Same | Same | +| Send | Mark, spinner and sent chip crossfade while the slot springs to its width (0.25s, no bounce) | Short crossfade, size at once | At once | +| Quote | Fades in over 160ms rising 3px after a short hover; switches between lines at once | Fades | At once | + +## Related + +- `ToolCall` for the tool calls an agent makes. +- `AgentStatus` for the live status of a running agent. +- `Checkbox` for a plain checklist. +- `Avatar` and `AvatarGroup` for people. +- `CopyButton` for copying a value with confirmation. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/zh-CN.md new file mode 100644 index 00000000..b125c7fe --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/zh-CN.md @@ -0,0 +1,212 @@ +--- +title: Meeting summary +summary: 一次通话的 AI 纪要:平台、参会者和时长,随后是流式出现的摘要、决定和待办事项;每一行都链接到它出自的转录原话,每项任务都可以勾选完成或发送到任务跟踪工具。 +source-hash: 3e51431ba295 +--- + + + +## 何时使用 + +`MeetingSummary` 是 AI 记录员在通话结束后留下的卡片。头部写明平台、日期、时长和参会的人。一段简短的总结过程之后,三个分区以每次几个词的节奏流式出现:摘要(Summary)、决定(Decisions)和待办事项(Action items)。每一行都带有时间戳,悬停在这一行上(或聚焦它的时间戳)会显示它出自的转录原话,读者可以把要点和实际说过的话逐条核对。待办事项可以勾选完成,也可以发送到问题跟踪工具或文档应用。 + +- **通话结束后的纪要**:用于 CRM、招聘工具、会议录制工具或团队工作区。 +- **把转录变成决定和有负责人的任务**,再由大家推送到自己的跟踪工具。 +- **任何需要可核对的摘要**:每一行都能对照它的出处。 + +## 何时不用 + +- **通话进行中的实时字幕或滚动转录**:直接显示陆续到达的转录本身。 +- **背后没有会议的普通任务列表**:在你自己的列表里使用一行行 `Checkbox`。 +- **包含很多章节的长文档**:把它渲染成文档页面,而不是卡片。 + +## 用法 + +把纪要放进 `Meeting`,并自己驱动 `Status`:请求进行中设为 `Summarizing`,请求完成时设为 `Ready`(纪要随后流式出现),失败时设为 `Error` 并附上 `ErrorText`。不设置 `Status` 时,卡片在滚动进入视图后会自己播放一段简短的总结过程。既没有 `Meeting`、`Summarizer` 也没有 `Simulation` 时,卡片直接显示"无转写记录"状态("No transcript to summarize"),页眉不显示详情。 + +```csharp +card.Meeting = new MeetingSummaryData +{ + Title = "Onboarding flow review", + Platform = "Video call", + StartsAt = new DateTime(2026, 10, 27, 10, 30, 0), + DurationMinutes = 48, + Attendees = [new("ines", "Ines Duarte", "Design"), new("kofi", "Kofi Mensah")], + Summary = [new("s1", "Most new teams stall on the invite step.", + new MeetingSummaryQuote("ines", TimeSpan.FromSeconds(252), "Seven of the ten teams got stuck on invites."))], + ActionItems = [new MeetingActionItem("a1", "Move the invite step after project creation", "kofi", + new DateOnly(2026, 11, 3))], +}; +card.Status = MeetingSummaryStatus.Summarizing; +// ... when your request resolves: +card.Status = MeetingSummaryStatus.Ready; +``` + +`Quote.At` 是从录音开始算起的时间,卡片把它显示为 `4:12`。`StartsAt` 按原样打印,因此请在传入之前完成时区换算。 + +### 模拟 + +想在没有模型的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `MeetingSummarySimulation`:纪要、要署名的模型,以及模拟发送的编号方式。卡片滚动进入视图后,会播放总结过程、流式显示纪要,并且只要没有人先操作卡片,就会勾选第二个待办事项并发送第一个;发送是模拟的,并会说明什么都没有发送。你自己的 `Meeting` 或 `Summarizer` 优先于模拟;把 `Simulation` 设为 null 会停止模拟并显示"无转写记录"状态。 + +```csharp +card.Simulation = new MeetingSummarySimulation +{ + Meeting = notes, + Model = new AgentModel("Qwen3.8-Max", ArcIcons.Sparkles), + ReferenceFormats = new Dictionary { ["issues"] = "ISS-{0}", ["docs"] = "Notes {0}" }, + FirstReferenceNumber = 142, +}; +``` + +上方的预览播放的就是一个模拟,其会议来自图库自己的演示数据。 + +### 你自己的会议 + + + +这张卡片的数据、状态和发送都由应用掌管。页脚的重新生成和错误状态中的重试都会触发 `RegenerateRequested`;自己驱动状态的卡片还会重新播放它的总结过程。 + +### 从你的模型流式获取 + + + +`Summarizer` 是一个异步流式委托。卡片自己驱动状态时,会在首次进入视图和重新生成时调用它:在第一个快照到达之前显示总结过程,之后流式显示你产出的每一个快照(每一个都是截至目前的纪要)。仍然吻合的文字会继续流式出现,抛出异常则会显示带有异常消息的错误状态。请把 `Meeting` 设为这次通话的头部信息,这样在总结期间也能显示标题和参会者。 + +```csharp +card.Meeting = header; // title, date and attendees, no notes yet +card.Summarizer = SummarizeAsync; + +async IAsyncEnumerable SummarizeAsync([EnumeratorCancellation] CancellationToken token) +{ + await foreach (var notes in myModel.StreamNotesAsync(transcript, token)) + { + yield return notes; + } +} +``` + +### 发送待办事项 + +每个待办事项为 `SendTargets` 中的每一项提供一个发送按钮:默认是通用的 `Issues` 和 `Docs`(可以传入带有你产品标志的 `MeetingSummarySendTarget`,或传入空列表以隐藏按钮)。按下按钮会触发 `SendRequested`;把 `Completion` 设为你的请求,并把令牌传下去。按钮先显示加载圈,然后显示一个标签,内含你返回的编号("ENG-31")和一个对勾;任务出错时则显示警示图标和 Retry。没有处理程序时,发送是模拟的,实时区域会说明什么都没有发送;其标签显示"Sent",或模拟的 `ReferenceFormats` 给出的编号。 + +```csharp +card.SendRequested += (_, e) => e.Completion = CreateIssueAsync(e.Item, e.Target, e.CancellationToken); + +async Task CreateIssueAsync(MeetingActionItem item, MeetingSummarySendTarget target, + CancellationToken token) +{ + var issue = await tracker.CreateAsync(item.Text, token); + return new MeetingSummarySendResult(issue.Key, issue.Url); +} +``` + +发送成功后会触发 `ActionItemSent`。卡片被移除或重新生成时,令牌会被取消,按钮回到它的标志。带有 `Url` 的已发送标签点击后会打开该链接。 + +### 勾选待办事项 + +勾选一个事项会触发 `ActionItemChanged`,参数中带有该事项及其新状态;勾选状态由卡片自己保存。`IsDone` 为 true 的事项一开始就是勾选状态。 + +### 转录原话 + +在任意一行上稍作悬停,它的原话就会在下方打开(窗口空间不足时翻到上方)。原话打开期间,移到另一行会立即切换。用键盘聚焦时间戳会显示它的原话;Enter、Space 或轻点会把它固定住,Escape 或在别处按下则会关闭。 + +### 状态 + +| Status | 主体 | +|---|---| +| `Summarizing` | 在占位行上方闪烁的 "Summarizing transcript" | +| `Ready` | 摘要、决定和待办事项(没有内容的分区不显示) | +| `Ready`,列表为空 | "No transcript to summarize" | +| `Error` | "Couldn't summarize this meeting"、`ErrorText` 和 Try again | + +### 窄卡片 + + + +卡片会填满容器,最宽 640px,最窄可到约 360px。宽度低于 460px 时,参会者头像组移到日期下方;低于 520px 时,负责人只显示名字,让每个事项下的信息保持在一行(屏幕阅读器仍会读出全名)。`AutoPlay="False"` 让卡片一开始就显示完成的纪要。 + +### 你自己的文字 + +`Labels` 包含卡片显示或朗读的每一个文字。替换这个对象即可本地化或改写: + +```csharp +card.Labels = new MeetingSummaryLabels +{ + Summary = "Résumé", + ActionItems = "Actions", + Due = due => "Pour le " + due.ToString("d MMM"), +}; +``` + +`Model` 设置页脚中署名的模型(一个带名称和标志的 `AgentModel`);设为 null 则隐藏署名。`MeetingSummaryData.PlatformMark` 用你的平台标志替换视频图标。 + +### 减弱动效 + +减弱动效时,纪要整段出现而不是流式出现,主体和发送按钮位置的尺寸立即改变,复选框、删除线和原话直接显示最终状态,不做描绘或滑动。所有播报照常进行。总结过程会等到卡片出现在屏幕上才开始。 + +### 内容准则 + +- 一条摘要是一句话,说明发生了什么;一条决定说明决定了什么,而不是谁争论了什么。 +- 待办事项以动词开头,并只写一位负责人。 +- 每段原话只保留支撑这一行的一两句话。 + +### 常见问题 + +**能显示已经完成的纪要吗?** 把 `Status` 设为 `Ready`(或把 `AutoPlay` 设为 false),纪要就会整段显示。 + +**勾选状态由谁保存?** 由卡片保存。`ActionItemChanged` 会告诉你每一次变化;`IsDone` 提供初始状态。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 卡片是一个以会议命名的分组,标题是二级标题,每个分区是三级标题。 +- 时间戳是按钮,原话打开期间报告为已展开;原话的文字是它的帮助文本。 +- 每个复选框都是具有复选框角色和勾选状态的按钮,每个复选框和发送按钮都以待办事项的文字命名,因此即使连成一串也能读得清楚。 +- 16px 的复选框有 32px 的点击区域,键盘焦点环紧贴方框。 +- 一个礼貌型实时区域会播报总结开始、总结完成、勾选的事项和发送结果;没有处理程序时还会说明 "Simulated, nothing was sent"。 +- 状态从不只依赖颜色:发送失败显示警示图标和 Retry,已发送的事项显示目标的标志、编号和对勾。 +- 平台名称先于日期读出;只显示名字的负责人对屏幕阅读器仍保留全名。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过参会者、每个时间戳、复选框和发送按钮,然后是复制和重新生成 | +| 聚焦时间戳 | 显示这一行的转录原话 | +| Enter、Space | 在时间戳上固定或关闭原话;在复选框上切换勾选;在发送按钮上发送该事项 | +| Escape | 关闭打开的原话 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 总结中 | 约一秒,标签闪烁 | 相同,不闪烁 | 相同,不闪烁 | +| 纪要到达 | 每 34ms 出现 3 到 8 个词,每块淡入(总共约 1.5 秒) | 立即整段出现 | 立即整段出现 | +| 主体在状态之间切换 | 高度弹簧过渡(0.24 秒,无回弹),新状态淡入 | 立即 | 立即 | +| 负责人、日期、按钮 | 文字流式出现时保留位置,文字完整后淡入 | 立即 | 立即 | +| 勾选事项 | 填充放大出现(160ms),对勾描绘出来(200ms,ease-out),1px 删除线横扫每一个换行,文字过渡为弱化色 | 立即到最终状态 | 立即到最终状态 | +| 取消勾选 | 反向进行,更快(120ms) | 立即 | 立即 | +| 按下复选框 | 方框缩放到 0.9,整行不缩放 | 相同 | 相同 | +| 发送 | 标志、加载圈和已发送标签交叉淡化,按钮位置弹簧过渡到新宽度(0.25 秒,无回弹) | 短暂交叉淡化,尺寸立即改变 | 立即 | +| 原话 | 短暂悬停后用 160ms 淡入并上升 3px;在行之间立即切换 | 淡入 | 立即 | + +## 相关组件 + +- `ToolCall`:智能体发起的工具调用。 +- `AgentStatus`:正在运行的智能体的实时状态。 +- `Checkbox`:普通的勾选清单。 +- `Avatar` 和 `AvatarGroup`:人员。 +- `CopyButton`:复制一个值并给出确认。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/zh-TW.md new file mode 100644 index 00000000..9e9c111f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/MeetingSummary/zh-TW.md @@ -0,0 +1,212 @@ +--- +title: Meeting summary +summary: 一次通話的 AI 會議紀錄:平台、與會者和時長,接著是串流出現的摘要、決議和待辦事項;每一行都連結到它出自的逐字稿原話,每項任務都可以勾選完成或傳送到議題追蹤工具。 +source-hash: 3e51431ba295 +--- + + + +## 何時使用 + +`MeetingSummary` 是 AI 記錄員在通話結束後留下的卡片。頁首寫明平台、日期、時長和與會的人。一段簡短的彙整過程之後,三個區段以每次幾個字詞的節奏串流出現:摘要(Summary)、決議(Decisions)和待辦事項(Action items)。每一行都帶有時間戳記,滑鼠停在這一行上(或聚焦它的時間戳記)會顯示它出自的逐字稿原話,讀者可以把重點和實際說過的話逐條核對。待辦事項可以勾選完成,也可以傳送到議題追蹤工具或文件應用程式。 + +- **通話結束後的會議紀錄**:用於 CRM、招募工具、會議錄製工具或團隊工作區。 +- **把逐字稿變成決議和有負責人的任務**,再由大家推送到自己的追蹤工具。 +- **任何需要可核對的摘要**:每一行都能對照它的出處。 + +## 何時不用 + +- **通話進行中的即時字幕或捲動逐字稿**:直接顯示陸續抵達的逐字稿本身。 +- **背後沒有會議的一般任務清單**:在你自己的清單裡使用一列列 `Checkbox`。 +- **包含很多章節的長文件**:把它算繪成文件頁面,而不是卡片。 + +## 用法 + +把會議紀錄放進 `Meeting`,並自己驅動 `Status`:請求進行中設為 `Summarizing`,請求完成時設為 `Ready`(紀錄隨後串流出現),失敗時設為 `Error` 並附上 `ErrorText`。不設定 `Status` 時,卡片在捲動進入畫面後會自己播放一段簡短的彙整過程。既沒有 `Meeting`、`Summarizer` 也沒有 `Simulation` 時,卡片直接顯示「無逐字稿」狀態("No transcript to summarize"),頁首不顯示詳細資訊。 + +```csharp +card.Meeting = new MeetingSummaryData +{ + Title = "Onboarding flow review", + Platform = "Video call", + StartsAt = new DateTime(2026, 10, 27, 10, 30, 0), + DurationMinutes = 48, + Attendees = [new("ines", "Ines Duarte", "Design"), new("kofi", "Kofi Mensah")], + Summary = [new("s1", "Most new teams stall on the invite step.", + new MeetingSummaryQuote("ines", TimeSpan.FromSeconds(252), "Seven of the ten teams got stuck on invites."))], + ActionItems = [new MeetingActionItem("a1", "Move the invite step after project creation", "kofi", + new DateOnly(2026, 11, 3))], +}; +card.Status = MeetingSummaryStatus.Summarizing; +// ... when your request resolves: +card.Status = MeetingSummaryStatus.Ready; +``` + +`Quote.At` 是從錄音開始算起的時間,卡片把它顯示為 `4:12`。`StartsAt` 依原樣印出,因此請在傳入之前完成時區換算。 + +### 模擬 + +想在沒有模型的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `MeetingSummarySimulation`:會議紀錄、要署名的模型,以及模擬傳送的編號方式。卡片捲動進入畫面後,會播放彙整過程、串流顯示紀錄,並且只要沒有人先操作卡片,就會勾選第二個待辦事項並傳送第一個;傳送是模擬的,並會說明什麼都沒有傳送。你自己的 `Meeting` 或 `Summarizer` 優先於模擬;把 `Simulation` 設為 null 會停止模擬並顯示「無逐字稿」狀態。 + +```csharp +card.Simulation = new MeetingSummarySimulation +{ + Meeting = notes, + Model = new AgentModel("Qwen3.8-Max", ArcIcons.Sparkles), + ReferenceFormats = new Dictionary { ["issues"] = "ISS-{0}", ["docs"] = "Notes {0}" }, + FirstReferenceNumber = 142, +}; +``` + +上方的預覽播放的就是一個模擬,其會議來自圖庫自己的示範資料。 + +### 你自己的會議 + + + +這張卡片的資料、狀態和傳送都由應用程式掌管。頁尾的重新產生和錯誤狀態中的重試都會觸發 `RegenerateRequested`;自己驅動狀態的卡片還會重新播放它的彙整過程。 + +### 從你的模型串流取得 + + + +`Summarizer` 是一個非同步串流委派。卡片自己驅動狀態時,會在第一次進入畫面和重新產生時呼叫它:在第一個快照抵達之前顯示彙整過程,之後串流顯示你產出的每一個快照(每一個都是截至目前的紀錄)。仍然吻合的文字會繼續串流出現,擲出例外則會顯示帶有例外訊息的錯誤狀態。請把 `Meeting` 設為這次通話的頁首資訊,這樣在彙整期間也能顯示標題和與會者。 + +```csharp +card.Meeting = header; // title, date and attendees, no notes yet +card.Summarizer = SummarizeAsync; + +async IAsyncEnumerable SummarizeAsync([EnumeratorCancellation] CancellationToken token) +{ + await foreach (var notes in myModel.StreamNotesAsync(transcript, token)) + { + yield return notes; + } +} +``` + +### 傳送待辦事項 + +每個待辦事項為 `SendTargets` 中的每一項提供一個傳送按鈕:預設是通用的 `Issues` 和 `Docs`(可以傳入帶有你產品標誌的 `MeetingSummarySendTarget`,或傳入空清單以隱藏按鈕)。按下按鈕會觸發 `SendRequested`;把 `Completion` 設為你的請求,並把權杖傳下去。按鈕先顯示載入圈,然後顯示一個標籤,內含你傳回的編號("ENG-31")和一個勾號;任務出錯時則顯示警示圖示和 Retry。沒有處理常式時,傳送是模擬的,即時區域會說明什麼都沒有傳送;其標籤顯示「Sent」,或模擬的 `ReferenceFormats` 提供的編號。 + +```csharp +card.SendRequested += (_, e) => e.Completion = CreateIssueAsync(e.Item, e.Target, e.CancellationToken); + +async Task CreateIssueAsync(MeetingActionItem item, MeetingSummarySendTarget target, + CancellationToken token) +{ + var issue = await tracker.CreateAsync(item.Text, token); + return new MeetingSummarySendResult(issue.Key, issue.Url); +} +``` + +傳送成功後會觸發 `ActionItemSent`。卡片被移除或重新產生時,權杖會被取消,按鈕回到它的標誌。帶有 `Url` 的已傳送標籤點按後會開啟該連結。 + +### 勾選待辦事項 + +勾選一個事項會觸發 `ActionItemChanged`,參數中帶有該事項及其新狀態;勾選狀態由卡片自己保存。`IsDone` 為 true 的事項一開始就是勾選狀態。 + +### 逐字稿原話 + +在任一行上稍作停留,它的原話就會在下方開啟(視窗空間不足時翻到上方)。原話開啟期間,移到另一行會立即切換。用鍵盤聚焦時間戳記會顯示它的原話;Enter、Space 或輕點會把它固定住,Escape 或在別處按下則會關閉。 + +### 狀態 + +| Status | 主體 | +|---|---| +| `Summarizing` | 在佔位列上方閃爍的 "Summarizing transcript" | +| `Ready` | 摘要、決議和待辦事項(沒有內容的區段不顯示) | +| `Ready`,清單為空 | "No transcript to summarize" | +| `Error` | "Couldn't summarize this meeting"、`ErrorText` 和 Try again | + +### 窄卡片 + + + +卡片會填滿容器,最寬 640px,最窄可到約 360px。寬度低於 460px 時,與會者頭像組移到日期下方;低於 520px 時,負責人只顯示名字,讓每個事項下的資訊保持在一行(螢幕閱讀器仍會讀出全名)。`AutoPlay="False"` 讓卡片一開始就顯示完成的紀錄。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或朗讀的每一個文字。替換這個物件即可在地化或改寫: + +```csharp +card.Labels = new MeetingSummaryLabels +{ + Summary = "Résumé", + ActionItems = "Actions", + Due = due => "Pour le " + due.ToString("d MMM"), +}; +``` + +`Model` 設定頁尾中署名的模型(一個帶名稱和標誌的 `AgentModel`);設為 null 則隱藏署名。`MeetingSummaryData.PlatformMark` 用你的平台標誌取代視訊圖示。 + +### 減少動態效果 + +減少動態效果時,紀錄整段出現而不是串流出現,主體和傳送按鈕位置的尺寸立即改變,核取方塊、刪除線和原話直接顯示最終狀態,不做描繪或滑動。所有播報照常進行。彙整過程會等到卡片出現在螢幕上才開始。 + +### 內容指引 + +- 一條摘要是一句話,說明發生了什麼;一條決議說明決定了什麼,而不是誰爭論了什麼。 +- 待辦事項以動詞開頭,並只寫一位負責人。 +- 每段原話只保留支撐這一行的一兩句話。 + +### 常見問題 + +**能顯示已經完成的紀錄嗎?** 把 `Status` 設為 `Ready`(或把 `AutoPlay` 設為 false),紀錄就會整段顯示。 + +**勾選狀態由誰保存?** 由卡片保存。`ActionItemChanged` 會告訴你每一次變化;`IsDone` 提供初始狀態。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 卡片是一個以會議命名的群組,標題是二級標題,每個區段是三級標題。 +- 時間戳記是按鈕,原話開啟期間回報為已展開;原話的文字是它的說明文字。 +- 每個核取方塊都是具有核取方塊角色和勾選狀態的按鈕,每個核取方塊和傳送按鈕都以待辦事項的文字命名,因此即使連成一串也能讀得清楚。 +- 16px 的核取方塊有 32px 的點按區域,鍵盤焦點環緊貼方框。 +- 一個禮貌型即時區域會播報彙整開始、彙整完成、勾選的事項和傳送結果;沒有處理常式時還會說明 "Simulated, nothing was sent"。 +- 狀態從不只依賴顏色:傳送失敗顯示警示圖示和 Retry,已傳送的事項顯示目標的標誌、編號和勾號。 +- 平台名稱先於日期讀出;只顯示名字的負責人對螢幕閱讀器仍保留全名。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過與會者、每個時間戳記、核取方塊和傳送按鈕,然後是複製和重新產生 | +| 聚焦時間戳記 | 顯示這一行的逐字稿原話 | +| Enter、Space | 在時間戳記上固定或關閉原話;在核取方塊上切換勾選;在傳送按鈕上傳送該事項 | +| Escape | 關閉開啟的原話 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 彙整中 | 約一秒,標籤閃爍 | 相同,不閃爍 | 相同,不閃爍 | +| 紀錄抵達 | 每 34ms 出現 3 到 8 個字詞,每塊淡入(總共約 1.5 秒) | 立即整段出現 | 立即整段出現 | +| 主體在狀態之間切換 | 高度彈簧過渡(0.24 秒,無回彈),新狀態淡入 | 立即 | 立即 | +| 負責人、日期、按鈕 | 文字串流出現時保留位置,文字完整後淡入 | 立即 | 立即 | +| 勾選事項 | 填色放大出現(160ms),勾號描繪出來(200ms,ease-out),1px 刪除線橫掃每一個換行,文字過渡為淡化色 | 立即到最終狀態 | 立即到最終狀態 | +| 取消勾選 | 反向進行,更快(120ms) | 立即 | 立即 | +| 按下核取方塊 | 方框縮放到 0.9,整列不縮放 | 相同 | 相同 | +| 傳送 | 標誌、載入圈和已傳送標籤交叉淡化,按鈕位置彈簧過渡到新寬度(0.25 秒,無回彈) | 短暫交叉淡化,尺寸立即改變 | 立即 | +| 原話 | 短暫停留後用 160ms 淡入並上升 3px;在列之間立即切換 | 淡入 | 立即 | + +## 相關 + +- `ToolCall`:代理程式發起的工具呼叫。 +- `AgentStatus`:正在執行的代理程式的即時狀態。 +- `Checkbox`:一般的勾選清單。 +- `Avatar` 和 `AvatarGroup`:人員。 +- `CopyButton`:複製一個值並給出確認。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/en-US.md new file mode 100644 index 00000000..bbe21da3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/en-US.md @@ -0,0 +1,230 @@ +--- +title: Model card +summary: A model details card where switching models rolls every price, limit and score in place, a volume slider estimates the monthly cost, benchmark bars show each model's rank, and Compare crossfades to a side by side table against any rival. +--- + + + +## When to use + +`ModelCard` describes one AI model: what it takes in, how much it holds, what it costs and how it scores against close +rivals. Switching models rolls every price, limit and score to its new value in place, a volume slider estimates the +monthly cost live, and a Compare switch crossfades to a side by side table against any rival. The card never changes +size between models, views or states. + +- **The detail pane of a model picker**, showing what the highlighted model can do and what it costs before someone + selects it. +- **Model pages in docs or a marketplace**, with limits, prices and a quick benchmark read against close rivals. +- **Settings where a team picks a default model** and wants a side by side check against an alternative. + +## When not to use + +- **Choosing between many models**: render a compact picker and show one `ModelCard` for the highlighted model; the card + is a detail view, not a row. +- **How full a live conversation is**: use `ContextWindow`. +- **Plan pricing**: use a pricing table; this card shows per token API prices. +- **Official leaderboards with many models**: the bar list shows the model and two rivals. + +## Usage + +Give the card a `Model`, a pool of `Rivals` and the `Benchmarks` to show. Pass one shared pool for every model: the card +filters out the model itself, so the benchmark rows and ranks stay stable while values roll. Without a `Model` the card +keeps its frame, reads "No model selected" and shows every value as not listed. + +```csharp +var model = new ModelCardModel +{ + Id = entry.ApiId, // from your model catalog + Name = entry.Name, + Provider = entry.ProviderName, + Summary = entry.Summary, + Modalities = ModelCardModalities.Text | ModelCardModalities.Image | ModelCardModalities.Tools, + ContextWindow = entry.ContextWindow, + MaxOutput = entry.MaxOutputTokens, + KnowledgeCutoff = entry.KnowledgeCutoff, + Pricing = new ModelCardPricing { Input = entry.Input, Output = entry.Output, CachedInput = entry.CachedInput }, + Scores = entry.Scores, // benchmark id → score; leave out what the vendor does not publish +}; + +card.Benchmarks = [new ModelCardBenchmark { Id = "gpqa-diamond", Name = "GPQA Diamond", ShortName = "Science" }]; +card.Rivals = catalog; // every model, the current one included +card.Model = model; +card.Note = "List prices and vendor-published scores, checked 2026-10-29."; +``` + +`ContextWindow` and `MaxOutput` are token counts (1,000,000 reads as 1M). `KnowledgeCutoff` is an ISO month such as +`"2026-03"` (shown as Mar 2026); any other text is shown as is. Prices are per million tokens in `Currency`. A missing +value reads "Not listed". `Note` is the footnote under the compare table: say where the figures come from and when they +were checked. Turn `IsSample` on while the figures are placeholders: the benchmark caption then ends in "Sample scores" +and the footnote says the prices and scores are samples. + +The demos on this page show real models with their vendors' list prices (standard tier, checked 2026-10-29) and only +the benchmark scores each vendor publishes; Gemini 4 Argon's price is introductory, and Terminal-Bench scores are +reported at each vendor's own effort setting. Each Terminal-Bench version is its own benchmark id, so a 4.0 score is +never ranked against a 2.1 score; a model without a published score reads "Not published" (the demos set `Labels.None` +and `Labels.NotListed`), never zero. + +### Anatomy + +1. **Header**: the model's mark in a tinted square (or the first letter of its name), the name and the provider, and a + Compare switch. +2. **Summary**: one sentence, two lines reserved (three on narrow cards), clamped. +3. **Modality chips**: all seven (text, image, PDF, audio, video, tools, reasoning); unsupported ones are dashed and muted. +4. **Specs**: context window with a gauge and the page count it holds (1M tokens is about 1,500 pages), max output and + knowledge cutoff. +5. **Prices** per million tokens: input, output and cached input (the cached cell shows only when some model in the + pool has a cached price). +6. **Cost estimate**: a monthly volume slider (1M to 1B tokens) and the estimated cost, billed by `InputShare`. +7. **Benchmarks**: a switcher, a caption, bars for the model and its first two rivals, and a readout with the rank. +8. **Footer**: the API model id, Copy model id and, for pickers, Use model. + +### Compare + + + +The Compare switch, or `View="Compare"`, crossfades to a table against one rival with a row per spec, price, the cost +at the chosen volume and every benchmark. The better value is set in a heavier weight with a check, the text "better" +for screen readers and its margin (+2.3, −20%, 2x). A tally under the table says who leads. Pick a rival from the menu, +or set `CompareWith` to its id. + +### Loading and errors + + + +`IsLoading` shows a pulsing skeleton, disables Compare and says "Loading model details". `ErrorText` replaces the body +with the message; `CanRetry` adds a Retry button that raises `RetryRequested`. Both keep the header and the card's size. +With no `Model` (Empty) the header reads "No model selected", Compare and the benchmarks hide and Copy model id is +disabled. + +### In a model picker + + + +`CanSelect` adds a primary Use model button that raises `SelectRequested` with the model; set `IsSelected` when it is +the current choice and the button reads Selected with a check. `SelectLabel` renames the button. + +```csharp +card.SelectRequested += (_, e) => settings.DefaultModel = e.Model.Id; +card.IsSelected = card.Model?.Id == settings.DefaultModel; +``` + +### Narrow cards + + + +The card fills its container up to 640px and reads its own width. Below 480px specs and prices become rows of label +and value, the estimate drops under the slider, the benchmark switcher moves under its heading and the footer buttons +move under the model id. Below 420px the summary reserves three lines; below 360px each bar moves under its name. From +520px the compare table has a label column and shows margins and mini bars; narrower, each label sits above its two values. + +### Events + +```csharp +card.ViewChanged += (_, e) => Save("view", e.View); +card.CompareWithChanged += (_, e) => Save("rival", e.Id); +card.BenchmarkChanged += (_, e) => Save("benchmark", e.Id); +card.MonthlyTokensChanged += (_, e) => Save("volume", e.Tokens); +card.IdCopied += (_, e) => Log("copied", e.Id); +``` + +`View`, `CompareWith`, `Benchmark` and `MonthlyTokens` are two-way, so you can bind them instead. `MonthlyTokens` snaps +to the nearest stop: 1M, 5M, 10M, 25M, 50M, 100M, 250M, 500M or 1B. + +### Your own words + +`Labels` holds every word the card shows or says; sentences with values are functions: + +```csharp +card.Labels = new ModelCardLabels +{ + Compare = "Vergleichen", + Rank = (rank, count) => $"Platz {rank} von {count}", +}; +``` + +### Reduced motion + +With reduced motion views, bars, numbers and values change in place and the skeleton holds still. With motion off +everything changes at once. + +### Content guidelines + +- Keep the summary to one sentence of about 90 characters. +- Give scores for the same benchmark ids on the model and its rivals. A row without a score shows None and the compare + table shows Not listed, so rows never appear or disappear. +- Turn `IsSample` on while the figures are placeholders; with real prices and scores leave it off and say where they come + from in `Note`. +- Rank only scores of the same benchmark version: give each version its own id. +- Marks are decorative; pass any 24 × 24 stroked glyph as `Mark` and a `MarkColor`, or leave it out for a monogram. + +### Common questions + +**Why does the card not shrink when I hide the compare view?** Every panel is stacked in one cell, so the card is always +as tall as the tallest panel. Switching never moves what is around it. + +**How is the monthly cost estimated?** `InputShare` of the volume is billed at the input price and the rest at the output +price (75% input by default). + +## API reference + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is named by the model's name. Each spec and price reads with its label. +- The compare table names each row by its label; the better value says "better", so the comparison never relies on color. +- A polite status region says what changed when you switch views, rivals or benchmarks ("Comparing Claude Opus 5.5 with + Gemini 4 Argon"). Copy model id announces Copied on its own. +- Benchmark bars are hidden from assistive technology; each row is focusable and named by its full readout (score, rank and + gap to the leader). Marks are decorative. +- Modality chips say supported or not supported; unsupported chips are dashed as well as muted. +- While loading Compare is disabled and the status says "Loading model details". An error is announced right away. +- Hidden panels take no pointer input and Tab never lands in them. +- Pointer focus draws no ring; keyboard focus shows Arc's focus ring. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the Compare switch, the volume slider, the benchmark switcher and each benchmark row (focus shows its readout), or the rival menu in the compare view, then Copy model id and Use model | +| Arrow keys, Page Up, Page Down, Home, End | On the volume slider, step through the monthly volumes; the estimate rolls to the new value | +| Space, Enter | Toggles Compare, copies the model id or selects the model | +| Arrow Left, Arrow Right | Move between benchmarks in the switcher | +| Enter, Arrow Down | Open the rival menu; arrows move between rivals, Enter picks one and Escape closes it | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Details, compare, loading, error | The old panel fades out in 100ms while the new one fades in over 160ms | At once | At once | +| A model switch: prices, limits, cost, pages, scores | Digits roll to the new value in tabular numerals | At once | At once | +| A model switch: name, summary, cutoff, readout | Crossfade in place (out 100ms, in 160ms) | At once | At once | +| Bars and the context gauge | Slide on a critically damped spring (0.24s, no bounce) | At once | At once | +| Bars on first view | Grow from zero once, 40ms apart, when the card is mostly in view | At once | At once | +| Hovering or focusing a benchmark row | The row tints, the others fade to half, the readout crossfades (160ms) | At once | At once | +| Loading | The skeleton pulses | Still | Still | + +Every slot is reserved: two summary lines, seven chips, every spec and price cell, three benchmark rows and a readout +that holds its lines, so the card keeps one size. + +## Related + +- `ContextWindow` for how full a live conversation's context is. +- `CopyButton`, `Switch`, `Slider` and `SegmentedControl`, which the card uses. +- `DropdownMenu` for the rival menu. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/zh-CN.md new file mode 100644 index 00000000..8c9f986d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/zh-CN.md @@ -0,0 +1,194 @@ +--- +title: Model card +summary: 模型详情卡片:切换模型时,每个价格、上限和分数都在原位滚动到新值,用量滑块估算每月费用,基准测试条显示各模型的排名,比较开关以交叉淡化切换到与任一对手并排的表格。 +source-hash: dd18090b77cf +--- + + + +## 何时使用 + +`ModelCard` 描述一个 AI 模型:它接受哪些输入、能容纳多少内容、费用多少,以及与相近对手相比得分如何。切换模型时,每个价格、上限和分数都在原位滚动到新值;用量滑块实时估算每月费用;比较开关以交叉淡化切换到与任一对手并排的表格。无论切换模型、视图还是状态,卡片尺寸都不变。 + +- **模型选择器的详情面板**:在有人选定之前,展示当前高亮的模型能做什么、费用多少。 +- **文档或市场中的模型页面**:展示上限、价格,以及与相近对手的快速基准对比。 +- **团队选择默认模型的设置页**:需要与备选模型并排核对。 + +## 何时不用 + +- **在许多模型中挑选**:渲染一个紧凑的选择器,并为高亮的模型显示一张 `ModelCard`;卡片是详情视图,不是列表行。 +- **显示进行中的对话占用了多少上下文**:使用 `ContextWindow`。 +- **套餐定价**:使用定价表;这张卡片显示的是按 token 计费的 API 价格。 +- **包含许多模型的官方排行榜**:基准测试条只显示当前模型和两个对手。 + +## 用法 + +给卡片设置 `Model`、一组 `Rivals` 以及要显示的 `Benchmarks`。所有模型共用同一组对手即可:卡片会自动排除模型本身,因此数值滚动时,基准测试行和排名保持稳定。没有 `Model` 时,卡片保留其框架,显示“No model selected”,并把每个值显示为未列出。 + +```csharp +var model = new ModelCardModel +{ + Id = entry.ApiId, // from your model catalog + Name = entry.Name, + Provider = entry.ProviderName, + Summary = entry.Summary, + Modalities = ModelCardModalities.Text | ModelCardModalities.Image | ModelCardModalities.Tools, + ContextWindow = entry.ContextWindow, + MaxOutput = entry.MaxOutputTokens, + KnowledgeCutoff = entry.KnowledgeCutoff, + Pricing = new ModelCardPricing { Input = entry.Input, Output = entry.Output, CachedInput = entry.CachedInput }, + Scores = entry.Scores, // benchmark id → score; leave out what the vendor does not publish +}; + +card.Benchmarks = [new ModelCardBenchmark { Id = "gpqa-diamond", Name = "GPQA Diamond", ShortName = "Science" }]; +card.Rivals = catalog; // every model, the current one included +card.Model = model; +card.Note = "List prices and vendor-published scores, checked 2026-10-29."; +``` + +`ContextWindow` 和 `MaxOutput` 是 token 数(1,000,000 显示为 1M)。`KnowledgeCutoff` 是 ISO 月份,例如 `"2026-03"`(显示为 Mar 2026),其他文本按原样显示。价格以 `Currency` 计,单位为每百万 token。缺少的值显示为“Not listed”。`Note` 是比较表下方的脚注:说明这些数据的来源和核对日期。数据仍是占位时打开 `IsSample`:基准测试说明末尾会加上“Sample scores”,脚注则说明价格和分数是示例。 + +本页的演示展示真实模型及其厂商标价(标准档,核对于 2026-10-29),并且只显示各厂商公布的基准测试分数;Gemini 4 Argon 的价格是推广价,Terminal-Bench 分数按各厂商自己选择的推理强度报告。每个 Terminal-Bench 版本都使用单独的基准测试 id,因此 4.0 的分数从不与 2.1 的分数一起排名;没有公布分数的模型显示为“Not published”(演示设置了 `Labels.None` 和 `Labels.NotListed`),而不是零。 + +### 结构 + +1. **头部**:模型标志放在着色的方块中(没有标志时显示名称首字母),旁边是名称和提供方,以及比较开关。 +2. **简介**:一句话,预留两行(窄卡片预留三行),超出部分截断。 +3. **模态标签**:全部七个(文本、图像、PDF、音频、视频、工具、推理);不支持的标签为虚线边框并变淡。 +4. **规格**:上下文窗口(带一个容量条和可容纳的页数,1M token 约为 1,500 页)、最大输出和知识截止时间。 +5. **价格**(每百万 token):输入、输出和缓存输入(只有当对手池中有模型提供缓存价格时才显示缓存单元格)。 +6. **费用估算**:每月用量滑块(1M 到 1B token)和估算费用,按 `InputShare` 计费。 +7. **基准测试**:切换器、说明文字、模型及其前两个对手的条形,以及带排名的读数。 +8. **底部**:API 模型 id、Copy model id 按钮,以及用于选择器的 Use model 按钮。 + +### 比较 + + + +比较开关或 `View="Compare"` 以交叉淡化切换到与一个对手对比的表格:每项规格、价格、所选用量下的费用和每个基准测试各占一行。较优的值以更粗的字重显示,并带有对勾、供屏幕阅读器朗读的“better”以及领先幅度(+2.3、−20%、2x)。表格下方的统计说明谁领先。从菜单中选择对手,或把 `CompareWith` 设为对手的 id。 + +### 加载与错误 + + + +`IsLoading` 显示一个脉动的骨架屏,禁用比较开关,并播报“Loading model details”。`ErrorText` 用消息替换卡片主体;`CanRetry` 添加一个 Retry 按钮,它会触发 `RetryRequested`。两种状态都保留头部和卡片尺寸。没有 `Model` 时(Empty),头部显示“No model selected”,比较开关和基准测试隐藏,Copy model id 被禁用。 + +### 用在模型选择器中 + + + +`CanSelect` 添加一个主要的 Use model 按钮,点击时以该模型触发 `SelectRequested`;当它是当前选择时设置 `IsSelected`,按钮会显示 Selected 和对勾。`SelectLabel` 用于重命名该按钮。 + +```csharp +card.SelectRequested += (_, e) => settings.DefaultModel = e.Model.Id; +card.IsSelected = card.Model?.Id == settings.DefaultModel; +``` + +### 窄卡片 + + + +卡片在 640px 以内填满容器,并读取自身宽度。低于 480px 时,规格和价格变为“标签与值”的行,费用估算移到滑块下方,基准测试切换器移到标题下方,底部按钮移到模型 id 下方。低于 420px 时,简介预留三行;低于 360px 时,每个条形移到名称下方。宽度达到 520px 时,比较表格带有标签列并显示领先幅度和迷你条形;更窄时,每个标签位于其两个值的上方。 + +### 事件 + +```csharp +card.ViewChanged += (_, e) => Save("view", e.View); +card.CompareWithChanged += (_, e) => Save("rival", e.Id); +card.BenchmarkChanged += (_, e) => Save("benchmark", e.Id); +card.MonthlyTokensChanged += (_, e) => Save("volume", e.Tokens); +card.IdCopied += (_, e) => Log("copied", e.Id); +``` + +`View`、`CompareWith`、`Benchmark` 和 `MonthlyTokens` 都是双向的,也可以直接绑定它们。`MonthlyTokens` 会吸附到最近的档位:1M、5M、10M、25M、50M、100M、250M、500M 或 1B。 + +### 你自己的措辞 + +`Labels` 包含卡片显示或朗读的每个词;带数值的句子是函数: + +```csharp +card.Labels = new ModelCardLabels +{ + Compare = "Vergleichen", + Rank = (rank, count) => $"Platz {rank} von {count}", +}; +``` + +### 减弱动效 + +在减弱动效下,视图、条形、数字和值都在原位直接变化,骨架屏保持静止。关闭动效时,所有变化立即完成。 + +### 内容准则 + +- 简介保持一句话,约 90 个字符。 +- 为模型和对手提供相同基准测试 id 的分数。没有分数的行显示 None,比较表格显示 Not listed,因此行不会出现或消失。 +- 数据仍是占位时打开 `IsSample`;使用真实价格和分数时保持关闭,并在 `Note` 中说明数据来源。 +- 只对同一版本的基准测试分数排名:每个版本使用自己的 id。 +- 标志只起装饰作用;可以把任意 24 × 24 的描边图形作为 `Mark` 传入并设置 `MarkColor`,或省略它以显示首字母。 + +### 常见问题 + +**为什么隐藏比较视图时卡片不会变小?** 所有面板都叠放在同一个单元格中,所以卡片始终与最高的面板一样高。切换不会移动周围的内容。 + +**每月费用如何估算?** 用量中 `InputShare` 的部分按输入价格计费,其余按输出价格计费(默认 75% 为输入)。 + +## API 参考 + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片以模型名称命名。每项规格和价格都会连同其标签一起朗读。 +- 比较表格的每一行以其标签命名;较优的值会读出“better”,因此对比不依赖颜色。 +- 切换视图、对手或基准测试时,一个礼貌的状态区域会说明发生了什么变化(“Comparing Claude Opus 5.5 with Gemini 4 Argon”)。Copy model id 会自行播报 Copied。 +- 基准测试条对辅助技术隐藏;每一行都可以获得焦点,并以完整的读数(分数、排名以及与领先者的差距)命名。标志只起装饰作用。 +- 模态标签会读出支持或不支持;不支持的标签既变淡又使用虚线边框。 +- 加载时比较开关被禁用,状态区域播报“Loading model details”。错误会立即播报。 +- 隐藏的面板不接收指针输入,Tab 也不会进入其中。 +- 指针获得焦点时不显示焦点环;键盘获得焦点时显示 Arc 的焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过比较开关、用量滑块、基准测试切换器和每个基准测试行(获得焦点时显示其读数),在比较视图中则经过对手菜单,然后是 Copy model id 和 Use model | +| 方向键、Page Up、Page Down、Home、End | 在用量滑块上逐档切换每月用量;估算费用滚动到新值 | +| Space、Enter | 切换比较、复制模型 id 或选择模型 | +| 左方向键、右方向键 | 在切换器中的基准测试之间移动 | +| Enter、下方向键 | 打开对手菜单;方向键在对手之间移动,Enter 选择,Escape 关闭 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 详情、比较、加载、错误 | 旧面板在 100ms 内淡出,同时新面板在 160ms 内淡入 | 立即 | 立即 | +| 切换模型:价格、上限、费用、页数、分数 | 数字以等宽数字滚动到新值 | 立即 | 立即 | +| 切换模型:名称、简介、截止时间、读数 | 原位交叉淡化(淡出 100ms,淡入 160ms) | 立即 | 立即 | +| 条形与上下文容量条 | 以临界阻尼弹簧滑动(0.24s,无回弹) | 立即 | 立即 | +| 首次出现的条形 | 卡片大部分进入视口时从零生长一次,间隔 40ms | 立即 | 立即 | +| 悬停或聚焦基准测试行 | 该行着色,其他行淡到一半,读数交叉淡化(160ms) | 立即 | 立即 | +| 加载 | 骨架屏脉动 | 静止 | 静止 | + +每个位置都已预留:两行简介、七个标签、每个规格和价格单元格、三个基准测试行,以及保持行数的读数,因此卡片尺寸始终不变。 + +## 相关组件 + +- `ContextWindow`:显示进行中的对话占用了多少上下文。 +- `CopyButton`、`Switch`、`Slider` 和 `SegmentedControl`:卡片内部使用的控件。 +- `DropdownMenu`:对手菜单。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/zh-TW.md new file mode 100644 index 00000000..54ee08d9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ModelCard/zh-TW.md @@ -0,0 +1,194 @@ +--- +title: Model card +summary: 模型詳情卡片:切換模型時,每個價格、上限和分數都在原位捲動到新值,用量滑桿估算每月費用,基準測試長條顯示各模型的排名,比較開關以交叉淡化切換到與任一對手並排的表格。 +source-hash: dd18090b77cf +--- + + + +## 何時使用 + +`ModelCard` 描述一個 AI 模型:它接受哪些輸入、能容納多少內容、費用多少,以及與相近對手相比得分如何。切換模型時,每個價格、上限和分數都在原位捲動到新值;用量滑桿即時估算每月費用;比較開關以交叉淡化切換到與任一對手並排的表格。無論切換模型、檢視還是狀態,卡片尺寸都不變。 + +- **模型選擇器的詳情面板**:在有人選定之前,呈現目前醒目提示的模型能做什麼、費用多少。 +- **文件或市集中的模型頁面**:呈現上限、價格,以及與相近對手的快速基準比較。 +- **團隊選擇預設模型的設定頁**:需要與備選模型並排核對。 + +## 何時不用 + +- **在許多模型中挑選**:呈現一個精簡的選擇器,並為醒目提示的模型顯示一張 `ModelCard`;卡片是詳情檢視,不是清單列。 +- **顯示進行中的對話用了多少上下文**:使用 `ContextWindow`。 +- **方案定價**:使用定價表;這張卡片顯示的是按 token 計費的 API 價格。 +- **包含許多模型的官方排行榜**:基準測試長條只顯示目前模型和兩個對手。 + +## 用法 + +為卡片設定 `Model`、一組 `Rivals` 以及要顯示的 `Benchmarks`。所有模型共用同一組對手即可:卡片會自動排除模型本身,因此數值捲動時,基準測試列和排名維持穩定。沒有 `Model` 時,卡片保留其框架,顯示「No model selected」,並把每個值顯示為未列出。 + +```csharp +var model = new ModelCardModel +{ + Id = entry.ApiId, // from your model catalog + Name = entry.Name, + Provider = entry.ProviderName, + Summary = entry.Summary, + Modalities = ModelCardModalities.Text | ModelCardModalities.Image | ModelCardModalities.Tools, + ContextWindow = entry.ContextWindow, + MaxOutput = entry.MaxOutputTokens, + KnowledgeCutoff = entry.KnowledgeCutoff, + Pricing = new ModelCardPricing { Input = entry.Input, Output = entry.Output, CachedInput = entry.CachedInput }, + Scores = entry.Scores, // benchmark id → score; leave out what the vendor does not publish +}; + +card.Benchmarks = [new ModelCardBenchmark { Id = "gpqa-diamond", Name = "GPQA Diamond", ShortName = "Science" }]; +card.Rivals = catalog; // every model, the current one included +card.Model = model; +card.Note = "List prices and vendor-published scores, checked 2026-10-29."; +``` + +`ContextWindow` 和 `MaxOutput` 是 token 數(1,000,000 顯示為 1M)。`KnowledgeCutoff` 是 ISO 月份,例如 `"2026-03"`(顯示為 Mar 2026),其他文字依原樣顯示。價格以 `Currency` 計,單位為每百萬 token。缺少的值顯示為「Not listed」。`Note` 是比較表下方的註腳:說明這些資料的來源與核對日期。資料仍是預留位置時開啟 `IsSample`:基準測試說明末尾會加上「Sample scores」,註腳則說明價格和分數是範例。 + +本頁的展示呈現真實模型及其廠商牌價(標準級距,核對於 2026-10-29),並且只顯示各廠商公布的基準測試分數;Gemini 4 Argon 的價格是推廣價,Terminal-Bench 分數依各廠商自行選擇的推理強度回報。每個 Terminal-Bench 版本都使用獨立的基準測試 id,因此 4.0 的分數從不與 2.1 的分數一起排名;沒有公布分數的模型顯示為「Not published」(展示設定了 `Labels.None` 和 `Labels.NotListed`),而不是零。 + +### 結構 + +1. **頁首**:模型標誌放在上色的方塊中(沒有標誌時顯示名稱首字母),旁邊是名稱和供應商,以及比較開關。 +2. **簡介**:一句話,預留兩行(窄卡片預留三行),超出部分截斷。 +3. **模態標籤**:全部七個(文字、圖片、PDF、音訊、影片、工具、推理);不支援的標籤為虛線外框並變淡。 +4. **規格**:上下文視窗(附一條容量條和可容納的頁數,1M token 約為 1,500 頁)、最大輸出和知識截止時間。 +5. **價格**(每百萬 token):輸入、輸出和快取輸入(只有對手池中有模型提供快取價格時才顯示快取儲存格)。 +6. **費用估算**:每月用量滑桿(1M 到 1B token)和估算費用,依 `InputShare` 計費。 +7. **基準測試**:切換器、說明文字、模型及其前兩個對手的長條,以及附排名的讀數。 +8. **頁尾**:API 模型 id、Copy model id 按鈕,以及用於選擇器的 Use model 按鈕。 + +### 比較 + + + +比較開關或 `View="Compare"` 以交叉淡化切換到與一個對手比較的表格:每項規格、價格、所選用量下的費用和每個基準測試各佔一列。較佳的值以較粗的字重顯示,並附有勾號、供螢幕閱讀器朗讀的「better」以及領先幅度(+2.3、−20%、2x)。表格下方的統計說明誰領先。從選單中選擇對手,或把 `CompareWith` 設為對手的 id。 + +### 載入與錯誤 + + + +`IsLoading` 顯示一個脈動的骨架畫面,停用比較開關,並朗讀「Loading model details」。`ErrorText` 以訊息取代卡片主體;`CanRetry` 加入一個 Retry 按鈕,它會觸發 `RetryRequested`。兩種狀態都保留頁首和卡片尺寸。沒有 `Model` 時(Empty),頁首顯示「No model selected」,比較開關和基準測試隱藏,Copy model id 被停用。 + +### 用在模型選擇器中 + + + +`CanSelect` 加入一個主要的 Use model 按鈕,按下時以該模型觸發 `SelectRequested`;當它是目前的選擇時設定 `IsSelected`,按鈕會顯示 Selected 和勾號。`SelectLabel` 用於重新命名該按鈕。 + +```csharp +card.SelectRequested += (_, e) => settings.DefaultModel = e.Model.Id; +card.IsSelected = card.Model?.Id == settings.DefaultModel; +``` + +### 窄卡片 + + + +卡片在 640px 以內填滿容器,並讀取自身寬度。低於 480px 時,規格和價格變成「標籤與值」的列,費用估算移到滑桿下方,基準測試切換器移到標題下方,頁尾按鈕移到模型 id 下方。低於 420px 時,簡介預留三行;低於 360px 時,每個長條移到名稱下方。寬度達到 520px 時,比較表格附有標籤欄並顯示領先幅度和迷你長條;更窄時,每個標籤位於其兩個值的上方。 + +### 事件 + +```csharp +card.ViewChanged += (_, e) => Save("view", e.View); +card.CompareWithChanged += (_, e) => Save("rival", e.Id); +card.BenchmarkChanged += (_, e) => Save("benchmark", e.Id); +card.MonthlyTokensChanged += (_, e) => Save("volume", e.Tokens); +card.IdCopied += (_, e) => Log("copied", e.Id); +``` + +`View`、`CompareWith`、`Benchmark` 和 `MonthlyTokens` 都是雙向的,也可以直接繫結它們。`MonthlyTokens` 會貼齊最近的級距:1M、5M、10M、25M、50M、100M、250M、500M 或 1B。 + +### 你自己的用語 + +`Labels` 包含卡片顯示或朗讀的每個字詞;帶數值的句子是函式: + +```csharp +card.Labels = new ModelCardLabels +{ + Compare = "Vergleichen", + Rank = (rank, count) => $"Platz {rank} von {count}", +}; +``` + +### 減少動態效果 + +在減少動態效果下,檢視、長條、數字和值都在原位直接變化,骨架畫面保持靜止。關閉動態效果時,所有變化立即完成。 + +### 文案規範 + +- 簡介保持一句話,約 90 個字元。 +- 為模型和對手提供相同基準測試 id 的分數。沒有分數的列顯示 None,比較表格顯示 Not listed,因此列不會出現或消失。 +- 資料仍是預留位置時開啟 `IsSample`;使用真實價格和分數時保持關閉,並在 `Note` 中說明資料來源。 +- 只對同一版本的基準測試分數排名:每個版本使用自己的 id。 +- 標誌只具裝飾作用;可以把任意 24 × 24 的描邊圖形作為 `Mark` 傳入並設定 `MarkColor`,或省略它以顯示首字母。 + +### 常見問題 + +**為什麼隱藏比較檢視時卡片不會變小?** 所有面板都疊放在同一個儲存格中,所以卡片始終與最高的面板一樣高。切換不會移動周圍的內容。 + +**每月費用如何估算?** 用量中 `InputShare` 的部分依輸入價格計費,其餘依輸出價格計費(預設 75% 為輸入)。 + +## API 參考 + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片以模型名稱命名。每項規格和價格都會連同其標籤一起朗讀。 +- 比較表格的每一列以其標籤命名;較佳的值會讀出「better」,因此比較不依賴顏色。 +- 切換檢視、對手或基準測試時,一個禮貌的狀態區域會說明發生了什麼變化(「Comparing Claude Opus 5.5 with Gemini 4 Argon」)。Copy model id 會自行朗讀 Copied。 +- 基準測試長條對輔助技術隱藏;每一列都可以取得焦點,並以完整的讀數(分數、排名以及與領先者的差距)命名。標誌只具裝飾作用。 +- 模態標籤會讀出支援或不支援;不支援的標籤既變淡又使用虛線外框。 +- 載入時比較開關被停用,狀態區域朗讀「Loading model details」。錯誤會立即朗讀。 +- 隱藏的面板不接收指標輸入,Tab 也不會進入其中。 +- 指標取得焦點時不顯示焦點環;鍵盤取得焦點時顯示 Arc 的焦點環。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過比較開關、用量滑桿、基準測試切換器和每個基準測試列(取得焦點時顯示其讀數),在比較檢視中則經過對手選單,然後是 Copy model id 和 Use model | +| 方向鍵、Page Up、Page Down、Home、End | 在用量滑桿上逐級切換每月用量;估算費用捲動到新值 | +| Space、Enter | 切換比較、複製模型 id 或選擇模型 | +| 左方向鍵、右方向鍵 | 在切換器中的基準測試之間移動 | +| Enter、下方向鍵 | 開啟對手選單;方向鍵在對手之間移動,Enter 選擇,Escape 關閉 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 詳情、比較、載入、錯誤 | 舊面板在 100ms 內淡出,同時新面板在 160ms 內淡入 | 立即 | 立即 | +| 切換模型:價格、上限、費用、頁數、分數 | 數字以等寬數字捲動到新值 | 立即 | 立即 | +| 切換模型:名稱、簡介、截止時間、讀數 | 原位交叉淡化(淡出 100ms,淡入 160ms) | 立即 | 立即 | +| 長條與上下文容量條 | 以臨界阻尼彈簧滑動(0.24s,無回彈) | 立即 | 立即 | +| 首次出現的長條 | 卡片大部分進入可視範圍時從零長出一次,間隔 40ms | 立即 | 立即 | +| 游標停留或聚焦基準測試列 | 該列上色,其他列淡到一半,讀數交叉淡化(160ms) | 立即 | 立即 | +| 載入 | 骨架畫面脈動 | 靜止 | 靜止 | + +每個位置都已預留:兩行簡介、七個標籤、每個規格和價格儲存格、三個基準測試列,以及保持行數的讀數,因此卡片尺寸始終不變。 + +## 相關元件 + +- `ContextWindow`:顯示進行中的對話用了多少上下文。 +- `CopyButton`、`Switch`、`Slider` 和 `SegmentedControl`:卡片內部使用的控制項。 +- `DropdownMenu`:對手選單。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/en-US.md new file mode 100644 index 00000000..8ce8dc51 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/en-US.md @@ -0,0 +1,250 @@ +--- +title: PII redaction +summary: A guardrail card where personal data is redacted before the prompt reaches the model, and the reply comes back with the original values put back. +--- + + + +## When to use + +`PiiRedaction` shows what a privacy guardrail does to a prompt. Scan marks each name, email, phone number and card +number in the message with its type, then swaps each one for a numbered placeholder such as `[EMAIL_1]` in a single +crossfade. Send carries the safe prompt along a small flow (you, the PII guard, the model), the reply streams back with +its placeholders, and the guard puts the original values back for the person. Show original flips the message between +the safe prompt and what was typed. + +- **Explaining or proving a guardrail** in an AI product: support assistants, health or finance copilots, enterprise + chat. +- **A settings or onboarding screen** where admins see what the PII filter does before they turn it on. +- **A playground** where people paste text and see exactly what would reach the model. + +## When not to use + +- **The conversation itself**: use a chat or composer and redact behind the scenes; show this card only where the + guardrail needs explaining. +- **Compliance**: do not rely on the built-in detector. Pass a real DLP or NER service through `Detector`. +- **An arbitrary tool step in a transcript**: use `ToolCall`. + +## Usage + +```xml + +``` + +```csharp +card.Names = [user.Name]; +card.Detector = async (text, token) => await dlp.ScanAsync(text, token); // spans with an exclusive end +card.Sender = async (prompt, token) => await chat.CompleteAsync(prompt, token); // may contain [NAME_1], [EMAIL_1] +card.Redacted += (_, e) => audit.Log("pii.redacted", e.Entities.Select(x => x.Type)); +``` + +`PiiRedaction` is a `TemplatedControl` in the Special package (`UseArcSpecial()`). It fills its container up to 640px. +With no `Message` and no `Simulation` the card shows an empty message field ("Paste a message with names, emails, phone +numbers or cards") and Scan is off until something is typed; the flow names the model "Model" until `ModelName` is set. + +### Simulation + +To show the card without a model (a demo, a design review, an onboarding tour), give it a `PiiRedactionSimulation`: the +message, the names to flag, the model the safe prompt goes to and the model's reply with its placeholders. While the card +has no `Message` it shows the simulation's and plays scan, send and restore on a loop until someone takes over; the reply +is simulated and the card says so. An edited message gets a short stand-in reply. Your own values win over the +simulation's, and setting `Simulation` to null stops it and shows the empty field. + +```csharp +card.Simulation = new PiiRedactionSimulation +{ + Message = "Hi, I'm Juniper Vale. Send the receipt to juniper.vale@example.com.", + Names = ["Juniper Vale"], + ModelName = "Grok 4.7", + Reply = "Done, [NAME_1]. The receipt is on its way to [EMAIL_1].", +}; +``` + +The preview above plays a simulation of an invented support conversation, the gallery's own demo data: a double charge +request from Juniper Vale with a test card number (4242 4242 4242 4242), an email on example.com and a phone number in +the fictional 555 range. + +### Anatomy + +1. **Head**: a shield, "PII guard" and, while the reply is simulated, a Simulated tag. +2. **Message**: the editable field before a scan; the marked original while it scans; then the safe prompt, with each + placeholder in its type's color over a rounded mark. Above it, the label ("Message", "Safe prompt" or "Original + message") and the Show original switch. +3. **Type cells**: Name, Email, Phone and Card, each reading "Not checked", a shimmering "Checking", its token + ("EMAIL_1", or "EMAIL_1 +1" for more) or "None found". A cell that found something takes a light tint. +4. **Flow strip**: you, the guard and the model, with a dot travelling each hop. The working node's label shimmers. +5. **Reply**: the model's mark and name over the reply, which streams in with its placeholders and then shows the + restored text with a mark under each value put back. +6. **Footer**: the status with an icon, Reset once there is something to reset, and the one primary action. + +### Phases + +The primary action is one button whose label follows the phase, so keyboard focus stays put through the whole flow. + +| Phase | Status | Primary action | +|---|---|---| +| `Idle` | Ready to scan | Scan | +| `Scanning` | Scanning for personal data | Stop | +| `Redacted` | 4 items redacted, or No personal data found | Send | +| `Sending` | Sending the safe prompt | Stop | +| `Thinking` | Waiting for Grok 4.7 | Stop | +| `Streaming` | Grok 4.7 is replying | Stop | +| `Restoring` | Restoring original values | Stop | +| `Done` | Reply restored | Run again | +| `Error` | The exception's message | Retry | + +Stop during a scan returns to the editable message; during a request it cancels the token passed to `Sender` and +returns to the safe prompt. Retry runs the failed step again. Reset cancels anything in flight and returns to the field. +`Scan()`, `Send()`, `Stop()` and `Reset()` do the same from code. + +### Playground + + + +`AutoPlay="False"` with `Message=""` gives a blank field. People paste their text and press Scan, or Cmd or Ctrl plus +Enter in the field. `Message` is two way, so the app can read what was typed. `IsEditable="False"` keeps the field read +only. + +### Your detector and your model + + + +`Detector` replaces the built-in detector with your DLP or NER service. It gets the text and a cancellation token and +returns `PiiMatch` spans (a type, a start and an exclusive end). `Sender` sends the safe prompt to your model and returns +its reply with the placeholders; the card restores them. Without a `Sender` the reply is simulated: the head shows the +Simulated tag and the status adds "Simulated, nothing was sent". A thrown exception in either shows its message with +Retry. + +`Redacted` reports the safe prompt and every `PiiEntity` (type, position, value, token and placeholder). `ReplyRestored` +reports the raw reply, the restored text and whether it was simulated. `PhaseChanged` follows every phase. `Phase`, +`SafePrompt` and `Reply` are read only and bindable; `ShowOriginal` is two way. `Entities` holds the last redaction's +entities and `IsSimulated` says whether the reply is simulated. + +`ModelName` and `ModelMark` (a 24 × 24 stroked glyph, the sparkle by default) name the model on the flow and above the +reply; `ModelMarkBrush` colors the mark. `NameBrush`, `EmailBrush`, `PhoneBrush` and `CardBrush` set the type colors +(Series1 to Series4 by default). + +### The helpers without the card + +```csharp +// Server side, before the prompt leaves your backend. +var result = PiiRedactor.Redact(message, PiiRedactor.Detect(message, [user.Name])); +var reply = PiiRedactor.Restore(await model.CompleteAsync(result.Prompt), result.Entities); +``` + +| Member | What it does | +|---|---| +| `PiiRedactor.Detect(text, names)` | Finds emails, card numbers (13 to 19 digits that pass Luhn), phone numbers (7 to 15 digits) and names (the list, plus "I'm", "I am", "this is" and "my name is" followed by capitalized words), sorted and without overlaps | +| `PiiRedactor.Redact(text, matches)` | Returns a `PiiRedactionResult` with the safe prompt and the entities; overlapping spans after the first are skipped and a repeated value keeps its token | +| `PiiRedactor.Restore(reply, entities)` | Puts the values back and leaves unknown tokens as they are | +| `PiiRedactor.PassesLuhn(value)` | The Luhn check on the digits of a value | + +| Type | Members | +|---|---| +| `PiiType` | `Name`, `Email`, `Phone`, `Card` | +| `PiiRedactionPhase` | `Idle`, `Scanning`, `Redacted`, `Sending`, `Thinking`, `Streaming`, `Restoring`, `Done`, `Error` | +| `PiiMatch` | `Type`, `Start`, `End` (exclusive), `Length` | +| `PiiEntity` | `Type`, `Start`, `End`, `Value`, `Token` (`EMAIL_1`), `Placeholder` (`[EMAIL_1]`), `PromptStart`, `PromptEnd` | +| `PiiRedactionResult` | `Prompt`, `Entities` | + +### Narrow containers + + + +The layout follows the card's own width. Below 520px the four type cells fall into two rows of two. Below 400px the card +tightens its padding and gaps and hides the Simulated tag (the status still announces it), and a long model name +truncates with an ellipsis in the flow. The text layers stack in one cell and the reply reserves the height of the +simulation's restored reply, so nothing jumps at 360px, 390px or desktop widths. + +### Content guidelines + +- Keep placeholders verbatim in your system prompt ("Refer to [EMAIL_1] exactly as written") so the guard can restore + them. +- Keep one token per value across a conversation if the model refers back to it: `Redact` numbers per call, so persist + the entity map between turns. +- Sample data in demos must be obviously fake: invented names, example.com emails, 555 numbers and test card numbers. + +### Best practices + +**Do** + +- Redact on the server in production and keep the entities in the request scope. +- Set `AutoPlay="False"` in product UI. +- Log the types of what was redacted, never the values next to the prompt. + +**Avoid** + +- Treating the built-in detector as complete: names beyond the list and simple introductions need an NER model. +- Sending the original message anywhere the card shows as safe. + +### Common questions + +**When does the autoplay stop?** For good as soon as someone edits the message, toggles Show original or presses a +button. It only runs while at least a quarter of the card is on screen, and never under reduced motion. + +**What happens to a placeholder the card does not know?** It stays as written, so a model that invents `[CARD_7]` shows +exactly that. + +**Can I keep the safe prompt but hide the reply?** Leave the card at `Redacted` and read `SafePrompt`; Send is the +person's choice. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- A polite status region announces each phase ("Scanning for personal data", "4 items redacted", "Reply restored") and, + when the reply is simulated, adds "Simulated, nothing was sent". +- Status never relies on color: every phase pairs an icon with a short label, each type cell reads its name with its + token or "None found", and the original message lists its marks ("juniper.vale@example.com: Email, redacted as + EMAIL_1"). +- Scan, Stop, Send, Run again and Retry are one button whose label changes, so focus stays put. Reset returns focus to the + message field. +- Only the visible text layer is exposed to assistive tech; the hidden layers that keep the height steady are not. +- The flow strip is decorative and hidden from screen readers, since the status region narrates each hop. +- Show original is the switch's own label, and the switch is disabled and dimmed until there is a safe prompt. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the message field (before a scan), Show original once there is a safe prompt, Reset when it shows, and the primary action | +| Cmd + Enter, Ctrl + Enter | Scans from the field; focus moves to the primary action, which now reads Stop | +| Space, Enter | Toggles Show original, or presses Scan, Stop, Send, Run again, Retry or Reset | + +## Motion + +- Scan marks each entity in turn, 120ms apart, with a 160ms tint in its type color, while the matching type cell + crossfades from "Checking" to its token. +- The swap to placeholders is one crossfade between two stacked text layers (the old one out in 100ms, the new one in + over 160ms), so no word slides or rewraps and the height never changes. Show original uses the same crossfade. +- Each hop of the flow is one dot travelling the connector in 460ms on an ease in out curve; the working node's label + shimmers. +- The reply streams three to six words every 34ms, each chunk only fading in over 160ms, then crossfades once to the + restored text. +- Labels, statuses and the Reset button crossfade in place; nothing bounces and nothing changes size between states. + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Marks | One after another, 120ms apart, 160ms tint | All at once | All at once | +| Layer swap | 100ms out, 160ms in | 100ms out, 160ms in | At once | +| Flow hops | A dot travels 460ms | None | None | +| Reply | Streams in chunks, then crossfades | Shows whole, then crossfades | At once | +| Autoplay | Loops while on screen | Off | Off | + +## Related + +- `ToolCall` for an arbitrary tool step in a transcript. +- `AgentStatus` for the live status of a whole agent run. +- `Switch` for the toggle on its own. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/zh-CN.md new file mode 100644 index 00000000..b46d25aa --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/zh-CN.md @@ -0,0 +1,204 @@ +--- +title: PII redaction +summary: 一张防护卡片:提示词送到模型之前,其中的个人数据会被脱敏;回复返回时,原始值再被放回。 +source-hash: f3818a4777ad +--- + + + +## 何时使用 + +`PiiRedaction` 展示隐私防护对提示词做了什么。扫描会在消息中标出每个姓名、邮箱、电话号码和卡号及其类型,然后在一次交叉淡化中把它们各自换成带编号的占位符,例如 `[EMAIL_1]`。发送会让安全提示词沿一条小流程(你、PII 防护、模型)传递,回复带着占位符流式返回,防护再为用户放回原始值。"显示原文"在安全提示词与用户输入的内容之间切换。 + +- **说明或证明一项防护措施**:客服助手、医疗或金融副驾驶、企业聊天等 AI 产品。 +- **设置或引导页面**:管理员在开启 PII 过滤之前先看到它会做什么。 +- **演练场**:用户粘贴文本,准确看到哪些内容会到达模型。 + +## 何时不用 + +- **对话本身**:使用聊天或输入框组件,在后台脱敏;只在需要解释防护的地方显示这张卡片。 +- **合规**:不要依赖内置检测器。通过 `Detector` 接入真正的 DLP 或 NER 服务。 +- **对话记录里任意的工具步骤**:使用 `ToolCall`。 + +## 用法 + +```xml + +``` + +```csharp +card.Names = [user.Name]; +card.Detector = async (text, token) => await dlp.ScanAsync(text, token); // spans with an exclusive end +card.Sender = async (prompt, token) => await chat.CompleteAsync(prompt, token); // may contain [NAME_1], [EMAIL_1] +card.Redacted += (_, e) => audit.Log("pii.redacted", e.Entities.Select(x => x.Type)); +``` + +`PiiRedaction` 是 Special 包(`UseArcSpecial()`)中的 `TemplatedControl`,宽度填满容器,最多 640px。既没有 `Message` 也没有 `Simulation` 时,卡片显示一个空的消息框("Paste a message with names, emails, phone numbers or cards"),在输入内容之前 Scan 处于关闭状态;在设置 `ModelName` 之前,流程条把模型显示为 "Model"。 + +### 模拟 + +想在没有模型的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `PiiRedactionSimulation`:消息、要标记的姓名、安全提示要发往的模型,以及带占位符的模型回复。卡片没有 `Message` 时会显示模拟的消息,并循环播放扫描、发送和还原,直到有人接手;回复是模拟的,卡片会说明这一点。编辑过的消息会得到一条简短的替身回复。你自己的值优先于模拟的值;把 `Simulation` 设为 null 会停止模拟并显示空的消息框。 + +```csharp +card.Simulation = new PiiRedactionSimulation +{ + Message = "Hi, I'm Juniper Vale. Send the receipt to juniper.vale@example.com.", + Names = ["Juniper Vale"], + ModelName = "Grok 4.7", + Reply = "Done, [NAME_1]. The receipt is on its way to [EMAIL_1].", +}; +``` + +上方的预览播放的是一段虚构客服对话的模拟,来自图库自己的演示数据:Juniper Vale 关于重复扣款的请求,其中有一个测试卡号(4242 4242 4242 4242)、一个 example.com 邮箱和一个虚构 555 号段的电话号码。 + +### 结构 + +1. **头部**:一个盾牌、"PII guard",以及回复为模拟时显示的 Simulated 标签。 +2. **消息**:扫描前是可编辑的输入框;扫描时是带标记的原文;之后是安全提示词,每个占位符以其类型的颜色显示在圆角标记上。上方是标签("Message"、"Safe prompt" 或 "Original message")和"显示原文"开关。 +3. **类型格**:Name、Email、Phone 和 Card,每格显示 "Not checked"、闪烁的 "Checking"、它的令牌("EMAIL_1",更多时为 "EMAIL_1 +1")或 "None found"。找到内容的格子会带上浅色底色。 +4. **流程条**:你、防护和模型,每一跳都有一个圆点经过。正在工作的节点标签会闪烁。 +5. **回复**:模型的标记和名称位于回复上方;回复带着占位符流式出现,随后显示还原后的文本,每个放回的值下方都有标记。 +6. **底部**:带图标的状态、有内容可重置时出现的 Reset,以及唯一的主操作。 + +### 阶段 + +主操作是同一个按钮,标签随阶段变化,因此键盘焦点在整个流程中保持不动。 + +| 阶段 | 状态 | 主操作 | +|---|---|---| +| `Idle` | Ready to scan | Scan | +| `Scanning` | Scanning for personal data | Stop | +| `Redacted` | 4 items redacted,或 No personal data found | Send | +| `Sending` | Sending the safe prompt | Stop | +| `Thinking` | Waiting for Grok 4.7 | Stop | +| `Streaming` | Grok 4.7 is replying | Stop | +| `Restoring` | Restoring original values | Stop | +| `Done` | Reply restored | Run again | +| `Error` | 异常的消息 | Retry | + +扫描中按 Stop 会回到可编辑的消息;请求中按 Stop 会取消传给 `Sender` 的令牌,并回到安全提示词。Retry 会重新运行失败的步骤。Reset 取消所有进行中的操作并回到输入框。`Scan()`、`Send()`、`Stop()` 和 `Reset()` 在代码中做同样的事。 + +### 演练场 + + + +`AutoPlay="False"` 配合 `Message=""` 得到一个空白输入框。用户粘贴文本后按 Scan,或在输入框里按 Cmd 或 Ctrl 加 Enter。`Message` 是双向的,应用可以读取输入的内容。`IsEditable="False"` 让输入框保持只读。 + +### 你的检测器和你的模型 + + + +`Detector` 用你的 DLP 或 NER 服务替换内置检测器。它接收文本和一个取消令牌,返回 `PiiMatch` 区间(类型、起点和不含在内的终点)。`Sender` 把安全提示词发送给你的模型,并返回带占位符的回复;卡片会还原它们。没有 `Sender` 时回复是模拟的:头部显示 Simulated 标签,状态会补充 "Simulated, nothing was sent"。两者任何一个抛出异常时,都会显示异常消息并提供 Retry。 + +`Redacted` 报告安全提示词和每个 `PiiEntity`(类型、位置、值、令牌和占位符)。`ReplyRestored` 报告原始回复、还原后的文本以及它是否为模拟。`PhaseChanged` 跟随每一次阶段变化。`Phase`、`SafePrompt` 和 `Reply` 只读且可绑定;`ShowOriginal` 是双向的。`Entities` 保存上一次脱敏的实体,`IsSimulated` 表示回复是否为模拟。 + +`ModelName` 和 `ModelMark`(24 × 24 的描边图形,默认是火花)在流程条和回复上方标明模型;`ModelMarkBrush` 为标记着色。`NameBrush`、`EmailBrush`、`PhoneBrush` 和 `CardBrush` 设置各类型的颜色(默认是 Series1 到 Series4)。 + +### 不带卡片的辅助方法 + +```csharp +// Server side, before the prompt leaves your backend. +var result = PiiRedactor.Redact(message, PiiRedactor.Detect(message, [user.Name])); +var reply = PiiRedactor.Restore(await model.CompleteAsync(result.Prompt), result.Entities); +``` + +| 成员 | 作用 | +|---|---| +| `PiiRedactor.Detect(text, names)` | 找出邮箱、卡号(13 到 19 位并通过 Luhn 校验)、电话号码(7 到 15 位)和姓名(名单中的姓名,以及 "I'm"、"I am"、"this is" 和 "my name is" 后面首字母大写的词),按位置排序且不重叠 | +| `PiiRedactor.Redact(text, matches)` | 返回包含安全提示词和实体的 `PiiRedactionResult`;第一个之后与之重叠的区间会被跳过,重复的值保留同一个令牌 | +| `PiiRedactor.Restore(reply, entities)` | 放回原始值,未知令牌保持原样 | +| `PiiRedactor.PassesLuhn(value)` | 对一个值中的数字做 Luhn 校验 | + +| 类型 | 成员 | +|---|---| +| `PiiType` | `Name`、`Email`、`Phone`、`Card` | +| `PiiRedactionPhase` | `Idle`、`Scanning`、`Redacted`、`Sending`、`Thinking`、`Streaming`、`Restoring`、`Done`、`Error` | +| `PiiMatch` | `Type`、`Start`、`End`(不含在内)、`Length` | +| `PiiEntity` | `Type`、`Start`、`End`、`Value`、`Token`(`EMAIL_1`)、`Placeholder`(`[EMAIL_1]`)、`PromptStart`、`PromptEnd` | +| `PiiRedactionResult` | `Prompt`、`Entities` | + +### 窄容器 + + + +布局跟随卡片自身的宽度。窄于 520px 时,四个类型格变成两行,每行两个。窄于 400px 时,卡片收紧内边距和间距并隐藏 Simulated 标签(状态仍会播报),过长的模型名称在流程条中以省略号截断。文字图层叠放在同一个单元格中,回复区预留模拟回复还原后的高度,因此在 360px、390px 或桌面宽度下都不会跳动。 + +### 内容指南 + +- 在系统提示词中要求模型原样保留占位符("Refer to [EMAIL_1] exactly as written"),以便防护能把它们还原。 +- 如果模型会回头引用某个值,请在整个对话中为它保留同一个令牌:`Redact` 每次调用单独编号,所以要在轮次之间保存实体映射。 +- 示例中的样例数据必须一看就是假的:虚构的姓名、example.com 邮箱、555 号码和测试卡号。 + +### 最佳实践 + +**建议** + +- 在生产环境中于服务端脱敏,并把实体保存在请求范围内。 +- 在产品界面中设置 `AutoPlay="False"`。 +- 只记录被脱敏内容的类型,绝不要把值和提示词记录在一起。 + +**避免** + +- 把内置检测器当作完整方案:名单之外的姓名和简单自我介绍之外的情况需要 NER 模型。 +- 把原始消息发送到卡片标示为安全的任何地方。 + +### 常见问题 + +**自动播放什么时候停止?** 只要有人编辑消息、切换"显示原文"或按下任何按钮,就永久停止。它只在卡片至少四分之一位于屏幕内时运行,减弱动效时从不运行。 + +**卡片不认识的占位符会怎样?** 保持原样,所以模型编造的 `[CARD_7]` 会原样显示。 + +**能只保留安全提示词而不显示回复吗?** 让卡片停在 `Redacted` 并读取 `SafePrompt`;是否发送由用户决定。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 一个礼貌的状态区域会播报每个阶段("Scanning for personal data"、"4 items redacted"、"Reply restored"),回复为模拟时还会补充 "Simulated, nothing was sent"。 +- 状态从不只依赖颜色:每个阶段都把图标和简短标签配对,每个类型格读出名称及其令牌或 "None found",原文会列出它的标记("juniper.vale@example.com: Email, redacted as EMAIL_1")。 +- Scan、Stop、Send、Run again 和 Retry 是同一个按钮,只是标签改变,所以焦点保持不动。Reset 会把焦点送回消息输入框。 +- 只有可见的文字图层会暴露给辅助技术;用来保持高度稳定的隐藏图层不会。 +- 流程条是装饰性的,对屏幕阅读器隐藏,因为状态区域已经播报了每一跳。 +- "显示原文"是开关自身的标签,在有安全提示词之前开关被禁用并变暗。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过消息输入框(扫描前)、有安全提示词后的"显示原文"、显示时的 Reset,以及主操作 | +| Cmd + Enter、Ctrl + Enter | 从输入框开始扫描;焦点移到主操作,此时它显示 Stop | +| Space、Enter | 切换"显示原文",或按下 Scan、Stop、Send、Run again、Retry 或 Reset | + +## 动效 + +- 扫描逐个标记实体,间隔 120ms,每个以其类型颜色在 160ms 内着色,同时对应的类型格从 "Checking" 交叉淡化为它的令牌。 +- 换成占位符是两个叠放文字图层之间的一次交叉淡化(旧图层 100ms 淡出,新图层 160ms 淡入),因此没有词会滑动或重新换行,高度也从不改变。"显示原文"使用同样的交叉淡化。 +- 流程的每一跳是一个圆点以缓入缓出曲线在 460ms 内走完连接线;正在工作的节点标签会闪烁。 +- 回复每 34ms 流入三到六个词,每块只在 160ms 内淡入,然后一次交叉淡化为还原后的文本。 +- 标签、状态和 Reset 按钮原地交叉淡化;没有回弹,状态之间也没有尺寸变化。 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 标记 | 逐个出现,间隔 120ms,160ms 着色 | 同时出现 | 同时出现 | +| 图层切换 | 100ms 淡出,160ms 淡入 | 100ms 淡出,160ms 淡入 | 立即 | +| 流程跳转 | 圆点移动 460ms | 无 | 无 | +| 回复 | 分块流入,然后交叉淡化 | 整段显示,然后交叉淡化 | 立即 | +| 自动播放 | 在屏幕内时循环 | 关闭 | 关闭 | + +## 相关组件 + +- `ToolCall`:对话记录里任意的工具步骤。 +- `AgentStatus`:整个智能体运行的实时状态。 +- `Switch`:单独使用的开关。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/zh-TW.md new file mode 100644 index 00000000..3068c4b7 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/PiiRedaction/zh-TW.md @@ -0,0 +1,204 @@ +--- +title: PII redaction +summary: 一張防護卡片:提示詞送到模型之前,其中的個人資料會先被遮蔽;回覆傳回時,原始值再被放回。 +source-hash: f3818a4777ad +--- + + + +## 何時使用 + +`PiiRedaction` 展示隱私防護對提示詞做了什麼。掃描會在訊息中標出每個姓名、電子郵件、電話號碼與卡號及其類型,接著在一次交叉淡化中把它們各自換成帶編號的預留位置,例如 `[EMAIL_1]`。傳送會讓安全提示詞沿著一條小流程(你、PII 防護、模型)傳遞,回覆帶著預留位置以串流方式傳回,防護再為使用者放回原始值。「顯示原文」會在安全提示詞與使用者輸入的內容之間切換。 + +- **說明或證明一項防護機制**:客服助理、醫療或金融副駕駛、企業聊天等 AI 產品。 +- **設定或導覽畫面**:管理員在啟用 PII 篩選之前先看到它會做什麼。 +- **實驗場**:使用者貼上文字,確切看到哪些內容會送到模型。 + +## 何時不用 + +- **對話本身**:使用聊天或輸入框元件,在背景遮蔽;只在需要說明防護的地方顯示這張卡片。 +- **法規遵循**:不要依賴內建偵測器。透過 `Detector` 接上真正的 DLP 或 NER 服務。 +- **對話紀錄中任意的工具步驟**:使用 `ToolCall`。 + +## 用法 + +```xml + +``` + +```csharp +card.Names = [user.Name]; +card.Detector = async (text, token) => await dlp.ScanAsync(text, token); // spans with an exclusive end +card.Sender = async (prompt, token) => await chat.CompleteAsync(prompt, token); // may contain [NAME_1], [EMAIL_1] +card.Redacted += (_, e) => audit.Log("pii.redacted", e.Entities.Select(x => x.Type)); +``` + +`PiiRedaction` 是 Special 套件(`UseArcSpecial()`)中的 `TemplatedControl`,寬度填滿容器,最多 640px。既沒有 `Message` 也沒有 `Simulation` 時,卡片顯示一個空的訊息框("Paste a message with names, emails, phone numbers or cards"),在輸入內容之前 Scan 處於關閉狀態;在設定 `ModelName` 之前,流程列把模型顯示為 "Model"。 + +### 模擬 + +想在沒有模型的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `PiiRedactionSimulation`:訊息、要標記的姓名、安全提示要送往的模型,以及附占位符的模型回覆。卡片沒有 `Message` 時會顯示模擬的訊息,並循環播放掃描、傳送和還原,直到有人接手;回覆是模擬的,卡片會說明這一點。編輯過的訊息會得到一則簡短的替身回覆。你自己的值優先於模擬的值;把 `Simulation` 設為 null 會停止模擬並顯示空的訊息框。 + +```csharp +card.Simulation = new PiiRedactionSimulation +{ + Message = "Hi, I'm Juniper Vale. Send the receipt to juniper.vale@example.com.", + Names = ["Juniper Vale"], + ModelName = "Grok 4.7", + Reply = "Done, [NAME_1]. The receipt is on its way to [EMAIL_1].", +}; +``` + +上方的預覽播放的是一段虛構客服對話的模擬,來自圖庫自己的示範資料:Juniper Vale 關於重複扣款的請求,其中有一組測試卡號(4242 4242 4242 4242)、一個 example.com 電子郵件,以及一個虛構 555 號段的電話號碼。 + +### 結構 + +1. **標頭**:一個盾牌、「PII guard」,以及回覆為模擬時顯示的 Simulated 標籤。 +2. **訊息**:掃描前是可編輯的輸入框;掃描時是帶標記的原文;之後是安全提示詞,每個預留位置以其類型的顏色顯示在圓角標記上。上方是標籤(「Message」、「Safe prompt」或「Original message」)與「顯示原文」開關。 +3. **類型格**:Name、Email、Phone 與 Card,每格顯示「Not checked」、閃爍的「Checking」、它的權杖(「EMAIL_1」,更多時為「EMAIL_1 +1」)或「None found」。找到內容的格子會帶上淺色底色。 +4. **流程列**:你、防護與模型,每一跳都有一個圓點經過。正在運作的節點標籤會閃爍。 +5. **回覆**:模型的標記與名稱位於回覆上方;回覆帶著預留位置以串流方式出現,接著顯示還原後的文字,每個放回的值下方都有標記。 +6. **頁尾**:帶圖示的狀態、有內容可重設時出現的 Reset,以及唯一的主要動作。 + +### 階段 + +主要動作是同一個按鈕,標籤隨階段變化,因此鍵盤焦點在整個流程中都不會移動。 + +| 階段 | 狀態 | 主要動作 | +|---|---|---| +| `Idle` | Ready to scan | Scan | +| `Scanning` | Scanning for personal data | Stop | +| `Redacted` | 4 items redacted,或 No personal data found | Send | +| `Sending` | Sending the safe prompt | Stop | +| `Thinking` | Waiting for Grok 4.7 | Stop | +| `Streaming` | Grok 4.7 is replying | Stop | +| `Restoring` | Restoring original values | Stop | +| `Done` | Reply restored | Run again | +| `Error` | 例外的訊息 | Retry | + +掃描中按 Stop 會回到可編輯的訊息;請求中按 Stop 會取消傳給 `Sender` 的權杖,並回到安全提示詞。Retry 會重新執行失敗的步驟。Reset 會取消所有進行中的作業並回到輸入框。`Scan()`、`Send()`、`Stop()` 與 `Reset()` 在程式碼中做同樣的事。 + +### 實驗場 + + + +`AutoPlay="False"` 搭配 `Message=""` 會得到空白的輸入框。使用者貼上文字後按 Scan,或在輸入框中按 Cmd 或 Ctrl 加 Enter。`Message` 是雙向的,應用程式可以讀取輸入的內容。`IsEditable="False"` 讓輸入框維持唯讀。 + +### 你的偵測器與你的模型 + + + +`Detector` 以你的 DLP 或 NER 服務取代內建偵測器。它接收文字與一個取消權杖,傳回 `PiiMatch` 區間(類型、起點與不含在內的終點)。`Sender` 把安全提示詞傳給你的模型,並傳回帶預留位置的回覆;卡片會將它們還原。沒有 `Sender` 時回覆是模擬的:標頭顯示 Simulated 標籤,狀態會補上「Simulated, nothing was sent」。兩者任一擲出例外時,都會顯示例外訊息並提供 Retry。 + +`Redacted` 回報安全提示詞與每個 `PiiEntity`(類型、位置、值、權杖與預留位置)。`ReplyRestored` 回報原始回覆、還原後的文字,以及是否為模擬。`PhaseChanged` 跟隨每一次階段變化。`Phase`、`SafePrompt` 與 `Reply` 為唯讀且可繫結;`ShowOriginal` 是雙向的。`Entities` 保存上一次遮蔽的實體,`IsSimulated` 表示回覆是否為模擬。 + +`ModelName` 與 `ModelMark`(24 × 24 的描邊圖形,預設為火花)在流程列與回覆上方標示模型;`ModelMarkBrush` 為標記上色。`NameBrush`、`EmailBrush`、`PhoneBrush` 與 `CardBrush` 設定各類型的顏色(預設為 Series1 到 Series4)。 + +### 不帶卡片的輔助方法 + +```csharp +// Server side, before the prompt leaves your backend. +var result = PiiRedactor.Redact(message, PiiRedactor.Detect(message, [user.Name])); +var reply = PiiRedactor.Restore(await model.CompleteAsync(result.Prompt), result.Entities); +``` + +| 成員 | 作用 | +|---|---| +| `PiiRedactor.Detect(text, names)` | 找出電子郵件、卡號(13 到 19 位且通過 Luhn 檢查)、電話號碼(7 到 15 位)與姓名(名單中的姓名,以及「I'm」、「I am」、「this is」與「my name is」後面首字母大寫的字),依位置排序且不重疊 | +| `PiiRedactor.Redact(text, matches)` | 傳回包含安全提示詞與實體的 `PiiRedactionResult`;第一個之後與之重疊的區間會被略過,重複的值保留同一個權杖 | +| `PiiRedactor.Restore(reply, entities)` | 放回原始值,未知的權杖維持原樣 | +| `PiiRedactor.PassesLuhn(value)` | 對一個值中的數字做 Luhn 檢查 | + +| 類型 | 成員 | +|---|---| +| `PiiType` | `Name`、`Email`、`Phone`、`Card` | +| `PiiRedactionPhase` | `Idle`、`Scanning`、`Redacted`、`Sending`、`Thinking`、`Streaming`、`Restoring`、`Done`、`Error` | +| `PiiMatch` | `Type`、`Start`、`End`(不含在內)、`Length` | +| `PiiEntity` | `Type`、`Start`、`End`、`Value`、`Token`(`EMAIL_1`)、`Placeholder`(`[EMAIL_1]`)、`PromptStart`、`PromptEnd` | +| `PiiRedactionResult` | `Prompt`、`Entities` | + +### 窄容器 + + + +版面跟隨卡片本身的寬度。窄於 520px 時,四個類型格會排成兩列,每列兩個。窄於 400px 時,卡片收緊內距與間距並隱藏 Simulated 標籤(狀態仍會朗讀),過長的模型名稱在流程列中以刪節號截斷。文字圖層疊放在同一個儲存格中,回覆區預留模擬回覆還原後的高度,因此在 360px、390px 或桌面寬度下都不會跳動。 + +### 內容指南 + +- 在系統提示詞中要求模型原樣保留預留位置(「Refer to [EMAIL_1] exactly as written」),讓防護能將它們還原。 +- 如果模型會回頭參照某個值,請在整段對話中為它保留同一個權杖:`Redact` 每次呼叫各自編號,所以要在輪次之間保存實體對應。 +- 範例中的樣本資料必須一看就是假的:虛構的姓名、example.com 電子郵件、555 號碼與測試卡號。 + +### 最佳做法 + +**建議** + +- 在正式環境中於伺服器端遮蔽,並把實體保留在請求範圍內。 +- 在產品介面中設定 `AutoPlay="False"`。 +- 只記錄被遮蔽內容的類型,絕不要把值和提示詞記錄在一起。 + +**避免** + +- 把內建偵測器當作完整方案:名單以外的姓名與簡單自我介紹以外的情況需要 NER 模型。 +- 把原始訊息傳送到卡片標示為安全的任何地方。 + +### 常見問題 + +**自動播放什麼時候停止?** 只要有人編輯訊息、切換「顯示原文」或按下任何按鈕,就會永久停止。它只在卡片至少四分之一位於畫面內時執行,減少動態效果時從不執行。 + +**卡片不認得的預留位置會怎樣?** 維持原樣,所以模型編造的 `[CARD_7]` 會原樣顯示。 + +**能只保留安全提示詞而不顯示回覆嗎?** 讓卡片停在 `Redacted` 並讀取 `SafePrompt`;是否傳送由使用者決定。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 一個禮貌的狀態區域會朗讀每個階段(「Scanning for personal data」、「4 items redacted」、「Reply restored」),回覆為模擬時還會補上「Simulated, nothing was sent」。 +- 狀態從不只依賴顏色:每個階段都把圖示和簡短標籤配對,每個類型格讀出名稱及其權杖或「None found」,原文會列出它的標記(「juniper.vale@example.com: Email, redacted as EMAIL_1」)。 +- Scan、Stop、Send、Run again 與 Retry 是同一個按鈕,只是標籤改變,所以焦點維持不動。Reset 會把焦點送回訊息輸入框。 +- 只有可見的文字圖層會提供給輔助技術;用來保持高度穩定的隱藏圖層不會。 +- 流程列是裝飾性的,對螢幕閱讀器隱藏,因為狀態區域已經朗讀了每一跳。 +- 「顯示原文」是開關本身的標籤,在有安全提示詞之前開關會停用並變暗。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過訊息輸入框(掃描前)、有安全提示詞後的「顯示原文」、顯示時的 Reset,以及主要動作 | +| Cmd + Enter、Ctrl + Enter | 從輸入框開始掃描;焦點移到主要動作,此時它顯示 Stop | +| Space、Enter | 切換「顯示原文」,或按下 Scan、Stop、Send、Run again、Retry 或 Reset | + +## 動態效果 + +- 掃描逐一標記實體,間隔 120ms,每個以其類型顏色在 160ms 內著色,同時對應的類型格從「Checking」交叉淡化為它的權杖。 +- 換成預留位置是兩個疊放文字圖層之間的一次交叉淡化(舊圖層 100ms 淡出,新圖層 160ms 淡入),因此不會有字滑動或重新換行,高度也從不改變。「顯示原文」使用同樣的交叉淡化。 +- 流程的每一跳是一個圓點以緩入緩出曲線在 460ms 內走完連接線;正在運作的節點標籤會閃爍。 +- 回覆每 34ms 流入三到六個字,每塊只在 160ms 內淡入,然後一次交叉淡化為還原後的文字。 +- 標籤、狀態與 Reset 按鈕原地交叉淡化;沒有回彈,狀態之間也沒有尺寸變化。 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 標記 | 逐一出現,間隔 120ms,160ms 著色 | 同時出現 | 同時出現 | +| 圖層切換 | 100ms 淡出,160ms 淡入 | 100ms 淡出,160ms 淡入 | 立即 | +| 流程跳轉 | 圓點移動 460ms | 無 | 無 | +| 回覆 | 分塊流入,然後交叉淡化 | 整段顯示,然後交叉淡化 | 立即 | +| 自動播放 | 在畫面內時循環 | 關閉 | 關閉 | + +## 相關元件 + +- `ToolCall`:對話紀錄中任意的工具步驟。 +- `AgentStatus`:整個代理程式執行的即時狀態。 +- `Switch`:單獨使用的開關。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/en-US.md new file mode 100644 index 00000000..c89ed2d3 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/en-US.md @@ -0,0 +1,166 @@ +--- +title: Pixel mascot +summary: A pixel-art robot that shows what an AI assistant is doing. It idles, thinks, types, celebrates and errors in frame-stepped sprite animations, and stays crisp at every integer size from 24px to 192px. +--- + + + +## When to use + +`PixelMascot` is a small pixel-art robot on a 24 × 24 grid that carries the mood of an assistant at work. Each status +has its own sprite animation on a fixed 40ms clock, and a status change morphs from the current pose one pixel at a +time. + +- **A friendly generating indicator**: next to an assistant reply or in a chat composer, at scale 2 or 3. +- **Empty states and onboarding**: a small character makes an AI feature feel approachable, at scale 4 to 8. +- **Image or canvas generation**: a mascot in the corner shows thinking, drawing and done. +- **Products with a playful or retro voice** that still want precise, calm motion. + +## When not to use + +- **Detailed agent progress** with steps and timings: use `AgentStatus` or `AgentTimeline`. +- **Dense tables or toolbars**, where a 24px character is noise: use a status dot or `ArcSpinner`. +- **Serious or regulated contexts** (finance, health, legal), where a character feels flippant: use `TextShimmer`. +- **Showing the model's reasoning**: use `ReasoningBlock`. + +## Usage + +```xml + + + + + + + + +``` + +`PixelMascot` is a `Control` in the Special package (register it with `UseArcSpecial()`). It is a fixed square of 24 +times `Scale` device-independent pixels and never reflows; pick a smaller scale on narrow layouts. It sits on a text +line (left aligned, vertically centered), so at scale 1 and 2 it lines up with the words beside it. + +### Statuses + +Set `Status` to one of five values. Each has its own shape as well as its own motion, so the state never depends on +color alone: + +1. **Idle**: the robot bobs on a 2s cycle, blinks and glances to the side on an 8s cycle. +2. **Thinking**: a thought bubble steps in at the top right and cycles three dots every 1.12s while the eyes look up at + it and one hand rises to the chin. +3. **Writing**: a keyboard steps up from below, the hands take turns on the keys and a bar on the chest fills one + stroke at a time, eight strokes every 1.6s, while the eyes scan along it and return. +4. **Done**: happy eyes, a jump with both arms up that holds at the top for 120ms and settles without a rebound, and + two sparkles that twinkle above the head. +5. **Error**: crossed eyes in the danger color on a tinted screen, a red antenna light and one shake. + +Map your own lifecycle to them: a request sent is `Thinking`, the first token is `Writing`, finished is `Done`, a +failure is `Error`, and settle back to `Idle` a couple of seconds after done. + + + +### The built-in loop + +Leave `Status` null and the mascot plays its own loop through every state: idle, thinking, writing, done, idle, +thinking, error. `ShownStatus` reads the status it is showing and `ShownStatusChanged` reports each change with the old +and the new status. To keep several mascots in step, put one `PixelMascotLoop` in resources and bind their `Status` to +its `Status`; `IsEnabled="False"` holds the current step, and `Advance()` and `Reset()` move it by hand. + + + +### Sizes + +`Scale` is an integer from 1 to 8: every sprite pixel is that many device-independent pixels, so 1 renders 24px, 2 +renders 48px, 4 renders 96px (the default) and 8 renders 192px. Values outside the range clamp. Each pixel is drawn as a +rectangle snapped to whole device pixels at the window's render scaling, without antialiasing, so the art stays crisp +at every scale and on every display. + + + +### Next to text + +At scale 1 and 2 the mascot fits on a line of text. Pair it with a `TextShimmer` label when people need to know exactly +what is happening: the mascot carries the mood, the text carries the detail. When the label already says what is +going on, set `Announce="False"` so screen readers do not hear it twice. + + + +### Accent + +The palette follows the theme neutrals (the outline, the shell, the dark screen and the floor shadow) and +`AccentBrush`, which is the theme's accent unless you set it. The accent lights the antenna, the chest light, the typed +bar and the sparkles; the eyes glow in a lighter shade of it on the dark screen. It looks right with Arc's near-neutral +default accent too. + + + +### Reduced motion + +Under reduced motion the mascot holds a clear resting pose for each status: open eyes, the bubble with three dots, the +keyboard, happy eyes or crossed eyes. The built-in loop still moves from status to status. + + + +## API reference + + + + + + + + + +| Type | Values | +|---|---| +| `PixelMascotStatus` | `Idle`, `Thinking`, `Writing`, `Done`, `Error` | + +## Accessibility + +- The sprite is one image whose name follows the status: "Assistant is thinking", with `AssistantName` filled in. + `Label` replaces the generated name, and `Labels` rewords or translates every status. +- Status changes are announced through a polite live region when `Status` is set by the app. The built-in loop stays + quiet. Set `Announce` to false when the surrounding UI already reports progress, to avoid double announcements. +- Status is never color alone: each state has its own shape (the thought bubble, the keyboard, happy eyes, crossed + eyes) and the name carries the words. +- The error state uses the danger token for the crossed eyes on a tinted screen, so it reads in light and dark themes + and with any accent. + +### Keyboard + +The mascot is decorative status, not a control: it takes no focus and has no keyboard interaction. + +| Keys | Action | +|---|---| +| `Tab` | Moves past the mascot; it is never focused | + +## Motion + +- Stepped on purpose: the sprite advances on a fixed 40ms clock and moves in whole pixels. Timings are authored as + curves in steps: the done jump rises two pixels in one step, holds at the top for 120ms and settles a pixel at a time + without a rebound. +- Status changes never cut: lids close before the eye shape changes and open after, eyes slide one pixel per step, arms + pass through the neighbouring poses, the thought bubble grows from a dot to a small bubble to the full one, and the + keyboard steps in and out a row at a time from wherever it is. A change in the middle reverses from the current + frame; the chest bar backspaces one stroke per step. +- Loops are seamless by construction: idle bobs on a 2s cycle with blinks and a glance on an 8s cycle, thinking cycles + three dots every 1.12s, writing types an eight-stroke bar every 1.6s while the eyes scan and return. +- One control draws the grid with merged rectangles per row; a step that changes no pixel skips the redraw, so it + repaints about 25 times a second at most and never measures or arranges again. +- The frame loop pauses off screen and while `IsPaused` is set (the current frame stays). + +| | Full | Reduced | Off | +|---|---|---|---| +| Sprite | Frame-stepped animation | Resting pose of each status | Resting pose of each status | +| Status change | Morphs one pixel per step | Switches pose | Switches pose | +| Built-in loop | Cycles | Cycles, one resting pose per step | Holds the current status | + +## Related + +| Component | Choose it when | +|---|---| +| `AgentStatus` | You need the agent's current step and elapsed time, not a mood | +| `TextShimmer` | A calm label is enough, or alongside the mascot for the detail | +| `ReasoningBlock` | You show what the model is thinking, not just that it thinks | +| `ArcSpinner` | Space is tight and a character would be noise | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/zh-CN.md new file mode 100644 index 00000000..e4822e92 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/zh-CN.md @@ -0,0 +1,150 @@ +--- +title: Pixel mascot +summary: 一个像素风机器人,展示 AI 助手正在做什么。它以逐帧的精灵动画待机、思考、打字、庆祝和报错,在 24px 到 192px 的每个整数尺寸下都保持清晰。 +source-hash: 651f6421fcbd +--- + + + +## 何时使用 + +`PixelMascot` 是一个画在 24 × 24 网格上的小像素机器人,用来传达助手工作时的状态与情绪。每种状态都有自己的精灵动画, +在固定的 40ms 时钟上逐帧推进;状态切换时从当前姿势出发,一次移动一个像素地变形过去。 + +- **友好的生成指示**:放在助手回复旁边或聊天输入框里,缩放 2 或 3。 +- **空状态与新手引导**:一个小角色让 AI 功能显得更亲切,缩放 4 到 8。 +- **图像或画布生成**:角落里的吉祥物展示思考、绘制和完成。 +- **语气活泼或复古的产品**,同时仍希望动效精确、安静。 + +## 何时不用 + +- **详细的智能体进度**(步骤与耗时):使用 `AgentStatus` 或 `AgentTimeline`。 +- **密集的表格或工具栏**,24px 的角色只会成为干扰:使用状态点或 `ArcSpinner`。 +- **严肃或受监管的场景**(金融、医疗、法律),角色会显得轻浮:使用 `TextShimmer`。 +- **展示模型的推理内容**:使用 `ReasoningBlock`。 + +## 用法 + +```xml + + + + + + + + +``` + +`PixelMascot` 是 Special 包中的一个 `Control`(用 `UseArcSpecial()` 注册)。它是边长为 24 乘以 `Scale` 个设备无关像素的 +固定正方形,从不重新排版;窄布局请选更小的缩放。它贴在文字行上(左对齐、垂直居中),所以在缩放 1 和 2 时与旁边的文字对齐。 + +### 状态 + +把 `Status` 设为五个值之一。每种状态除了动效不同,形状也不同,因此状态从不只靠颜色区分: + +1. **Idle(待机)**:机器人以 2 秒为周期上下轻晃,以 8 秒为周期眨眼并向旁边瞥一眼。 +2. **Thinking(思考)**:右上角逐步出现一个思考气泡,每 1.12 秒循环点亮三个点;眼睛向上看着它,一只手抬到下巴。 +3. **Writing(书写)**:键盘从下方逐步升起,双手轮流敲键,胸前的进度条一次一笔地填满,每 1.6 秒打完八笔,眼睛沿着它扫过去再回来。 +4. **Done(完成)**:开心的眼睛,双臂举起跳一下,在最高处停留 120ms,落下时没有回弹;头顶两颗闪光一闪一闪。 +5. **Error(出错)**:危险色的叉形眼睛配上带色调的屏幕、红色的天线灯,并摇晃一次。 + +把你自己的请求生命周期映射到它们上:请求发出是 `Thinking`,收到第一个 token 是 `Writing`,结束是 `Done`,失败是 +`Error`,完成后过一两秒回到 `Idle`。 + + + +### 内置循环 + +把 `Status` 留空,吉祥物会自己循环播放所有状态:待机、思考、书写、完成、待机、思考、出错。`ShownStatus` 读取它正在显示的 +状态,`ShownStatusChanged` 在每次变化时报告旧状态和新状态。要让多个吉祥物步调一致,在资源中放一个 `PixelMascotLoop`, +把它们的 `Status` 绑定到它的 `Status`;`IsEnabled="False"` 停在当前一步,`Advance()` 与 `Reset()` 可以手动推进或复位。 + + + +### 尺寸 + +`Scale` 是 1 到 8 的整数:每个精灵像素占这么多个设备无关像素,所以 1 渲染为 24px,2 为 48px,4 为 96px(默认),8 为 +192px。超出范围的值会被限制在范围内。每个像素都画成对齐到窗口渲染缩放下整数设备像素的矩形,并且不做抗锯齿, +因此在每种缩放、每块屏幕上都保持清晰。 + + + +### 与文字并排 + +缩放 1 和 2 时,吉祥物能放进一行文字里。需要让用户确切知道发生了什么时,配上 `TextShimmer` 标签:吉祥物传达情绪,文字传达细节。 +如果标签已经说明了正在发生的事,设置 `Announce="False"`,避免屏幕阅读器读两遍。 + + + +### 强调色 + +调色板跟随主题的中性色(轮廓、外壳、深色屏幕和地面阴影)以及 `AccentBrush`;不设置时它就是主题的强调色。强调色点亮天线、 +胸灯、打字进度条和闪光;眼睛在深色屏幕上以更亮的同色调发光。配合 Arc 默认的近中性强调色同样好看。 + + + +### 减少动态效果 + +在减少动态效果下,吉祥物为每种状态保持一个清晰的静止姿势:睁开的眼睛、带三个点的气泡、键盘、开心的眼睛或叉形眼睛。 +内置循环仍会在状态之间切换。 + + + +## API 参考 + + + + + + + + + +| 类型 | 取值 | +|---|---| +| `PixelMascotStatus` | `Idle`、`Thinking`、`Writing`、`Done`、`Error` | + +## 无障碍 + +- 精灵是一张图像,名称跟随状态变化,例如 "Assistant is thinking",其中填入 `AssistantName`。`Label` 会替换生成的名称, + `Labels` 可以改写或翻译每种状态的措辞。 +- 当 `Status` 由应用设置时,状态变化通过礼貌(polite)的实时区域播报;内置循环保持安静。周围界面已经报告进度时, + 把 `Announce` 设为 false,避免重复播报。 +- 状态从不只靠颜色:每种状态都有自己的形状(思考气泡、键盘、开心的眼睛、叉形眼睛),名称承载文字信息。 +- 出错状态在带色调的屏幕上用危险色 Token 画叉形眼睛,因此在浅色和深色主题、任何强调色下都能看清。 + +### 键盘 + +吉祥物是装饰性的状态展示,不是控件:它不获取焦点,也没有键盘交互。 + +| 按键 | 操作 | +|---|---| +| `Tab` | 跳过吉祥物;它从不获得焦点 | + +## 动效 + +- 刻意的逐帧:精灵在固定的 40ms 时钟上推进,以整像素移动。时间曲线以步为单位编写:完成时的跳跃一步升起两个像素, + 在最高处停留 120ms,再一次一个像素地落下,没有回弹。 +- 状态切换从不硬切:眼睛形状变化前眼睑先闭上、之后再睁开,眼睛每步滑动一个像素,手臂经过相邻的姿势,思考气泡从一个点 + 长成小气泡再到完整气泡,键盘从当前位置一次一行地进出。切换到一半时从当前帧反向;胸前的进度条每步退格一笔。 +- 循环天然无缝:待机以 2 秒周期上下轻晃,8 秒周期内眨眼和瞥视;思考每 1.12 秒循环三个点;书写每 1.6 秒打出八笔进度条, + 眼睛扫过去再回来。 +- 一个控件按行合并矩形绘制整个网格;没有像素变化的一步会跳过重绘,所以每秒最多重绘约 25 次,并且从不重新测量或排列。 +- 帧循环在离开屏幕时以及设置 `IsPaused` 时暂停(停在当前帧)。 + +| | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 精灵 | 逐帧动画 | 每种状态的静止姿势 | 每种状态的静止姿势 | +| 状态切换 | 每步变形一个像素 | 直接换姿势 | 直接换姿势 | +| 内置循环 | 循环播放 | 循环,每一步显示静止姿势 | 停在当前状态 | + +## 相关组件 + +| 组件 | 何时选择 | +|---|---| +| `AgentStatus` | 需要智能体当前的步骤和耗时,而不是情绪 | +| `TextShimmer` | 一个安静的标签就够了,或与吉祥物搭配承载细节 | +| `ReasoningBlock` | 要展示模型在想什么,而不只是它在思考 | +| `ArcSpinner` | 空间紧张,角色会成为干扰 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/zh-TW.md new file mode 100644 index 00000000..9b3991b9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/PixelMascot/zh-TW.md @@ -0,0 +1,150 @@ +--- +title: Pixel mascot +summary: 一個像素風機器人,呈現 AI 助理正在做什麼。它以逐格的精靈動畫待機、思考、打字、慶祝和出錯,在 24px 到 192px 的每個整數尺寸下都保持清晰。 +source-hash: 651f6421fcbd +--- + + + +## 何時使用 + +`PixelMascot` 是一個畫在 24 × 24 網格上的小像素機器人,用來傳達助理工作時的狀態與情緒。每種狀態都有自己的精靈動畫, +在固定的 40ms 時脈上逐格推進;狀態切換時從目前的姿勢出發,一次移動一個像素地變形過去。 + +- **友善的生成指示**:放在助理回覆旁邊或聊天輸入框裡,縮放 2 或 3。 +- **空狀態與新手導覽**:一個小角色讓 AI 功能顯得更親切,縮放 4 到 8。 +- **影像或畫布生成**:角落裡的吉祥物呈現思考、繪製和完成。 +- **語氣活潑或復古的產品**,同時仍希望動態效果精確、安靜。 + +## 何時不用 + +- **詳細的代理程式進度**(步驟與耗時):使用 `AgentStatus` 或 `AgentTimeline`。 +- **密集的表格或工具列**,24px 的角色只會成為干擾:使用狀態點或 `ArcSpinner`。 +- **嚴肅或受監管的情境**(金融、醫療、法律),角色會顯得輕浮:使用 `TextShimmer`。 +- **呈現模型的推理內容**:使用 `ReasoningBlock`。 + +## 用法 + +```xml + + + + + + + + +``` + +`PixelMascot` 是 Special 套件中的一個 `Control`(用 `UseArcSpecial()` 註冊)。它是邊長為 24 乘以 `Scale` 個裝置無關像素的 +固定正方形,從不重新排版;窄版面請選較小的縮放。它貼在文字行上(靠左對齊、垂直置中),所以在縮放 1 和 2 時與旁邊的文字對齊。 + +### 狀態 + +把 `Status` 設為五個值之一。每種狀態除了動態不同,形狀也不同,因此狀態從不只靠顏色區分: + +1. **Idle(待機)**:機器人以 2 秒為週期上下輕晃,以 8 秒為週期眨眼並往旁邊瞄一眼。 +2. **Thinking(思考)**:右上角逐步出現一個思考泡泡,每 1.12 秒循環點亮三個點;眼睛往上看著它,一隻手抬到下巴。 +3. **Writing(撰寫)**:鍵盤從下方逐步升起,雙手輪流敲鍵,胸前的進度列一次一筆地填滿,每 1.6 秒打完八筆,眼睛沿著它掃過去再回來。 +4. **Done(完成)**:開心的眼睛,雙臂舉起跳一下,在最高處停留 120ms,落下時沒有回彈;頭頂兩顆閃光一閃一閃。 +5. **Error(出錯)**:危險色的叉形眼睛配上帶色調的螢幕、紅色的天線燈,並搖晃一次。 + +把你自己的請求生命週期對應到它們:請求送出是 `Thinking`,收到第一個 token 是 `Writing`,結束是 `Done`,失敗是 +`Error`,完成後過一兩秒回到 `Idle`。 + + + +### 內建循環 + +把 `Status` 留空,吉祥物會自己循環播放所有狀態:待機、思考、撰寫、完成、待機、思考、出錯。`ShownStatus` 讀取它正在顯示的 +狀態,`ShownStatusChanged` 在每次變化時回報舊狀態和新狀態。要讓多個吉祥物步調一致,在資源中放一個 `PixelMascotLoop`, +把它們的 `Status` 繫結到它的 `Status`;`IsEnabled="False"` 停在目前這一步,`Advance()` 與 `Reset()` 可以手動推進或重設。 + + + +### 尺寸 + +`Scale` 是 1 到 8 的整數:每個精靈像素佔這麼多個裝置無關像素,所以 1 算繪為 24px,2 為 48px,4 為 96px(預設),8 為 +192px。超出範圍的值會被限制在範圍內。每個像素都畫成對齊到視窗算繪縮放下整數裝置像素的矩形,而且不做反鋸齒, +因此在每種縮放、每塊螢幕上都保持清晰。 + + + +### 與文字並排 + +縮放 1 和 2 時,吉祥物能放進一行文字裡。需要讓使用者確切知道發生了什麼時,搭配 `TextShimmer` 標籤:吉祥物傳達情緒,文字傳達細節。 +如果標籤已經說明了正在發生的事,設定 `Announce="False"`,避免螢幕閱讀器唸兩次。 + + + +### 強調色 + +調色盤跟隨主題的中性色(輪廓、外殼、深色螢幕和地面陰影)以及 `AccentBrush`;不設定時它就是主題的強調色。強調色點亮天線、 +胸燈、打字進度列和閃光;眼睛在深色螢幕上以更亮的同色調發光。搭配 Arc 預設的近中性強調色同樣好看。 + + + +### 減少動態效果 + +在減少動態效果下,吉祥物為每種狀態保持一個清楚的靜止姿勢:張開的眼睛、帶三個點的泡泡、鍵盤、開心的眼睛或叉形眼睛。 +內建循環仍會在狀態之間切換。 + + + +## API 參考 + + + + + + + + + +| 型別 | 值 | +|---|---| +| `PixelMascotStatus` | `Idle`、`Thinking`、`Writing`、`Done`、`Error` | + +## 無障礙 + +- 精靈是一張影像,名稱跟隨狀態變化,例如 "Assistant is thinking",其中填入 `AssistantName`。`Label` 會取代產生的名稱, + `Labels` 可以改寫或翻譯每種狀態的措辭。 +- 當 `Status` 由應用程式設定時,狀態變化透過禮貌(polite)的即時區域播報;內建循環保持安靜。周圍介面已經回報進度時, + 把 `Announce` 設為 false,避免重複播報。 +- 狀態從不只靠顏色:每種狀態都有自己的形狀(思考泡泡、鍵盤、開心的眼睛、叉形眼睛),名稱承載文字資訊。 +- 出錯狀態在帶色調的螢幕上用危險色 Token 畫叉形眼睛,因此在淺色和深色主題、任何強調色下都看得清楚。 + +### 鍵盤 + +吉祥物是裝飾性的狀態呈現,不是控制項:它不取得焦點,也沒有鍵盤互動。 + +| 按鍵 | 動作 | +|---|---| +| `Tab` | 略過吉祥物;它從不取得焦點 | + +## 動態效果 + +- 刻意的逐格:精靈在固定的 40ms 時脈上推進,以整像素移動。時間曲線以步為單位編寫:完成時的跳躍一步升起兩個像素, + 在最高處停留 120ms,再一次一個像素地落下,沒有回彈。 +- 狀態切換從不硬切:眼睛形狀變化前眼瞼先闔上、之後再張開,眼睛每步滑動一個像素,手臂經過相鄰的姿勢,思考泡泡從一個點 + 長成小泡泡再到完整泡泡,鍵盤從目前位置一次一列地進出。切換到一半時從目前這一格反向;胸前的進度列每步倒退一筆。 +- 循環天生無縫:待機以 2 秒週期上下輕晃,8 秒週期內眨眼和瞄視;思考每 1.12 秒循環三個點;撰寫每 1.6 秒打出八筆進度列, + 眼睛掃過去再回來。 +- 一個控制項按列合併矩形繪製整個網格;沒有像素變化的一步會略過重繪,所以每秒最多重繪約 25 次,而且從不重新測量或排列。 +- 影格循環在離開螢幕時以及設定 `IsPaused` 時暫停(停在目前這一格)。 + +| | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 精靈 | 逐格動畫 | 每種狀態的靜止姿勢 | 每種狀態的靜止姿勢 | +| 狀態切換 | 每步變形一個像素 | 直接換姿勢 | 直接換姿勢 | +| 內建循環 | 循環播放 | 循環,每一步顯示靜止姿勢 | 停在目前狀態 | + +## 相關元件 + +| 元件 | 何時選擇 | +|---|---| +| `AgentStatus` | 需要代理程式目前的步驟和耗時,而不是情緒 | +| `TextShimmer` | 一個安靜的標籤就夠了,或與吉祥物搭配承載細節 | +| `ReasoningBlock` | 要呈現模型在想什麼,而不只是它在思考 | +| `ArcSpinner` | 空間有限,角色會成為干擾 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/en-US.md new file mode 100644 index 00000000..0921fbcc --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/en-US.md @@ -0,0 +1,185 @@ +--- +title: Provider keys +summary: A settings card for bring-your-own-key AI products: each provider's masked key, status and last use, with reveal, copy, rotate and remove with undo, and an inline form that tests a new key before saving. +--- + + + +## When to use + +`ProviderKeys` is the settings card of a product where people bring their own AI provider keys. Each provider gets a +row with its mark, the masked key (`sk-••••4f2a`), a status as icon plus label and when the key was last used. A key +can be revealed for five seconds, copied, rotated or removed with a short undo window. Adding or rotating a key opens an +inline form under the row that checks the format as you type, then tests the connection before saving. + +- **Workspace or account settings** where people add their own keys for the providers you support. +- **An onboarding step** that asks for a first key and shows when it connects. +- **A gateway admin page** that shows which providers are connected, which keys were rejected and when each was used. +- **Rotating a leaked key**, or removing one with a moment to undo it. + +## When not to use + +- **Choosing a model for a request**: use a model picker. +- **Your own product's API keys for customers**, with scopes and expiry: use a table. +- **Spend or rate limits**: use a usage meter. + +## Usage + +Set `Providers` to the providers you support, bind `Keys` (two-way) to what your server returns, handle `KeySaved` and +`KeyRemoved` to persist, and set `TestKey` to test keys on your server. Providers without a key read Not set. Without +`TestKey` a test fails with "No key test is set up", so no key reads Connected without a check. + +```csharp +card.Providers = +[ + new ProviderKeyProvider { Id = "anthropic", Name = "Anthropic", KeyPrefix = "sk-ant-api03-" }, + new ProviderKeyProvider { Id = "google", Name = "Google" }, // no documented prefix: no prefix check +]; +card.Keys = await api.GetProviderKeysAsync(); // one ProviderKey per provider +card.TestKey = request => api.TestProviderKeyAsync(request.Provider, request.Key, request.CancellationToken); +card.KeySaved += (_, e) => api.SaveProviderKey(e.Provider, e.Key); +card.KeyRemoved += (_, e) => api.DeleteProviderKey(e.Provider); +``` + +### Anatomy + +1. **Mark**: the provider's glyph (or a monogram) in a tinted rounded square. +2. **Name over key**: the provider's name, then the masked key in the mono face (the full key while revealed). +3. **Status over last use**: Connected, Invalid or Not set with an icon, then "Used 2 min ago", "Never used" or the + rejection reason. Pressing the status tests the stored key again. +4. **Actions**: reveal with a countdown ring, copy, rotate and remove; Add key on an empty row; Undo after a removal. +5. **Inline form**: the key input with Cancel and Save, and a helper line that says what is wrong, Testing connection + or Connected. + +### Your API + + + +`TestKey` receives a `ProviderKeyTestRequest`: the provider's id, the pasted key when adding or rotating (no key when +retesting a stored one, so test it on your server by provider) and a token that is cancelled when the form closes. +Return `ProviderKeyTestResult.Ok(detail)` or `ProviderKeyTestResult.Fail("401 Unauthorized")`; an exception counts as a +failure with its message. Rotation keeps the old key on the row until the new one passes, then swaps. + +| Event | When | +|---|---| +| `KeysChanged` | Every change: added, rotated, removed, restored with Undo, or retested. `Keys` is already updated. | +| `KeySaved` | A new key passed its test. Persist it; the row already shows it as connected. | +| `KeyRemoved` | The undo window closed, so a removal reaches your server only once it sticks. | +| `KeyCopied` | A key was copied, for an audit log. | + +Never send full keys to the client in production: give each `ProviderKey` only `Last4` and leave `Value` null. The row +then shows the masked key and turns reveal and copy off. + +### Empty state + + + +An empty `Keys` list shows every row as Not set with an Add key button. `Providers` chooses which providers get a row +and in what order; each `ProviderKeyProvider` gives the name, the key prefix and minimum length the form checks, and a +`Mark` glyph or a `Monogram`. Use the prefix the provider documents, and leave `KeyPrefix` empty when it documents +none. + +### Simulated tests + +For a demo or a prototype, set `Simulation` instead of `TestKey`: a `ProviderKeysSimulation` waits `Delay` (700ms), +fails a stored invalid key again with its own error (or `RejectedError`) and connects everything else. Nothing is sent, +and every announced result ends with "Simulated, nothing was sent." The demos on this page use real provider names and +the key prefixes their docs show; the keys are placeholders and the rejected key is invented. + +```xml + + + + + +``` + +### Loading + + + +`IsLoading` shows placeholder rows with the real row geometry while keys load. + +### Narrow cards + + + +At 560px and wider each row is one line of columns. Below that (`NarrowWidth`) the status moves beside the name, the +key gets its own line and last use shares a line with the actions; the form stacks its input above Cancel and Save. +The card holds at 360px with no horizontal scroll. + +### Your own words + +`Labels` holds every word the card shows or says, and `Title`, `Description` and `Footer` set the card's own text +(null hides the description or the footer): + +```csharp +card.Labels = new ProviderKeysLabels +{ + Connected = "Verbunden", + NotSet = "Nicht gesetzt", + AddKey = "Schlüssel hinzufügen", + CopyName = provider => $"{provider}-Schlüssel kopieren", +}; +``` + +### Best practices + +- Test keys from your server (a models list request is enough) and return the provider's own error. +- Pass `Last4` only; reveal and copy are for products that keep keys on the device. +- Keep `RevealDuration` short; five seconds is enough to read or compare a key. + +## API reference + + + + + + + + + + + +## Accessibility + +- The card is named by its title. Every icon button has a provider-specific name, such as "Copy the Anthropic key". +- Status is never color alone: an icon plus Connected, Invalid, Not set or Testing. The status button's name includes + the rejection reason ("OpenAI key: Invalid, 401 Unauthorized. Test again"). +- Reveal is a toggle that reports pressed; Rotate and Add key report whether the inline form is expanded. +- A polite live region announces reveals, copies, test results, removals and undo. Results of a `Simulation` add + "Simulated, nothing was sent." +- The form's input is named "Anthropic API key" and its help text is the helper line; format errors and rejections + mark it invalid. +- Focus is managed: the form focuses its input, Cancel and Escape return focus to the button that opened it, Remove moves + focus to Undo and Undo returns it to Remove. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through each row's status (retest), reveal, copy, rotate and remove, or Add key on an empty row | +| Enter, Space | Presses the focused button | +| Enter | In the form, checks the format, then tests and saves the key | +| Escape | In the form, cancels and returns focus to the button that opened it | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Status, key, last use and helper text | One crossfade in place: out 100ms, in 160ms rising 2px | Short crossfade, no rise | At once | +| Inline form opens or closes | Height spring (0.24s, no bounce) with a short fade | At once | At once | +| Reveal countdown | A hairline ring around the button empties linearly over `RevealDuration` | No ring (the key still masks itself) | No ring | +| Undo countdown | A small ring in Undo empties over `UndoDuration` | No ring | No ring | +| Eye icon | The new icon scales up from 0.6 and fades in over 160ms | Fades only | At once | +| Pressing an icon button | Scales to 0.94 on a spring | No scale | No scale | +| Testing spinner | One turn every 0.8s | One turn every 1.6s | Still | + +Each text sits in a fixed cell and the status column has a fixed width, so nothing resizes when a status or a key +changes. Relative times refresh every 30 seconds. + +## Related + +- `CopyButton` for copying a single value with confirmation. +- `Input` for a single line field with a label and an error. +- `ContextWindow` and `AgentStatus` for other parts of an AI product's settings and runs. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/zh-CN.md new file mode 100644 index 00000000..05a7706b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/zh-CN.md @@ -0,0 +1,162 @@ +--- +title: Provider keys +summary: 面向自带密钥(BYOK)AI 产品的设置卡片:显示每个提供商的掩码密钥、状态和最近使用时间,支持显示、复制、轮换和可撤销的删除,并通过内联表单在保存前测试新密钥。 +source-hash: 1ebaeb7e3937 +--- + + + +## 何时使用 + +`ProviderKeys` 是让用户自带 AI 提供商密钥的产品里的设置卡片。每个提供商占一行,显示其标志、掩码密钥(`sk-••••4f2a`)、由图标加文字组成的状态,以及密钥最近一次使用的时间。密钥可以显示五秒、复制、轮换,或在短暂的撤销窗口内删除。添加或轮换密钥时,会在该行下方打开一个内联表单,在输入时检查格式,并在保存前测试连接。 + +- **工作区或账户设置**,用户在这里为你支持的提供商添加自己的密钥。 +- **引导步骤**,请求第一个密钥并显示它何时连接成功。 +- **网关管理页面**,显示哪些提供商已连接、哪些密钥被拒绝,以及每个密钥的使用时间。 +- **轮换已泄露的密钥**,或删除密钥并留出片刻撤销。 + +## 何时不用 + +- **为某次请求选择模型**:使用模型选择器。 +- **你自己产品面向客户的 API 密钥**,带有权限范围和过期时间:使用表格。 +- **花费或速率限制**:使用用量计量。 + +## 用法 + +把 `Providers` 设为你支持的提供商,把 `Keys`(双向)绑定到服务器返回的数据,处理 `KeySaved` 和 `KeyRemoved` 以持久化,并设置 `TestKey` 在服务器上测试密钥。没有密钥的提供商显示为 Not set。没有 `TestKey` 时,测试会以 "No key test is set up" 失败,因此任何密钥都不会未经检查就显示为 Connected。 + +```csharp +card.Providers = +[ + new ProviderKeyProvider { Id = "anthropic", Name = "Anthropic", KeyPrefix = "sk-ant-api03-" }, + new ProviderKeyProvider { Id = "google", Name = "Google" }, // no documented prefix: no prefix check +]; +card.Keys = await api.GetProviderKeysAsync(); // one ProviderKey per provider +card.TestKey = request => api.TestProviderKeyAsync(request.Provider, request.Key, request.CancellationToken); +card.KeySaved += (_, e) => api.SaveProviderKey(e.Provider, e.Key); +card.KeyRemoved += (_, e) => api.DeleteProviderKey(e.Provider); +``` + +### 结构 + +1. **标志**:提供商的图标(或首字母)放在带色调的圆角方块里。 +2. **名称与密钥**:提供商名称,下方是等宽字体的掩码密钥(显示时为完整密钥)。 +3. **状态与最近使用**:带图标的 Connected、Invalid 或 Not set,下方是 "Used 2 min ago"、"Never used" 或拒绝原因。按下状态会重新测试已保存的密钥。 +4. **操作**:带倒计时圆环的显示、复制、轮换和删除;空行显示 Add key;删除后显示 Undo。 +5. **内联表单**:密钥输入框与 Cancel、Save,以及一行提示,说明哪里不对、Testing connection 或 Connected。 + +### 你的 API + + + +`TestKey` 接收一个 `ProviderKeyTestRequest`:提供商的 id、添加或轮换时粘贴的密钥(重新测试已保存的密钥时没有密钥,请在服务器上按提供商测试),以及一个在表单关闭时取消的令牌。返回 `ProviderKeyTestResult.Ok(detail)` 或 `ProviderKeyTestResult.Fail("401 Unauthorized")`;抛出的异常按失败处理,并使用其消息。轮换时,该行会保留旧密钥,直到新密钥通过测试才替换。 + +| 事件 | 何时触发 | +|---|---| +| `KeysChanged` | 每次变化:添加、轮换、删除、通过 Undo 恢复或重新测试。此时 `Keys` 已更新。 | +| `KeySaved` | 新密钥通过了测试。在这里持久化;该行已显示为已连接。 | +| `KeyRemoved` | 撤销窗口关闭,所以删除只有在确定后才会到达你的服务器。 | +| `KeyCopied` | 密钥已被复制,可用于审计日志。 | + +在生产环境中不要把完整密钥发送到客户端:每个 `ProviderKey` 只提供 `Last4`,`Value` 保持为 null。该行随后显示掩码密钥,并关闭显示和复制。 + +### 空状态 + + + +空的 `Keys` 列表会让每一行显示为 Not set,并带有 Add key 按钮。`Providers` 决定哪些提供商有一行以及它们的顺序;每个 `ProviderKeyProvider` 提供名称、表单检查的密钥前缀和最小长度,以及 `Mark` 图标或 `Monogram`。使用提供商文档中给出的前缀;文档没有给出前缀时,让 `KeyPrefix` 保持为空。 + +### 模拟测试 + +用于演示或原型时,设置 `Simulation` 而不是 `TestKey`:`ProviderKeysSimulation` 等待 `Delay`(700ms),让已保存的无效密钥以其自身的错误(或 `RejectedError`)再次失败,其余密钥都连接成功。不会发送任何内容,每个播报的结果都以 "Simulated, nothing was sent." 结尾。本页的演示使用真实的提供商名称以及其文档给出的密钥前缀;密钥是占位符,被拒绝的密钥是虚构的。 + +```xml + + + + + +``` + + +### 加载中 + + + +`IsLoading` 在密钥加载期间显示占位行,几何尺寸与真实的行一致。 + +### 窄卡片 + + + +宽度在 560px 及以上时,每一行是一行多列。低于该宽度(`NarrowWidth`)时,状态移到名称旁边,密钥单独占一行,最近使用与操作共用一行;表单把输入框放在 Cancel 和 Save 上方。卡片在 360px 宽时仍不会出现横向滚动。 + +### 你自己的文字 + +`Labels` 包含卡片显示或朗读的每一个词,`Title`、`Description` 和 `Footer` 设置卡片自己的文字(设为 null 会隐藏描述或页脚): + +```csharp +card.Labels = new ProviderKeysLabels +{ + Connected = "Verbunden", + NotSet = "Nicht gesetzt", + AddKey = "Schlüssel hinzufügen", + CopyName = provider => $"{provider}-Schlüssel kopieren", +}; +``` + +### 最佳实践 + +- 在服务器上测试密钥(请求一次模型列表就够了),并返回提供商自己的错误。 +- 只传 `Last4`;显示和复制适用于把密钥保存在设备上的产品。 +- 让 `RevealDuration` 保持简短;五秒足以读取或比对一个密钥。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 卡片以其标题命名。每个图标按钮都有针对提供商的名称,例如 "Copy the Anthropic key"。 +- 状态从不只靠颜色表达:图标加上 Connected、Invalid、Not set 或 Testing。状态按钮的名称包含拒绝原因("OpenAI key: Invalid, 401 Unauthorized. Test again")。 +- 显示是一个报告按下状态的开关;Rotate 和 Add key 报告内联表单是否展开。 +- 一个礼貌的实时区域会播报显示、复制、测试结果、删除和撤销。`Simulation` 的测试结果会附加 "Simulated, nothing was sent." +- 表单的输入框名为 "Anthropic API key",其帮助文本就是提示行;格式错误和拒绝会把它标记为无效。 +- 焦点受控:表单会聚焦其输入框,Cancel 和 Escape 会把焦点还给打开表单的按钮,Remove 会把焦点移到 Undo,Undo 再把它还给 Remove。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过每一行的状态(重新测试)、显示、复制、轮换和删除,空行则是 Add key | +| Enter、Space | 按下获得焦点的按钮 | +| Enter | 在表单中检查格式,然后测试并保存密钥 | +| Escape | 在表单中取消,并把焦点还给打开它的按钮 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 状态、密钥、最近使用和提示文字 | 原地交叉淡化一次:淡出 100ms,淡入 160ms 并上升 2px | 短暂交叉淡化,不上升 | 立即 | +| 内联表单打开或关闭 | 高度弹簧(0.24s,无回弹)并伴随短暂淡化 | 立即 | 立即 | +| 显示倒计时 | 按钮周围的细线圆环在 `RevealDuration` 内线性清空 | 没有圆环(密钥仍会自动遮盖) | 没有圆环 | +| 撤销倒计时 | Undo 内的小圆环在 `UndoDuration` 内清空 | 没有圆环 | 没有圆环 | +| 眼睛图标 | 新图标从 0.6 放大并在 160ms 内淡入 | 仅淡入 | 立即 | +| 按下图标按钮 | 以弹簧缩放到 0.94 | 不缩放 | 不缩放 | +| 测试中的加载圈 | 每 0.8s 转一圈 | 每 1.6s 转一圈 | 静止 | + +每段文字都位于固定的单元格中,状态列宽度固定,所以状态或密钥变化时不会改变尺寸。相对时间每 30 秒刷新一次。 + +## 相关组件 + +- `CopyButton`:复制单个值并即时确认。 +- `Input`:带标签和错误提示的单行输入框。 +- `ContextWindow` 和 `AgentStatus`:AI 产品设置与运行的其他部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/zh-TW.md new file mode 100644 index 00000000..708ccdbc --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderKeys/zh-TW.md @@ -0,0 +1,162 @@ +--- +title: Provider keys +summary: 為自備金鑰(BYOK)AI 產品設計的設定卡片:顯示每個供應商的遮罩金鑰、狀態與最近使用時間,支援顯示、複製、輪替與可復原的移除,並透過內嵌表單在儲存前測試新金鑰。 +source-hash: 1ebaeb7e3937 +--- + + + +## 何時使用 + +`ProviderKeys` 是讓使用者自備 AI 供應商金鑰的產品中的設定卡片。每個供應商佔一列,顯示其標誌、遮罩金鑰(`sk-••••4f2a`)、由圖示加文字組成的狀態,以及金鑰最近一次使用的時間。金鑰可以顯示五秒、複製、輪替,或在短暫的復原視窗內移除。新增或輪替金鑰時,會在該列下方開啟一個內嵌表單,在輸入時檢查格式,並在儲存前測試連線。 + +- **工作區或帳戶設定**,使用者在這裡為你支援的供應商新增自己的金鑰。 +- **新手引導步驟**,請求第一把金鑰並顯示它何時連線成功。 +- **閘道管理頁面**,顯示哪些供應商已連線、哪些金鑰被拒絕,以及每把金鑰的使用時間。 +- **輪替已外洩的金鑰**,或移除金鑰並保留片刻復原。 + +## 何時不用 + +- **為某次請求選擇模型**:使用模型選擇器。 +- **你自己產品提供給客戶的 API 金鑰**,帶有權限範圍與到期時間:使用表格。 +- **花費或速率限制**:使用用量計量。 + +## 用法 + +把 `Providers` 設為你支援的供應商,把 `Keys`(雙向)繫結到伺服器傳回的資料,處理 `KeySaved` 與 `KeyRemoved` 以保存,並設定 `TestKey` 在伺服器上測試金鑰。沒有金鑰的供應商顯示為 Not set。沒有 `TestKey` 時,測試會以 "No key test is set up" 失敗,因此任何金鑰都不會未經檢查就顯示為 Connected。 + +```csharp +card.Providers = +[ + new ProviderKeyProvider { Id = "anthropic", Name = "Anthropic", KeyPrefix = "sk-ant-api03-" }, + new ProviderKeyProvider { Id = "google", Name = "Google" }, // no documented prefix: no prefix check +]; +card.Keys = await api.GetProviderKeysAsync(); // one ProviderKey per provider +card.TestKey = request => api.TestProviderKeyAsync(request.Provider, request.Key, request.CancellationToken); +card.KeySaved += (_, e) => api.SaveProviderKey(e.Provider, e.Key); +card.KeyRemoved += (_, e) => api.DeleteProviderKey(e.Provider); +``` + +### 結構 + +1. **標誌**:供應商的圖示(或字首)放在帶色調的圓角方塊裡。 +2. **名稱與金鑰**:供應商名稱,下方是等寬字型的遮罩金鑰(顯示時為完整金鑰)。 +3. **狀態與最近使用**:帶圖示的 Connected、Invalid 或 Not set,下方是 "Used 2 min ago"、"Never used" 或拒絕原因。按下狀態會重新測試已儲存的金鑰。 +4. **操作**:帶倒數圓環的顯示、複製、輪替與移除;空白列顯示 Add key;移除後顯示 Undo。 +5. **內嵌表單**:金鑰輸入框與 Cancel、Save,以及一行提示,說明哪裡不對、Testing connection 或 Connected。 + +### 你的 API + + + +`TestKey` 接收一個 `ProviderKeyTestRequest`:供應商的 id、新增或輪替時貼上的金鑰(重新測試已儲存的金鑰時沒有金鑰,請在伺服器上依供應商測試),以及一個在表單關閉時取消的權杖。傳回 `ProviderKeyTestResult.Ok(detail)` 或 `ProviderKeyTestResult.Fail("401 Unauthorized")`;擲出的例外視為失敗,並使用其訊息。輪替時,該列會保留舊金鑰,直到新金鑰通過測試才替換。 + +| 事件 | 何時觸發 | +|---|---| +| `KeysChanged` | 每次變更:新增、輪替、移除、透過 Undo 復原或重新測試。此時 `Keys` 已更新。 | +| `KeySaved` | 新金鑰通過了測試。在這裡保存;該列已顯示為已連線。 | +| `KeyRemoved` | 復原視窗關閉,所以移除只有在確定後才會傳到你的伺服器。 | +| `KeyCopied` | 金鑰已被複製,可用於稽核記錄。 | + +在正式環境中不要把完整金鑰傳到用戶端:每個 `ProviderKey` 只提供 `Last4`,`Value` 保持為 null。該列隨後顯示遮罩金鑰,並關閉顯示與複製。 + +### 空白狀態 + + + +空的 `Keys` 清單會讓每一列顯示為 Not set,並帶有 Add key 按鈕。`Providers` 決定哪些供應商有一列以及它們的順序;每個 `ProviderKeyProvider` 提供名稱、表單檢查的金鑰前綴與最小長度,以及 `Mark` 圖示或 `Monogram`。使用供應商文件中提供的前綴;文件沒有提供前綴時,讓 `KeyPrefix` 保持空白。 + +### 模擬測試 + +用於展示或原型時,設定 `Simulation` 而非 `TestKey`:`ProviderKeysSimulation` 等待 `Delay`(700ms),讓已儲存的無效金鑰以其自身的錯誤(或 `RejectedError`)再次失敗,其餘金鑰都連線成功。不會傳送任何內容,每個朗讀的結果都以 "Simulated, nothing was sent." 結尾。本頁的展示使用真實的供應商名稱以及其文件提供的金鑰前綴;金鑰是預留位置,被拒絕的金鑰是虛構的。 + +```xml + + + + + +``` + + +### 載入中 + + + +`IsLoading` 在金鑰載入期間顯示預留列,幾何尺寸與真實的列一致。 + +### 窄卡片 + + + +寬度在 560px 以上時,每一列是一行多欄。低於該寬度(`NarrowWidth`)時,狀態移到名稱旁邊,金鑰獨佔一行,最近使用與操作共用一行;表單把輸入框放在 Cancel 與 Save 上方。卡片在 360px 寬時仍不會出現水平捲動。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或朗讀的每一個詞,`Title`、`Description` 與 `Footer` 設定卡片自己的文字(設為 null 會隱藏描述或頁尾): + +```csharp +card.Labels = new ProviderKeysLabels +{ + Connected = "Verbunden", + NotSet = "Nicht gesetzt", + AddKey = "Schlüssel hinzufügen", + CopyName = provider => $"{provider}-Schlüssel kopieren", +}; +``` + +### 最佳做法 + +- 在伺服器上測試金鑰(請求一次模型清單就足夠),並傳回供應商自己的錯誤。 +- 只傳 `Last4`;顯示與複製適用於把金鑰保存在裝置上的產品。 +- 讓 `RevealDuration` 保持簡短;五秒足以讀取或比對一把金鑰。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 卡片以其標題命名。每個圖示按鈕都有針對供應商的名稱,例如 "Copy the Anthropic key"。 +- 狀態從不只靠顏色表達:圖示加上 Connected、Invalid、Not set 或 Testing。狀態按鈕的名稱包含拒絕原因("OpenAI key: Invalid, 401 Unauthorized. Test again")。 +- 顯示是一個回報按下狀態的切換鈕;Rotate 與 Add key 回報內嵌表單是否展開。 +- 一個禮貌的即時區域會朗讀顯示、複製、測試結果、移除與復原。`Simulation` 的測試結果會附加 "Simulated, nothing was sent." +- 表單的輸入框名為 "Anthropic API key",其說明文字就是提示行;格式錯誤與拒絕會把它標記為無效。 +- 焦點受到管理:表單會聚焦其輸入框,Cancel 與 Escape 會把焦點還給開啟表單的按鈕,Remove 會把焦點移到 Undo,Undo 再把它還給 Remove。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過每一列的狀態(重新測試)、顯示、複製、輪替與移除,空白列則是 Add key | +| Enter、Space | 按下取得焦點的按鈕 | +| Enter | 在表單中檢查格式,然後測試並儲存金鑰 | +| Escape | 在表單中取消,並把焦點還給開啟它的按鈕 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 狀態、金鑰、最近使用與提示文字 | 原地交叉淡化一次:淡出 100ms,淡入 160ms 並上升 2px | 短暫交叉淡化,不上升 | 立即 | +| 內嵌表單開啟或關閉 | 高度彈簧(0.24s,無回彈)並伴隨短暫淡化 | 立即 | 立即 | +| 顯示倒數 | 按鈕周圍的細線圓環在 `RevealDuration` 內線性清空 | 沒有圓環(金鑰仍會自動遮罩) | 沒有圓環 | +| 復原倒數 | Undo 內的小圓環在 `UndoDuration` 內清空 | 沒有圓環 | 沒有圓環 | +| 眼睛圖示 | 新圖示從 0.6 放大並在 160ms 內淡入 | 僅淡入 | 立即 | +| 按下圖示按鈕 | 以彈簧縮放到 0.94 | 不縮放 | 不縮放 | +| 測試中的載入圈 | 每 0.8s 轉一圈 | 每 1.6s 轉一圈 | 靜止 | + +每段文字都位於固定的儲存格中,狀態欄寬度固定,所以狀態或金鑰變化時不會改變尺寸。相對時間每 30 秒重新整理一次。 + +## 相關元件 + +- `CopyButton`:複製單一值並即時確認。 +- `Input`:帶標籤與錯誤提示的單行輸入框。 +- `ContextWindow` 與 `AgentStatus`:AI 產品設定與執行的其他部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/en-US.md new file mode 100644 index 00000000..f8e35673 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/en-US.md @@ -0,0 +1,213 @@ +--- +title: Provider status +summary: A status card for the AI providers your product depends on: health as an icon and label, median latency and a 30 day uptime bar with a tooltip per day, incidents that open into their updates, and a visible countdown to the next check. +--- + + + +## When to use + +`ProviderStatus` is a card that lists the AI providers your product routes to. Each row shows the provider's mark, its +health as an icon and a label, the median (p50) latency and a 30 day uptime bar of small ticks with a tooltip for each +day. A row with an incident is a button that opens the incident's updates, newest first. The header sums up health +across the providers and checks again on a visible countdown ring. + +- **Live health of the model providers** your product depends on, on a settings page or an admin dashboard. +- **Explaining degraded AI features** to people or admins, with the provider's incident updates in place. +- **Routing and fallback settings**, where people need to see which providers are healthy right now. + +## When not to use + +- **A full public status page** with components, subscriptions and history: build a page, not a card. +- **One provider's state inside a chat header**: use `AgentStatus` or a small badge. +- **Detailed latency over time**: use a chart. + +## Usage + +Give it your providers and a `RefreshHandler` that fetches fresh status from your own aggregation endpoint. Do not poll +the providers' status pages from the client. The handler returns the new providers, or updates `Providers` itself and +returns `null`. It runs on the countdown and from the refresh button; a newer check cancels the older one through the +token. + +```csharp +card.Providers = await statusApi.GetProvidersAsync(); +card.RefreshHandler = async token => +{ + var response = await http.GetAsync("/api/provider-status", token); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Couldn’t reach the status feeds. Showing the last results."); + } + return await response.Content.ReadFromJsonAsync>(token); +}; +card.IncidentOpened += (_, e) => OpenBrowser(e.Incident.Url); +card.Footnote = "Latency is the median over the last 5 minutes."; +``` + +Without providers, a handler or a simulation the card shows its empty state: the header reads "No providers" and the +refresh controls hide. + +### Simulated checks + +For a demo or a prototype, leave `Providers` and `RefreshHandler` unset and give the card a `ProviderStatusSimulation` +with a story of `ProviderStatusSimulationStep`s. The card shows the first step at once and runs a simulated check every +`Interval` (8 seconds): each check takes `CheckDelay` (900ms) and moves to the next step, looping, so incidents open, post +updates and resolve; a step with `Fails` shows the error state and keeps the last results. Its Status page link says the +action is simulated, and without a `Footnote` the card notes that the data is a sample. + +```csharp +card.Simulation = new ProviderStatusSimulation +{ + Steps = + [ + new ProviderStatusSimulationStep { Providers = beforeTheIncident }, + new ProviderStatusSimulationStep { Providers = incidentOpened }, + new ProviderStatusSimulationStep { Fails = true }, + new ProviderStatusSimulationStep { Providers = resolved }, + ], +}; +card.Footnote = "Sample data: the incidents and numbers here are invented."; +``` + +The preview plays such a story with real provider names and the status page components their pages list; every incident, +latency and uptime figure in it is invented, never a provider's real history, and its footnote says so. + +### Your providers + + + +Each `ProviderStatusItem` has an `Id` (rows are reused while it stays), a `Name`, a `Detail` line such as the models you +call, its `Health`, the median latency in `LatencyMs` and its `Days`. The card formats the numbers (412 ms, 1.92 s, +99.61%), so keep them as plain numbers. + +`Mark` is a 24 × 24 stroked glyph drawn at 16px in the text color; pass your provider's logo path, and `MarkBrush` to +color it. Without a mark the row shows the name's first letter in a tinted square. + +### Uptime bar + +`Days` is the daily history, oldest first. The bar shows the 30 days ending on the last one, which is labeled Today, and +pads missing days as No data. Each day has a `Date`, an `Uptime` from 0 to 100 (or null), an optional `Health` (derived +from the uptime when left out: below 99.9 degraded, below 99 an outage) and an optional `Note` for its tooltip. The +percentage beside the bar is the mean of the days with data. + +One tooltip serves the whole card: pointing at a tick, tapping it or dragging sideways across the bar moves it from day +to day. + +### Incidents + +Set `Incident` on a provider to make its row a button that opens the incident's `Updates`. Each update has a `Stage` +(Investigating, Identified, Monitoring, Resolved), a time in `At` (shown as HH:MM UTC) or a preformatted `AtText`, and a +`Message`. Updates show newest first; a new one fades in at the top while the panel grows. + +`ExpandedId` names the open provider and is two-way; `ExpandedChanged` reports the person opening and closing rows. The +Status page link under the updates raises `IncidentOpened`. When nothing handles it, the link opens the incident's +`Url`; with neither, the link is hidden. + +### Checks and errors + +`RefreshInterval` sets the time between automatic checks (30 seconds by default). The countdown ring drains toward the +next check and pauses while the card is off screen or hidden. While a check runs, a short spinner and a shimmering +"Checking" take its place. When the handler throws, the header shows the exception's message, the last results stay on +screen and the countdown turns into a retry that waits at most 10 seconds. `RefreshInterval="{x:Null}"` turns +automatic checks off and keeps the refresh button. Without a handler the refresh controls are hidden. `CheckNow()` +checks at once. + +### States + + + +`IsLoading` shows skeleton rows with the same row height for the first load. `ErrorText` shows an error of your own in +the header, such as a failed first load. + +### Narrow cards + + + +From 560px of card width each row is one line with columns for status, p50 latency, the uptime bar and the 30 day +percentage. Narrower cards take two lines per row: the name and the health above, with the latency under the name, and +a full-width bar with its percentage below. The rule follows the card, not the window, and holds up at 360px; long +names and details end with an ellipsis. + +### Your own words + +`Labels` holds every word the card shows or says; numbers, dates and sentences with values are functions: + +```csharp +card.Labels = new ProviderStatusLabels +{ + Operational = "Opérationnel", + Degraded = "Dégradé", + Outage = "Panne", + NextCheck = seconds => $"Prochaine vérification dans {seconds} s", +}; +``` + +### Best practices + +- Wire `RefreshHandler` to your own endpoint that aggregates the providers' feeds. +- Label sample or synthetic numbers with `Footnote`. +- Keep `Detail` short, such as the models you call; it ends with an ellipsis when space runs out. +- Keep an incident on its provider for a while after it is resolved, so people can read what happened. + +## API reference + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is a group named by its title. Rows are a list; each row is named by the provider and its health. +- An incident row has a full-row button that reports expanded or collapsed and is named with the provider, its health + and the incident's title. +- Health is never color alone: every status shows an icon and a label, and each day carries its health in its tooltip + and in the bar's name. +- Each uptime bar is one tab stop named by a summary of its 30 days. The arrow keys move through the days and a polite + live region reads each one, so 180 ticks never flood the tab order. +- The same live region announces checks, failed checks, `Notice` and simulated actions ("Simulated, no requests were + sent"). +- Keyboard focus shows a ring just inside the row button and around the bar; pointer focus draws none. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the refresh button, each incident row and each uptime bar | +| Enter, Space | On an incident row, opens or closes its updates | +| Arrow Left, Arrow Right | On an uptime bar, moves the day tooltip one day back or forward | +| Home, End | On an uptime bar, jumps to the oldest day or today | +| Escape | On an uptime bar, hides the day tooltip | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Health, latency, uptime and the summary change | One crossfade in a reserved cell: out in 100ms, in over 160ms with a 2px rise | Crossfade, no rise | At once | +| Incident opens or closes | Height springs to the measured content (0.24s, no bounce), content fades | At once | At once | +| A new update arrives | Fades in at the top while the panel springs to its new height | At once | At once | +| A tick's health changes | Color changes over 240ms | Color changes over 240ms | At once | +| Countdown | The ring drains linearly; the seconds change once a second | Same | Same | +| Checking | A short spinner and a shimmering label | Still label, no spinner | Still label, no spinner | +| Skeleton rows | Pulse | Still | Still | +| Chevron | Turns on a spring | At once | At once | + +The countdown pauses while the card is off screen or its window is hidden. Rows have fixed columns, so nothing reflows +when a value changes. + +## Related + +- `AgentStatus` for the live status of one agent run. +- `ToolCall` for a tool call in an agent transcript. +- `Tooltip` for short supporting text. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/zh-CN.md new file mode 100644 index 00000000..c73ec3af --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/zh-CN.md @@ -0,0 +1,177 @@ +--- +title: Provider status +summary: 一张展示产品所依赖的 AI 服务商状态的卡片:以图标和标签表示健康状况,显示中位延迟和每天都带提示的 30 天可用率条,事件可以展开查看更新,下一次检查有可见的倒计时。 +source-hash: 59e821a9305c +--- + + + +## 何时使用 + +`ProviderStatus` 是一张卡片,列出产品所路由到的 AI 服务商。每一行显示服务商的标志、以图标和标签表示的健康状况、中位(p50)延迟,以及由一个个小刻度组成的 30 天可用率条,每一天都有自己的提示。带有事件的行是一个按钮,点击后按从新到旧的顺序展开事件的更新。标题栏汇总所有服务商的健康状况,并按可见的倒计时环再次检查。 + +- **产品所依赖的模型服务商的实时健康状况**,放在设置页或管理仪表板上。 +- **向用户或管理员解释 AI 功能为何降级**,并就地展示服务商的事件更新。 +- **路由与回退设置**,需要让人看清此刻哪些服务商是健康的。 + +## 何时不用 + +- **完整的公开状态页**,包含组件、订阅和历史:应当做成一个页面,而不是卡片。 +- **聊天标题栏中单个服务商的状态**:使用 `AgentStatus` 或一个小徽标。 +- **随时间变化的详细延迟**:使用图表。 + +## 用法 + +传入你的服务商,以及一个从你自己的聚合端点获取最新状态的 `RefreshHandler`。不要在客户端轮询服务商的状态页。处理程序返回新的服务商列表,或者自行更新 `Providers` 并返回 `null`。它在倒计时结束时和点击刷新按钮时运行;新的检查会通过取消令牌取消较早的检查。 + +```csharp +card.Providers = await statusApi.GetProvidersAsync(); +card.RefreshHandler = async token => +{ + var response = await http.GetAsync("/api/provider-status", token); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Couldn’t reach the status feeds. Showing the last results."); + } + return await response.Content.ReadFromJsonAsync>(token); +}; +card.IncidentOpened += (_, e) => OpenBrowser(e.Incident.Url); +card.Footnote = "Latency is the median over the last 5 minutes."; +``` + +既没有服务商、处理程序,也没有模拟时,卡片显示空状态:标题区显示 "No providers",刷新控件隐藏。 + +### 模拟检查 + +用于演示或原型时,不设置 `Providers` 和 `RefreshHandler`,而是给卡片一个带有若干 `ProviderStatusSimulationStep` 的 `ProviderStatusSimulation`。卡片立即显示第一步,并每隔 `Interval`(8 秒)进行一次模拟检查:每次检查耗时 `CheckDelay`(900ms),然后进入下一步并循环,因此事件会开启、发布更新并解决;带有 `Fails` 的步骤会显示错误状态并保留上一次的结果。其 Status page 链接会说明该操作是模拟的;没有 `Footnote` 时,卡片会注明数据是示例。 + +```csharp +card.Simulation = new ProviderStatusSimulation +{ + Steps = + [ + new ProviderStatusSimulationStep { Providers = beforeTheIncident }, + new ProviderStatusSimulationStep { Providers = incidentOpened }, + new ProviderStatusSimulationStep { Fails = true }, + new ProviderStatusSimulationStep { Providers = resolved }, + ], +}; +card.Footnote = "Sample data: the incidents and numbers here are invented."; +``` + +预览播放的就是这样一段故事,使用真实的服务商名称及其状态页列出的组件;其中每个事件、延迟和可用率数字都是虚构的,绝不是服务商的真实历史,脚注也会说明这一点。 + +### 你的服务商 + + + +每个 `ProviderStatusItem` 有一个 `Id`(只要它还在,行就会被复用)、一个 `Name`、一行 `Detail`(例如你调用的模型)、它的 `Health`、以 `LatencyMs` 表示的中位延迟,以及它的 `Days`。卡片会自行格式化数字(412 ms、1.92 s、99.61%),因此请以普通数字传入。 + +`Mark` 是一个 24 × 24 的描边图形,以 16px 和文字颜色绘制;传入服务商标志的路径即可,`MarkBrush` 用于为它着色。没有标志时,行内会在一个着色的方块中显示名称的首字母。 + +### 可用率条 + +`Days` 是按日记录的历史,最早的在前。可用率条显示截至最后一天的 30 天,最后一天标为 Today,缺失的日期补为 No data。每一天有一个 `Date`、一个 0 到 100 的 `Uptime`(或 null)、一个可选的 `Health`(省略时由可用率推导:低于 99.9 为降级,低于 99 为中断),以及一条用于提示的可选 `Note`。条旁边的百分比是所有有数据的日子的平均值。 + +整张卡片共用一个提示:指向某个刻度、轻点它,或在条上横向拖动,提示就会逐日移动。 + +### 事件 + +在服务商上设置 `Incident`,它所在的行就会变成一个按钮,用于展开事件的 `Updates`。每条更新有一个 `Stage`(Investigating、Identified、Monitoring、Resolved)、一个以 `At` 表示的时间(显示为 HH:MM UTC)或预先格式化的 `AtText`,以及一条 `Message`。更新按从新到旧显示;新的更新会在顶部淡入,同时面板随之变高。 + +`ExpandedId` 指明当前展开的服务商,是双向的;`ExpandedChanged` 报告用户展开和收起行。更新下方的 Status page 链接会触发 `IncidentOpened`。没有任何处理程序处理它时,链接会打开事件的 `Url`;两者都没有时,链接隐藏。 + +### 检查与错误 + +`RefreshInterval` 设置两次自动检查之间的时间(默认 30 秒)。倒计时环朝下一次检查逐渐消退,卡片不在屏幕上或被隐藏时会暂停。检查进行期间,一个短暂的加载圈和一个闪烁的 "Checking" 取而代之。处理程序抛出异常时,标题栏显示异常的消息,上一次的结果保留在屏幕上,倒计时变为重试,最多等待 10 秒。`RefreshInterval="{x:Null}"` 会关闭自动检查,但保留刷新按钮。没有处理程序时,刷新控件隐藏。`CheckNow()` 会立即检查。 + +### 状态 + + + +`IsLoading` 在首次加载时显示行高相同的骨架行。`ErrorText` 在标题栏中显示你自己的错误,例如首次加载失败。 + +### 窄卡片 + + + +卡片宽度不小于 560px 时,每一行占一行,分列显示状态、p50 延迟、可用率条和 30 天百分比。更窄的卡片每行占两行:上面是名称和健康状况,延迟位于名称下方;下面是占满宽度的可用率条及其百分比。这条规则跟随卡片,而不是窗口,在 360px 时依然可用;过长的名称和说明以省略号结尾。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的每一个词;数字、日期以及带数值的句子是函数: + +```csharp +card.Labels = new ProviderStatusLabels +{ + Operational = "Opérationnel", + Degraded = "Dégradé", + Outage = "Panne", + NextCheck = seconds => $"Prochaine vérification dans {seconds} s", +}; +``` + +### 最佳实践 + +- 把 `RefreshHandler` 接到你自己的、汇总各服务商状态源的端点上。 +- 用 `Footnote` 标注示例数据或合成数据。 +- `Detail` 保持简短,例如你调用的模型;空间不够时它会以省略号结尾。 +- 事件解决后,在服务商上再保留一段时间,让人能读到发生了什么。 + +## API 参考 + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片是一个以标题命名的分组。各行组成一个列表;每一行以服务商及其健康状况命名。 +- 事件行有一个覆盖整行的按钮,会报告展开或收起状态,并以服务商、健康状况和事件标题命名。 +- 健康状况从不只靠颜色表达:每种状态都有图标和标签,每一天的健康状况也写在它的提示和可用率条的名称中。 +- 每个可用率条是一个 Tab 停靠点,以其 30 天的摘要命名。方向键在各天之间移动,一个礼貌的实时区域逐一朗读,因此 180 个刻度不会挤满 Tab 顺序。 +- 同一个实时区域还会播报检查、失败的检查、`Notice` 以及模拟操作("Simulated, no requests were sent")。 +- 键盘焦点在行按钮内侧和可用率条周围显示焦点环;指针焦点不显示。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次移到刷新按钮、每个事件行和每个可用率条 | +| Enter、Space | 在事件行上展开或收起其更新 | +| 左方向键、右方向键 | 在可用率条上把日期提示向前或向后移动一天 | +| Home、End | 在可用率条上跳到最早的一天或今天 | +| Escape | 在可用率条上隐藏日期提示 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 健康状况、延迟、可用率和摘要变化 | 在预留的单元格中做一次交叉淡化:旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px | 交叉淡化,不上升 | 立即 | +| 事件展开或收起 | 高度以弹簧过渡到测得的内容高度(0.24s,无回弹),内容淡入淡出 | 立即 | 立即 | +| 新的更新到达 | 在顶部淡入,同时面板以弹簧过渡到新高度 | 立即 | 立即 | +| 刻度的健康状况变化 | 颜色在 240ms 内过渡 | 颜色在 240ms 内过渡 | 立即 | +| 倒计时 | 环线性消退;秒数每秒更新一次 | 相同 | 相同 | +| 检查中 | 短暂的加载圈和闪烁的标签 | 静止的标签,没有加载圈 | 静止的标签,没有加载圈 | +| 骨架行 | 脉动 | 静止 | 静止 | +| 箭头 | 以弹簧转动 | 立即 | 立即 | + +卡片不在屏幕上或所在窗口被隐藏时,倒计时会暂停。各行的列宽固定,因此数值变化时不会重新排版。 + +## 相关组件 + +- `AgentStatus`:单次智能体运行的实时状态。 +- `ToolCall`:智能体对话记录中的一次工具调用。 +- `Tooltip`:简短的辅助说明文字。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/zh-TW.md new file mode 100644 index 00000000..343d61de --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ProviderStatus/zh-TW.md @@ -0,0 +1,177 @@ +--- +title: Provider status +summary: 一張呈現產品所依賴的 AI 服務供應商狀態的卡片:以圖示和標籤表示健康狀態,顯示中位延遲與每天都附提示的 30 天可用率條,事件可以展開查看更新,下一次檢查有可見的倒數計時。 +source-hash: 59e821a9305c +--- + + + +## 何時使用 + +`ProviderStatus` 是一張卡片,列出產品所路由到的 AI 服務供應商。每一列顯示供應商的標誌、以圖示和標籤表示的健康狀態、中位(p50)延遲,以及由一個個小刻度組成的 30 天可用率條,每一天都有自己的提示。帶有事件的列是一個按鈕,點擊後依從新到舊的順序展開事件的更新。標題列彙總所有供應商的健康狀態,並依可見的倒數計時環再次檢查。 + +- **產品所依賴的模型供應商的即時健康狀態**,放在設定頁或管理儀表板上。 +- **向使用者或管理員說明 AI 功能為何降級**,並就地呈現供應商的事件更新。 +- **路由與備援設定**,需要讓人看清此刻哪些供應商是健康的。 + +## 何時不用 + +- **完整的公開狀態頁**,包含元件、訂閱與歷史記錄:應該做成一個頁面,而不是卡片。 +- **聊天標題列中單一供應商的狀態**:使用 `AgentStatus` 或一個小徽章。 +- **隨時間變化的詳細延遲**:使用圖表。 + +## 用法 + +傳入你的供應商,以及一個從你自己的彙整端點取得最新狀態的 `RefreshHandler`。不要在用戶端輪詢供應商的狀態頁。處理常式回傳新的供應商清單,或自行更新 `Providers` 並回傳 `null`。它會在倒數結束時和按下重新整理按鈕時執行;新的檢查會透過取消權杖取消較早的檢查。 + +```csharp +card.Providers = await statusApi.GetProvidersAsync(); +card.RefreshHandler = async token => +{ + var response = await http.GetAsync("/api/provider-status", token); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Couldn’t reach the status feeds. Showing the last results."); + } + return await response.Content.ReadFromJsonAsync>(token); +}; +card.IncidentOpened += (_, e) => OpenBrowser(e.Incident.Url); +card.Footnote = "Latency is the median over the last 5 minutes."; +``` + +既沒有供應商、處理常式,也沒有模擬時,卡片顯示空白狀態:標題列顯示 "No providers",重新整理控制項會隱藏。 + +### 模擬檢查 + +用於展示或原型時,不設定 `Providers` 和 `RefreshHandler`,而是給卡片一個帶有若干 `ProviderStatusSimulationStep` 的 `ProviderStatusSimulation`。卡片立即顯示第一步,並每隔 `Interval`(8 秒)進行一次模擬檢查:每次檢查耗時 `CheckDelay`(900ms),然後進入下一步並循環,因此事件會開啟、發布更新並解決;帶有 `Fails` 的步驟會顯示錯誤狀態並保留上一次的結果。其 Status page 連結會說明該操作是模擬的;沒有 `Footnote` 時,卡片會註明資料是範例。 + +```csharp +card.Simulation = new ProviderStatusSimulation +{ + Steps = + [ + new ProviderStatusSimulationStep { Providers = beforeTheIncident }, + new ProviderStatusSimulationStep { Providers = incidentOpened }, + new ProviderStatusSimulationStep { Fails = true }, + new ProviderStatusSimulationStep { Providers = resolved }, + ], +}; +card.Footnote = "Sample data: the incidents and numbers here are invented."; +``` + +預覽播放的就是這樣一段故事,使用真實的供應商名稱及其狀態頁列出的元件;其中每個事件、延遲與可用率數字都是虛構的,絕不是供應商的真實歷史,註腳也會說明這一點。 + +### 你的供應商 + + + +每個 `ProviderStatusItem` 都有一個 `Id`(只要它還在,該列就會被重複使用)、一個 `Name`、一行 `Detail`(例如你呼叫的模型)、它的 `Health`、以 `LatencyMs` 表示的中位延遲,以及它的 `Days`。卡片會自行格式化數字(412 ms、1.92 s、99.61%),因此請以一般數字傳入。 + +`Mark` 是一個 24 × 24 的描邊圖形,以 16px 和文字顏色繪製;傳入供應商標誌的路徑即可,`MarkBrush` 用來為它上色。沒有標誌時,該列會在一個上色的方塊中顯示名稱的第一個字母。 + +### 可用率條 + +`Days` 是逐日的歷史記錄,最早的在前。可用率條顯示截至最後一天的 30 天,最後一天標為 Today,缺少的日期補為 No data。每一天有一個 `Date`、一個 0 到 100 的 `Uptime`(或 null)、一個選用的 `Health`(省略時由可用率推導:低於 99.9 為降級,低於 99 為中斷),以及一則用於提示的選用 `Note`。可用率條旁的百分比是所有有資料日子的平均值。 + +整張卡片共用一個提示:指向某個刻度、輕觸它,或在可用率條上橫向拖曳,提示就會逐日移動。 + +### 事件 + +在供應商上設定 `Incident`,它所在的列就會變成一個按鈕,用來展開事件的 `Updates`。每則更新有一個 `Stage`(Investigating、Identified、Monitoring、Resolved)、一個以 `At` 表示的時間(顯示為 HH:MM UTC)或預先格式化的 `AtText`,以及一則 `Message`。更新依從新到舊顯示;新的更新會在最上方淡入,同時面板隨之變高。 + +`ExpandedId` 指出目前展開的供應商,是雙向的;`ExpandedChanged` 回報使用者展開和收合列。更新下方的 Status page 連結會觸發 `IncidentOpened`。沒有任何處理常式處理它時,連結會開啟事件的 `Url`;兩者都沒有時,連結會隱藏。 + +### 檢查與錯誤 + +`RefreshInterval` 設定兩次自動檢查之間的時間(預設 30 秒)。倒數計時環朝下一次檢查逐漸消退,卡片不在畫面上或被隱藏時會暫停。檢查進行期間,一個短暫的載入圈和一個閃爍的 "Checking" 會取而代之。處理常式擲回例外時,標題列顯示例外的訊息,上一次的結果留在畫面上,倒數變為重試,最多等待 10 秒。`RefreshInterval="{x:Null}"` 會關閉自動檢查,但保留重新整理按鈕。沒有處理常式時,重新整理控制項會隱藏。`CheckNow()` 會立即檢查。 + +### 狀態 + + + +`IsLoading` 在第一次載入時顯示列高相同的骨架列。`ErrorText` 在標題列中顯示你自己的錯誤,例如第一次載入失敗。 + +### 窄卡片 + + + +卡片寬度不小於 560px 時,每一列佔一行,分欄顯示狀態、p50 延遲、可用率條與 30 天百分比。更窄的卡片每列佔兩行:上面是名稱和健康狀態,延遲位於名稱下方;下面是佔滿寬度的可用率條及其百分比。這條規則跟隨卡片,而不是視窗,在 360px 時依然可用;過長的名稱和說明以刪節號結尾。 + +### 自訂文字 + +`Labels` 包含卡片顯示或朗讀的每一個字詞;數字、日期以及帶有數值的句子是函式: + +```csharp +card.Labels = new ProviderStatusLabels +{ + Operational = "Opérationnel", + Degraded = "Dégradé", + Outage = "Panne", + NextCheck = seconds => $"Prochaine vérification dans {seconds} s", +}; +``` + +### 最佳做法 + +- 把 `RefreshHandler` 接到你自己的、彙整各供應商狀態來源的端點。 +- 用 `Footnote` 標示範例資料或合成資料。 +- `Detail` 保持簡短,例如你呼叫的模型;空間不足時它會以刪節號結尾。 +- 事件解決後,在供應商上再保留一段時間,讓人能讀到發生了什麼事。 + +## API 參考 + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片是一個以標題命名的群組。各列組成一份清單;每一列以供應商及其健康狀態命名。 +- 事件列有一個涵蓋整列的按鈕,會回報展開或收合狀態,並以供應商、健康狀態和事件標題命名。 +- 健康狀態從不只靠顏色表達:每種狀態都有圖示和標籤,每一天的健康狀態也寫在它的提示和可用率條的名稱中。 +- 每個可用率條是一個 Tab 停駐點,以其 30 天的摘要命名。方向鍵在各天之間移動,一個禮貌的即時區域逐一朗讀,因此 180 個刻度不會塞滿 Tab 順序。 +- 同一個即時區域也會播報檢查、失敗的檢查、`Notice` 以及模擬操作("Simulated, no requests were sent")。 +- 鍵盤焦點會在列按鈕內側和可用率條周圍顯示焦點環;指標焦點則不顯示。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序移到重新整理按鈕、每個事件列和每個可用率條 | +| Enter、Space | 在事件列上展開或收合其更新 | +| 左方向鍵、右方向鍵 | 在可用率條上把日期提示往前或往後移動一天 | +| Home、End | 在可用率條上跳到最早的一天或今天 | +| Escape | 在可用率條上隱藏日期提示 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 健康狀態、延遲、可用率與摘要變化 | 在預留的儲存格中做一次交叉淡化:舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px | 交叉淡化,不上升 | 立即 | +| 事件展開或收合 | 高度以彈簧過渡到量得的內容高度(0.24s,無回彈),內容淡入淡出 | 立即 | 立即 | +| 新的更新抵達 | 在最上方淡入,同時面板以彈簧過渡到新高度 | 立即 | 立即 | +| 刻度的健康狀態變化 | 顏色在 240ms 內過渡 | 顏色在 240ms 內過渡 | 立即 | +| 倒數計時 | 環線性消退;秒數每秒更新一次 | 相同 | 相同 | +| 檢查中 | 短暫的載入圈和閃爍的標籤 | 靜止的標籤,沒有載入圈 | 靜止的標籤,沒有載入圈 | +| 骨架列 | 脈動 | 靜止 | 靜止 | +| 箭頭 | 以彈簧轉動 | 立即 | 立即 | + +卡片不在畫面上或所在視窗被隱藏時,倒數計時會暫停。各列的欄寬固定,因此數值變化時不會重新排版。 + +## 相關元件 + +- `AgentStatus`:單次代理程式執行的即時狀態。 +- `ToolCall`:代理程式對話記錄中的一次工具呼叫。 +- `Tooltip`:簡短的輔助說明文字。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/en-US.md new file mode 100644 index 00000000..54ebbf90 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/en-US.md @@ -0,0 +1,191 @@ +--- +title: Reasoning block +summary: The collapsible thinking block of an AI reply: a card whose label counts the seconds while the model thinks, with the latest reasoning streaming under it, and the full reasoning as steps on a quiet rail once it has finished. +--- + + + +## When to use + +`ReasoningBlock` is the "thinking" part of an AI reply. While the model thinks, a shimmering "Thinking" label counts the +seconds and the newest words of its reasoning stream under it, two lines tall. When it finishes, the card folds to +"Thought for 7s" with a chevron, and a click opens the whole reasoning as steps with headings, with a footer that says +how it ended and a button that copies it. The answer stays the focus; the reasoning is one click away. + +- **Chat replies from a model that reasons**, above the answer. +- **Agent logs** where each turn starts with what the model worked out. +- **Restoring past messages** where the reasoning stays folded with its original thinking time. +- **Audit and review views** where people read how a result was reached. + +## When not to use + +- **A tool the agent called**: use `ToolCall`; a reasoning block holds text, not an action with input and output. +- **The agent's plan**: use `AgentPlan`; a plan is a checklist the person can edit, reasoning is a record. +- **A one line busy indicator** with no reasoning to show. +- **Content the model must not reveal**: only pass reasoning you want people to read. + +## Usage + +Set `Status` and pass the reasoning as one growing string in `Reasoning`: blank lines split paragraphs, and a line +written as `**Heading**` or `## Heading` starts a new step. A heading that is still streaming in is held back, so raw +asterisks never show. With no data the block reads "Thought process" and has nothing to open. + +```csharp +// While streaming: set the growing text, then flip the status when the answer starts. +block.Reasoning = thinkingText; +block.Status = done ? ReasoningStatus.Done : ReasoningStatus.Thinking; + +// A finished message from history. +block.Reasoning = message.Reasoning; +block.Elapsed = TimeSpan.FromSeconds(message.ThinkingSeconds); +``` + +Or give it steps of your own and append to their `Text` as the model streams: + +```xml + + + +``` + +The seconds are counted by the block while it thinks and kept when it ends. Set `Elapsed` yourself when your service +reports the time. A finished block with no time reads "Thought process". + +### Anatomy + +1. **Head**: the whole top row (44px) is one button: the mark (18px, the accent), the label and its time, Stop or Retry when + they apply, and a chevron at the far end. +2. **Preview**: while thinking and closed, the newest words of the latest step, two lines at most, muted. +3. **Steps**: when open, each step is a heading over its text beside a rail with a hollow dot. +4. **Footer**: a check and "Done", or "Stopped", with a button that copies the reasoning. + +### States + +The tabs above show a block live, finished, interrupted and with no data; the statuses read: + +| Status | Head reads | Open part | +|---|---|---| +| `Thinking` | A shimmering "Thinking for 3s", optionally a Stop button | The steps so far, streaming | +| `Done` | "Thought for 7s" | The steps, then "Done" and Copy | +| `Stopped` | "Stopped after 4s" | What streamed, then "Stopped" and Copy | +| `Error` | An alert and "Thinking interrupted", with Retry | The steps, then the reason in `ErrorText` | + +### Stopping + + + +`AllowsStop` shows a Stop button while the model thinks. It raises `StopRequested`: abort your request, then set +`Status` to `Stopped`. A failed block shows Retry and raises `RetryRequested`. The demo plays a simulation, which Stop +ends on its own. + +### Plain + + + +`Variant="Plain"` drops the border and the side padding so the block sits flush inside a message bubble. + +### Simulated model + +For a demo or a prototype without a model, give the block a `ReasoningBlockSimulation` with the reasoning to play. The +block thinks, counts the seconds and streams the text three words every 45ms, then finishes; it plays only while it has +no reasoning, steps, status or time of its own, starts the simulation over each time it takes it up, and pauses it off +screen. Stop ends it as Stopped, `Restart()` plays it again, and `Finished` tells you when it is done. Your own data +always wins: set `Reasoning` and the simulation steps aside. + +```xml + + + + + +``` + +```csharp +// Or from code, with a reasoning you recorded. +block.Simulation = new ReasoningBlockSimulation { Reasoning = recordedReasoning }; +``` + +### Your own words + +`Labels` holds every word the block shows or says. Replace the object to localize or reword: + +```csharp +block.Labels = new ReasoningBlockLabels +{ + Thinking = "Reflexion", + Thought = "Réfléchi", + Duration = seconds => $"pendant {seconds}s", +}; +``` + +`Mark` replaces the sparkle with your own 24 × 24 stroked glyph, and `MarkBrush` its color. + +### Reduced motion + +With reduced motion the label does not shimmer, the chevron and the panels change at once and text trades places with a +short fade. With motion off everything changes at once. The seconds still count: they are information, not decoration. + +### Content guidelines + +- Headings are a few words and name what the model did ("Check what is open"), not what it felt. +- A step is a short paragraph. Split long reasoning into steps instead of one wall of text. +- Time reads "for 7s" or "for 1m 5s"; a stopped block reads "after 4s". + +### Best practices + +- Put the block above the answer and keep it closed once the answer arrives. +- Stream by setting the growing string, or by appending to `ReasoningStep.Text`; do not rebuild the list on every token. +- Pass `ErrorText` with a failure so the person sees why it stopped, next to Retry. + +### Common questions + +**Can the person open it while it thinks?** Yes. The preview folds away and the steps stream in place. + +**How do I show a fixed time?** Set `Elapsed`; the block then shows that time and does not count. + +## API reference + + + + + + + + + +## Accessibility + +- The head is a button that reports expanded or collapsed; its name reads the label and time, then what pressing does + ("Thought for 7s, Show reasoning"). +- A stopped or failed block says so in the head, not only by color: an alert icon and "Thinking interrupted". +- Keyboard focus shows a 2px ring inside the head; pointer focus draws none. +- Every word is in `ReasoningBlockLabels`. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the head, then to Stop or Retry when shown, then to Copy inside the open block | +| Enter, Space | Opens or closes the steps from the head; on a button, presses it | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Label while thinking | Shimmer, one pass each 1.8s | Still | Still | +| Label changes to "Thought" | Old text fades out over 100ms, new text fades in over 160ms and rises 2px | Fades over 130ms | At once | +| Preview appears or leaves | Height springs (0.26s, no bounce), text fades over 160ms | At once | At once | +| A line wraps in the preview | The text glides up on a 0.24s spring instead of jumping | At once | At once | +| Steps open or close | The same 0.26s height spring, content fades over 160ms | At once | At once | +| A step arrives while open | Appended, fades in on its own; nothing else moves | At once | At once | +| Chevron | Turns on a spring | At once | At once | + +The head is 44px high in every state and the preview is always two lines tall, so a Stop or Retry button appearing, or a +line streaming in, never changes the block's height. + +## Related + +- `ToolCall` for the tool calls an agent makes. +- `AgentPlan` for the plan an agent writes before it acts. +- `AgentStatus` for the live status of a whole run. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/zh-CN.md new file mode 100644 index 00000000..49f3974b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/zh-CN.md @@ -0,0 +1,176 @@ +--- +title: Reasoning block +summary: AI 回复中可折叠的思考块:一张卡片,模型思考时标签逐秒计时,最新的推理在其下方流式出现,完成后以轨道上的步骤呈现完整推理。 +source-hash: 3c55fed32f02 +--- + + + +## 何时使用 + +`ReasoningBlock` 是 AI 回复里的"思考"部分。模型思考时,一个闪烁的 "Thinking" 标签逐秒计时,其推理的最新几个词在下方流式出现,最多两行。完成后,卡片折叠为带箭头的 "Thought for 7s",点击一次即可把完整推理展开为带标题的步骤,并在底部说明结果如何,还有一个复制按钮。答案始终是焦点,推理只需一次点击。 + +- **会推理的模型所生成的聊天回复**,位于答案上方。 +- **智能体日志**:每一轮都以模型想明白了什么开头。 +- **恢复历史消息**:推理保持折叠,并保留原来的思考时间。 +- **审计与复核视图**:人们需要阅读结果是如何得出的。 + +## 何时不用 + +- **智能体调用的工具**:使用 `ToolCall`;推理块容纳的是文字,而不是带输入与输出的动作。 +- **智能体的计划**:使用 `AgentPlan`;计划是人可以编辑的清单,推理是一份记录。 +- **没有推理可显示的单行忙碌指示**。 +- **模型不应透露的内容**:只传入你希望人们阅读的推理。 + +## 用法 + +设置 `Status`,并把推理作为一个不断增长的字符串传给 `Reasoning`:空行分隔段落,写成 `**Heading**` 或 `## Heading` 的行会开始一个新的步骤。仍在流式输出中的标题会先被隐藏,所以不会出现裸露的星号。没有数据时,该块显示 "Thought process",也没有可展开的内容。 + +```csharp +// While streaming: set the growing text, then flip the status when the answer starts. +block.Reasoning = thinkingText; +block.Status = done ? ReasoningStatus.Done : ReasoningStatus.Thinking; + +// A finished message from history. +block.Reasoning = message.Reasoning; +block.Elapsed = TimeSpan.FromSeconds(message.ThinkingSeconds); +``` + +或者自行提供步骤,并在模型流式输出时向其 `Text` 追加内容: + +```xml + + + +``` + +思考期间由该块自行计秒,结束后保留结果。如果你的服务会报告耗时,请自行设置 `Elapsed`。没有时间的已完成块显示 "Thought process"。 + +### 结构 + +1. **头部**:整个顶部一行就是一个按钮:标记(18px,强调色)、标签及其时间、需要时出现的停止或重试按钮,以及最右端的箭头。 +2. **预览**:思考中且处于收起状态时,显示最新步骤的最新几个词,最多两行,颜色偏淡。 +3. **步骤**:展开时,每个步骤是标题加正文,旁边是带空心圆点的轨道。 +4. **底部**:一个对勾加 "Done",或 "Stopped",以及复制推理的按钮。 + +### 状态 + +上方的标签页展示了实时、已完成、已中断和没有数据的块;各状态显示如下: + +| 状态 | 头部显示 | 展开部分 | +|---|---|---| +| `Thinking` | 闪烁的 "Thinking for 3s",可带停止按钮 | 目前为止的步骤,流式出现 | +| `Done` | "Thought for 7s" | 步骤,然后是 "Done" 和复制 | +| `Stopped` | "Stopped after 4s" | 已流出的内容,然后是 "Stopped" 和复制 | +| `Error` | 警示图标和 "Thinking interrupted",带重试 | 步骤,然后是 `ErrorText` 中的原因 | + +### 停止 + + + +`AllowsStop` 会在模型思考时显示停止按钮。它引发 `StopRequested`:终止你的请求,然后把 `Status` 设为 `Stopped`。失败的块会显示重试并引发 `RetryRequested`。示例播放的是一段模拟,停止按钮会自行结束它。 + +### 精简样式 + + + +`Variant="Plain"` 去掉边框和左右内边距,使该块贴合地放在消息气泡中。 + +### 模拟模型 + +用于没有模型的演示或原型时,给该块一个 `ReasoningBlockSimulation`,并传入要播放的推理。该块会进入思考、逐秒计时,并以每 45ms 三个词的速度流式输出文字,然后完成;只有在它没有自己的推理、步骤、状态或时间时才会播放,每次接手模拟时都会从头开始,离开屏幕时暂停。停止按钮会把它结束为 Stopped,`Restart()` 会重新播放,`Finished` 会在播放完成时通知你。你自己的数据始终优先:设置 `Reasoning` 后,模拟就会让位。 + +```xml + + + + + +``` + +```csharp +// Or from code, with a reasoning you recorded. +block.Simulation = new ReasoningBlockSimulation { Reasoning = recordedReasoning }; +``` + +### 你自己的文字 + +`Labels` 包含该块显示或朗读的每一个词。替换该对象即可本地化或改写: + +```csharp +block.Labels = new ReasoningBlockLabels +{ + Thinking = "Reflexion", + Thought = "Réfléchi", + Duration = seconds => $"pendant {seconds}s", +}; +``` + +`Mark` 可以用你自己的 24 × 24 描边图标替换星芒,`MarkBrush` 设置其颜色。 + +### 减弱动效 + +减弱动效时,标签不再闪烁,箭头与面板立即变化,文字以短暂淡入淡出交替。关闭动效时一切立即变化。秒数仍会计时:它是信息,不是装饰。 + +### 内容准则 + +- 标题只有几个词,说明模型做了什么("Check what is open"),而不是它的感受。 +- 一个步骤是一小段文字。把长推理拆成多个步骤,不要写成一大块。 +- 时间写作 "for 7s" 或 "for 1m 5s";已停止的块写作 "after 4s"。 + +### 最佳实践 + +- 把该块放在答案上方,答案出现后保持收起。 +- 通过设置不断增长的字符串,或向 `ReasoningStep.Text` 追加来流式输出;不要在每个 token 上重建列表。 +- 失败时传入 `ErrorText`,让人在重试旁边看到停止的原因。 + +### 常见问题 + +**思考时人可以展开它吗?** 可以。预览会收起,步骤在原地流式出现。 + +**如何显示固定的时间?** 设置 `Elapsed`;该块随后显示该时间且不再计时。 + +## API 参考 + + + + + + + + + +## 无障碍 + +- 头部是一个按钮,会报告其展开或收起;它的名称先读出标签与时间,再读出按下后的结果("Thought for 7s, Show reasoning")。 +- 已停止或失败的块不只靠颜色表达:头部有警示图标和 "Thinking interrupted"。 +- 键盘焦点在头部内显示 2px 的焦点环;指针焦点不绘制。 +- 每个文字都位于 `ReasoningBlockLabels` 中。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 移动到头部,然后是出现的停止或重试,再到展开块内的复制按钮 | +| Enter, Space | 在头部展开或收起步骤;在按钮上则按下该按钮 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 思考中的标签 | 闪烁,每 1.8 秒一次 | 静止 | 静止 | +| 标签变为 "Thought" | 旧文字 100ms 淡出,新文字 160ms 淡入并上升 2px | 淡入淡出,130ms | 立即 | +| 预览出现或离开 | 高度弹簧变化(0.26 秒,无回弹),文字淡入 160ms | 立即 | 立即 | +| 预览中某一行换行 | 文字以 0.24 秒的弹簧向上滑动,而不是跳动 | 立即 | 立即 | +| 步骤展开或收起 | 同一根 0.26 秒的高度弹簧,内容淡入 160ms | 立即 | 立即 | +| 展开时来了新步骤 | 追加并单独淡入;其他内容不动 | 立即 | 立即 | +| 箭头 | 以弹簧转动 | 立即 | 立即 | + +头部在每种状态下都是 44px 高,预览始终是两行高,所以出现停止或重试按钮、或一行文字流入,都不会改变块的高度。 + +## 相关组件 + +- `ToolCall`:智能体发起的工具调用。 +- `AgentPlan`:智能体在行动前写下的计划。 +- `AgentStatus`:整次运行的实时状态。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/zh-TW.md new file mode 100644 index 00000000..80df724f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReasoningBlock/zh-TW.md @@ -0,0 +1,176 @@ +--- +title: Reasoning block +summary: AI 回覆中可摺疊的思考區塊:一張卡片,模型思考時標籤逐秒計時,最新的推理在其下方串流出現,完成後以軌道上的步驟呈現完整推理。 +source-hash: 3c55fed32f02 +--- + + + +## 何時使用 + +`ReasoningBlock` 是 AI 回覆裡的「思考」部分。模型思考時,一個閃爍的 "Thinking" 標籤逐秒計時,其推理最新的幾個字詞在下方串流出現,最多兩行。完成後,卡片摺疊為帶箭頭的 "Thought for 7s",點擊一次即可把完整推理展開為帶標題的步驟,並在底部說明結果如何,還有一個複製按鈕。答案始終是焦點,推理只需一次點擊。 + +- **會推理的模型所產生的聊天回覆**,位於答案上方。 +- **代理日誌**:每一輪都以模型想明白了什麼開頭。 +- **還原歷史訊息**:推理保持摺疊,並保留原來的思考時間。 +- **稽核與覆核檢視**:人們需要閱讀結果是如何得出的。 + +## 何時不用 + +- **代理呼叫的工具**:使用 `ToolCall`;推理區塊容納的是文字,而不是帶輸入與輸出的動作。 +- **代理的計畫**:使用 `AgentPlan`;計畫是人可以編輯的清單,推理是一份紀錄。 +- **沒有推理可顯示的單行忙碌指示**。 +- **模型不應透露的內容**:只傳入你希望人們閱讀的推理。 + +## 用法 + +設定 `Status`,並把推理作為一個不斷增長的字串傳給 `Reasoning`:空行分隔段落,寫成 `**Heading**` 或 `## Heading` 的行會開始一個新的步驟。仍在串流輸出中的標題會先被隱藏,所以不會出現裸露的星號。沒有資料時,該區塊顯示 "Thought process",也沒有可展開的內容。 + +```csharp +// While streaming: set the growing text, then flip the status when the answer starts. +block.Reasoning = thinkingText; +block.Status = done ? ReasoningStatus.Done : ReasoningStatus.Thinking; + +// A finished message from history. +block.Reasoning = message.Reasoning; +block.Elapsed = TimeSpan.FromSeconds(message.ThinkingSeconds); +``` + +或者自行提供步驟,並在模型串流輸出時向其 `Text` 追加內容: + +```xml + + + +``` + +思考期間由該區塊自行計秒,結束後保留結果。如果你的服務會回報耗時,請自行設定 `Elapsed`。沒有時間的已完成區塊顯示 "Thought process"。 + +### 結構 + +1. **頭部**:整個頂部一行就是一個按鈕:標記(18px,強調色)、標籤及其時間、需要時出現的停止或重試按鈕,以及最右端的箭頭。 +2. **預覽**:思考中且處於收合狀態時,顯示最新步驟最新的幾個字詞,最多兩行,顏色偏淡。 +3. **步驟**:展開時,每個步驟是標題加內文,旁邊是帶空心圓點的軌道。 +4. **底部**:一個勾選加 "Done",或 "Stopped",以及複製推理的按鈕。 + +### 狀態 + +上方的頁籤展示了即時、已完成、已中斷和沒有資料的區塊;各狀態顯示如下: + +| 狀態 | 頭部顯示 | 展開部分 | +|---|---|---| +| `Thinking` | 閃爍的 "Thinking for 3s",可帶停止按鈕 | 目前為止的步驟,串流出現 | +| `Done` | "Thought for 7s" | 步驟,然後是 "Done" 和複製 | +| `Stopped` | "Stopped after 4s" | 已串流出的內容,然後是 "Stopped" 和複製 | +| `Error` | 警示圖示和 "Thinking interrupted",帶重試 | 步驟,然後是 `ErrorText` 中的原因 | + +### 停止 + + + +`AllowsStop` 會在模型思考時顯示停止按鈕。它引發 `StopRequested`:終止你的請求,然後把 `Status` 設為 `Stopped`。失敗的區塊會顯示重試並引發 `RetryRequested`。範例播放的是一段模擬,停止按鈕會自行結束它。 + +### 精簡樣式 + + + +`Variant="Plain"` 去掉邊框和左右內距,使該區塊貼合地放在訊息氣泡中。 + +### 模擬模型 + +用於沒有模型的展示或原型時,給該區塊一個 `ReasoningBlockSimulation`,並傳入要播放的推理。該區塊會進入思考、逐秒計時,並以每 45ms 三個詞的速度串流輸出文字,然後完成;只有在它沒有自己的推理、步驟、狀態或時間時才會播放,每次接手模擬時都會從頭開始,離開螢幕時暫停。停止按鈕會把它結束為 Stopped,`Restart()` 會重新播放,`Finished` 會在播放完成時通知你。你自己的資料始終優先:設定 `Reasoning` 後,模擬就會讓位。 + +```xml + + + + + +``` + +```csharp +// Or from code, with a reasoning you recorded. +block.Simulation = new ReasoningBlockSimulation { Reasoning = recordedReasoning }; +``` + +### 你自己的文字 + +`Labels` 包含該區塊顯示或朗讀的每一個詞。替換該物件即可在地化或改寫: + +```csharp +block.Labels = new ReasoningBlockLabels +{ + Thinking = "Reflexion", + Thought = "Réfléchi", + Duration = seconds => $"pendant {seconds}s", +}; +``` + +`Mark` 可以用你自己的 24 × 24 描邊圖示替換星芒,`MarkBrush` 設定其顏色。 + +### 減少動態效果 + +減少動態效果時,標籤不再閃爍,箭頭與面板立即變化,文字以短暫淡入淡出交替。關閉動態效果時一切立即變化。秒數仍會計時:它是資訊,不是裝飾。 + +### 內容指引 + +- 標題只有幾個詞,說明模型做了什麼("Check what is open"),而不是它的感受。 +- 一個步驟是一小段文字。把長推理拆成多個步驟,不要寫成一大塊。 +- 時間寫作 "for 7s" 或 "for 1m 5s";已停止的區塊寫作 "after 4s"。 + +### 最佳實務 + +- 把該區塊放在答案上方,答案出現後保持收合。 +- 透過設定不斷增長的字串,或向 `ReasoningStep.Text` 追加來串流輸出;不要在每個 token 上重建清單。 +- 失敗時傳入 `ErrorText`,讓人在重試旁邊看到停止的原因。 + +### 常見問題 + +**思考時人可以展開它嗎?** 可以。預覽會收合,步驟在原地串流出現。 + +**如何顯示固定的時間?** 設定 `Elapsed`;該區塊隨後顯示該時間且不再計時。 + +## API 參考 + + + + + + + + + +## 無障礙 + +- 頭部是一個按鈕,會回報其展開或收合;它的名稱先讀出標籤與時間,再讀出按下後的結果("Thought for 7s, Show reasoning")。 +- 已停止或失敗的區塊不只靠顏色表達:頭部有警示圖示和 "Thinking interrupted"。 +- 鍵盤焦點在頭部內顯示 2px 的焦點環;指標焦點不繪製。 +- 每個文字都位於 `ReasoningBlockLabels` 中。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 移動到頭部,然後是出現的停止或重試,再到展開區塊內的複製按鈕 | +| Enter, Space | 在頭部展開或收合步驟;在按鈕上則按下該按鈕 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 思考中的標籤 | 閃爍,每 1.8 秒一次 | 靜止 | 靜止 | +| 標籤變為 "Thought" | 舊文字 100ms 淡出,新文字 160ms 淡入並上升 2px | 淡入淡出,130ms | 立即 | +| 預覽出現或離開 | 高度彈簧變化(0.26 秒,無回彈),文字淡入 160ms | 立即 | 立即 | +| 預覽中某一行換行 | 文字以 0.24 秒的彈簧向上滑動,而不是跳動 | 立即 | 立即 | +| 步驟展開或收合 | 同一根 0.26 秒的高度彈簧,內容淡入 160ms | 立即 | 立即 | +| 展開時來了新步驟 | 追加並單獨淡入;其他內容不動 | 立即 | 立即 | +| 箭頭 | 以彈簧轉動 | 立即 | 立即 | + +頭部在每種狀態下都是 44px 高,預覽始終是兩行高,所以出現停止或重試按鈕、或一行文字流入,都不會改變區塊的高度。 + +## 相關 + +- `ToolCall`:代理發起的工具呼叫。 +- `AgentPlan`:代理在行動前寫下的計畫。 +- `AgentStatus`:整次執行的即時狀態。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/en-US.md new file mode 100644 index 00000000..c0025515 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/en-US.md @@ -0,0 +1,215 @@ +--- +title: Referral card +summary: A give-and-get referral card where people copy or share their invite link, invite friends by email as chips, and watch invites move from invited to signed up to earned, with the credit total counting up and a milestone reward unlocking. +--- + + + +## When to use + +`ReferralCard` is a whole referral program in one card. The ask sits on top ("Give $20, get $20"), the invite link waits +in a quiet field with Copy and three share buttons, and an email field turns each typed address into a chip. Below, three +rolling numbers count who was invited, who signed up and what was earned, a segmented bar shows how close the next reward +is, and the people invited move from Invited to Signed up to Earned in place. + +- **A referral or invite program** in account settings, billing or an onboarding checklist. +- **Showing what invites have earned** and how close the person is to a bonus reward. + +## When not to use + +- **Team member invites with roles and permissions**: use a members table. +- **Public leaderboards or affiliate payouts** with many columns: use a table. +- **A one-off share link** with nothing to track: a field with a `CopyButton` is enough. + +## Usage + +Pass the referrals from your backend in `Referrals`, newest first, and replace the list when sign-ups and payments come +in: the counts, the milestone and the list are all derived from it. Sending invites calls `InviteHandler`; return when the +server has accepted them, or throw with the message the person should read. + +```csharp +card.Link = $"https://app.example.com/r/{me.Code}"; +card.Reward = 20; +card.Referrals = referrals; // IReadOnlyList, newest first +card.Milestone = new ReferralMilestone +{ + Goal = 5, + Reward = "3 months free", + Detail = "Applied to your next invoice", +}; +card.InviteHandler = async (emails, token) => +{ + var response = await http.PostAsJsonAsync("/api/referrals/invite", emails, token); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Could not send the invites. Nothing was sent."); + } +}; +card.MilestoneReached += (_, e) => analytics.Track("referral_milestone_unlocked"); +``` + +With no `Referrals` and no `Simulation` the card is empty: the counts read zero, the list says "No invites yet. Friends +you invite show up here.", and with no `Reward` the heading reads "Invite friends"; `Link` and `Milestone` are empty too. +The package ships no sample program. To show a program moving (a preview, onboarding), give the card a +`ReferralCardSimulation`: the list it starts from and the beats that move friends through the stages. While `Referrals` +is null the card shows the simulation's list and plays its beats once when it is 40% on screen; the preview above plays +the gallery's invented program this way (a friend who signed up subscribes, an invited friend signs up and subscribes, +and the fifth paid referral unlocks the milestone). + +```csharp +card.Simulation = new ReferralCardSimulation +{ + Referrals = program, // the list it starts from, newest first + Beats = + [ + new(TimeSpan.FromSeconds(1.4), "p4", ReferralStage.Earned), + new(TimeSpan.FromSeconds(2.2), "p5", ReferralStage.SignedUp, "Ana Souza") + ] +}; +``` + +Setting `Referrals` hands the card to your data and stops the program; give each card its own simulation. `Simulate` +turns the playback off, or on for a card with your own list (then the first address of each simulated send moves through +the stages). + +### Anatomy + +1. **The ask**: a gift mark, the heading built from `Reward` and `Currency`, and a line that says what both sides get. +2. **Invite link**: the domain muted and the code in full color, Copy at the end, and three share buttons: a post, a + network and email. +3. **Invite by email**: chips in a fixed-height field that scrolls sideways, Send, and a hint underneath. +4. **Funnel**: Invited, Signed up and Earned as rolling numbers in tabular digits. +5. **Milestone**: one segment per paid referral needed (up to 12) and a line with the reward it unlocks. +6. **Your invites**: each person with an avatar, the address, the date and the stage as a glyph plus a word, in a list of + fixed height. + +### Your data + + + +When your service reports a sign-up or a payment, replace `Referrals` with the new list; rows keep their place by `Id` and +only what changed moves. After a successful send the addresses join the top of the list as Invited until your next list +arrives. + +### Sharing + +Each share button raises `ShareRequested` with the channel and a ready URL. Open it and set `Handled`; when no handler +does, the share is simulated and the live region says nothing was posted. The email button builds a `mailto:` link with +`ShareMessage` and the link in its body. The post and network buttons use generic glyphs, not network logos: give them +your network's share intent in `PostShareUrlFormat` and `NetworkShareUrlFormat`, where `{url}` and `{text}` are filled in. + +```csharp +card.ShareMessage = "I use our invoicing app. Join with my link and we both get $20."; +card.PostShareUrlFormat = "https://social.example/share?url={url}&text={text}"; +card.ShareRequested += (_, e) => +{ + Launcher.LaunchUriAsync(new Uri(e.Url)); + e.Handled = true; +}; + +// The same URLs elsewhere: +var mail = ReferralCard.BuildShareUrl(ReferralShareChannel.Email, link, message); +``` + +### Inviting by email + +Typing an address and pressing Enter, a comma, a space or Tab turns it into a chip; pasting a list adds every address in +it. An address that is not valid becomes a chip marked with an alert, and Send refuses until it is fixed or removed. A +duplicate, an address already in the list and anything past `MaxInvites` (10) are refused with a note. While the invites go +out the field turns read only and Send shows a spinner; a failure keeps the chips and shows the reason. + +### Narrow cards + + + +The card is fluid up to 560px. Below a 470px card width the share buttons take their own full row and the milestone +line keeps room for two lines. Long addresses truncate while the date and the stage stay visible. + +### Your own words + +`Labels` holds every word the card shows or says; counts, amounts and dates are functions. `Title` and `Description` +replace the heading and its line outright. + +```csharp +card.Labels = new ReferralCardLabels +{ + Title = reward => $"Offrez {reward}, recevez {reward}", + Send = "Envoyer", + StageEarned = reward => $"{reward} gagnés", +}; +``` + +### Content guidelines + +- Keep `Reward`, `Currency` and the copy consistent with your real program terms; the heading is built from the reward. +- Name the milestone reward in a few words ("3 months free") and put the fine print in `Detail`. +- The previews' people, amounts and milestone are invented; pass your own program before you ship. + +### Common questions + +**Does the card send email itself?** No. `InviteHandler` sends; without one the send is simulated and the hint says so. + +**Why does the list keep its height?** New invites open at the top inside a list of fixed height, so the card never grows +and nothing below it moves. + +## API reference + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- Share buttons are icon buttons with names and tooltips (Share in a post, Share with your network, Share by email); a + polite live region reports what happened, including "Simulated, nothing was posted." +- The hint under the email field describes the field and is announced politely. An error shows an alert icon plus words + and turns the field's edge to the danger color, never color alone; a chip that is not valid says so in its name. +- Stages are a glyph plus a word. The milestone bar reads as a progress bar with its value as help text, such as + "4 of 5 paid referrals". +- Each funnel number is named with its label; the list of people is named "Your invites". +- While sending, the field turns read only instead of disabled, so keyboard focus stays put. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through Copy, the three share buttons, the email field and Send | +| Enter, comma, space or Tab | In the email field, turns the typed address into a chip | +| Enter | In an empty email field, sends the chips | +| Backspace | In an empty email field, removes the last chip; each chip also has its own remove button | +| Ctrl/Cmd + V | Pasting addresses separated by commas, spaces or new lines adds them all | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Funnel numbers | Roll to the new value in 600ms on the standard curve, in tabular digits | Change at once | Change at once | +| Stage words, the milestone line, share icons and the hint | Old out in 100ms, new in over 160ms rising 2px | Short crossfade, no rise | At once | +| Milestone segments | Fill from the left in 240ms; the fill turns to the success color when unlocked | The same | At once | +| A new invite | Opens at the top on a bounce-free 0.24s height spring inside the fixed-height list while it fades in over 160ms | At once | At once | +| Simulated program | Plays once when 40% on screen | Plays twice as fast | Plays twice as fast | + +Every end state is the same at every level; reduced motion only drops the roll, the rise and the height travel. + +## Related + +- `CopyButton` for copying a single value with confirmation. +- `TagInput` for free-form tags without addresses or sending. +- `Avatar` for the people in the list. +- `AgentStatus` for another compact live status card. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/zh-CN.md new file mode 100644 index 00000000..b9e8e518 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/zh-CN.md @@ -0,0 +1,187 @@ +--- +title: Referral card +summary: 双向奖励的推荐卡片:复制或分享邀请链接,用标签形式按邮箱邀请好友,看着邀请从已邀请到已注册再到已赚取,奖励金额随之累加,里程碑奖励随之解锁。 +source-hash: c111993428ce +--- + + + +## 何时使用 + +`ReferralCard` 把整个推荐计划放进一张卡片。顶部是邀约("Give $20, get $20"),邀请链接放在一个安静的字段里,旁边是复制按钮和三个分享按钮,邮箱字段会把输入的每个地址变成一个标签。下方三个滚动数字分别统计已邀请、已注册和已赚取,分段进度条显示离下一个奖励还有多远,被邀请的人在原位从 Invited 变为 Signed up 再变为 Earned。 + +- **推荐或邀请计划**,放在账户设置、账单或新手引导清单中。 +- **展示邀请带来的收益**,以及离额外奖励还有多远。 + +## 何时不用 + +- **带角色和权限的团队成员邀请**:使用成员表格。 +- **公开排行榜或多列的联盟佣金结算**:使用表格。 +- **一次性的分享链接**,没有需要跟踪的内容:一个字段加 `CopyButton` 就够了。 + +## 用法 + +把后端返回的推荐记录按从新到旧传给 `Referrals`,注册和付款发生时替换这个列表:计数、里程碑和列表都由它推导而来。发送邀请会调用 `InviteHandler`;服务器接受后返回即可,失败时抛出异常,异常消息就是用户会看到的文字。 + +```csharp +card.Link = $"https://app.example.com/r/{me.Code}"; +card.Reward = 20; +card.Referrals = referrals; // IReadOnlyList, newest first +card.Milestone = new ReferralMilestone +{ + Goal = 5, + Reward = "3 months free", + Detail = "Applied to your next invoice", +}; +card.InviteHandler = async (emails, token) => +{ + var response = await http.PostAsJsonAsync("/api/referrals/invite", emails, token); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Could not send the invites. Nothing was sent."); + } +}; +card.MilestoneReached += (_, e) => analytics.Track("referral_milestone_unlocked"); +``` + +没有 `Referrals` 也没有 `Simulation` 时,卡片为空:计数为零,列表显示 "No invites yet. Friends you invite show up here.",没有 `Reward` 时标题为 "Invite friends";`Link` 和 `Milestone` 也为空。包里不附带示例计划。要展示一个推进中的计划(预览、新手引导),请给卡片一个 `ReferralCardSimulation`:它起始的列表,以及让好友依次经历各个阶段的节拍。`Referrals` 为 null 时,卡片显示模拟的列表,并在卡片有 40% 进入屏幕时播放一次节拍;上方的预览就是这样播放展示库编写的虚构计划(一位已注册的好友完成订阅,一位已邀请的好友先注册再订阅,第五个付费推荐解锁里程碑)。 + +```csharp +card.Simulation = new ReferralCardSimulation +{ + Referrals = program, // the list it starts from, newest first + Beats = + [ + new(TimeSpan.FromSeconds(1.4), "p4", ReferralStage.Earned), + new(TimeSpan.FromSeconds(2.2), "p5", ReferralStage.SignedUp, "Ana Souza") + ] +}; +``` + +设置 `Referrals` 会把卡片交给你的数据并停止计划;每张卡片使用自己的模拟。`Simulate` 可以关掉播放;对使用你自己列表的卡片也可以打开它(此时每次模拟发送的第一个地址会依次经历各个阶段)。 + +### 结构 + +1. **邀约**:礼物标记、由 `Reward` 和 `Currency` 生成的标题,以及说明双方各得什么的一行文字。 +2. **邀请链接**:域名为弱化色、邀请码为正文色,末尾是复制按钮,另有三个分享按钮:动态、人脉和邮件。 +3. **按邮箱邀请**:固定高度、可横向滚动的字段中的标签,发送按钮,以及下方的提示。 +4. **漏斗**:已邀请、已注册和已赚取,以等宽数字滚动显示。 +5. **里程碑**:每个所需的付费推荐一个分段(最多 12 段),以及一行说明它解锁的奖励。 +6. **你的邀请**:每个人的头像、邮箱、日期和阶段(图标加文字),放在固定高度的列表中。 + +### 你的数据 + + + +当服务报告有人注册或付款时,用新列表替换 `Referrals`;各行按 `Id` 保持位置,只有变化的部分会动。发送成功后,这些地址会以 Invited 状态出现在列表顶部,直到你的下一个列表到来。 + +### 分享 + +每个分享按钮都会触发 `ShareRequested`,带上渠道和一个现成的 URL。打开它并设置 `Handled`;没有处理程序这样做时,分享是模拟的,实时区域会说明什么也没有发布。邮件按钮会生成一个 `mailto:` 链接,正文是 `ShareMessage` 和链接。动态和人脉按钮使用通用图标,而不是社交网络的标志:把你所用网络的分享地址写进 `PostShareUrlFormat` 和 `NetworkShareUrlFormat`,其中的 `{url}` 和 `{text}` 会被填入。 + +```csharp +card.ShareMessage = "I use our invoicing app. Join with my link and we both get $20."; +card.PostShareUrlFormat = "https://social.example/share?url={url}&text={text}"; +card.ShareRequested += (_, e) => +{ + Launcher.LaunchUriAsync(new Uri(e.Url)); + e.Handled = true; +}; + +// The same URLs elsewhere: +var mail = ReferralCard.BuildShareUrl(ReferralShareChannel.Email, link, message); +``` + +### 按邮箱邀请 + +输入一个地址后按 Enter、逗号、空格或 Tab,它就会变成一个标签;粘贴一串地址会把其中每一个都加进来。无效的地址会变成带警示图标的标签,在修正或移除之前发送会被拒绝。重复的地址、已在列表中的地址以及超过 `MaxInvites`(10)的部分会被拒绝并给出说明。邀请发送期间,字段变为只读,发送按钮显示加载动画;失败时标签保留,并显示原因。 + +### 窄卡片 + + + +卡片宽度自适应,最宽 560px。卡片宽度小于 470px 时,分享按钮单独占满一行,里程碑那一行预留两行的高度。过长的邮箱地址以省略号截断,日期和阶段始终可见。 + +### 你自己的文字 + +`Labels` 包含卡片显示或朗读的每一个词;数量、金额和日期是函数。`Title` 和 `Description` 会直接替换标题及其下方的文字。 + +```csharp +card.Labels = new ReferralCardLabels +{ + Title = reward => $"Offrez {reward}, recevez {reward}", + Send = "Envoyer", + StageEarned = reward => $"{reward} gagnés", +}; +``` + +### 内容准则 + +- `Reward`、`Currency` 和文案要与真实的计划条款一致;标题由奖励金额生成。 +- 用几个词说明里程碑奖励("3 months free"),细则放在 `Detail` 里。 +- 预览中的人物、金额和里程碑都是虚构的;上线前请传入你自己的计划。 + +### 常见问题 + +**卡片会自己发邮件吗?** 不会。由 `InviteHandler` 发送;没有它时发送是模拟的,提示也会这样说明。 + +**为什么列表高度不变?** 新邀请在固定高度的列表顶部展开,所以卡片不会变高,下方的内容也不会移动。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 分享按钮是带名称和工具提示的图标按钮(Share in a post、Share with your network、Share by email);一个礼貌的实时区域会报告发生了什么,包括 "Simulated, nothing was posted."。 +- 邮箱字段下方的提示描述该字段,并以礼貌方式朗读。出错时显示警示图标加文字,字段边框变为危险色,从不只靠颜色;无效的标签会在名称中说明。 +- 阶段是图标加文字。里程碑进度条以进度条的身份朗读,数值作为帮助文本,例如 "4 of 5 paid referrals"。 +- 每个漏斗数字都带有它的标签作为名称;人员列表的名称是 "Your invites"。 +- 发送期间字段变为只读而不是禁用,所以键盘焦点保持不动。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过复制按钮、三个分享按钮、邮箱字段和发送按钮 | +| Enter、逗号、空格或 Tab | 在邮箱字段中把输入的地址变成标签 | +| Enter | 在空的邮箱字段中发送标签 | +| Backspace | 在空的邮箱字段中删除最后一个标签;每个标签也有自己的删除按钮 | +| Ctrl/Cmd + V | 粘贴以逗号、空格或换行分隔的地址,会全部加入 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 漏斗数字 | 以标准曲线在 600ms 内滚动到新值,使用等宽数字 | 立即变化 | 立即变化 | +| 阶段文字、里程碑那一行、分享图标和提示 | 旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px | 短暂交叉淡化,不上升 | 立即变化 | +| 里程碑分段 | 240ms 内从左侧填充;解锁时填充变为成功色 | 相同 | 立即变化 | +| 新的邀请 | 在固定高度的列表顶部以无回弹的 0.24s 高度弹簧展开,同时在 160ms 内淡入 | 立即出现 | 立即出现 | +| 模拟计划 | 进入屏幕 40% 时播放一次 | 以两倍速度播放 | 以两倍速度播放 | + +各个等级的最终状态都相同;减弱动效只去掉滚动、上升和高度变化。 + +## 相关组件 + +- `CopyButton`:复制单个值并给出确认。 +- `TagInput`:不涉及邮箱和发送的自由标签。 +- `Avatar`:列表中的人员。 +- `AgentStatus`:另一个紧凑的实时状态卡片。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/zh-TW.md new file mode 100644 index 00000000..38be3141 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReferralCard/zh-TW.md @@ -0,0 +1,187 @@ +--- +title: Referral card +summary: 雙向獎勵的推薦卡片:複製或分享邀請連結,以標籤形式依電子郵件邀請好友,看著邀請從已邀請到已註冊再到已賺取,獎勵金額隨之累加,里程碑獎勵隨之解鎖。 +source-hash: c111993428ce +--- + + + +## 何時使用 + +`ReferralCard` 把整個推薦計畫放進一張卡片。頂部是邀約("Give $20, get $20"),邀請連結放在一個安靜的欄位裡,旁邊是複製按鈕和三個分享按鈕,電子郵件欄位會把輸入的每個地址變成一個標籤。下方三個滾動數字分別統計已邀請、已註冊和已賺取,分段進度列顯示離下一個獎勵還有多遠,被邀請的人在原位從 Invited 變為 Signed up 再變為 Earned。 + +- **推薦或邀請計畫**,放在帳戶設定、帳單或新手引導清單中。 +- **呈現邀請帶來的收益**,以及離額外獎勵還有多遠。 + +## 何時不用 + +- **帶角色和權限的團隊成員邀請**:使用成員表格。 +- **公開排行榜或多欄的聯盟佣金結算**:使用表格。 +- **一次性的分享連結**,沒有需要追蹤的內容:一個欄位加 `CopyButton` 就夠了。 + +## 用法 + +把後端傳回的推薦紀錄依由新到舊傳給 `Referrals`,註冊和付款發生時替換這份清單:計數、里程碑和清單都由它推導而來。傳送邀請會呼叫 `InviteHandler`;伺服器接受後返回即可,失敗時拋出例外,例外訊息就是使用者會看到的文字。 + +```csharp +card.Link = $"https://app.example.com/r/{me.Code}"; +card.Reward = 20; +card.Referrals = referrals; // IReadOnlyList, newest first +card.Milestone = new ReferralMilestone +{ + Goal = 5, + Reward = "3 months free", + Detail = "Applied to your next invoice", +}; +card.InviteHandler = async (emails, token) => +{ + var response = await http.PostAsJsonAsync("/api/referrals/invite", emails, token); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Could not send the invites. Nothing was sent."); + } +}; +card.MilestoneReached += (_, e) => analytics.Track("referral_milestone_unlocked"); +``` + +沒有 `Referrals` 也沒有 `Simulation` 時,卡片為空:計數為零,清單顯示 "No invites yet. Friends you invite show up here.",沒有 `Reward` 時標題為 "Invite friends";`Link` 與 `Milestone` 也為空。套件不附帶範例計畫。要展示一個推進中的計畫(預覽、新手導覽),請給卡片一個 `ReferralCardSimulation`:它起始的清單,以及讓好友依序經歷各個階段的節拍。`Referrals` 為 null 時,卡片顯示模擬的清單,並在卡片有 40% 進入畫面時播放一次節拍;上方的預覽就是這樣播放展示庫撰寫的虛構計畫(一位已註冊的好友完成訂閱,一位已邀請的好友先註冊再訂閱,第五個付費推薦解鎖里程碑)。 + +```csharp +card.Simulation = new ReferralCardSimulation +{ + Referrals = program, // the list it starts from, newest first + Beats = + [ + new(TimeSpan.FromSeconds(1.4), "p4", ReferralStage.Earned), + new(TimeSpan.FromSeconds(2.2), "p5", ReferralStage.SignedUp, "Ana Souza") + ] +}; +``` + +設定 `Referrals` 會把卡片交給你的資料並停止計畫;每張卡片使用自己的模擬。`Simulate` 可以關掉播放;對使用你自己清單的卡片也可以開啟它(此時每次模擬傳送的第一個地址會依序經歷各個階段)。 + +### 結構 + +1. **邀約**:禮物標記、由 `Reward` 和 `Currency` 產生的標題,以及說明雙方各得什麼的一行文字。 +2. **邀請連結**:網域為弱化色、邀請碼為內文色,末端是複製按鈕,另有三個分享按鈕:貼文、人脈和電子郵件。 +3. **依電子郵件邀請**:固定高度、可橫向捲動的欄位中的標籤,傳送按鈕,以及下方的提示。 +4. **漏斗**:已邀請、已註冊和已賺取,以等寬數字滾動顯示。 +5. **里程碑**:每個所需的付費推薦一個分段(最多 12 段),以及一行說明它解鎖的獎勵。 +6. **你的邀請**:每個人的頭像、電子郵件、日期和階段(圖示加文字),放在固定高度的清單中。 + +### 你的資料 + + + +當服務回報有人註冊或付款時,用新清單替換 `Referrals`;各列依 `Id` 保持位置,只有變化的部分會動。傳送成功後,這些地址會以 Invited 狀態出現在清單頂部,直到你的下一份清單到來。 + +### 分享 + +每個分享按鈕都會觸發 `ShareRequested`,附上管道和一個現成的 URL。開啟它並設定 `Handled`;沒有處理常式這樣做時,分享是模擬的,即時區域會說明什麼也沒有發布。電子郵件按鈕會產生一個 `mailto:` 連結,內文是 `ShareMessage` 和連結。貼文和人脈按鈕使用通用圖示,而不是社群網路的標誌:把你所用網路的分享網址寫進 `PostShareUrlFormat` 和 `NetworkShareUrlFormat`,其中的 `{url}` 和 `{text}` 會被填入。 + +```csharp +card.ShareMessage = "I use our invoicing app. Join with my link and we both get $20."; +card.PostShareUrlFormat = "https://social.example/share?url={url}&text={text}"; +card.ShareRequested += (_, e) => +{ + Launcher.LaunchUriAsync(new Uri(e.Url)); + e.Handled = true; +}; + +// The same URLs elsewhere: +var mail = ReferralCard.BuildShareUrl(ReferralShareChannel.Email, link, message); +``` + +### 依電子郵件邀請 + +輸入一個地址後按 Enter、逗號、空白鍵或 Tab,它就會變成一個標籤;貼上一串地址會把其中每一個都加進來。無效的地址會變成帶警示圖示的標籤,在修正或移除之前傳送會被拒絕。重複的地址、已在清單中的地址以及超過 `MaxInvites`(10)的部分會被拒絕並附上說明。邀請傳送期間,欄位變為唯讀,傳送按鈕顯示載入動畫;失敗時標籤保留,並顯示原因。 + +### 窄卡片 + + + +卡片寬度自動調整,最寬 560px。卡片寬度小於 470px 時,分享按鈕單獨佔滿一列,里程碑那一行預留兩行的高度。過長的電子郵件地址以刪節號截斷,日期和階段始終可見。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或朗讀的每一個詞;數量、金額和日期是函式。`Title` 和 `Description` 會直接替換標題及其下方的文字。 + +```csharp +card.Labels = new ReferralCardLabels +{ + Title = reward => $"Offrez {reward}, recevez {reward}", + Send = "Envoyer", + StageEarned = reward => $"{reward} gagnés", +}; +``` + +### 內容指引 + +- `Reward`、`Currency` 和文案要與實際的計畫條款一致;標題由獎勵金額產生。 +- 用幾個詞說明里程碑獎勵("3 months free"),細則放在 `Detail` 裡。 +- 預覽中的人物、金額與里程碑都是虛構的;上線前請傳入你自己的計畫。 + +### 常見問題 + +**卡片會自己寄信嗎?** 不會。由 `InviteHandler` 傳送;沒有它時傳送是模擬的,提示也會這樣說明。 + +**為什麼清單高度不變?** 新邀請在固定高度的清單頂部展開,所以卡片不會變高,下方的內容也不會移動。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 分享按鈕是帶名稱和工具提示的圖示按鈕(Share in a post、Share with your network、Share by email);一個禮貌的即時區域會回報發生了什麼,包括 "Simulated, nothing was posted."。 +- 電子郵件欄位下方的提示描述該欄位,並以禮貌方式朗讀。出錯時顯示警示圖示加文字,欄位邊框變為危險色,從不只靠顏色;無效的標籤會在名稱中說明。 +- 階段是圖示加文字。里程碑進度列以進度列的身分朗讀,數值作為說明文字,例如 "4 of 5 paid referrals"。 +- 每個漏斗數字都以它的標籤作為名稱;人員清單的名稱是 "Your invites"。 +- 傳送期間欄位變為唯讀而不是停用,所以鍵盤焦點保持不動。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過複製按鈕、三個分享按鈕、電子郵件欄位和傳送按鈕 | +| Enter、逗號、空白鍵或 Tab | 在電子郵件欄位中把輸入的地址變成標籤 | +| Enter | 在空的電子郵件欄位中傳送標籤 | +| Backspace | 在空的電子郵件欄位中刪除最後一個標籤;每個標籤也有自己的刪除按鈕 | +| Ctrl/Cmd + V | 貼上以逗號、空白或換行分隔的地址,會全部加入 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 漏斗數字 | 以標準曲線在 600ms 內滾動到新值,使用等寬數字 | 立即變化 | 立即變化 | +| 階段文字、里程碑那一行、分享圖示和提示 | 舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px | 短暫交叉淡化,不上升 | 立即變化 | +| 里程碑分段 | 240ms 內從左側填滿;解鎖時填色變為成功色 | 相同 | 立即變化 | +| 新的邀請 | 在固定高度的清單頂部以無回彈的 0.24s 高度彈簧展開,同時在 160ms 內淡入 | 立即出現 | 立即出現 | +| 模擬計畫 | 進入畫面 40% 時播放一次 | 以兩倍速度播放 | 以兩倍速度播放 | + +各個等級的最終狀態都相同;減少動態效果只去掉滾動、上升和高度變化。 + +## 相關 + +- `CopyButton`:複製單一值並給出確認。 +- `TagInput`:不涉及電子郵件和傳送的自由標籤。 +- `Avatar`:清單中的人員。 +- `AgentStatus`:另一個精簡的即時狀態卡片。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/en-US.md new file mode 100644 index 00000000..f40949ba --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/en-US.md @@ -0,0 +1,235 @@ +--- +title: Reply draft +summary: An AI reply drafter under a received email: quick replies steer a fresh streaming draft, tone and length rewrite it with one crossfade, and the reader edits it before Insert. +--- + + + +## When to use + +`ReplyDraft` shows a received email with an AI reply under it. The model streams a draft in quick, softly fading chunks; +quick reply chips steer a fresh draft, the tone and length groups rewrite the finished one with a single crossfade, and +the reader can fix a word in place before pressing Insert. Insert hands the text, with the reader's edits, to your +composer: the card never sends anything itself. + +- **Drafting a reply** to an email, ticket or message with a model, where a person reviews and edits before sending. +- **Offering intents plus tone and length** for generated text in an inbox, support desk or CRM. +- **Showing an AI writing feature** on a landing page with a realistic example that plays by itself. + +## When not to use + +- **Free-form chat with a model**: use a chat thread and composer; the draft here answers one message. +- **Inline autocomplete while typing**: use ghost text at the caret instead of a separate draft box. +- **Sending the email**: this card inserts a draft; your composer owns sending. + +## Usage + +Give the card the message, your quick replies and a generator that streams your model's reply. Every new action (a chip, +a tone or length change, Regenerate) cancels the previous request's token, so pass it on to your client. + +```csharp +draft.Message = new ReplyDraftMessage +{ + FromName = email.From.Name, + FromEmail = email.From.Address, + Subject = email.Subject, + Body = email.PlainText, // blank lines split paragraphs + ReceivedAt = email.Received, +}; +draft.Suggestions = [new ReplyDraftSuggestion("yes", "Sounds good"), new ReplyDraftSuggestion("later", "Next week")]; +draft.Model = new AgentModel("Mistral Medium 3.5"); +draft.Generator = request => replies.StreamAsync(email.Id, request.Tone, request.Length, + request.Suggestion?.Id, request.Mode, request.Previous, request.CancellationToken); +draft.InsertHandler = async args => await composer.InsertAsync(args.Draft); +``` + +A message of yours without a `Generator` cannot be drafted: the draft box shows its line with Draft reply turned off. With +no `Message` and no `Simulation` the card is empty: a blank email, no quick replies, "No reply drafted yet." and Draft +reply turned off. + +### Simulation + +To show the card without a model (a demo, a design review, an onboarding tour), give it a `ReplyDraftSimulation`: the +email, the quick replies, the model to credit and the replies you wrote for each quick reply, tone and length +(`ReplyDraftVersion`). The card streams the matching reply after a short read, says it is simulated and nothing was +sent, and tours tones, length, a second quick reply and Insert on a loop until someone presses or types inside it. Your +own `Message` wins over the simulation, and setting `Simulation` to null stops it and shows the empty card. Leave +`AutoPlay` off in products. + +```csharp +draft.Simulation = new ReplyDraftSimulation +{ + Message = email, + Suggestions = [new ReplyDraftSuggestion("yes", "Sounds good")], + Model = new AgentModel("Mistral Medium 3.5"), + Replies = + [ + new ReplyDraftVersion("yes", ReplyDraftTone.Friendly, ReplyDraftLength.Medium, "Hi Maya,\n\nSounds good!\n\nSam"), + new ReplyDraftVersion("yes", ReplyDraftTone.Formal, ReplyDraftLength.Medium, "Dear Maya,\n\nThat suits me.\n\nSam"), + ], +}; +``` + +`ReplyDraftSimulation.Generate` streams the same replies as a `Generator`, for when you wrap the simulation yourself. The +preview above plays a simulation whose email and replies are the gallery's own demo data. + +### Anatomy + +1. **Email**: a mail glyph, the subject and the time; the sender's avatar, name, address and recipient; the body, + indented under the avatar. +2. **Reply heading**: the model's mark and "Reply with ..." with the status at the right: an icon and a word, which + shimmers while the model works. +3. **Quick replies**: toggle chips; the one steering the draft is filled with the accent. +4. **Draft box**: the draft streams in here and becomes editable text when it is done. +5. **Tone and length**: two small radio groups with a gliding pill, separated by a hairline. +6. **Actions**: the keyboard hint, Regenerate and Insert. + +### States + +| Status | The draft box shows | Status line | +|---|---|---| +| `Idle` | A shimmering "Reading Maya's email" line, or Draft reply when `AutoStart` is off | "No draft yet" | +| `Drafting` | The draft streaming in | A shimmering "Drafting" | +| `Rewriting` | The old draft, dimmed and read-only | A shimmering "Rewriting" | +| `Ready` | The editable draft | A check and "Ready to edit" | +| `Inserting` | The draft; Insert shows a spinner | A shimmering "Inserting" | +| `Inserted` | The draft | A double check and "Inserted" | +| `Failed` | The error with Try again | An alert and "Couldn't draft" | + +### Your own model + + + +`Generator` returns an `IAsyncEnumerable` of text deltas. When your service returns the whole reply at once, +wrap it with `ReplyDraftStreams.FromText` and the card streams it locally in chunks of 3 to 8 words. A request's `Mode` +is `Draft` for a fresh reply and `Rewrite` when the tone or length changed on a finished draft: then restyle +`Previous`, which holds the text on screen with the reader's edits. Throw to show the error state. + +`DraftCompleted` fires each time a draft or rewrite finishes, with the text, tone, length and quick reply. + +### Inserting + + + +Insert, or Cmd+Enter (Ctrl+Enter) from anywhere inside the card, raises `InsertRequested` and then awaits +`InsertHandler`, showing a spinner on the button meanwhile. A handler that throws keeps the draft and says "Couldn't +insert". With no handler, and no `InsertRequested` handler that marks the event handled, the insert is simulated and the +announcement says that nothing was sent. + +### Starting on demand + +`AutoStart` writes the first draft as soon as the card scrolls into view. Set it to false to show a short line and a +Draft reply button instead, so the model only runs when the reader asks. `StartDraft()` and `Regenerate()` do the same +from code. + +### Narrow containers + + + +The card fills its container up to 600px. Below 480px the padding drops to 16px and the body loses its avatar indent; +below 460px the hairline between the groups hides and Length moves to its own row. Chips wrap, long names and addresses +truncate, and nothing scrolls sideways. After a touch press, options grow to 32px and chips to 36px. + +### Your own words + +`Labels` holds every word the card shows or says; replace the object to localize or reword it: + +```csharp +draft.Labels = new ReplyDraftLabels +{ + Ready = "Prêt à modifier", + Insert = "Insérer", + ReplyWith = model => "Répondre avec " + model, +}; +``` + +`SourceMark` replaces the mail glyph before the subject with your own 24 × 24 stroked mark, and `AgentModel.Mark` the +model's sparkle. + +### Content guidelines + +- Quick reply labels are two or three words ("Sounds good", "Next week"); their ids are what your prompt understands. +- Keep drafts plain text: the draft box is a plain-text editor. +- Say what the insert does in your own composer; the card only hands over the text. + +### Best practices + +- Pass the request's cancellation token to your model client: every new action aborts the previous request. +- Treat `Rewrite` as restyling `Previous`, so the reader's edits survive a tone change. +- Leave `AutoPlay` off in products; it is for marketing and docs pages. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is named after the sender ("Email from Maya Chen"); the reply area is a named section. +- Quick replies are toggle buttons that report whether they are pressed; tone and length are radio groups where only the + selected option is a tab stop. +- The finished draft is a plain-text editable text box named "Draft reply", so readers can fix a word before Insert; the + edits are what Insert hands over. +- A polite live region announces drafting, rewriting, ready, inserted and errors; simulated actions say that nothing was + sent. It stays quiet while the tour plays on its own. +- Status uses an icon plus a short word, never color alone. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the quick replies, the draft text, tone, length, Regenerate and Insert | +| Arrow keys | Inside Tone or Length, move the selection and rewrite the draft | +| Home, End | Inside Tone or Length, jump to the first or last option | +| Enter, Space | Activate a quick reply, an option or a button | +| Cmd+Enter, Ctrl+Enter | Insert the finished draft from anywhere inside the card | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A draft streams | Chunks of 3 to 8 words every 34ms; each chunk fades in over 140ms | The draft appears whole | The draft appears whole | +| Tone or length rewrite | The old draft dims, fades out in 100ms; the new one fades in over 160ms rising 2px | At once | At once | +| Draft box height | Springs to its height (0.26s, no bounce); held during a regenerate until the new draft finishes | At once | At once | +| Tone and length pill | Glides on the same bounce-free spring | Jumps | Jumps | +| Status | Crossfades (100ms out, 160ms in); working words shimmer | Crossfade without shimmer | At once | +| Chip or option press | Scales to 0.97 | No scale | No scale | + +## Related + +- `Textarea` for a plain multiline field the person writes in themselves. +- `ToolCall` and `AgentStatus` for what an agent is doing. +- `SegmentedControl` for switching between a small set of views. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/zh-CN.md new file mode 100644 index 00000000..5c43dea9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/zh-CN.md @@ -0,0 +1,208 @@ +--- +title: Reply draft +summary: 收到的邮件下方的 AI 回复起草器:快捷回复引导一份新的流式草稿,语气与长度以一次交叉淡化改写它,读者在插入前可以编辑。 +source-hash: 19e28ee0c566 +--- + + + +## 何时使用 + +`ReplyDraft` 显示一封收到的邮件,并在其下方给出 AI 回复。模型以快速、柔和淡入的片段流式写出草稿;快捷回复标签引导一份新的草稿,语气与长度两组选项以一次交叉淡化改写已完成的草稿,读者可以在按下 Insert 之前就地修改某个词。Insert 把文字(包含读者的修改)交给你的编辑器:卡片本身从不发送任何内容。 + +- **用模型起草回复**:回复邮件、工单或消息,由人审阅并编辑后再发送。 +- **提供意图以及语气与长度选项**:用于收件箱、客服台或 CRM 中生成的文字。 +- **在落地页展示 AI 写作功能**:一个会自动播放的逼真示例。 + +## 何时不用 + +- **与模型自由对话**:使用聊天线程与输入框;这里的草稿只回复一条消息。 +- **输入时的行内自动补全**:在光标处使用幽灵文字,而不是单独的草稿框。 +- **发送邮件**:此卡片只插入草稿,发送由你的编辑器负责。 + +## 用法 + +把消息、你的快捷回复以及一个流式输出模型回复的生成器交给卡片。每个新操作(标签、语气或长度的变化、Regenerate)都会取消上一个请求的令牌,所以请把它传给你的客户端。 + +```csharp +draft.Message = new ReplyDraftMessage +{ + FromName = email.From.Name, + FromEmail = email.From.Address, + Subject = email.Subject, + Body = email.PlainText, // blank lines split paragraphs + ReceivedAt = email.Received, +}; +draft.Suggestions = [new ReplyDraftSuggestion("yes", "Sounds good"), new ReplyDraftSuggestion("later", "Next week")]; +draft.Model = new AgentModel("Mistral Medium 3.5"); +draft.Generator = request => replies.StreamAsync(email.Id, request.Tone, request.Length, + request.Suggestion?.Id, request.Mode, request.Previous, request.CancellationToken); +draft.InsertHandler = async args => await composer.InsertAsync(args.Draft); +``` + +你自己的邮件如果没有 `Generator`,就无法起草:草稿框显示提示行,"Draft reply"按钮处于关闭状态。既没有 `Message` 也没有 `Simulation` 时,卡片是空的:一封空白邮件,没有快捷回复,显示"No reply drafted yet.","Draft reply"按钮处于关闭状态。 + +### 模拟 + +想在没有模型的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `ReplyDraftSimulation`:邮件、快捷回复、要标注的模型,以及你为每个快捷回复、语气和长度写好的回复(`ReplyDraftVersion`)。卡片会在短暂的阅读之后流式输出匹配的回复,说明这是模拟、没有发送任何内容,并循环演示语气、长度、第二个快捷回复和插入,直到有人在卡片内按下或输入。你自己的 `Message` 优先于模拟;把 `Simulation` 设为 null 会停止模拟并显示空卡片。在产品中请关闭 `AutoPlay`。 + +```csharp +draft.Simulation = new ReplyDraftSimulation +{ + Message = email, + Suggestions = [new ReplyDraftSuggestion("yes", "Sounds good")], + Model = new AgentModel("Mistral Medium 3.5"), + Replies = + [ + new ReplyDraftVersion("yes", ReplyDraftTone.Friendly, ReplyDraftLength.Medium, "Hi Maya,\n\nSounds good!\n\nSam"), + new ReplyDraftVersion("yes", ReplyDraftTone.Formal, ReplyDraftLength.Medium, "Dear Maya,\n\nThat suits me.\n\nSam"), + ], +}; +``` + +`ReplyDraftSimulation.Generate` 以 `Generator` 的形式流式输出同样的回复,供你自行包装模拟时使用。上方的预览播放的就是一个模拟,其邮件和回复来自图库自己的演示数据。 + +### 结构 + +1. **邮件**:邮件图标、主题与时间;发件人的头像、姓名、地址与收件人;正文缩进在头像下方。 +2. **回复标题**:模型的标记与 "Reply with ...",右侧是状态:一个图标加一个词,模型工作时会闪烁。 +3. **快捷回复**:可切换的标签;正在引导草稿的那个以强调色填充。 +4. **草稿框**:草稿在这里流式出现,完成后变为可编辑的文字。 +5. **语气与长度**:两个带滑动胶囊的小型单选组,中间以细线分隔。 +6. **操作**:键盘提示、Regenerate 与 Insert。 + +### 状态 + +| Status | 草稿框显示 | 状态行 | +|---|---|---| +| `Idle` | 一行闪烁的 "Reading Maya's email",或在 `AutoStart` 关闭时显示 Draft reply | "No draft yet" | +| `Drafting` | 正在流式写出的草稿 | 闪烁的 "Drafting" | +| `Rewriting` | 变暗且只读的旧草稿 | 闪烁的 "Rewriting" | +| `Ready` | 可编辑的草稿 | 对勾与 "Ready to edit" | +| `Inserting` | 草稿;Insert 显示加载圈 | 闪烁的 "Inserting" | +| `Inserted` | 草稿 | 双对勾与 "Inserted" | +| `Failed` | 错误信息与 Try again | 警示图标与 "Couldn't draft" | + +### 使用你自己的模型 + + + +`Generator` 返回由文字增量组成的 `IAsyncEnumerable`。当你的服务一次返回整段回复时,用 `ReplyDraftStreams.FromText` 包装它,卡片会在本地以每块 3 到 8 个词的片段流式显示。请求的 `Mode` 为 `Draft` 表示新的回复,为 `Rewrite` 表示已完成的草稿改变了语气或长度:此时请改写 `Previous`,它保存屏幕上包含读者修改的文字。抛出异常即可显示错误状态。 + +每当一份草稿或改写完成,`DraftCompleted` 就会触发,并带有文字、语气、长度与快捷回复。 + +### 插入 + + + +Insert,或在卡片内任意位置按 Cmd+Enter(Ctrl+Enter),会先引发 `InsertRequested`,再等待 `InsertHandler`,其间按钮上显示加载圈。处理程序抛出异常时保留草稿,并显示 "Couldn't insert"。既没有处理程序,也没有把 `InsertRequested` 标记为已处理的事件处理程序时,插入是模拟的,播报会说明没有发送任何内容。 + +### 按需开始 + +`AutoStart` 会在卡片滚动进入视图时立即写出第一份草稿。将它设为 false 则改为显示一行简短说明和 Draft reply 按钮,只在读者要求时才运行模型。`StartDraft()` 与 `Regenerate()` 在代码中做同样的事。 + +### 窄容器 + + + +卡片填满容器,最宽 600px。低于 480px 时内边距降为 16px,正文不再缩进到头像下方;低于 460px 时两组之间的细线隐藏,Length 移到单独一行。标签会换行,过长的姓名与地址会截断,不会出现横向滚动。触摸按下之后,选项增大到 32px,标签增大到 36px。 + +### 自定义文案 + +`Labels` 包含卡片显示或播报的每个词;替换整个对象即可本地化或改写: + +```csharp +draft.Labels = new ReplyDraftLabels +{ + Ready = "Prêt à modifier", + Insert = "Insérer", + ReplyWith = model => "Répondre avec " + model, +}; +``` + +`SourceMark` 用你自己的 24 × 24 描边标记替换主题前的邮件图标,`AgentModel.Mark` 替换模型的星芒。 + +### 内容准则 + +- 快捷回复的标签用两三个词("Sounds good"、"Next week");它们的 id 是你的提示词能理解的内容。 +- 草稿保持纯文本:草稿框是纯文本编辑器。 +- 在你自己的编辑器中说明插入会做什么;卡片只负责交出文字。 + +### 最佳实践 + +- 把请求的取消令牌传给模型客户端:每个新操作都会中止上一个请求。 +- 把 `Rewrite` 当作对 `Previous` 的改写,这样读者的修改在语气变化后仍然保留。 +- 在产品中关闭 `AutoPlay`;它用于营销页与文档页。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片以发件人命名("Email from Maya Chen");回复区域是一个有名称的区块。 +- 快捷回复是会报告是否按下的切换按钮;语气与长度是单选组,只有选中的选项是 Tab 停靠点。 +- 完成的草稿是名为 "Draft reply" 的纯文本可编辑文本框,读者可以在 Insert 之前修改某个词;Insert 交出的就是修改后的内容。 +- 礼貌型实时区域会播报起草、改写、就绪、已插入与错误;模拟的操作会说明没有发送任何内容。演示自动播放时它保持安静。 +- 状态使用图标加一个短词,从不只靠颜色。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过快捷回复、草稿文字、语气、长度、Regenerate 与 Insert | +| 方向键 | 在 Tone 或 Length 中移动选择并改写草稿 | +| Home、End | 在 Tone 或 Length 中跳到第一个或最后一个选项 | +| Enter、Space | 激活快捷回复、选项或按钮 | +| Cmd+Enter、Ctrl+Enter | 在卡片内任意位置插入已完成的草稿 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 草稿流式出现 | 每 34ms 一块 3 到 8 个词;每块在 140ms 内淡入 | 草稿整体出现 | 草稿整体出现 | +| 语气或长度改写 | 旧草稿变暗,在 100ms 内淡出;新草稿在 160ms 内淡入并上升 2px | 立即 | 立即 | +| 草稿框高度 | 弹簧过渡到新高度(0.26s,无回弹);重新生成期间保持高度直到新草稿完成 | 立即 | 立即 | +| 语气与长度的胶囊 | 以同样的无回弹弹簧滑动 | 直接跳到位 | 直接跳到位 | +| 状态 | 交叉淡化(淡出 100ms,淡入 160ms);工作中的词会闪烁 | 交叉淡化,不闪烁 | 立即 | +| 按下标签或选项 | 缩小到 0.97 | 不缩放 | 不缩放 | + +## 相关组件 + +- `Textarea`:由人自己书写的普通多行输入框。 +- `ToolCall` 与 `AgentStatus`:展示智能体正在做什么。 +- `SegmentedControl`:在少量视图之间切换。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/zh-TW.md new file mode 100644 index 00000000..87148942 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ReplyDraft/zh-TW.md @@ -0,0 +1,208 @@ +--- +title: Reply draft +summary: 收到的郵件下方的 AI 回覆起草器:快速回覆引導一份新的串流草稿,語氣與長度以一次交叉淡化改寫它,讀者在插入前可以編輯。 +source-hash: 19e28ee0c566 +--- + + + +## 何時使用 + +`ReplyDraft` 顯示一封收到的郵件,並在其下方提供 AI 回覆。模型以快速、柔和淡入的片段串流寫出草稿;快速回覆標籤引導一份新的草稿,語氣與長度兩組選項以一次交叉淡化改寫已完成的草稿,讀者可以在按下 Insert 之前就地修改某個字詞。Insert 把文字(包含讀者的修改)交給你的編輯器:卡片本身從不寄出任何內容。 + +- **用模型起草回覆**:回覆郵件、工單或訊息,由人審閱並編輯後再寄出。 +- **提供意圖以及語氣與長度選項**:用於收件匣、客服台或 CRM 中產生的文字。 +- **在登陸頁展示 AI 寫作功能**:一個會自動播放的逼真範例。 + +## 何時不用 + +- **與模型自由對話**:使用聊天串與輸入框;這裡的草稿只回覆一則訊息。 +- **輸入時的行內自動完成**:在游標處使用幽靈文字,而不是獨立的草稿框。 +- **寄出郵件**:此卡片只插入草稿,寄出由你的編輯器負責。 + +## 用法 + +把訊息、你的快速回覆以及一個串流輸出模型回覆的產生器交給卡片。每個新操作(標籤、語氣或長度的變更、Regenerate)都會取消上一個請求的權杖,所以請把它傳給你的用戶端。 + +```csharp +draft.Message = new ReplyDraftMessage +{ + FromName = email.From.Name, + FromEmail = email.From.Address, + Subject = email.Subject, + Body = email.PlainText, // blank lines split paragraphs + ReceivedAt = email.Received, +}; +draft.Suggestions = [new ReplyDraftSuggestion("yes", "Sounds good"), new ReplyDraftSuggestion("later", "Next week")]; +draft.Model = new AgentModel("Mistral Medium 3.5"); +draft.Generator = request => replies.StreamAsync(email.Id, request.Tone, request.Length, + request.Suggestion?.Id, request.Mode, request.Previous, request.CancellationToken); +draft.InsertHandler = async args => await composer.InsertAsync(args.Draft); +``` + +你自己的郵件如果沒有 `Generator`,就無法起草:草稿框顯示提示行,「Draft reply」按鈕處於關閉狀態。既沒有 `Message` 也沒有 `Simulation` 時,卡片是空的:一封空白郵件,沒有快速回覆,顯示「No reply drafted yet.」,「Draft reply」按鈕處於關閉狀態。 + +### 模擬 + +想在沒有模型的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `ReplyDraftSimulation`:郵件、快速回覆、要標示的模型,以及你為每個快速回覆、語氣和長度寫好的回覆(`ReplyDraftVersion`)。卡片會在短暫的閱讀之後串流輸出相符的回覆,說明這是模擬、沒有寄出任何內容,並循環示範語氣、長度、第二個快速回覆和插入,直到有人在卡片內按下或輸入。你自己的 `Message` 優先於模擬;把 `Simulation` 設為 null 會停止模擬並顯示空卡片。在產品中請關閉 `AutoPlay`。 + +```csharp +draft.Simulation = new ReplyDraftSimulation +{ + Message = email, + Suggestions = [new ReplyDraftSuggestion("yes", "Sounds good")], + Model = new AgentModel("Mistral Medium 3.5"), + Replies = + [ + new ReplyDraftVersion("yes", ReplyDraftTone.Friendly, ReplyDraftLength.Medium, "Hi Maya,\n\nSounds good!\n\nSam"), + new ReplyDraftVersion("yes", ReplyDraftTone.Formal, ReplyDraftLength.Medium, "Dear Maya,\n\nThat suits me.\n\nSam"), + ], +}; +``` + +`ReplyDraftSimulation.Generate` 以 `Generator` 的形式串流輸出同樣的回覆,供你自行包裝模擬時使用。上方的預覽播放的就是一個模擬,其郵件和回覆來自圖庫自己的示範資料。 + +### 結構 + +1. **郵件**:郵件圖示、主旨與時間;寄件人的頭像、姓名、地址與收件人;內文縮排在頭像下方。 +2. **回覆標題**:模型的標記與 "Reply with ...",右側是狀態:一個圖示加一個詞,模型運作時會閃爍。 +3. **快速回覆**:可切換的標籤;正在引導草稿的那個以強調色填滿。 +4. **草稿框**:草稿在這裡串流出現,完成後變為可編輯的文字。 +5. **語氣與長度**:兩個帶滑動膠囊的小型單選群組,中間以細線分隔。 +6. **操作**:鍵盤提示、Regenerate 與 Insert。 + +### 狀態 + +| Status | 草稿框顯示 | 狀態列 | +|---|---|---| +| `Idle` | 一行閃爍的 "Reading Maya's email",或在 `AutoStart` 關閉時顯示 Draft reply | "No draft yet" | +| `Drafting` | 正在串流寫出的草稿 | 閃爍的 "Drafting" | +| `Rewriting` | 變暗且唯讀的舊草稿 | 閃爍的 "Rewriting" | +| `Ready` | 可編輯的草稿 | 勾號與 "Ready to edit" | +| `Inserting` | 草稿;Insert 顯示載入圈 | 閃爍的 "Inserting" | +| `Inserted` | 草稿 | 雙勾號與 "Inserted" | +| `Failed` | 錯誤訊息與 Try again | 警示圖示與 "Couldn't draft" | + +### 使用你自己的模型 + + + +`Generator` 傳回由文字增量組成的 `IAsyncEnumerable`。當你的服務一次傳回整段回覆時,用 `ReplyDraftStreams.FromText` 包裝它,卡片會在本機以每塊 3 到 8 個詞的片段串流顯示。請求的 `Mode` 為 `Draft` 表示新的回覆,為 `Rewrite` 表示已完成的草稿變更了語氣或長度:此時請改寫 `Previous`,它保存螢幕上包含讀者修改的文字。擲回例外即可顯示錯誤狀態。 + +每當一份草稿或改寫完成,`DraftCompleted` 就會觸發,並帶有文字、語氣、長度與快速回覆。 + +### 插入 + + + +Insert,或在卡片內任意位置按 Cmd+Enter(Ctrl+Enter),會先引發 `InsertRequested`,再等待 `InsertHandler`,其間按鈕上顯示載入圈。處理常式擲回例外時保留草稿,並顯示 "Couldn't insert"。既沒有處理常式,也沒有把 `InsertRequested` 標記為已處理的事件處理常式時,插入是模擬的,播報會說明沒有寄出任何內容。 + +### 依需求開始 + +`AutoStart` 會在卡片捲動進入檢視時立即寫出第一份草稿。將它設為 false 則改為顯示一行簡短說明和 Draft reply 按鈕,只在讀者要求時才執行模型。`StartDraft()` 與 `Regenerate()` 在程式碼中做同樣的事。 + +### 窄容器 + + + +卡片填滿容器,最寬 600px。低於 480px 時內距降為 16px,內文不再縮排到頭像下方;低於 460px 時兩組之間的細線隱藏,Length 移到單獨一行。標籤會換行,過長的姓名與地址會截斷,不會出現水平捲動。觸控按下之後,選項增大到 32px,標籤增大到 36px。 + +### 自訂文案 + +`Labels` 包含卡片顯示或播報的每個詞;替換整個物件即可在地化或改寫: + +```csharp +draft.Labels = new ReplyDraftLabels +{ + Ready = "Prêt à modifier", + Insert = "Insérer", + ReplyWith = model => "Répondre avec " + model, +}; +``` + +`SourceMark` 用你自己的 24 × 24 描邊標記取代主旨前的郵件圖示,`AgentModel.Mark` 取代模型的星芒。 + +### 內容準則 + +- 快速回覆的標籤用兩三個詞("Sounds good"、"Next week");它們的 id 是你的提示詞能理解的內容。 +- 草稿保持純文字:草稿框是純文字編輯器。 +- 在你自己的編輯器中說明插入會做什麼;卡片只負責交出文字。 + +### 最佳做法 + +- 把請求的取消權杖傳給模型用戶端:每個新操作都會中止上一個請求。 +- 把 `Rewrite` 當作對 `Previous` 的改寫,這樣讀者的修改在語氣變更後仍然保留。 +- 在產品中關閉 `AutoPlay`;它用於行銷頁與文件頁。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片以寄件人命名("Email from Maya Chen");回覆區域是一個有名稱的區塊。 +- 快速回覆是會回報是否按下的切換按鈕;語氣與長度是單選群組,只有選取的選項是 Tab 停駐點。 +- 完成的草稿是名為 "Draft reply" 的純文字可編輯文字方塊,讀者可以在 Insert 之前修改某個字詞;Insert 交出的就是修改後的內容。 +- 禮貌型即時區域會播報起草、改寫、就緒、已插入與錯誤;模擬的操作會說明沒有寄出任何內容。示範自動播放時它保持安靜。 +- 狀態使用圖示加一個短詞,從不只靠顏色。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過快速回覆、草稿文字、語氣、長度、Regenerate 與 Insert | +| 方向鍵 | 在 Tone 或 Length 中移動選取並改寫草稿 | +| Home、End | 在 Tone 或 Length 中跳到第一個或最後一個選項 | +| Enter、Space | 啟用快速回覆、選項或按鈕 | +| Cmd+Enter、Ctrl+Enter | 在卡片內任意位置插入已完成的草稿 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 草稿串流出現 | 每 34ms 一塊 3 到 8 個詞;每塊在 140ms 內淡入 | 草稿整體出現 | 草稿整體出現 | +| 語氣或長度改寫 | 舊草稿變暗,在 100ms 內淡出;新草稿在 160ms 內淡入並上升 2px | 立即 | 立即 | +| 草稿框高度 | 彈簧過渡到新高度(0.26s,無回彈);重新產生期間保持高度直到新草稿完成 | 立即 | 立即 | +| 語氣與長度的膠囊 | 以同樣的無回彈彈簧滑動 | 直接跳到位 | 直接跳到位 | +| 狀態 | 交叉淡化(淡出 100ms,淡入 160ms);運作中的詞會閃爍 | 交叉淡化,不閃爍 | 立即 | +| 按下標籤或選項 | 縮小到 0.97 | 不縮放 | 不縮放 | + +## 相關元件 + +- `Textarea`:由人自己書寫的一般多行輸入框。 +- `ToolCall` 與 `AgentStatus`:呈現智慧代理正在做什麼。 +- `SegmentedControl`:在少量檢視之間切換。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/en-US.md new file mode 100644 index 00000000..a547d164 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/en-US.md @@ -0,0 +1,198 @@ +--- +title: Response feedback +summary: A feedback control for AI replies: thumbs down opens an inline panel with reason chips and a note, thumbs up asks what was good, and a submit shows a short thank-you before folding back into the row. +--- + + + +## When to use + +`ResponseFeedback` is a whole AI reply card: the person's message in a bubble, the model's mark and name, the reply and an +action row with copy, an optional regenerate and the thumbs. `FeedbackActions` is the same control on its own, for message +rows you already render. A thumbs down opens an inline panel with reason chips and an optional note; a thumbs up fills and +asks what was good; a submit shows a short thank-you, then the panel folds back into the row. + +- **Thumbs up and down on AI replies** in a chat, a copilot or a search answer. +- **Structured reasons** (inaccurate, unsafe) for model evaluation or support triage. +- **A message action row** with copy and regenerate next to the feedback. + +## When not to use + +- **Star or numeric ratings** of a product or a session: use a rating control. +- **Emoji reactions** to a post in a thread: use `Reactions`. +- **General product feedback** that is not about one reply: use a feedback form. +- **Long surveys** with many questions: use a form or a dialog. + +## Usage + +With no data `ResponseFeedback` shows the frame alone: the model's mark, an empty reply and the action row, whose +feedback is simulated until you set a `FeedbackHandler`. Give it your reply, the model's name, the question it answers +and the text to copy, and pass `MessageId` so every submission can be stored against the message: + +```xml + + + +``` + +`Prompt`, `ModelName` and `CopyText` are null by default: the question, the name and copy stay hidden until you set +them. `ModelMark` takes a 24 × 24 stroked glyph drawn at 16px (a neutral sparkle by default). + +### Your own message rows + + + +`FeedbackActions` is the row on its own. `Leading` holds actions before the thumbs (use 16px icons in small 36px buttons, +such as an icon-only `CopyButton`) and `Trailing` is pinned to the end of the row, for a time or a token count. + +### Anatomy + +1. **Thumbs**: two 36px round toggle buttons. The active one fills on top of its outline, so nothing moves. +2. **Panel**: after a thumbs down, "What went wrong?" with a close button, the reason chips, the note and a footer with the + privacy note beside Cancel and Submit. After a thumbs up, the lighter "What was good?" row with Skip and Send. +3. **Thank-you**: a tick that draws once, "Thanks for your feedback" and a second line; it holds for `ThanksDuration` + (1.6s), then the panel folds away. +4. **Error line**: an icon and a short message next to the actions; Submit becomes Try again. A rating that could not be + saved shows the same line in the row with its own Try again. + +### Saving feedback + + + +Set `FeedbackHandler`. It is called with a `Rating` submission on every thumb press and a `Details` one when reasons or a +note are submitted. Throw to show the error and Try again; the token is cancelled when the panel closes or the rating +changes, so pass it on to your request: + +```csharp +reply.FeedbackHandler = async (feedback, cancellation) => +{ + using var response = await http.PostAsJsonAsync("/api/feedback", new + { + feedback.MessageId, + stage = feedback.Stage.ToString(), + rating = feedback.Rating.ToString(), + feedback.Reasons, + feedback.Comment, + }, cancellation); + response.EnsureSuccessStatusCode(); +}; +``` + +Store the rating on the `Rating` stage (`None` means the person cleared it) and attach the reasons and the note on the +`Details` stage. Without a handler the sends are simulated: the thank-you line and the announcement say nothing was sent. +`FeedbackSubmitted` is raised for every submission either way, and `RatingChanged` for every thumb press. + +`Rating` is two-way: set it to show a rating loaded from your database, or bind it to own the state. A rating set from +outside checks the thumb without opening the panel or sending anything. + +### Reasons and the note + + + +`NegativeReasons` and `PositiveReasons` are the chips. An empty `PositiveReasons` skips the follow-up row after a thumbs +up. A chip labelled Other moves focus to the note and changes its placeholder. `AllowComment="False"` drops the note, +`CommentPlaceholder` and `CommentMaxLength` (1000) shape it, and `PrivacyNote` (null hides it) is the quiet line beside +Submit. + +### While the reply streams + + + +Set `IsRatingEnabled="False"` while the reply is still streaming, so people rate the finished answer. + +### Your own words + +`Labels` holds every word the control shows or announces. Replace the object to localize or reword: + +```csharp +reply.Labels = new ResponseFeedbackLabels +{ + GoodResponse = "Bonne réponse", + BadResponse = "Mauvaise réponse", + NegativeTitle = "Qu'est-ce qui n'allait pas ?", + Submit = "Envoyer", +}; +``` + +### Narrow containers + +The card pads 16px below 480px of width and 24px above, and the chips wrap, so it holds at 360px without scrolling. Below +400px the privacy note and Cancel give way (Close covers Cancel), so the error line and Try again always fit on one row. +Chips grow from 28 to 32px tall after a touch. + +### Content guidelines + +- Keep reasons to one or two words ("Too long", "Unsafe") so the chips stay on one or two lines at 360px. +- Say what is shared in the privacy note, in a few words. +- The thank-you line thanks the person and says what happens next; it does not ask for more. + +### Best practices + +- Use `ResponseFeedback` for a single assistant reply with its actions, and `FeedbackActions` when you render your own + messages. +- Always pass `MessageId` and persist both stages. +- Throw from the handler to show the error; the control handles Try again and aborts stale calls through the token. + +### Common questions + +**Can people change their mind?** Yes. Pressing the other thumb switches the rating and the panel; pressing the active +thumb clears it. Every press is sent. + +**What does Escape do?** It closes the open panel and returns focus to the thumb that opened it. The rating stays. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- The thumbs are toggle buttons that report pressed, named "Good response" and "Bad response", with the same tooltips. +- Reason chips are toggle buttons inside a group named by the panel's title. Selected chips invert to a solid fill, so the + state never relies on hue. +- A keyboard press on thumbs down moves focus to the first reason; a pointer press leaves focus where it is. After a + keyboard submit, focus lands on the thank-you line and returns to the thumb when the panel folds. +- A polite live region announces rating changes, sending, the thank-you state and errors, and says when nothing was sent + because the send is simulated. +- Errors show an icon and text next to the actions, never color alone. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through copy, regenerate, thumbs up and thumbs down, then into the open panel | +| Enter, Space | Presses a thumb, toggles a reason chip, or activates Submit, Send, Skip and Cancel. Pressing the active thumb clears the rating | +| Escape | Closes the open panel and returns focus to the thumb that opened it. The rating stays | +| Cmd + Enter, Ctrl + Enter | Submits from the note field | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Panel opens or closes | The measured height springs (0.24s, no bounce) while the panel fades over 160ms | Height cuts at once, the panel fades over 160ms | At once | +| Form to thank-you | One crossfade: out 100ms, in 160ms rising 2px, while the height springs to the new content | 120ms crossfade, height cuts | At once | +| Thumb fills | The filled glyph fades in over 160ms on top of the outline; nothing moves or resizes | Same fade | At once | +| Press | Scales to 0.94 at once and springs back on release | No scale | No scale | +| Success tick | Draws once in 280ms | Shown at once | Shown at once | + +The thumbs never change size and the row stretches to its container, so the row's width does not move as a thumb fills or +the panel opens. Every end state and announcement is the same at every motion level. + +## Related + +- `ReasoningBlock` for the model's thinking above the reply. +- `ToolCall` for the tools an agent called. +- `CopyButton` for the copy action in your own rows. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/zh-CN.md new file mode 100644 index 00000000..a6d11c58 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/zh-CN.md @@ -0,0 +1,173 @@ +--- +title: Response feedback +summary: 面向 AI 回复的反馈控件:点踩会在行内展开一个带原因标签和备注的面板,点赞会询问哪里做得好,提交后短暂显示感谢,随后收回到操作行中。 +source-hash: 7d49fffaded3 +--- + + + +## 何时使用 + +`ResponseFeedback` 是一整张 AI 回复卡片:气泡中的用户消息、模型的标志与名称、回复内容,以及包含复制、可选的重新生成和点赞点踩的操作行。`FeedbackActions` 是同一控件的单独版本,用于你已经自己渲染的消息行。点踩会在行内展开一个面板,其中有原因标签和可选的备注;点赞会填充图标并询问哪里做得好;提交后显示简短的感谢,然后面板收回到操作行中。 + +- **对 AI 回复点赞或点踩**:聊天、Copilot 或搜索答案中。 +- **结构化的原因**(不准确、不安全),用于模型评估或客服分流。 +- **消息操作行**:在反馈旁边提供复制和重新生成。 + +## 何时不用 + +- **对产品或会话的星级或数字评分**:使用评分控件。 +- **对帖子的表情回应**:使用 `Reactions`。 +- **与单条回复无关的一般产品反馈**:使用反馈表单。 +- **包含许多问题的长问卷**:使用表单或对话框。 + +## 用法 + +没有数据时,`ResponseFeedback` 只显示框架:模型标识、空的回复和操作行;在你设置 `FeedbackHandler` 之前,反馈为模拟发送。传入你的回复、模型名称、它所回答的问题和要复制的文本,并传入 `MessageId`,以便每次提交都能与该消息关联存储: + +```xml + + + +``` + +`Prompt`、`ModelName` 和 `CopyText` 默认为 null:在你设置之前,问题、名称和复制都保持隐藏。`ModelMark` 接受一个 24 × 24 的描边图形,以 16px 绘制(默认是一个中性的闪光图标)。 + +### 你自己的消息行 + + + +`FeedbackActions` 就是单独的操作行。`Leading` 放在点赞点踩之前的操作(在 36px 的小按钮中使用 16px 图标,例如仅图标的 `CopyButton`),`Trailing` 固定在行尾,用于显示时间或令牌数。 + +### 结构 + +1. **点赞与点踩**:两个 36px 的圆形切换按钮。激活的那个会在轮廓之上填充,因此没有任何东西移动。 +2. **面板**:点踩后显示 "What went wrong?" 和关闭按钮、原因标签、备注,以及底部的隐私说明与 Cancel、Submit 按钮。点赞后显示更轻量的 "What was good?" 行,带有 Skip 和 Send。 +3. **感谢**:一个只绘制一次的对勾、"Thanks for your feedback" 和第二行文字;停留 `ThanksDuration`(1.6 秒)后面板收起。 +4. **错误行**:操作按钮旁的图标与简短消息;Submit 变为 Try again。评分保存失败时,操作行中会显示同样的错误行,并带有它自己的 Try again。 + +### 保存反馈 + + + +设置 `FeedbackHandler`。每次按下点赞或点踩时,它都会收到一个 `Rating` 提交;提交原因或备注时,会收到一个 `Details` 提交。抛出异常即可显示错误和 Try again;面板关闭或评分改变时令牌会被取消,所以请把它传给你的请求: + +```csharp +reply.FeedbackHandler = async (feedback, cancellation) => +{ + using var response = await http.PostAsJsonAsync("/api/feedback", new + { + feedback.MessageId, + stage = feedback.Stage.ToString(), + rating = feedback.Rating.ToString(), + feedback.Reasons, + feedback.Comment, + }, cancellation); + response.EnsureSuccessStatusCode(); +}; +``` + +在 `Rating` 阶段存储评分(`None` 表示用户清除了评分),在 `Details` 阶段附加原因和备注。没有处理程序时发送是模拟的:感谢行和播报都会说明没有发送任何内容。无论哪种情况,每次提交都会引发 `FeedbackSubmitted`,每次按下点赞或点踩都会引发 `RatingChanged`。 + +`Rating` 是双向的:设置它可以显示从数据库读取的评分,绑定它则可以自己掌管状态。从外部设置的评分只会勾选对应按钮,不会打开面板,也不会发送任何内容。 + +### 原因与备注 + + + +`NegativeReasons` 和 `PositiveReasons` 是原因标签。`PositiveReasons` 为空时,点赞后不显示后续行。标为 Other 的标签会把焦点移到备注并更换其占位文字。`AllowComment="False"` 去掉备注,`CommentPlaceholder` 与 `CommentMaxLength`(1000)决定它的样子,`PrivacyNote`(null 时隐藏)是 Submit 旁边那行低调的说明。 + +### 回复仍在流式输出时 + + + +回复仍在流式输出时设置 `IsRatingEnabled="False"`,让人们评价的是完成后的答案。 + +### 你自己的措辞 + +`Labels` 包含控件显示或播报的每一个词。替换整个对象即可本地化或改写: + +```csharp +reply.Labels = new ResponseFeedbackLabels +{ + GoodResponse = "Bonne réponse", + BadResponse = "Mauvaise réponse", + NegativeTitle = "Qu'est-ce qui n'allait pas ?", + Submit = "Envoyer", +}; +``` + +### 窄容器 + +宽度低于 480px 时卡片内边距为 16px,以上为 24px;原因标签会换行,因此在 360px 下也不需要横向滚动。宽度低于 400px 时,隐私说明和 Cancel 会让出位置(Close 代替 Cancel),因此错误行和 Try again 总能放在同一行。触摸操作之后,原因标签的高度从 28px 增加到 32px。 + +### 内容准则 + +- 原因保持一两个词("Too long"、"Unsafe"),这样在 360px 下标签只占一两行。 +- 在隐私说明中用几个词说明会分享什么。 +- 感谢行感谢用户并说明接下来会发生什么,不再索取更多内容。 + +### 最佳实践 + +- 单条助手回复及其操作使用 `ResponseFeedback`;自己渲染消息时使用 `FeedbackActions`。 +- 始终传入 `MessageId`,并持久化两个阶段。 +- 在处理程序中抛出异常以显示错误;控件会处理 Try again,并通过令牌中止过期的调用。 + +### 常见问题 + +**用户可以改变主意吗?** 可以。按下另一个按钮会切换评分和面板;再次按下当前激活的按钮会清除评分。每次按下都会发送。 + +**Escape 有什么作用?** 它会关闭打开的面板,并把焦点还给打开它的那个按钮。评分保持不变。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 点赞和点踩是会报告按下状态的切换按钮,名称为 "Good response" 和 "Bad response",工具提示相同。 +- 原因标签是切换按钮,位于以面板标题命名的分组中。选中的标签会反转为实心填充,因此状态从不依赖色相。 +- 用键盘按下点踩会把焦点移到第一个原因;用指针按下则保持焦点不变。用键盘提交后,焦点落在感谢行上,面板收起时回到按钮。 +- 一个礼貌(polite)的实时区域播报评分变化、发送中、感谢状态和错误,并在发送为模拟时说明没有发送任何内容。 +- 错误显示在操作按钮旁,带有图标和文字,从不只靠颜色。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过复制、重新生成、点赞和点踩,然后进入打开的面板 | +| Enter、Space | 按下点赞或点踩、切换原因标签,或激活 Submit、Send、Skip 和 Cancel。再次按下当前激活的按钮会清除评分 | +| Escape | 关闭打开的面板,并把焦点还给打开它的按钮。评分保持不变 | +| Cmd + Enter、Ctrl + Enter | 在备注框中提交 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 面板打开或关闭 | 测得的高度以弹簧变化(0.24 秒,无回弹),同时面板在 160ms 内淡入淡出 | 高度立即变化,面板在 160ms 内淡入淡出 | 立即 | +| 表单切换为感谢 | 一次交叉淡化:旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px,同时高度以弹簧过渡到新内容 | 120ms 交叉淡化,高度立即变化 | 立即 | +| 按钮填充 | 实心图形在轮廓之上 160ms 淡入;没有任何东西移动或改变尺寸 | 同样的淡入 | 立即 | +| 按下 | 立即缩放到 0.94,松开时以弹簧恢复 | 不缩放 | 不缩放 | +| 成功对勾 | 在 280ms 内绘制一次 | 立即显示 | 立即显示 | + +按钮的尺寸从不改变,操作行随容器拉伸,因此按钮填充或面板打开时行宽不会变化。所有结束状态和播报在每个动效等级下都相同。 + +## 相关组件 + +- `ReasoningBlock`:回复上方模型的思考过程。 +- `ToolCall`:智能体调用的工具。 +- `CopyButton`:在你自己的消息行中提供复制操作。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/zh-TW.md new file mode 100644 index 00000000..2674a450 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ResponseFeedback/zh-TW.md @@ -0,0 +1,173 @@ +--- +title: Response feedback +summary: 針對 AI 回覆的意見回饋控制項:按倒讚會在行內展開一個附原因標籤與備註的面板,按讚會詢問哪裡做得好,送出後短暫顯示感謝,隨後收回到操作列中。 +source-hash: 7d49fffaded3 +--- + + + +## 何時使用 + +`ResponseFeedback` 是一整張 AI 回覆卡片:泡泡中的使用者訊息、模型的標誌與名稱、回覆內容,以及包含複製、可選的重新產生和讚與倒讚的操作列。`FeedbackActions` 是同一個控制項的單獨版本,用於你已經自行繪製的訊息列。按倒讚會在行內展開一個面板,其中有原因標籤和可選的備註;按讚會填滿圖示並詢問哪裡做得好;送出後顯示簡短的感謝,接著面板收回到操作列中。 + +- **對 AI 回覆按讚或倒讚**:聊天、Copilot 或搜尋答案中。 +- **結構化的原因**(不正確、不安全),用於模型評估或客服分流。 +- **訊息操作列**:在意見回饋旁提供複製與重新產生。 + +## 何時不用 + +- **對產品或工作階段的星級或數字評分**:使用評分控制項。 +- **對貼文的表情回應**:使用 `Reactions`。 +- **與單則回覆無關的一般產品意見**:使用意見回饋表單。 +- **包含許多問題的長問卷**:使用表單或對話框。 + +## 用法 + +沒有資料時,`ResponseFeedback` 只顯示框架:模型標誌、空白的回覆與操作列;在你設定 `FeedbackHandler` 之前,意見回饋為模擬傳送。傳入你的回覆、模型名稱、它所回答的問題與要複製的文字,並傳入 `MessageId`,讓每次送出都能與該訊息一起儲存: + +```xml + + + +``` + +`Prompt`、`ModelName` 與 `CopyText` 預設為 null:在你設定之前,問題、名稱與複製都保持隱藏。`ModelMark` 接受一個 24 × 24 的描邊圖形,以 16px 繪製(預設為中性的閃光圖示)。 + +### 你自己的訊息列 + + + +`FeedbackActions` 就是單獨的操作列。`Leading` 放在讚與倒讚之前的操作(在 36px 的小按鈕中使用 16px 圖示,例如僅圖示的 `CopyButton`),`Trailing` 固定在列尾,用來顯示時間或權杖數。 + +### 結構 + +1. **讚與倒讚**:兩個 36px 的圓形切換按鈕。啟用的那個會在外框之上填滿,因此沒有任何東西移動。 +2. **面板**:按倒讚後顯示 "What went wrong?" 與關閉按鈕、原因標籤、備註,以及底部的隱私說明與 Cancel、Submit 按鈕。按讚後顯示較輕量的 "What was good?" 列,附 Skip 與 Send。 +3. **感謝**:一個只繪製一次的勾號、"Thanks for your feedback" 與第二行文字;停留 `ThanksDuration`(1.6 秒)後面板收起。 +4. **錯誤列**:操作按鈕旁的圖示與簡短訊息;Submit 會變成 Try again。評分儲存失敗時,操作列中會顯示同樣的錯誤列,並附有它自己的 Try again。 + +### 儲存意見回饋 + + + +設定 `FeedbackHandler`。每次按下讚或倒讚時,它都會收到一筆 `Rating` 送出;送出原因或備註時,會收到一筆 `Details` 送出。擲回例外即可顯示錯誤與 Try again;面板關閉或評分改變時權杖會被取消,所以請把它傳給你的請求: + +```csharp +reply.FeedbackHandler = async (feedback, cancellation) => +{ + using var response = await http.PostAsJsonAsync("/api/feedback", new + { + feedback.MessageId, + stage = feedback.Stage.ToString(), + rating = feedback.Rating.ToString(), + feedback.Reasons, + feedback.Comment, + }, cancellation); + response.EnsureSuccessStatusCode(); +}; +``` + +在 `Rating` 階段儲存評分(`None` 表示使用者清除了評分),在 `Details` 階段附加原因與備註。沒有處理常式時傳送是模擬的:感謝列與播報都會說明沒有傳送任何內容。無論哪種情況,每次送出都會引發 `FeedbackSubmitted`,每次按下讚或倒讚都會引發 `RatingChanged`。 + +`Rating` 是雙向的:設定它可以顯示從資料庫讀取的評分,繫結它則可以自行掌管狀態。從外部設定的評分只會勾選對應按鈕,不會開啟面板,也不會傳送任何內容。 + +### 原因與備註 + + + +`NegativeReasons` 與 `PositiveReasons` 是原因標籤。`PositiveReasons` 為空時,按讚後不顯示後續列。標為 Other 的標籤會把焦點移到備註並更換其預留位置文字。`AllowComment="False"` 會移除備註,`CommentPlaceholder` 與 `CommentMaxLength`(1000)決定它的樣子,`PrivacyNote`(null 時隱藏)是 Submit 旁那行低調的說明。 + +### 回覆仍在串流時 + + + +回覆仍在串流時設定 `IsRatingEnabled="False"`,讓使用者評價的是完成後的答案。 + +### 你自己的用語 + +`Labels` 包含控制項顯示或播報的每一個字詞。替換整個物件即可在地化或改寫: + +```csharp +reply.Labels = new ResponseFeedbackLabels +{ + GoodResponse = "Bonne réponse", + BadResponse = "Mauvaise réponse", + NegativeTitle = "Qu'est-ce qui n'allait pas ?", + Submit = "Envoyer", +}; +``` + +### 窄容器 + +寬度低於 480px 時卡片內距為 16px,以上為 24px;原因標籤會換行,因此在 360px 下也不需要水平捲動。寬度低於 400px 時,隱私說明與 Cancel 會讓出位置(Close 代替 Cancel),因此錯誤列與 Try again 一定能放在同一列。觸控操作之後,原因標籤的高度從 28px 增加到 32px。 + +### 內容準則 + +- 原因保持一兩個詞("Too long"、"Unsafe"),這樣在 360px 下標籤只佔一兩行。 +- 在隱私說明中用幾個字說明會分享什麼。 +- 感謝列感謝使用者並說明接下來會發生什麼,不再索取更多內容。 + +### 最佳做法 + +- 單則助理回覆及其操作使用 `ResponseFeedback`;自行繪製訊息時使用 `FeedbackActions`。 +- 一律傳入 `MessageId`,並保存兩個階段。 +- 在處理常式中擲回例外以顯示錯誤;控制項會處理 Try again,並透過權杖中止過期的呼叫。 + +### 常見問題 + +**使用者可以改變心意嗎?** 可以。按下另一個按鈕會切換評分與面板;再次按下目前啟用的按鈕會清除評分。每次按下都會傳送。 + +**Escape 有什麼作用?** 它會關閉開啟的面板,並把焦點交還給開啟它的那個按鈕。評分保持不變。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 讚與倒讚是會回報按下狀態的切換按鈕,名稱為 "Good response" 與 "Bad response",工具提示相同。 +- 原因標籤是切換按鈕,位於以面板標題命名的群組中。選取的標籤會反轉為實心填滿,因此狀態從不依賴色相。 +- 以鍵盤按下倒讚會把焦點移到第一個原因;以指標按下則焦點維持不變。以鍵盤送出後,焦點落在感謝列上,面板收起時回到按鈕。 +- 一個禮貌(polite)的即時區域會播報評分變化、傳送中、感謝狀態與錯誤,並在傳送為模擬時說明沒有傳送任何內容。 +- 錯誤顯示在操作按鈕旁,附有圖示與文字,從不只靠顏色。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過複製、重新產生、讚與倒讚,然後進入開啟的面板 | +| Enter、Space | 按下讚或倒讚、切換原因標籤,或啟用 Submit、Send、Skip 與 Cancel。再次按下目前啟用的按鈕會清除評分 | +| Escape | 關閉開啟的面板,並把焦點交還給開啟它的按鈕。評分保持不變 | +| Cmd + Enter、Ctrl + Enter | 在備註欄中送出 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 面板開啟或關閉 | 量測到的高度以彈簧變化(0.24 秒,無回彈),同時面板在 160ms 內淡入淡出 | 高度立即變化,面板在 160ms 內淡入淡出 | 立即 | +| 表單切換為感謝 | 一次交叉淡化:舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px,同時高度以彈簧過渡到新內容 | 120ms 交叉淡化,高度立即變化 | 立即 | +| 按鈕填滿 | 實心圖形在外框之上 160ms 淡入;沒有任何東西移動或改變尺寸 | 同樣的淡入 | 立即 | +| 按下 | 立即縮放到 0.94,放開時以彈簧恢復 | 不縮放 | 不縮放 | +| 成功勾號 | 在 280ms 內繪製一次 | 立即顯示 | 立即顯示 | + +按鈕的尺寸從不改變,操作列隨容器延展,因此按鈕填滿或面板開啟時列寬不會變化。所有結束狀態與播報在每個動態效果等級下都相同。 + +## 相關元件 + +- `ReasoningBlock`:回覆上方模型的思考過程。 +- `ToolCall`:代理程式呼叫的工具。 +- `CopyButton`:在你自己的訊息列中提供複製操作。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/en-US.md new file mode 100644 index 00000000..b2db07d9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/en-US.md @@ -0,0 +1,193 @@ +--- +title: Sketch to image +summary: An image generation visual that paints the way an artist does: pencil strokes sketch the scene, a round brush blocks in flat colors region by region, grain and warm light refine it, and it resolves into the finished picture. +--- + + + +## When to use + +`SketchToImage` is a waiting state for image generation that shows the picture taking shape instead of a spinner. +Pencil lines sketch the scene stroke by stroke while a pen tip lifts and travels between strokes, a round brush sweeps +flat colors in region by region from back to front, soft detail passes, film grain and a warm light refine it, and it +resolves into the finished picture. Under the canvas a stage label with an icon sits over the prompt, beside a thin +progress line, a percent and a Replay button. + +- **An image canvas** while the first result renders. +- **A chat reply that returns an image**, in place of the image until it is ready. +- **A landing or onboarding section** that explains text to image or sketch to image generation. + +## When not to use + +- **Text or code generation**: use a streaming reply or a shimmering label. +- **Long batch jobs with many images**: use a progress list with thumbnails. +- **Real intermediate frames from the model**: show those frames instead of a simulated painting. +- **Exact progress**: the painting is paced by `Duration`, not by your backend, so do not use it where the percent must be true. + +## Usage + +Give the card a `Scene` (what it draws, below) and a `Prompt`. With no `Status` it plays the whole pass on its own, holds +the picture for about two seconds, fades back to blank paper and paints again. Nothing is generated: it is a simulated +preview over the scene you give it. Without a `Scene` or an `Image` the card shows its empty state: blank paper, the +Waiting label at 0% and no prompt, and it does not loop (the Empty tab above). The alpine scene on this page is demo data +the Gallery draws in code; it is not part of the library. + +```xml + +``` + +### Following a request + + + +Set `Status` to follow a real generation. Each stage plays to its end and holds there with a shimmering label until the +next one arrives; a later stage travels through the ones in between, and moving back (for example to `Idle` for a new +request) fades the canvas to blank paper first. Showing the card with `Done` shows the picture straight away. Map your +backend's progress like this: + +| Backend | `Status` | +|---|---| +| Queued | `Idle` or `Sketching` | +| Early steps | `Sketching` | +| Middle steps | `Blocking` | +| Late steps | `Refining` | +| Image ready | `Done` | + +```csharp +card.Status = SketchToImageStatus.Blocking; +card.StageChanged += (_, e) => Log($"Now {e.Stage}"); +card.Completed += (_, _) => ShowResult(); +card.ReplayRequested += (_, _) => Regenerate(); +``` + +`Progress` (0 to 1) takes the place of the stage ends when you would rather pass a fraction: sketching ends at 0.38, +blocking at 0.62, refining at 0.88. The painting still travels there at its own pace. + +### Anatomy + +1. **Canvas**: blank paper at 3:2, rounded. Pencil strokes, the brush blocks, two soft passes of the picture, grain, a + warm light and the sharp picture are layered on it. +2. **Progress line**: a 2px line under the canvas in the tone color. +3. **Stage label**: an icon (pen, brush, sparkles, check) and a word that shimmers while the stage is in progress. +4. **Prompt**: one muted line under the label; long prompts end in an ellipsis with the full text in a tooltip. +5. **Percent and Replay**: the percent keeps a fixed width; Replay fades to blank paper and paints again. + +### Stages + +| Stage | Share of `Duration` | What happens | +|---|---|---| +| `Sketching` | 38% | Pencil strokes draw in order, each in time with its length; the pen tip lifts and travels between them | +| `Blocking` | 24% | A round brush sweeps back and forth through each region, back to front; the pencil dims to a guide | +| `Refining` | 26% | Two pre-blurred passes of the picture, a film grain and a warm light fade in; the pencil fades out | +| `Done` | 12% | The sharp picture crossfades in | + +### Sizes + + + +`Size` sets the card's maximum width (340, 520 or 720px) and the pencil's weight so lines stay crisp at every size. The +card fills its container up to that width. `Small` and cards narrower than 400px show only Replay's icon. For a small +indicator in a composer use `Size="Small"` with `ShowsReplay="False"`. + +### Tone + + + +`Tone="Neutral"` draws the pencil, the pen tip, the stage icon and the progress line in the foreground color instead of +the accent. + +### Your own scene + + + +`Scene` sets what is drawn on a 600 × 400 canvas: `Lines` are SVG path strings drawn in order, `Regions` are painted back +to front, each a list of shapes with a flat `Fill` and a finished `Finish` brush and a `Brush` rectangle the round brush +sweeps through, and `Light` is where the warm light comes from. A shape with no `Fill` appears only in the finished +picture (haze, highlights). Trace the lines and regions from your picture so the sketch matches it. + +Set `Image` to resolve into your real result: it fills the canvas in the soft passes and the final crossfade. Keep the +scene of your own as an illustrative placeholder, or trace one from your image. + +```csharp +card.Image = new Bitmap(resultStream); +card.Status = SketchToImageStatus.Done; +``` + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +card.Labels = new SketchToImageLabels +{ + Sketching = "Esquisse", + Blocking = "Aplats de couleur", + Refining = "Finitions", + Done = "Terminé", + Replay = "Rejouer", +}; +``` + +### Reduced motion + +With reduced motion the card shows the end of the current stage, the finished picture when it plays on its own. A +status change or Replay is a short fade between end states with no drawing or travel. With motion off everything changes +at once. + +### Best practices + +- Keep one per view. +- Show the real image after `Done`, or pass it as `Image` so the painting resolves into it. +- Write a prompt people recognize; it names what is being painted. + +## API reference + + + + + + + + + + + +## Accessibility + +- The canvas is one image whose name is the stage and the scene ("Sketching: Layered mountain ridges under a warm evening + sky") and becomes the scene's description when done; without a scene it is the stage alone ("Waiting"). Its drawing + layers are hidden from assistive technology. +- A polite status region says each stage when `Status` is set, and that the preview is simulated. While the card loops + on its own it says once that it is a looping simulated preview and stays quiet. +- The progress line is a read-only progress bar from 0 to 100. +- A stage is never shown by color alone: each pairs an icon with a word. +- Every word is in `SketchToImageLabels`. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the Replay button | +| Enter, Space | On Replay, fades the canvas back to blank paper and paints again | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Pencil strokes | Each draws on an in-out curve, in time with its length; the pen tip follows the head and lifts (shrinks, dims) as it travels to the next | End state | End state | +| Brush blocks | A wide round brush sweeps back and forth through each region, back to front, with a tip in the region's color; the pencil dims to a guide | End state | End state | +| Refining | Two pre-blurred passes, a static grain and a warm light fade in (only their opacity moves), then the sharp picture crossfades in | End state | End state | +| Loop | Holds the picture 2s, fades to blank paper over 520ms while the progress line drains, resets while invisible and paints again | Holds the picture | Holds the picture | +| Status change, Replay | Plays forward, or fades to paper first when moving back | 160ms fade out and in between end states | At once | +| Stage label | Old label fades out in 100ms, new one fades in over 160ms rising 2px; shimmers while in progress | 120ms crossfade, no shimmer | At once | + +One clock drives a single 0 to 1 timeline. A frame redraws the ink layer from cached geometry and sets layer opacities; +it allocates nothing. The clock stops when the card is off screen and while nothing moves; the label changes only with +the stage and the percent only when its rounded value does. + +## Related + +- `ToolCall` for the actions an agent takes while it works. +- `AgentStatus` for the live status of a whole run. +- `SegmentedControl` to switch between a few related views. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/zh-CN.md new file mode 100644 index 00000000..38b4ff7d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/zh-CN.md @@ -0,0 +1,167 @@ +--- +title: Sketch to image +summary: 像画家一样作画的图像生成视觉效果:铅笔线条勾出场景,圆头画笔逐个区域铺上平涂色块,颗粒与暖光加以润色,最后呈现完成的画面。 +source-hash: 6727bfbb3306 +--- + + + +## 何时使用 + +`SketchToImage` 是图像生成的等待状态,展示画面逐渐成形,而不是一个加载圈。铅笔线条一笔一笔勾出场景,笔尖在笔画之间抬起并移动;圆头画笔从后到前逐个区域来回扫过,铺上平涂色块;柔和的细节层、胶片颗粒与暖光加以润色,最后呈现完成的画面。画布下方,带图标的阶段标签位于提示词上方,旁边是一条细进度线、百分比和"重播"按钮。 + +- **图像画布**:第一张结果渲染期间。 +- **返回图像的聊天回复**:在图像就绪前代替它显示。 +- **落地页或引导页的介绍区块**:说明文生图或草图生图。 + +## 何时不用 + +- **文本或代码生成**:使用流式回复或闪烁的标签。 +- **包含大量图像的长批处理任务**:使用带缩略图的进度列表。 +- **模型提供真实的中间帧**:直接展示这些帧,而不是模拟的绘画过程。 +- **精确的进度**:绘画按 `Duration` 推进,而不是按你的后端,所以不要用在百分比必须真实的地方。 + +## 用法 + +给卡片一个 `Scene`(它要画的内容,见下文)和一个 `Prompt`。不设置 `Status` 时,卡片自行播放完整一遍,在完成的画面上停留约两秒,淡回空白画纸后再画一遍。这里并不生成任何东西:它是在你提供的场景上播放的模拟预览。没有 `Scene` 也没有 `Image` 时,卡片显示空状态:空白画纸、0% 的 Waiting 标签、没有提示词,也不会循环(上方的 Empty 选项卡)。本页的高山场景是 Gallery 用代码绘制的演示数据,不属于组件库。 + +```xml + +``` + +### 跟随一次请求 + + + +设置 `Status` 以跟随真实的生成。每个阶段播放到结尾并停在那里,标签保持闪烁,直到下一个阶段到来;后面的阶段会依次经过中间的阶段,而往回走(例如为新请求回到 `Idle`)时会先把画布淡回空白画纸。以 `Done` 显示卡片时会直接展示画面。按如下方式映射后端的进度: + +| 后端 | `Status` | +|---|---| +| 排队中 | `Idle` 或 `Sketching` | +| 前期步骤 | `Sketching` | +| 中期步骤 | `Blocking` | +| 后期步骤 | `Refining` | +| 图像就绪 | `Done` | + +```csharp +card.Status = SketchToImageStatus.Blocking; +card.StageChanged += (_, e) => Log($"Now {e.Stage}"); +card.Completed += (_, _) => ShowResult(); +card.ReplayRequested += (_, _) => Regenerate(); +``` + +如果你更想传一个比例,可以用 `Progress`(0 到 1)代替各阶段的终点:勾线在 0.38 结束,铺色在 0.62,润色在 0.88。绘画仍按自己的节奏前往那里。 + +### 结构 + +1. **画布**:3:2 的空白画纸,带圆角。铅笔笔画、画笔色块、两层柔和的画面、颗粒、暖光与清晰的画面叠在上面。 +2. **进度线**:画布下方一条 2px 的线,使用色调颜色。 +3. **阶段标签**:图标(钢笔、画笔、星光、对勾)加一个词,阶段进行中时闪烁。 +4. **提示词**:标签下方一行弱化文字;过长的提示词以省略号结尾,完整文字显示在提示框中。 +5. **百分比与重播**:百分比宽度固定;"重播"淡回空白画纸后重新作画。 + +### 阶段 + +| 阶段 | 占 `Duration` 的比例 | 发生什么 | +|---|---|---| +| `Sketching` | 38% | 铅笔笔画按顺序绘出,每一笔的时长与其长度相称;笔尖在笔画之间抬起并移动 | +| `Blocking` | 24% | 圆头画笔从后到前在每个区域来回扫过;铅笔线淡为参考线 | +| `Refining` | 26% | 两层预先模糊的画面、胶片颗粒与暖光淡入;铅笔线淡出 | +| `Done` | 12% | 清晰的画面交叉淡入 | + +### 尺寸 + + + +`Size` 设置卡片的最大宽度(340、520 或 720px)与铅笔的粗细,让线条在每种尺寸下都保持清晰。卡片在该宽度内填满容器。`Small` 以及窄于 400px 的卡片只显示"重播"的图标。在输入框里做小型指示时,使用 `Size="Small"` 并设 `ShowsReplay="False"`。 + +### 色调 + + + +`Tone="Neutral"` 用前景色而不是强调色绘制铅笔、笔尖、阶段图标与进度线。 + +### 自定义场景 + + + +`Scene` 设置在 600 × 400 画布上绘制的内容:`Lines` 是按顺序绘制的 SVG 路径字符串;`Regions` 从后到前上色,每个区域是一组形状(带平涂的 `Fill` 与完成后的 `Finish` 画刷)加一个供圆头画笔扫过的 `Brush` 矩形;`Light` 是暖光的来源位置。没有 `Fill` 的形状只出现在完成的画面里(雾气、高光)。按你的画面描出线条与区域,让草图与之吻合。 + +设置 `Image` 可让绘画呈现为你真实的结果:它会在柔和层与最后的交叉淡入中填满画布。可以用你自己的场景作为示意占位,也可以根据你的图像描一个场景。 + +```csharp +card.Image = new Bitmap(resultStream); +card.Status = SketchToImageStatus.Done; +``` + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的所有文字。替换整个对象即可本地化或改写: + +```csharp +card.Labels = new SketchToImageLabels +{ + Sketching = "Esquisse", + Blocking = "Aplats de couleur", + Refining = "Finitions", + Done = "Terminé", + Replay = "Rejouer", +}; +``` + +### 减少动态效果 + +减少动态效果时,卡片显示当前阶段的终态;自行播放时显示完成的画面。状态变化或重播变为终态之间的短暂淡入淡出,没有绘制或移动。关闭动效时一切立即变化。 + +### 最佳实践 + +- 每个视图只放一个。 +- `Done` 之后展示真实图像,或把它作为 `Image` 传入,让绘画呈现为它。 +- 写一个人们认得出的提示词;它说明正在画的是什么。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 画布是一张图像,其名称由阶段与场景组成("Sketching: Layered mountain ridges under a warm evening sky"),完成后变为场景的描述;没有场景时只有阶段名称("Waiting")。各绘制层对辅助技术隐藏。 +- 设置了 `Status` 时,一个礼貌的状态区域会朗读每个阶段,并说明这是模拟预览。卡片自行循环时,它只说明一次这是循环的模拟预览,之后保持安静。 +- 进度线是一个从 0 到 100 的只读进度条。 +- 阶段从不只靠颜色表示:每个阶段都由图标加文字组成。 +- 所有文字都在 `SketchToImageLabels` 中。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 移到"重播"按钮 | +| Enter、Space | 在"重播"上时,把画布淡回空白画纸并重新作画 | + +## 动效 + +| 变化 | 完整 | 减少 | 关闭 | +|---|---|---|---| +| 铅笔笔画 | 每一笔沿缓入缓出曲线绘出,时长与长度相称;笔尖跟随笔头,移到下一笔时抬起(缩小、变淡) | 终态 | 终态 | +| 画笔色块 | 宽大的圆头画笔从后到前在每个区域来回扫过,笔尖为区域的颜色;铅笔线淡为参考线 | 终态 | 终态 | +| 润色 | 两层预先模糊的画面、静态颗粒与暖光淡入(只有不透明度在变),然后清晰的画面交叉淡入 | 终态 | 终态 | +| 循环 | 在画面上停留 2s,用 520ms 淡回空白画纸,同时进度线回落,在不可见时重置并重新作画 | 停在画面上 | 停在画面上 | +| 状态变化、重播 | 向前播放;往回走时先淡回画纸 | 终态之间 160ms 淡出再淡入 | 立即 | +| 阶段标签 | 旧标签 100ms 淡出,新标签用 160ms 淡入并上升 2px;进行中时闪烁 | 120ms 交叉淡化,不闪烁 | 立即 | + +一个时钟驱动唯一一条 0 到 1 的时间线。每一帧从缓存的几何重绘墨迹层并设置各层的不透明度,不分配任何内存。卡片不在屏幕上或没有任何东西在动时,时钟停止;标签只随阶段变化,百分比只在取整后的值变化时更新。 + +## 相关组件 + +- `ToolCall`:代理工作时执行的动作。 +- `AgentStatus`:整个运行过程的实时状态。 +- `SegmentedControl`:在少数几个相关视图之间切换。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/zh-TW.md new file mode 100644 index 00000000..41feec01 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SketchToImage/zh-TW.md @@ -0,0 +1,167 @@ +--- +title: Sketch to image +summary: 像畫家一樣作畫的影像生成視覺效果:鉛筆線條勾出場景,圓頭畫筆逐個區域鋪上平塗色塊,顆粒與暖光加以潤飾,最後呈現完成的畫面。 +source-hash: 6727bfbb3306 +--- + + + +## 何時使用 + +`SketchToImage` 是影像生成的等待狀態,呈現畫面逐漸成形,而不是一個載入圈。鉛筆線條一筆一筆勾出場景,筆尖在筆畫之間抬起並移動;圓頭畫筆由後往前逐個區域來回掃過,鋪上平塗色塊;柔和的細節層、底片顆粒與暖光加以潤飾,最後呈現完成的畫面。畫布下方,帶圖示的階段標籤位於提示詞上方,旁邊是一條細進度線、百分比和「重播」按鈕。 + +- **影像畫布**:第一張結果算繪期間。 +- **回傳影像的聊天回覆**:在影像就緒前代替它顯示。 +- **登陸頁或導覽頁的介紹區塊**:說明文生圖或草圖生圖。 + +## 何時不用 + +- **文字或程式碼生成**:使用串流回覆或閃爍的標籤。 +- **包含大量影像的長批次作業**:使用帶縮圖的進度清單。 +- **模型提供真實的中間影格**:直接顯示這些影格,而不是模擬的繪畫過程。 +- **精確的進度**:繪畫依 `Duration` 推進,而不是依你的後端,所以不要用在百分比必須真實的地方。 + +## 用法 + +給卡片一個 `Scene`(它要畫的內容,見下文)和一個 `Prompt`。不設定 `Status` 時,卡片自行播放完整一遍,在完成的畫面上停留約兩秒,淡回空白畫紙後再畫一遍。這裡並不生成任何東西:它是在你提供的場景上播放的模擬預覽。沒有 `Scene` 也沒有 `Image` 時,卡片顯示空白狀態:空白畫紙、0% 的 Waiting 標籤、沒有提示詞,也不會循環(上方的 Empty 分頁)。本頁的高山場景是 Gallery 以程式碼繪製的展示資料,不屬於元件庫。 + +```xml + +``` + +### 跟隨一次請求 + + + +設定 `Status` 以跟隨真實的生成。每個階段播放到結尾並停在那裡,標籤保持閃爍,直到下一個階段到來;後面的階段會依序經過中間的階段,而往回走(例如為新請求回到 `Idle`)時會先把畫布淡回空白畫紙。以 `Done` 顯示卡片時會直接呈現畫面。依下列方式對應後端的進度: + +| 後端 | `Status` | +|---|---| +| 排隊中 | `Idle` 或 `Sketching` | +| 前期步驟 | `Sketching` | +| 中期步驟 | `Blocking` | +| 後期步驟 | `Refining` | +| 影像就緒 | `Done` | + +```csharp +card.Status = SketchToImageStatus.Blocking; +card.StageChanged += (_, e) => Log($"Now {e.Stage}"); +card.Completed += (_, _) => ShowResult(); +card.ReplayRequested += (_, _) => Regenerate(); +``` + +如果你比較想傳一個比例,可以用 `Progress`(0 到 1)取代各階段的終點:勾線在 0.38 結束,鋪色在 0.62,潤飾在 0.88。繪畫仍依自己的節奏前往那裡。 + +### 結構 + +1. **畫布**:3:2 的空白畫紙,帶圓角。鉛筆筆畫、畫筆色塊、兩層柔和的畫面、顆粒、暖光與清晰的畫面疊在上面。 +2. **進度線**:畫布下方一條 2px 的線,使用色調顏色。 +3. **階段標籤**:圖示(鋼筆、畫筆、星光、勾號)加一個詞,階段進行中時閃爍。 +4. **提示詞**:標籤下方一行淡化文字;過長的提示詞以刪節號結尾,完整文字顯示在提示框中。 +5. **百分比與重播**:百分比寬度固定;「重播」淡回空白畫紙後重新作畫。 + +### 階段 + +| 階段 | 占 `Duration` 的比例 | 發生什麼 | +|---|---|---| +| `Sketching` | 38% | 鉛筆筆畫依序繪出,每一筆的時長與其長度相稱;筆尖在筆畫之間抬起並移動 | +| `Blocking` | 24% | 圓頭畫筆由後往前在每個區域來回掃過;鉛筆線淡為參考線 | +| `Refining` | 26% | 兩層預先模糊的畫面、底片顆粒與暖光淡入;鉛筆線淡出 | +| `Done` | 12% | 清晰的畫面交叉淡入 | + +### 尺寸 + + + +`Size` 設定卡片的最大寬度(340、520 或 720px)與鉛筆的粗細,讓線條在每種尺寸下都保持清晰。卡片在該寬度內填滿容器。`Small` 以及窄於 400px 的卡片只顯示「重播」的圖示。在輸入框裡做小型指示時,使用 `Size="Small"` 並設 `ShowsReplay="False"`。 + +### 色調 + + + +`Tone="Neutral"` 以前景色而不是強調色繪製鉛筆、筆尖、階段圖示與進度線。 + +### 自訂場景 + + + +`Scene` 設定在 600 × 400 畫布上繪製的內容:`Lines` 是依序繪製的 SVG 路徑字串;`Regions` 由後往前上色,每個區域是一組形狀(帶平塗的 `Fill` 與完成後的 `Finish` 筆刷)加一個供圓頭畫筆掃過的 `Brush` 矩形;`Light` 是暖光的來源位置。沒有 `Fill` 的形狀只出現在完成的畫面裡(霧氣、亮部)。依你的畫面描出線條與區域,讓草圖與之吻合。 + +設定 `Image` 可讓繪畫呈現為你真實的結果:它會在柔和層與最後的交叉淡入中填滿畫布。可以用你自己的場景作為示意佔位,也可以依你的影像描一個場景。 + +```csharp +card.Image = new Bitmap(resultStream); +card.Status = SketchToImageStatus.Done; +``` + +### 自訂文案 + +`Labels` 包含卡片顯示或朗讀的所有文字。替換整個物件即可在地化或改寫: + +```csharp +card.Labels = new SketchToImageLabels +{ + Sketching = "Esquisse", + Blocking = "Aplats de couleur", + Refining = "Finitions", + Done = "Terminé", + Replay = "Rejouer", +}; +``` + +### 減少動態效果 + +減少動態效果時,卡片顯示目前階段的終態;自行播放時顯示完成的畫面。狀態變化或重播變為終態之間的短暫淡入淡出,沒有繪製或移動。關閉動態效果時一切立即變化。 + +### 最佳做法 + +- 每個檢視只放一個。 +- `Done` 之後顯示真實影像,或把它作為 `Image` 傳入,讓繪畫呈現為它。 +- 寫一個大家認得出的提示詞;它說明正在畫的是什麼。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 畫布是一張影像,其名稱由階段與場景組成("Sketching: Layered mountain ridges under a warm evening sky"),完成後變為場景的描述;沒有場景時只有階段名稱("Waiting")。各繪製層對輔助技術隱藏。 +- 設定了 `Status` 時,一個禮貌的狀態區域會朗讀每個階段,並說明這是模擬預覽。卡片自行循環時,它只說明一次這是循環的模擬預覽,之後保持安靜。 +- 進度線是一個從 0 到 100 的唯讀進度列。 +- 階段從不只靠顏色表示:每個階段都由圖示加文字組成。 +- 所有文字都在 `SketchToImageLabels` 中。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 移到「重播」按鈕 | +| Enter、Space | 在「重播」上時,把畫布淡回空白畫紙並重新作畫 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 鉛筆筆畫 | 每一筆沿緩入緩出曲線繪出,時長與長度相稱;筆尖跟隨筆頭,移到下一筆時抬起(縮小、變淡) | 終態 | 終態 | +| 畫筆色塊 | 寬大的圓頭畫筆由後往前在每個區域來回掃過,筆尖為區域的顏色;鉛筆線淡為參考線 | 終態 | 終態 | +| 潤飾 | 兩層預先模糊的畫面、靜態顆粒與暖光淡入(只有不透明度在變),然後清晰的畫面交叉淡入 | 終態 | 終態 | +| 循環 | 在畫面上停留 2s,用 520ms 淡回空白畫紙,同時進度線回落,在不可見時重設並重新作畫 | 停在畫面上 | 停在畫面上 | +| 狀態變化、重播 | 向前播放;往回走時先淡回畫紙 | 終態之間 160ms 淡出再淡入 | 立即 | +| 階段標籤 | 舊標籤 100ms 淡出,新標籤以 160ms 淡入並上升 2px;進行中時閃爍 | 120ms 交叉淡化,不閃爍 | 立即 | + +一個時鐘驅動唯一一條 0 到 1 的時間軸。每一影格從快取的幾何重繪墨跡層並設定各層的不透明度,不配置任何記憶體。卡片不在螢幕上或沒有任何東西在動時,時鐘停止;標籤只隨階段變化,百分比只在取整後的值變化時更新。 + +## 相關元件 + +- `ToolCall`:代理工作時執行的動作。 +- `AgentStatus`:整個執行過程的即時狀態。 +- `SegmentedControl`:在少數幾個相關檢視之間切換。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/en-US.md new file mode 100644 index 00000000..27205d47 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/en-US.md @@ -0,0 +1,233 @@ +--- +title: Smart form fill +summary: A paste-to-fill form card: AI reads an email signature or messy notes and fills a contact form field by field, with a confidence dot per value, its source lit up in the text, review for guesses and Undo. +--- + + + +## When to use + +`SmartFormFill` turns pasted text into a record. People paste an email signature, a forwarded message or notes into the +well; AI reads it and fills the form one field after another. Each value fades in with a confidence dot, the words it came +from light up in the pasted text, guesses ask for review before the form can be saved, and Undo puts the old values back. + +- **Creating a contact or a lead** from an email signature or call notes. +- **Onboarding and CRM flows** where typing six fields by hand is the main friction. +- **Vendor, company or shipping records** that arrive as unstructured text; pass your own fields. +- **Any form where AI-filled data must be checked** before it is saved, with visible confidence and an easy review path. + +## When not to use + +- **Plain data entry** with no unstructured source: use `Input` and the other fields directly. +- **Long multi-page applications**: use a step-by-step form and add a paste step to one page. +- **Bulk imports of many records**: use a table import flow instead of one card. + +## Usage + +Give the card your model through `Extract`. It receives the pasted text and a `CancellationToken`, and returns a +`SmartFormFillResult`: a `SmartFormFillValue` per field id with the value, a confidence from 0 to 1, the exact substring of +the text it came from and, for a guess, a note that says why. Fields the result leaves out keep their value. `Submit` +saves the form; the button shows a loading state while its task runs. + +```csharp +card.Extract = async (text, token) => +{ + // Ask your model for structured output: { field: { value, confidence, source, note } }. + var contact = await contacts.ExtractAsync(text, token); + var result = new SmartFormFillResult(); + foreach (var field in contact.Fields) + { + result[field.Id] = new SmartFormFillValue(field.Value, field.Confidence, field.Source, field.Note); + } + return result; +}; +card.Submit = values => crm.SaveContactAsync(values); +``` + +With no extractor the card reads with a simulated reader: the `Result` of a sample whose text was pasted, and a small +rule-based parser for anything else (nothing is sent). With no `Extract` and no `Simulation` the form starts empty, with no +Try sample button and no model named. + +### Simulation + +To show the card without a model (a demo, a design review, an onboarding tour), give it a `SmartFormFillSimulation`: the +samples behind Try sample, each with the result a read returns (`SmartFormFillSample.Result`), and the model to name. +With full motion the card then plays itself in a loop (paste, fill, check, undo) until someone clicks or types inside it. +Your own `Extract` wins over the simulation, and setting `Simulation` to null stops it and clears what it filled. Set +`AutoPlay="False"` in product screens. + +```csharp +card.Simulation = new SmartFormFillSimulation +{ + ModelName = "DeepSeek-V4.1-Flash-1017", + Samples = + [ + new SmartFormFillSample("Email signature", signature) + { + Result = new SmartFormFillResult + { + ["name"] = new SmartFormFillValue("Maya Okafor", 0.98, "Maya Okafor"), + ["email"] = new SmartFormFillValue("maya.okafor@example.com", 0.99, "maya.okafor@example.com"), + }, + }, + ], +}; +``` + +The preview above plays a simulation whose texts and results are the gallery's own demo data. + +### Anatomy + +1. **Header**: the title and one line under it (`Title`, `Description`; an empty description hides the line). +2. **Paste well**: a heading with the model that reads ("Read by DeepSeek-V4.1-Flash-1017"), the paste area, and Try sample, Edit text + and Fill. Once a read starts, the paste area becomes the pasted text, where each field's source is highlighted. +3. **Form**: the fields in order, two half fields to a row. Each has its label, a confidence badge (a dot plus a percentage, + or Review, Checked or Edited), an Arc `Input`, and for a guess a warning ring, a check button and the reason under it. + A company field shows the company's logo or a monogram at its start. +4. **Footer**: the status with an icon, then Undo and the submit button. + +### Reading and filling + +Pasting into the area (Cmd/Ctrl+V) starts a read right away; Cmd/Ctrl+Enter or Fill reads typed text; Try sample pastes the +next sample. While it reads, the status shimmers "Reading with DeepSeek-V4.1-Flash-1017" and Fill turns into Stop. Stop or Escape cancels +the extractor's token and nothing changes. + +When the result arrives, fields fill in form order 140ms apart. The source of the field being filled takes a stronger tint +in the pasted text; afterwards hovering or focusing a field does the same, so cause and effect read across the card. +`Filled` is raised once every field from a read has landed, and `ValuesChanged` on every change, typed or filled. + +```csharp +card.Filled += (_, e) => analytics.Track("contact-filled", e.FilledCount, e.ReviewCount); +card.ValuesChanged += (_, e) => draft.Save(e.Values); +``` + +`ReadAsync(text)`, `Stop()`, `TrySampleAsync()`, `Check(id)`, `Undo()` and `SubmitAsync()` do the same from code; +`GetFieldValue` and `SetFieldValue` read and write one field. + + + +### Review and Undo + +A value whose confidence is below `ReviewThreshold` (0.8 by default) asks for review: its badge reads Review, a warning ring +surrounds the input, the reason shows under it and a check button sits at its end. Checking it, or editing the value, +clears the review. Save does not go through while a field still asks for review: it says how many are left and focuses the +first. Undo restores the values from before the last fill and raises `Undone` with them. + +### Your own fields + + + +`Fields` replaces the contact form. A field has an `Id` (the key in values and results), a `Label`, a `Kind` (`Text`, +`Email`, `Phone`, `Url` or `Company`, which adds the logo slot), a `Placeholder` and a `Span` (`Half` or `Full`). +`DefaultValues` fills fields before anything is read. `Samples` sets the texts behind Try sample (give each a `Result` +for the simulated reader to return); none hides the button and the autoplay. + +```xml + + + + +``` + +`ResolveLogo` gives the company field your own mark: return a control for a company name, or null for the monogram. +`ModelName` and `ModelMark` name the model beside the paste area. + +### Narrow columns + + + +The card follows its own width, not the window's. From 720px of content the well sits left of the form; below that it +stacks on top at a fixed height. Under 440px every field takes the full row and the footer puts the status above the +actions. It holds at 360px without horizontal scrolling. + +### Your own words + +`Labels` holds every other word the card shows or says; replace the object to localize or reword it: + +```csharp +card.Labels = new SmartFormFillLabels +{ + TrySample = "Beispiel", + Fill = "Ausfüllen", + Reading = model => $"{model} liest", + Filled = (filled, review) => $"{filled} Felder ausgefüllt", +}; +``` + +### Content guidelines + +- Write `Description` as one plain sentence about what to paste. +- Keep notes short and concrete: "Two numbers in the text; picked the mobile.", not "Low confidence". +- Return the exact source substring; a paraphrased source cannot be highlighted. + +### Best practices + +- Keep `ReviewThreshold` around 0.8 so inferred states, paraphrased titles and one of several phone numbers are flagged + rather than saved silently. +- Pass the token on to your HTTP call so Stop, Escape and a new paste really cancel the request. +- Turn `AutoPlay` off in product screens; the loop is for docs and marketing pages. + +### Common questions + +**Can people type instead of pasting?** Yes. Every field is an ordinary input, and typed text in the paste area is read +with Fill or Cmd/Ctrl+Enter. + +**What happens to values people typed before a read?** A read changes only the fields it returns; Undo brings back what +was there before it. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- The footer status is a polite live region: it announces reading, how many fields were filled and how many need review, + checks, undo and save. With the built-in reader it adds "Simulated". +- Each filled input describes its confidence ("Filled by AI, 94% confident"); a guess also gives the reason and reports a + Review status until it is checked or edited. +- Status is never color alone: confidence shows a dot plus a percentage or a word, and the footer pairs an icon with text. +- The check button inside a field that needs review is named after the field ("Mark Phone as checked"). Save focuses the + first field that still needs review. +- The pasted text view is a focusable, labelled region ("Pasted text"), so keyboard users can scroll it; keyboard focus + draws a ring around it. + +### Keyboard + +| Keys | Action | +|---|---| +| Cmd/Ctrl + V | Pastes into the text area; the read starts right away | +| Cmd/Ctrl + Enter | Reads the typed text in the text area | +| Escape | Stops a read in progress; nothing changes | +| Tab | Moves through the text area, Try sample, Fill, every field, the check buttons, Undo and Save | +| Enter, Space | Presses the focused button | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Values arrive | In form order, 140ms apart | All at once | All at once | +| A value lands | Fades in over 200ms; the input breathes an accent tint for 900ms; nothing moves | At once, no tint | At once | +| Active source in the text | Its highlight takes the full tint over 160ms | Same | At once | +| Badges, logo and status | Old out in 100ms, new in over 160ms rising 2px | Short crossfade | At once | +| Reading label | A calm shimmer | Still | Still | +| Review ring | Fades in over 160ms | At once | At once | +| Autoplay loop | Plays while the card is on screen | Never plays | Never plays | + +No layout animates: values, badges and highlights change in place, so the card never jumps while it fills. The loop pauses +when the card leaves the screen, and a read in flight is cancelled when the card is removed. + +## Related + +- `Input` and `Textarea` for the fields the card is built from. +- `ToolCall` for showing what an agent did with a tool. +- `ContextWindow` for how much of a model's context a conversation uses. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/zh-CN.md new file mode 100644 index 00000000..8ee798e2 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/zh-CN.md @@ -0,0 +1,198 @@ +--- +title: Smart form fill +summary: 粘贴即填的表单卡片:AI 读取邮件签名或零散笔记,逐个字段填写联系人表单,每个值带置信度圆点,来源在文本中高亮,猜测的值需要复核,并可撤销。 +source-hash: 1f6fca89440b +--- + + + +## 何时使用 + +`SmartFormFill` 把粘贴的文本变成一条记录。用户把邮件签名、转发的邮件或笔记粘贴到左侧区域,AI 读取后逐个字段填写表单。每个值淡入时带一个置信度圆点,它所来自的文字在粘贴文本中高亮;猜测的值在保存前需要复核,"撤销"会恢复原来的值。 + +- **从邮件签名或通话笔记创建联系人或线索**。 +- **入职与 CRM 流程**,手动输入六个字段是主要负担。 +- **以非结构化文本形式到来的供应商、公司或收货记录**;传入你自己的字段。 +- **任何 AI 填写的数据需要先核对再保存的表单**,置信度可见,复核路径简单。 + +## 何时不用 + +- **没有非结构化来源的普通录入**:直接使用 `Input` 等字段。 +- **很长的多页申请**:使用分步表单,并在其中一页加入粘贴步骤。 +- **批量导入大量记录**:使用表格导入流程,而不是一张卡片。 + +## 用法 + +通过 `Extract` 把你的模型交给卡片。它接收粘贴的文本和一个 `CancellationToken`,返回 `SmartFormFillResult`:按字段 id 给出一个 `SmartFormFillValue`,包含值、0 到 1 的置信度、该值在文本中的原文子串,以及猜测时说明原因的备注。结果中没有的字段保持原值。`Submit` 保存表单;任务运行期间按钮显示加载状态。 + +```csharp +card.Extract = async (text, token) => +{ + // Ask your model for structured output: { field: { value, confidence, source, note } }. + var contact = await contacts.ExtractAsync(text, token); + var result = new SmartFormFillResult(); + foreach (var field in contact.Fields) + { + result[field.Id] = new SmartFormFillValue(field.Value, field.Confidence, field.Source, field.Note); + } + return result; +}; +card.Submit = values => crm.SaveContactAsync(values); +``` + +没有提取器时,卡片使用模拟读取器:粘贴的文本如果是某个示例的文本,就返回该示例的 `Result`,其他文本由一个基于规则的小型解析器处理(不会发送任何内容)。既没有 `Extract` 也没有 `Simulation` 时,表单一开始是空的,没有 Try sample 按钮,也不显示模型。 + +### 模拟 + +想在没有模型的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `SmartFormFillSimulation`:Try sample 背后的示例,每个都带有读取时返回的结果(`SmartFormFillSample.Result`),以及要显示的模型。动效为完整时,卡片会自动循环演示(粘贴、填写、核对、撤销),直到有人在卡片内点击或输入。你自己的 `Extract` 优先于模拟;把 `Simulation` 设为 null 会停止模拟并清除它填写的内容。产品界面中请设置 `AutoPlay="False"`。 + +```csharp +card.Simulation = new SmartFormFillSimulation +{ + ModelName = "DeepSeek-V4.1-Flash-1017", + Samples = + [ + new SmartFormFillSample("Email signature", signature) + { + Result = new SmartFormFillResult + { + ["name"] = new SmartFormFillValue("Maya Okafor", 0.98, "Maya Okafor"), + ["email"] = new SmartFormFillValue("maya.okafor@example.com", 0.99, "maya.okafor@example.com"), + }, + }, + ], +}; +``` + +上方的预览播放的就是一个模拟,其文本和结果来自图库自己的演示数据。 + +### 结构 + +1. **标题区**:标题及其下方的一行说明(`Title`、`Description`;说明为空时隐藏这一行)。 +2. **粘贴区**:标题行显示负责读取的模型("Read by DeepSeek-V4.1-Flash-1017"),然后是粘贴框,以及 Try sample、Edit text 和 Fill。开始读取后,粘贴框变为粘贴的文本,每个字段的来源都在其中高亮。 +3. **表单**:按顺序排列的字段,两个半宽字段占一行。每个字段有标签、置信度标记(圆点加百分比,或 Review、Checked、Edited)、一个 Arc `Input`;猜测的值还有警告色描边、核对按钮和其下方的原因。公司字段在开头显示公司的标志或首字母。 +4. **底栏**:带图标的状态,然后是"撤销"和提交按钮。 + +### 读取与填写 + +在粘贴框中粘贴(Cmd/Ctrl+V)会立即开始读取;Cmd/Ctrl+Enter 或 Fill 读取手动输入的文本;Try sample 粘贴下一个示例。读取期间,状态以微光显示 "Reading with DeepSeek-V4.1-Flash-1017",Fill 变为 Stop。Stop 或 Escape 会取消提取器的令牌,什么都不会改变。 + +结果到达后,字段按表单顺序每隔 140ms 填写一个。正在填写的字段,其来源在粘贴文本中显示更深的底色;之后悬停或聚焦某个字段也会如此,让因果关系在卡片上一目了然。一次读取的所有字段都填好后触发 `Filled`;每次值变化(无论输入还是填写)都触发 `ValuesChanged`。 + +```csharp +card.Filled += (_, e) => analytics.Track("contact-filled", e.FilledCount, e.ReviewCount); +card.ValuesChanged += (_, e) => draft.Save(e.Values); +``` + +`ReadAsync(text)`、`Stop()`、`TrySampleAsync()`、`Check(id)`、`Undo()` 和 `SubmitAsync()` 在代码中完成同样的操作;`GetFieldValue` 和 `SetFieldValue` 读写单个字段。 + + + +### 复核与撤销 + +置信度低于 `ReviewThreshold`(默认 0.8)的值需要复核:标记显示 Review,输入框外有一圈警告色描边,下方显示原因,末尾有一个核对按钮。核对它或修改其值即可结束复核。仍有字段需要复核时无法保存:卡片会说明还剩几个并聚焦第一个。"撤销"恢复上一次填写之前的值,并以这些值触发 `Undone`。 + +### 自定义字段 + + + +`Fields` 替换默认的联系人表单。每个字段有 `Id`(值和结果中的键)、`Label`、`Kind`(`Text`、`Email`、`Phone`、`Url` 或 `Company`,后者带标志位)、`Placeholder` 和 `Span`(`Half` 或 `Full`)。`DefaultValues` 在读取之前预先填写字段。`Samples` 设置 Try sample 背后的文本(为每个示例提供 `Result`,供模拟读取器返回);没有示例时会隐藏该按钮并关闭自动演示。 + +```xml + + + + +``` + +`ResolveLogo` 为公司字段提供你自己的标志:按公司名返回一个控件,返回 null 则显示首字母。`ModelName` 和 `ModelMark` 指定粘贴区旁显示的模型。 + +### 窄栏 + + + +卡片跟随自身宽度,而不是窗口宽度。内容宽度达到 720px 时粘贴区位于表单左侧;更窄时粘贴区以固定高度叠在表单上方。低于 440px 时每个字段独占一行,底栏把状态放在操作按钮上方。宽度为 360px 时也不会出现横向滚动。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的其他所有文字;替换整个对象即可本地化或改写: + +```csharp +card.Labels = new SmartFormFillLabels +{ + TrySample = "Beispiel", + Fill = "Ausfüllen", + Reading = model => $"{model} liest", + Filled = (filled, review) => $"{filled} Felder ausgefüllt", +}; +``` + +### 内容指南 + +- `Description` 用一句平实的话说明要粘贴什么。 +- 备注简短具体:"Two numbers in the text; picked the mobile.",而不是"置信度低"。 +- 返回原文中的确切子串;改写过的来源无法高亮。 + +### 最佳实践 + +- 把 `ReviewThreshold` 保持在 0.8 左右,让推断出的州名、改写过的职位、多个电话号码中选出的一个被标记出来,而不是被悄悄保存。 +- 把令牌传给你的 HTTP 调用,这样 Stop、Escape 和新的粘贴才能真正取消请求。 +- 在产品界面中关闭 `AutoPlay`;循环演示是为文档和营销页面准备的。 + +### 常见问题 + +**用户可以手动输入而不是粘贴吗?** 可以。每个字段都是普通的输入框;在粘贴框中输入的文本可以用 Fill 或 Cmd/Ctrl+Enter 读取。 + +**读取之前用户已输入的值会怎样?** 一次读取只修改它返回的字段;"撤销"会恢复读取之前的内容。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 底栏状态是一个礼貌的实时区域:会播报读取中、填写了多少字段及其中多少需要复核、核对、撤销和保存。使用内置读取器时会附加 "Simulated"。 +- 每个已填写的输入框都描述其置信度("Filled by AI, 94% confident");猜测的值还会给出原因,并在核对或修改之前报告 Review 状态。 +- 状态从不只靠颜色表达:置信度显示为圆点加百分比或文字,底栏的图标总是配有文字。 +- 需要复核的字段内的核对按钮以字段命名("Mark Phone as checked")。保存会聚焦第一个仍需复核的字段。 +- 粘贴文本视图是一个可聚焦、带标签的区域("Pasted text"),键盘用户可以滚动它;键盘聚焦时会在其周围显示焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Cmd/Ctrl + V | 粘贴到文本框;立即开始读取 | +| Cmd/Ctrl + Enter | 读取文本框中输入的文本 | +| Escape | 停止正在进行的读取;什么都不会改变 | +| Tab | 依次经过文本框、Try sample、Fill、每个字段、核对按钮、撤销和保存 | +| Enter、Space | 按下获得焦点的按钮 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 值到达 | 按表单顺序,每隔 140ms 一个 | 全部同时 | 全部同时 | +| 一个值落位 | 200ms 淡入;输入框泛起 900ms 的强调色底色;没有位移 | 立即,没有底色 | 立即 | +| 文本中当前的来源 | 高亮在 160ms 内变为完整底色 | 相同 | 立即 | +| 标记、标志与状态 | 旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px | 短暂交叉淡化 | 立即 | +| 读取标签 | 平缓的微光 | 静止 | 静止 | +| 复核描边 | 160ms 淡入 | 立即 | 立即 | +| 自动演示循环 | 卡片在屏幕上时播放 | 从不播放 | 从不播放 | + +没有任何布局动画:值、标记和高亮都在原位变化,所以卡片填写时不会跳动。卡片离开屏幕时循环暂停,卡片被移除时正在进行的读取会被取消。 + +## 相关组件 + +- `Input` 和 `Textarea`:卡片所用的字段。 +- `ToolCall`:展示智能体对工具做了什么。 +- `ContextWindow`:展示一段对话占用了模型上下文的多少。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/zh-TW.md new file mode 100644 index 00000000..c66a22f9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SmartFormFill/zh-TW.md @@ -0,0 +1,198 @@ +--- +title: Smart form fill +summary: 貼上即填的表單卡片:AI 讀取電子郵件簽名或零散筆記,逐一填寫聯絡人表單的欄位,每個值附上信心度圓點,來源在文字中醒目標示,猜測的值需要複核,並可復原。 +source-hash: 1f6fca89440b +--- + + + +## 何時使用 + +`SmartFormFill` 把貼上的文字變成一筆紀錄。使用者把電子郵件簽名、轉寄的郵件或筆記貼到左側區域,AI 讀取後逐一填寫表單欄位。每個值淡入時附上一個信心度圓點,它所來自的文字在貼上的內容中醒目標示;猜測的值在儲存前需要複核,「復原」會還原先前的值。 + +- **從電子郵件簽名或通話筆記建立聯絡人或潛在客戶**。 +- **新手引導與 CRM 流程**,手動輸入六個欄位是主要負擔。 +- **以非結構化文字形式送來的供應商、公司或收件紀錄**;傳入你自己的欄位。 +- **任何 AI 填寫的資料需要先核對再儲存的表單**,信心度一目了然,複核路徑簡單。 + +## 何時不用 + +- **沒有非結構化來源的一般輸入**:直接使用 `Input` 等欄位。 +- **很長的多頁申請**:使用分步驟表單,並在其中一頁加入貼上步驟。 +- **大量匯入多筆紀錄**:使用表格匯入流程,而不是一張卡片。 + +## 用法 + +透過 `Extract` 把你的模型交給卡片。它接收貼上的文字和一個 `CancellationToken`,回傳 `SmartFormFillResult`:依欄位 id 提供一個 `SmartFormFillValue`,包含值、0 到 1 的信心度、該值在文字中的原文子字串,以及猜測時說明原因的備註。結果中沒有的欄位維持原值。`Submit` 儲存表單;工作執行期間按鈕顯示載入狀態。 + +```csharp +card.Extract = async (text, token) => +{ + // Ask your model for structured output: { field: { value, confidence, source, note } }. + var contact = await contacts.ExtractAsync(text, token); + var result = new SmartFormFillResult(); + foreach (var field in contact.Fields) + { + result[field.Id] = new SmartFormFillValue(field.Value, field.Confidence, field.Source, field.Note); + } + return result; +}; +card.Submit = values => crm.SaveContactAsync(values); +``` + +沒有擷取器時,卡片使用模擬讀取器:貼上的文字如果是某個範例的文字,就傳回該範例的 `Result`,其他文字由一個以規則為基礎的小型剖析器處理(不會傳送任何內容)。既沒有 `Extract` 也沒有 `Simulation` 時,表單一開始是空的,沒有 Try sample 按鈕,也不顯示模型。 + +### 模擬 + +想在沒有模型的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `SmartFormFillSimulation`:Try sample 背後的範例,每個都附有讀取時傳回的結果(`SmartFormFillSample.Result`),以及要顯示的模型。動態效果為完整時,卡片會自動循環示範(貼上、填寫、核對、復原),直到有人在卡片內點按或輸入。你自己的 `Extract` 優先於模擬;把 `Simulation` 設為 null 會停止模擬並清除它填寫的內容。產品畫面中請設定 `AutoPlay="False"`。 + +```csharp +card.Simulation = new SmartFormFillSimulation +{ + ModelName = "DeepSeek-V4.1-Flash-1017", + Samples = + [ + new SmartFormFillSample("Email signature", signature) + { + Result = new SmartFormFillResult + { + ["name"] = new SmartFormFillValue("Maya Okafor", 0.98, "Maya Okafor"), + ["email"] = new SmartFormFillValue("maya.okafor@example.com", 0.99, "maya.okafor@example.com"), + }, + }, + ], +}; +``` + +上方的預覽播放的就是一個模擬,其文字和結果來自圖庫自己的示範資料。 + +### 結構 + +1. **標題區**:標題及其下方的一行說明(`Title`、`Description`;說明為空時隱藏這一行)。 +2. **貼上區**:標題列顯示負責讀取的模型("Read by DeepSeek-V4.1-Flash-1017"),接著是貼上框,以及 Try sample、Edit text 和 Fill。開始讀取後,貼上框會變成貼上的文字,每個欄位的來源都在其中醒目標示。 +3. **表單**:依序排列的欄位,兩個半寬欄位佔一列。每個欄位有標籤、信心度標記(圓點加百分比,或 Review、Checked、Edited)、一個 Arc `Input`;猜測的值還有警告色外框、核對按鈕和下方的原因。公司欄位在開頭顯示公司的標誌或首字母。 +4. **底列**:附圖示的狀態,接著是「復原」和送出按鈕。 + +### 讀取與填寫 + +在貼上框中貼上(Cmd/Ctrl+V)會立即開始讀取;Cmd/Ctrl+Enter 或 Fill 讀取手動輸入的文字;Try sample 貼上下一個範例。讀取期間,狀態以微光顯示 "Reading with DeepSeek-V4.1-Flash-1017",Fill 變成 Stop。Stop 或 Escape 會取消擷取器的權杖,什麼都不會改變。 + +結果送達後,欄位依表單順序每隔 140ms 填寫一個。正在填寫的欄位,其來源在貼上的文字中顯示較深的底色;之後游標停留或聚焦某個欄位也會如此,讓因果關係在卡片上一目了然。一次讀取的所有欄位都填好後觸發 `Filled`;每次值改變(無論輸入或填寫)都觸發 `ValuesChanged`。 + +```csharp +card.Filled += (_, e) => analytics.Track("contact-filled", e.FilledCount, e.ReviewCount); +card.ValuesChanged += (_, e) => draft.Save(e.Values); +``` + +`ReadAsync(text)`、`Stop()`、`TrySampleAsync()`、`Check(id)`、`Undo()` 和 `SubmitAsync()` 在程式碼中完成相同的操作;`GetFieldValue` 和 `SetFieldValue` 讀寫單一欄位。 + + + +### 複核與復原 + +信心度低於 `ReviewThreshold`(預設 0.8)的值需要複核:標記顯示 Review,輸入框外有一圈警告色外框,下方顯示原因,末端有一個核對按鈕。核對它或修改其值即可結束複核。仍有欄位需要複核時無法儲存:卡片會說明還剩幾個並聚焦第一個。「復原」還原上一次填寫之前的值,並以這些值觸發 `Undone`。 + +### 自訂欄位 + + + +`Fields` 取代預設的聯絡人表單。每個欄位有 `Id`(值與結果中的鍵)、`Label`、`Kind`(`Text`、`Email`、`Phone`、`Url` 或 `Company`,後者附標誌位置)、`Placeholder` 和 `Span`(`Half` 或 `Full`)。`DefaultValues` 在讀取之前預先填寫欄位。`Samples` 設定 Try sample 背後的文字(為每個範例提供 `Result`,供模擬讀取器傳回);沒有範例時會隱藏該按鈕並關閉自動示範。 + +```xml + + + + +``` + +`ResolveLogo` 為公司欄位提供你自己的標誌:依公司名稱回傳一個控制項,回傳 null 則顯示首字母。`ModelName` 和 `ModelMark` 指定貼上區旁顯示的模型。 + +### 窄欄 + + + +卡片依自身寬度調整,而不是視窗寬度。內容寬度達 720px 時貼上區位於表單左側;更窄時貼上區以固定高度疊在表單上方。低於 440px 時每個欄位獨佔一列,底列把狀態放在操作按鈕上方。寬度為 360px 時也不會出現水平捲動。 + +### 自訂文字 + +`Labels` 包含卡片顯示或朗讀的其他所有文字;取代整個物件即可在地化或改寫: + +```csharp +card.Labels = new SmartFormFillLabels +{ + TrySample = "Beispiel", + Fill = "Ausfüllen", + Reading = model => $"{model} liest", + Filled = (filled, review) => $"{filled} Felder ausgefüllt", +}; +``` + +### 內容指引 + +- `Description` 用一句平實的話說明要貼上什麼。 +- 備註簡短具體:"Two numbers in the text; picked the mobile.",而不是「信心度低」。 +- 回傳原文中確切的子字串;改寫過的來源無法醒目標示。 + +### 最佳實務 + +- 把 `ReviewThreshold` 維持在 0.8 左右,讓推斷出的州名、改寫過的職稱、多組電話號碼中選出的一組被標示出來,而不是被悄悄儲存。 +- 把權杖傳給你的 HTTP 呼叫,這樣 Stop、Escape 和新的貼上才能真正取消請求。 +- 在產品畫面中關閉 `AutoPlay`;循環示範是為文件與行銷頁面準備的。 + +### 常見問題 + +**使用者可以手動輸入而不是貼上嗎?** 可以。每個欄位都是一般的輸入框;在貼上框中輸入的文字可以用 Fill 或 Cmd/Ctrl+Enter 讀取。 + +**讀取之前使用者已輸入的值會怎樣?** 一次讀取只修改它回傳的欄位;「復原」會還原讀取之前的內容。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 底列狀態是一個禮貌的即時區域:會播報讀取中、填寫了多少欄位及其中多少需要複核、核對、復原和儲存。使用內建讀取器時會附加 "Simulated"。 +- 每個已填寫的輸入框都描述其信心度("Filled by AI, 94% confident");猜測的值還會提供原因,並在核對或修改之前回報 Review 狀態。 +- 狀態從不只靠顏色表達:信心度顯示為圓點加百分比或文字,底列的圖示一律搭配文字。 +- 需要複核的欄位內的核對按鈕以欄位命名("Mark Phone as checked")。儲存會聚焦第一個仍需複核的欄位。 +- 貼上文字的檢視是一個可聚焦、附標籤的區域("Pasted text"),鍵盤使用者可以捲動它;鍵盤聚焦時會在周圍顯示焦點環。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Cmd/Ctrl + V | 貼到文字框;立即開始讀取 | +| Cmd/Ctrl + Enter | 讀取文字框中輸入的文字 | +| Escape | 停止進行中的讀取;什麼都不會改變 | +| Tab | 依序經過文字框、Try sample、Fill、每個欄位、核對按鈕、復原和儲存 | +| Enter、Space | 按下取得焦點的按鈕 | + +## 動效 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 值送達 | 依表單順序,每隔 140ms 一個 | 全部同時 | 全部同時 | +| 一個值就位 | 200ms 淡入;輸入框泛起 900ms 的強調色底色;沒有位移 | 立即,沒有底色 | 立即 | +| 文字中目前的來源 | 標示在 160ms 內變為完整底色 | 相同 | 立即 | +| 標記、標誌與狀態 | 舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px | 短暫交叉淡化 | 立即 | +| 讀取標籤 | 平緩的微光 | 靜止 | 靜止 | +| 複核外框 | 160ms 淡入 | 立即 | 立即 | +| 自動示範循環 | 卡片在畫面上時播放 | 從不播放 | 從不播放 | + +沒有任何版面動畫:值、標記和醒目標示都在原處改變,所以卡片填寫時不會跳動。卡片離開畫面時循環暫停,卡片被移除時進行中的讀取會被取消。 + +## 相關 + +- `Input` 和 `Textarea`:卡片使用的欄位。 +- `ToolCall`:呈現代理對工具做了什麼。 +- `ContextWindow`:呈現一段對話佔用了模型上下文的多少。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/en-US.md new file mode 100644 index 00000000..4360500b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/en-US.md @@ -0,0 +1,188 @@ +--- +title: Social proof toast +summary: A stack of social proof notifications for landing and pricing pages, where recent trials, upgrades and verified purchases from people and companies arrive every few seconds; it pauses and fans out on hover, and visitors can dismiss any toast. +--- + + + +## When to use + +`SocialProofToast` shows what other people are doing right now: a trial started, a team upgraded, a verified purchase. +A new toast rises in at the front every few seconds while the older ones step back behind it. Hovering the stack, or +moving keyboard focus into it, pauses the cycle and fans the toasts out so each one can be read or dismissed. + +- **Landing and pricing pages** that show recent trials, purchases or upgrades. +- **Launch or waitlist pages** with a live feed of people joining and teams upgrading. +- **Near a checkout button**, to highlight verified purchases from known companies. + +## When not to use + +- **Feedback after the visitor's own action** (saved, copied, failed): use `Toast`. +- **Inside a signed in product** where the activity is not marketing: use an activity feed list. +- **When you have no real events to show.** Fabricated social proof erodes trust; a simulation is for previews only. + +## Usage + +Add the stack and give it your feed in `Events`, oldest first. It shows one toast every `Interval` (4 seconds by +default) after an `InitialDelay` (0.8 seconds), keeps up to `MaxVisible` toasts (3) and starts over at the end unless +`Loop` is false. With no events and no simulation (below) the stack stays empty until you push one. + +```csharp +// One row of your billing backend's recent activity. +record Signup(string Id, string FirstName, string Action, string City, DateTimeOffset CreatedAt, bool IsPaid); + +stack.Events.AddRange(recent.Select(r => new SocialProofEvent(r.Id, r.FirstName, r.Action) +{ + Meta = r.City, + Time = r.CreatedAt, + VerifiedLabel = r.IsPaid ? "Verified purchase" : null, +})); +``` + +### Your events + + + +Each `SocialProofEvent` has a stable `Id`, a `Name` (a first name or a company, in medium weight) and an `Action` read +after it. `Meta` adds short context such as a city or a seat count. `Time` renders as relative time (Just now, 4 min +ago, 2 h ago); `TimeText` is shown as is. `IsVerified` or `VerifiedLabel` adds a check badge with a label. + +People are drawn as an `Avatar` with their initials, or their `Portrait`. Set `Actor` to `Company` for a rounded square +with the first letter of the name, or with your own mark in `Logo` (a 24 × 24 stroked glyph drawn in the text color, so +it follows the theme). + +Call `Push` to show an event the moment it happens, for example from a server-sent event. It goes to the front at once +and the countdown to the next toast starts over. + +For a preview, leave `Events` empty and give the stack a `SocialProofToastSimulation`: its events, oldest first, each a +`SocialProofToastSimulationEvent` with the event and how long `Ago` it happened. Every pass measures the times back from +that moment, so the feed never drifts into "40 min ago". Your own `Events` set it aside, and setting `Simulation` to null +empties the stack. Every name, company and place in the previews here is invented. + +```csharp +stack.Simulation = new SocialProofToastSimulation +{ + Events = + [ + new SocialProofToastSimulationEvent { Event = new SocialProofEvent("s1", "Sofia", "started a Pro trial") { Meta = "Madrid" }, Ago = TimeSpan.FromMinutes(14) }, + new SocialProofToastSimulationEvent { Event = new SocialProofEvent("s2", "Mateo", "bought the Team plan") { Meta = "Lisbon" }, Ago = TimeSpan.FromMinutes(11) }, + ], +}; +``` + +### Anatomy + +1. **Stack**: toasts share one 64px slot. The front toast sits at the edge; each one behind it steps back 10px and + shrinks 5%, with its text faded out. +2. **Mark**: a 36px round avatar for a person, a rounded square for a company. +3. **Sentence**: the name and the action on one line, truncated with an ellipsis. +4. **Second line**: the meta and the time, then the verified badge (an icon and a label). +5. **Dismiss**: a 24px button that appears on hover or keyboard focus, and always on touch screens. + +### Corner position + + + +`Position` pins the stack to a window corner: `BottomLeft`, `BottomRight`, `TopLeft` or `TopRight`. It is 360px wide +and 24px from the edges; on a window narrower than 480px it spans the width with 16px gutters. Top corners stack +downward. `Inline` (the default) keeps it in the layout, filling its container's width; use it for docs and previews, +and a corner on a real page. + +### Selecting and limiting + + + +`IsSelectable` makes each toast a button that raises `Selected`, for example to open pricing or a customer story. +`Limit` stops after that many toasts in a session so the page stays calm. `Shown` fires each time a toast appears (for +impression tracking) and `Dismissed` when a visitor closes one. + +### Pausing + +The cycle pauses on its own while the stack is hovered or holds keyboard focus, while it is scrolled out of view and +while it is hidden, and the countdown keeps its remaining time. Set `IsPaused` while a modal or cookie banner +is open. `ExpandOnHover="False"` keeps the stack closed on hover (it still pauses). `Prefill` shows that many toasts +at once without an entrance, which is useful for previews and screenshots. `Restart` clears the stack and starts the +feed again. + +### Your own words + +`Labels` holds every word the stack shows or says. Replace the object to localize or reword: + +```csharp +stack.Labels = new SocialProofToastLabels +{ + Region = "Actividad reciente", + JustNow = "Ahora mismo", + MinutesAgo = minutes => $"hace {minutes} min", + Dismiss = sentence => "Cerrar: " + sentence, +}; +``` + +### Content guidelines + +- Keep the action short and concrete: "started a Pro trial", "upgraded to Business". +- Use first names for people and the company name for teams. Do not show surnames or email addresses. +- Put one fact in `Meta`: a city or a seat count, not both. + +### Best practices + +- Feed it real, recent events from your billing or analytics backend. +- Keep it calm: an interval of 4 to 8 seconds and a limit of about 5 to 8 toasts per session. +- Pause it while modals or cookie banners are open. + +### Common questions + +**Do new toasts interrupt screen readers?** No. New toasts are not announced; only dismissals are confirmed, politely. + +**How many toasts are on screen?** At most `MaxVisible`, plus one fading out from the back. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- The stack is a labelled region ("Recent activity"). New toasts are not announced so screen reader users are not + interrupted every few seconds; a dismissal is confirmed in a polite status. +- Each dismiss button names the toast it closes, such as "Dismiss: Sofia started a Pro trial". +- Toasts fading out past the limit are hidden from assistive technology and take no input. Hover and keyboard focus + both pause the cycle, so nothing moves while someone reads or acts. +- The verified state is an icon plus a text label, never color alone. Avatars are not read separately because the name + is in the sentence. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the dismiss button of each visible toast (and to the toast itself when `IsSelectable` is set). Focus inside the stack pauses it and fans it out | +| Enter, Space | Dismisses the toast from its dismiss button, or selects the focused toast when `IsSelectable` is set | +| Escape | Dismisses the focused toast, or the front one. Focus moves to the next toast | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A new toast arrives | Rises 16px and fades in at the front; the others step back 10px and shrink 5% each on a bounce-free spring (visual duration 0.24s) | Fades in; the others move at once | At once | +| Covered toasts | Their content fades out over 160ms so text never overlaps | Same fade | At once | +| Hover or focus | The stack fans out on the same spring | At once | At once | +| Dismiss | Fades and shrinks slightly over 160ms; the rest close the gap on the spring | Fades; the rest move at once | At once | +| Past the limit | The back toast fades out over 160ms | Same fade | At once | + +Toasts are always 64px high, so nothing resizes between states, and the inline stack reserves the height of the fanned +stack so hovering never moves the page. The cycle keeps running with reduced motion and with motion off: it is +content, not decoration. + +## Related + +- `Toast` for a brief confirmation of the visitor's own action. +- `Avatar` for the people in the feed. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/zh-CN.md new file mode 100644 index 00000000..dc1a1668 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/zh-CN.md @@ -0,0 +1,162 @@ +--- +title: Social proof toast +summary: 用于落地页和定价页的社交证明通知堆叠:来自个人和公司的最新试用、升级与已验证购买每隔几秒出现一条;悬停时暂停并展开,访客可以关闭任意一条通知。 +source-hash: ff6c35ef997f +--- + + + +## 何时使用 + +`SocialProofToast` 展示其他人此刻正在做什么:开始了试用、团队完成了升级、一笔已验证的购买。每隔几秒,一条新通知从前方升起,较早的通知依次退到它后面。指针悬停在堆叠上,或键盘焦点进入其中,会暂停轮播并把通知展开,方便逐条阅读或关闭。 + +- **落地页和定价页**:展示最近的试用、购买或升级。 +- **发布页或候补名单页**:实时显示有人加入、有团队升级。 +- **结账按钮附近**:突出知名公司的已验证购买。 + +## 何时不用 + +- **访客自己操作之后的反馈**(已保存、已复制、失败):使用 `Toast`。 +- **已登录的产品内部**,且这些动态不是营销内容:使用动态列表。 +- **没有真实事件可展示时。** 捏造的社交证明会损害信任;模拟只用于预览。 + +## 用法 + +添加堆叠,并在 `Events` 中按从旧到新的顺序传入你的动态。在 `InitialDelay`(0.8 秒)之后,它每隔 `Interval`(默认 4 秒)显示一条通知,最多保留 `MaxVisible` 条(3 条),到末尾后从头开始,除非 `Loop` 为 false。没有事件也没有模拟(见下文)时,堆叠保持为空,直到你推送一条。 + +```csharp +// 计费后端近期动态中的一行。 +record Signup(string Id, string FirstName, string Action, string City, DateTimeOffset CreatedAt, bool IsPaid); + +stack.Events.AddRange(recent.Select(r => new SocialProofEvent(r.Id, r.FirstName, r.Action) +{ + Meta = r.City, + Time = r.CreatedAt, + VerifiedLabel = r.IsPaid ? "Verified purchase" : null, +})); +``` + +### 你自己的事件 + + + +每个 `SocialProofEvent` 都有稳定的 `Id`、一个 `Name`(名字或公司名,以中等字重显示)以及紧随其后的 `Action`。`Meta` 补充简短的上下文,例如城市或席位数。`Time` 显示为相对时间(Just now、4 min ago、2 h ago);`TimeText` 原样显示。`IsVerified` 或 `VerifiedLabel` 会加上一个带文字的对勾徽章。 + +个人显示为带姓名首字母的 `Avatar`,或其 `Portrait` 头像照片。把 `Actor` 设为 `Company` 时显示为圆角方块,内含名称的首字母,或你在 `Logo` 中提供的标志(24 × 24 的描边图形,以文字颜色绘制,因此跟随主题)。 + +调用 `Push` 可以在事件发生的那一刻显示它,例如来自服务器推送的事件。它会立即出现在最前面,下一条通知的倒计时也会重新开始。 + +用于预览时,让 `Events` 保持为空,并给堆叠一个 `SocialProofToastSimulation`:它的事件按从旧到新排列,每条都是一个 `SocialProofToastSimulationEvent`,包含事件本身以及它发生在多久之前(`Ago`)。每一轮都会从那一刻往回计算时间,因此动态永远不会漂移成 "40 min ago"。你自己的 `Events` 会让它让位,把 `Simulation` 设为 null 会清空堆叠。这里预览中的所有名字、公司和地点都是虚构的。 + +```csharp +stack.Simulation = new SocialProofToastSimulation +{ + Events = + [ + new SocialProofToastSimulationEvent { Event = new SocialProofEvent("s1", "Sofia", "started a Pro trial") { Meta = "Madrid" }, Ago = TimeSpan.FromMinutes(14) }, + new SocialProofToastSimulationEvent { Event = new SocialProofEvent("s2", "Mateo", "bought the Team plan") { Meta = "Lisbon" }, Ago = TimeSpan.FromMinutes(11) }, + ], +}; +``` + +### 结构 + +1. **堆叠**:所有通知共用一个 64px 的位置。最前面的通知贴着边缘;其后的每一条后退 10px、缩小 5%,文字淡出。 +2. **标识**:个人是 36px 的圆形头像,公司是圆角方块。 +3. **句子**:名称与动作在同一行,过长时以省略号截断。 +4. **第二行**:附加信息与时间,然后是已验证徽章(图标加文字)。 +5. **关闭**:24px 的按钮,在悬停或键盘聚焦时出现,在触摸屏上始终可见。 + +### 角落位置 + + + +`Position` 把堆叠固定在窗口的某个角落:`BottomLeft`、`BottomRight`、`TopLeft` 或 `TopRight`。它宽 360px,距离边缘 24px;窗口窄于 480px 时,它横跨整个宽度并留出 16px 边距。顶部角落向下堆叠。`Inline`(默认)让它留在布局中并填满容器宽度;文档和预览用它,真实页面请用角落位置。 + +### 选择与限制 + + + +`IsSelectable` 让每条通知成为一个按钮并引发 `Selected`,例如用来打开定价页或客户案例。`Limit` 在一次会话显示这么多条通知后停止,让页面保持安静。每当一条通知出现时触发 `Shown`(用于曝光统计),访客关闭一条通知时触发 `Dismissed`。 + +### 暂停 + +堆叠被悬停或持有键盘焦点时、滚出视野时以及被隐藏时,轮播都会自动暂停,倒计时保留剩余时间。模态框或 Cookie 横幅打开期间请设置 `IsPaused`。`ExpandOnHover="False"` 让堆叠在悬停时保持收起(仍会暂停)。`Prefill` 会立即显示这么多条通知且不播放入场动画,适合预览和截图。`Restart` 清空堆叠并从头开始播放动态。 + +### 自定义文案 + +`Labels` 包含堆叠显示或朗读的所有文字。替换整个对象即可本地化或改写: + +```csharp +stack.Labels = new SocialProofToastLabels +{ + Region = "Actividad reciente", + JustNow = "Ahora mismo", + MinutesAgo = minutes => $"hace {minutes} min", + Dismiss = sentence => "Cerrar: " + sentence, +}; +``` + +### 内容指南 + +- 动作要简短具体:"started a Pro trial"、"upgraded to Business"。 +- 个人用名字,团队用公司名称。不要显示姓氏或邮箱地址。 +- `Meta` 只放一项信息:城市或席位数,不要两者都放。 + +### 最佳实践 + +- 使用来自计费或分析后端的真实、近期事件。 +- 保持安静:间隔 4 到 8 秒,每次会话限制在 5 到 8 条左右。 +- 模态框或 Cookie 横幅打开时暂停它。 + +### 常见问题 + +**新通知会打断屏幕阅读器吗?** 不会。新通知不会被播报;只有关闭操作会以礼貌方式确认。 + +**屏幕上最多有几条通知?** 最多 `MaxVisible` 条,另加一条正从后方淡出的通知。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 堆叠是一个带标签的区域("Recent activity")。新通知不会被播报,屏幕阅读器用户不会每隔几秒就被打断;关闭一条通知后会以礼貌的状态提示确认。 +- 每个关闭按钮都会说明它关闭的是哪条通知,例如 "Dismiss: Sofia started a Pro trial"。 +- 超出限制而正在淡出的通知对辅助技术隐藏,也不接受输入。悬停和键盘焦点都会暂停轮播,因此有人阅读或操作时不会有东西移动。 +- 已验证状态是图标加文字标签,从不只靠颜色表达。头像不会被单独朗读,因为名称已经在句子里。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 移到每条可见通知的关闭按钮(设置 `IsSelectable` 时也会移到通知本身)。焦点进入堆叠会暂停并展开它 | +| Enter、Space | 在关闭按钮上关闭该通知;设置 `IsSelectable` 时选择获得焦点的通知 | +| Escape | 关闭获得焦点的通知,或最前面的通知。焦点移到下一条通知 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 新通知到达 | 在最前面上升 16px 并淡入;其他通知以无回弹弹簧(视觉时长 0.24s)各后退 10px、缩小 5% | 淡入;其他通知立即就位 | 立即 | +| 被遮住的通知 | 内容在 160ms 内淡出,文字从不重叠 | 同样淡出 | 立即 | +| 悬停或聚焦 | 堆叠以同一弹簧展开 | 立即 | 立即 | +| 关闭 | 在 160ms 内淡出并略微缩小;其余通知以弹簧补上空位 | 淡出;其余立即就位 | 立即 | +| 超出数量 | 最后面的通知在 160ms 内淡出 | 同样淡出 | 立即 | + +通知始终是 64px 高,因此各状态之间没有尺寸变化;内联堆叠预留了展开后的高度,悬停时页面不会移动。动效减弱或关闭时轮播仍会继续:它是内容,而不是装饰。 + +## 相关组件 + +- `Toast`:对访客自身操作的简短确认。 +- `Avatar`:动态中的人物。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/zh-TW.md new file mode 100644 index 00000000..36923201 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SocialProofToast/zh-TW.md @@ -0,0 +1,162 @@ +--- +title: Social proof toast +summary: 用於到達頁與定價頁的社會認同通知堆疊:來自個人與公司的最新試用、升級與已驗證購買每隔幾秒出現一則;滑鼠懸停時暫停並展開,訪客可以關閉任何一則通知。 +source-hash: ff6c35ef997f +--- + + + +## 何時使用 + +`SocialProofToast` 呈現其他人此刻正在做什麼:開始了試用、團隊完成了升級、一筆已驗證的購買。每隔幾秒,一則新通知從前方升起,較早的通知依序退到它後面。指標懸停在堆疊上,或鍵盤焦點進入其中,會暫停輪播並把通知展開,方便逐則閱讀或關閉。 + +- **到達頁與定價頁**:呈現最近的試用、購買或升級。 +- **發表頁或候補名單頁**:即時顯示有人加入、有團隊升級。 +- **結帳按鈕附近**:突顯知名公司的已驗證購買。 + +## 何時不用 + +- **訪客自己操作之後的回饋**(已儲存、已複製、失敗):使用 `Toast`。 +- **已登入的產品內部**,且這些動態不是行銷內容:使用動態清單。 +- **沒有真實事件可呈現時。** 捏造的社會認同會損害信任;模擬只用於預覽。 + +## 用法 + +加入堆疊,並在 `Events` 中依從舊到新的順序傳入你的動態。在 `InitialDelay`(0.8 秒)之後,它每隔 `Interval`(預設 4 秒)顯示一則通知,最多保留 `MaxVisible` 則(3 則),播到結尾後從頭開始,除非 `Loop` 為 false。沒有事件也沒有模擬(見下文)時,堆疊維持空白,直到你推送一則。 + +```csharp +// 計費後端近期動態中的一列。 +record Signup(string Id, string FirstName, string Action, string City, DateTimeOffset CreatedAt, bool IsPaid); + +stack.Events.AddRange(recent.Select(r => new SocialProofEvent(r.Id, r.FirstName, r.Action) +{ + Meta = r.City, + Time = r.CreatedAt, + VerifiedLabel = r.IsPaid ? "Verified purchase" : null, +})); +``` + +### 你自己的事件 + + + +每個 `SocialProofEvent` 都有穩定的 `Id`、一個 `Name`(名字或公司名稱,以中等字重顯示)以及緊接其後的 `Action`。`Meta` 補充簡短的脈絡,例如城市或席次數。`Time` 顯示為相對時間(Just now、4 min ago、2 h ago);`TimeText` 原樣顯示。`IsVerified` 或 `VerifiedLabel` 會加上一個附文字的勾選徽章。 + +個人以帶姓名縮寫的 `Avatar` 呈現,或使用其 `Portrait` 大頭照。把 `Actor` 設為 `Company` 時顯示為圓角方塊,內含名稱的第一個字母,或你在 `Logo` 中提供的標誌(24 × 24 的描邊圖形,以文字顏色繪製,因此會跟隨佈景主題)。 + +呼叫 `Push` 可以在事件發生的那一刻顯示它,例如來自伺服器推播的事件。它會立刻出現在最前面,下一則通知的倒數也會重新開始。 + +用於預覽時,讓 `Events` 維持空白,並給堆疊一個 `SocialProofToastSimulation`:它的事件依從舊到新排列,每則都是一個 `SocialProofToastSimulationEvent`,包含事件本身以及它發生在多久之前(`Ago`)。每一輪都會從那一刻往回計算時間,因此動態永遠不會漂移成 "40 min ago"。你自己的 `Events` 會讓它讓位,把 `Simulation` 設為 null 會清空堆疊。這裡預覽中的所有名字、公司與地點都是虛構的。 + +```csharp +stack.Simulation = new SocialProofToastSimulation +{ + Events = + [ + new SocialProofToastSimulationEvent { Event = new SocialProofEvent("s1", "Sofia", "started a Pro trial") { Meta = "Madrid" }, Ago = TimeSpan.FromMinutes(14) }, + new SocialProofToastSimulationEvent { Event = new SocialProofEvent("s2", "Mateo", "bought the Team plan") { Meta = "Lisbon" }, Ago = TimeSpan.FromMinutes(11) }, + ], +}; +``` + +### 結構 + +1. **堆疊**:所有通知共用一個 64px 的位置。最前面的通知貼齊邊緣;其後的每一則後退 10px、縮小 5%,文字淡出。 +2. **標識**:個人是 36px 的圓形大頭貼,公司是圓角方塊。 +3. **句子**:名稱與動作在同一行,過長時以刪節號截斷。 +4. **第二行**:附加資訊與時間,接著是已驗證徽章(圖示加文字)。 +5. **關閉**:24px 的按鈕,在懸停或鍵盤聚焦時出現,在觸控螢幕上一律顯示。 + +### 角落位置 + + + +`Position` 把堆疊固定在視窗的某個角落:`BottomLeft`、`BottomRight`、`TopLeft` 或 `TopRight`。它寬 360px,距離邊緣 24px;視窗窄於 480px 時,它會橫跨整個寬度並留下 16px 邊距。頂端角落向下堆疊。`Inline`(預設)讓它留在版面中並填滿容器寬度;文件與預覽用它,真實頁面請用角落位置。 + +### 選取與限制 + + + +`IsSelectable` 讓每則通知成為一個按鈕並引發 `Selected`,例如用來開啟定價頁或客戶案例。`Limit` 在一個工作階段顯示這麼多則通知後停止,讓頁面保持安靜。每當一則通知出現時觸發 `Shown`(用於曝光統計),訪客關閉一則通知時觸發 `Dismissed`。 + +### 暫停 + +堆疊被懸停或持有鍵盤焦點時、捲出視野時以及被隱藏時,輪播都會自動暫停,倒數會保留剩餘時間。強制回應視窗或 Cookie 橫幅開啟期間請設定 `IsPaused`。`ExpandOnHover="False"` 讓堆疊在懸停時保持收合(仍會暫停)。`Prefill` 會立即顯示這麼多則通知且不播放進場動畫,適合預覽與螢幕截圖。`Restart` 清空堆疊並從頭開始播放動態。 + +### 自訂文字 + +`Labels` 包含堆疊顯示或朗讀的所有文字。替換整個物件即可在地化或改寫: + +```csharp +stack.Labels = new SocialProofToastLabels +{ + Region = "Actividad reciente", + JustNow = "Ahora mismo", + MinutesAgo = minutes => $"hace {minutes} min", + Dismiss = sentence => "Cerrar: " + sentence, +}; +``` + +### 內容指引 + +- 動作要簡短具體:"started a Pro trial"、"upgraded to Business"。 +- 個人用名字,團隊用公司名稱。不要顯示姓氏或電子郵件地址。 +- `Meta` 只放一項資訊:城市或席次數,不要兩者都放。 + +### 最佳做法 + +- 使用來自計費或分析後端的真實、近期事件。 +- 保持安靜:間隔 4 到 8 秒,每個工作階段限制在 5 到 8 則左右。 +- 強制回應視窗或 Cookie 橫幅開啟時暫停它。 + +### 常見問題 + +**新通知會打斷螢幕閱讀器嗎?** 不會。新通知不會被播報;只有關閉操作會以禮貌方式確認。 + +**畫面上最多有幾則通知?** 最多 `MaxVisible` 則,另加一則正從後方淡出的通知。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 堆疊是一個有標籤的區域("Recent activity")。新通知不會被播報,螢幕閱讀器使用者不會每隔幾秒就被打斷;關閉一則通知後會以禮貌的狀態提示確認。 +- 每個關閉按鈕都會說明它關閉的是哪則通知,例如 "Dismiss: Sofia started a Pro trial"。 +- 超出限制而正在淡出的通知對輔助科技隱藏,也不接受輸入。懸停與鍵盤焦點都會暫停輪播,因此有人閱讀或操作時不會有東西移動。 +- 已驗證狀態是圖示加文字標籤,從不只靠顏色表達。大頭貼不會被單獨朗讀,因為名稱已經在句子裡。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 移到每則可見通知的關閉按鈕(設定 `IsSelectable` 時也會移到通知本身)。焦點進入堆疊會暫停並展開它 | +| Enter、Space | 在關閉按鈕上關閉該通知;設定 `IsSelectable` 時選取取得焦點的通知 | +| Escape | 關閉取得焦點的通知,或最前面的通知。焦點移到下一則通知 | + +## 動效 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 新通知到達 | 在最前面上升 16px 並淡入;其他通知以無回彈彈簧(視覺時長 0.24s)各後退 10px、縮小 5% | 淡入;其他通知立即就位 | 立即 | +| 被遮住的通知 | 內容在 160ms 內淡出,文字從不重疊 | 同樣淡出 | 立即 | +| 懸停或聚焦 | 堆疊以同一彈簧展開 | 立即 | 立即 | +| 關閉 | 在 160ms 內淡出並略微縮小;其餘通知以彈簧補上空位 | 淡出;其餘立即就位 | 立即 | +| 超出數量 | 最後面的通知在 160ms 內淡出 | 同樣淡出 | 立即 | + +通知一律是 64px 高,因此各狀態之間沒有尺寸變化;內嵌堆疊預留了展開後的高度,懸停時頁面不會移動。動態效果減弱或關閉時輪播仍會繼續:它是內容,而不是裝飾。 + +## 相關 + +- `Toast`:對訪客自身操作的簡短確認。 +- `Avatar`:動態中的人物。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/en-US.md new file mode 100644 index 00000000..67722ea0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/en-US.md @@ -0,0 +1,189 @@ +--- +title: Source cards +summary: A row of cited sources under an AI answer: cards stream into reserved slots while the model searches, the overflow collapses into a stack of site marks, hovering lifts a preview with the passage, and an inline [n] citation highlights its card. +--- + + + +## When to use + +`SourceCards` shows the pages an AI answer relied on. While the model searches, a spinner and a shimmering "Searching +3 of 7" sit over reserved slots, and each result fades into its slot as it arrives, so the row never shifts. When the +search is done the status reads "7 sources · 2.1s", the answer streams in above the cards, and a `SourceCitation` chip in +the answer highlights the card it cites. Past `Max` the rest collapse into a stack of overlapping marks ("+4 more"). +Hovering or focusing a card lifts a preview with the passage the answer used. + +- **Citations under an AI search or docs assistant answer.** +- **Live search progress** ("Searching 3 of 8") while an agent browses. +- **Linking inline [n] references** in generated text to the pages they came from. + +## When not to use + +- **A full search results page** with many results and filters: use a list layout instead. +- **Tool calls with arguments and outputs**: use `ToolCall`. +- **A single link preview inside prose**: use a tooltip or a hover card. + +## Usage + +Give the cards your sources in citation order, so source 1 is cited as [1]. Append to an observable list as results +arrive while `Status` is `Searching` and `Total` says how many will come; switch to `Done` when the model starts +answering. Put the answer in `Content`: it sits between the status line and the cards, and its height springs as it +streams so the cards glide down. + +```csharp +var sources = new AvaloniaList(); +cards.Sources = sources; +cards.Query = "webhook signature verification"; +cards.Total = 8; +cards.Status = SourceCardsStatus.Searching; + +// As each result arrives. +sources.Add(new SourceCardsSource(result.Title, result.Url) { Site = result.Site, Snippet = result.Passage }); + +// When the model starts answering. +cards.Duration = searchTime; +cards.Status = SourceCardsStatus.Done; +``` + +With no `Sources` the cards show the state you set; untouched, they read "No sources found". For a demo or a prototype +without a search, give them a `SourceCardsSimulation` with the sources and the answer to play (a word written as `[n]` +becomes a citation). It plays while `Sources` is null and only on screen, its `Scenario` picks Live, Error or Empty, and +it starts over when the scenario changes or the person presses Retry. Its cards open nothing; setting `Sources` ends it. + +```csharp +cards.Simulation = new SourceCardsSimulation +{ + Sources = recordedResults, // SourceCardsSource[] in citation order + Query = "webhook signature verification", + Answer = "Read the raw body before you parse it [1].", + Duration = TimeSpan.FromSeconds(1.4), + Scenario = SourceCardsScenario.Live, +}; +``` + +### Streaming an answer with citations + + + +A `SourceCitation` is an inline chip. Put it in the answer with `InlineUIContainer`; inside `SourceCards` it names its +source for screen readers, turns accent while its card is highlighted, and pressing it highlights the card for 2.6s +(opening the stack when the card is in it). + +```xml + + + + + + + + + +``` + +Set `Highlighted` yourself to hold a highlight (for example while the reader hovers a sentence), or call +`HighlightSource(n)` for a timed one. `HighlightChanged` reports citation presses. + +### The stack and narrow containers + + + +Wide containers show `Max` + 1 equal columns; below 540px the grid has two columns and the stack is the last tile. +The stack, the Show all toggle and a citation that points into the stack open it (`IsExpanded`, two-way) and raise +`ExpandedChanged`. + +### Your own reader and failures + + + +Pressing a card raises `SourceInvoked` and then opens the address in the browser. Mark the event handled to open the +source in your own reader instead, or set `OpensLinks="False"` so cards never open links. A failed search keeps what +arrived, drops its reserved slots and shows "Search failed" with `ErrorText`; `AllowsRetry` adds Retry (and Search +again when nothing was found), both raising `RetryRequested`. + +### Status line + +| Status | Icon | Reads | +|---|---|---| +| `Searching` | Spinner | "Searching 3 of 7" and the query | +| `Done` | Globe | "7 sources · 2.1s" | +| `Done`, no sources | Crossed search | "No sources found" and the query | +| `Error` | Alert | "Search failed" and `ErrorText` | + +Set `IsStatusVisible="False"` when your UI already shows the search progress; Show all and Retry stay. + +### Site marks + +The card draws a lettered disc from the domain (W for webref.example). Pass the site's favicon shape as a 24 × 24 stroked +geometry in `SourceCardsSource.Icon` rather than inventing a logo. + +### Your own words + +`Labels` holds every word the cards and their citations show or say. Replace the object to localize or reword: + +```csharp +cards.Labels = new SourceCardsLabels +{ + Searching = "Recherche", + Count = n => n == 1 ? "1 source" : $"{n} sources", + Progress = (n, total) => total is { } t ? $"{n} sur {t}" : $"{n}", +}; +``` + +### Best practices + +- Keep sources in citation order so [n] maps to card n; append, do not rebuild the list. +- Pass `Total` while searching so the slots are reserved and nothing shifts. +- Do not wrap cards in your own links: handle `SourceInvoked` for in-app reading. + +## API reference + + + + + + + + + + + +## Accessibility + +- The cards are a list named "Sources" that reports itself busy while searching; reserved slots are hidden from + assistive technology. +- A polite live region says each status once: searching for the query, the number found, no sources, or the failure. + Counts do not announce on every card. The simulation says that it is simulated and that its cards open nothing. +- Each card's name reads its number, title and domain, and says when it opens in the browser. While its preview is open + the passage is the card's help text. +- `SourceCitation` is a real button named "Source 2: Request: text() method, webref.example"; its highlight is never color + alone, because the card also gains an outline. +- The status always pairs an icon with words. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through Retry or Show all, the citations, each card and the stack. A focused card shows its preview | +| Enter, Space | Opens a card (or raises `SourceInvoked`), opens the stack, or highlights a citation's card | +| Escape | Closes the open preview | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A card arrives | Fades in rising 4px over 180ms; its slot fades out over 120ms | Fades, no rise | At once | +| Reserved slots | Bars pulse | Still | Still | +| The stack closes, or a failed search drops its slots | Leaving cells fade in place over 120ms while the stack fades into its cell; the grid's height glides once | Fades, height at once | At once | +| A count changes | Rolls 5px in a reserved width (tabular digits) | At once | At once | +| Status icon and words | Crossfade, 100ms out and 160ms in | 120ms crossfade | At once | +| The answer streams | Its height springs without bounce, so the cards glide down; a width change cuts straight to the new height | At once | At once | +| A preview opens | After a 320ms hover, fades and rises 4px from 0.98 scale over 160ms; it then switches between cards at once | Fades | At once | + +Nothing bounces. The simulation pauses when it is scrolled off screen. + +## Related + +- `ToolCall` for the tool calls an agent makes. +- `AgentPlan` for the plan an agent follows while it searches. +- `AgentStatus` for the live status of a whole run. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/zh-CN.md new file mode 100644 index 00000000..7c6e1445 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/zh-CN.md @@ -0,0 +1,167 @@ +--- +title: Source cards +summary: AI 回答下方的一排引用来源:模型搜索时卡片流入预留的位置,超出的部分收拢为一叠网站标记,悬停时浮出带原文片段的预览,正文中的 [n] 引用会高亮对应的卡片。 +source-hash: 5ae8e5408b8e +--- + + + +## 何时使用 + +`SourceCards` 展示 AI 回答所依据的网页。模型搜索时,一个加载圈和闪烁的 "Searching 3 of 7" 位于预留的位置之上,每条结果到达时淡入自己的位置,因此这一排不会跳动。搜索完成后,状态行显示 "7 sources · 2.1s",回答在卡片上方流式出现,回答中的 `SourceCitation` 标签会高亮它所引用的卡片。超过 `Max` 的来源收拢为一叠相互重叠的标记("+4 more")。悬停或聚焦一张卡片时,会浮出一个预览,显示回答所用的原文片段。 + +- **AI 搜索或文档助手回答下方的引用。** +- **实时的搜索进度**("Searching 3 of 8"),在智能体浏览网页时显示。 +- **把生成文本中的行内 [n] 引用**链接到它们出自的网页。 + +## 何时不用 + +- **完整的搜索结果页**,包含大量结果和筛选条件:请改用列表布局。 +- **带参数和输出的工具调用**:使用 `ToolCall`。 +- **正文中的单个链接预览**:使用工具提示或悬停卡片。 + +## 用法 + +按引用顺序把来源交给卡片,这样第 1 个来源就以 [1] 引用。`Status` 为 `Searching` 且 `Total` 说明将有多少条结果时,随着结果到达向一个可观察列表追加;模型开始作答时切换为 `Done`。把回答放进 `Content`:它位于状态行和卡片之间,流式输出时高度以弹簧变化,卡片随之平滑下移。 + +```csharp +var sources = new AvaloniaList(); +cards.Sources = sources; +cards.Query = "webhook signature verification"; +cards.Total = 8; +cards.Status = SourceCardsStatus.Searching; + +// As each result arrives. +sources.Add(new SourceCardsSource(result.Title, result.Url) { Site = result.Site, Snippet = result.Passage }); + +// When the model starts answering. +cards.Duration = searchTime; +cards.Status = SourceCardsStatus.Done; +``` + +没有 `Sources` 时,卡片显示你设置的状态;什么都不设置时显示 "No sources found"。用于没有搜索的演示或原型时,给卡片一个 `SourceCardsSimulation`,并传入要播放的来源和回答(写成 `[n]` 的词会变成引用)。它在 `Sources` 为 null 且显示在屏幕上时播放,`Scenario` 可选 Live、Error 或 Empty,切换场景或按下重试时会从头开始。模拟的卡片不会打开任何链接;设置 `Sources` 后模拟即结束。 + +```csharp +cards.Simulation = new SourceCardsSimulation +{ + Sources = recordedResults, // SourceCardsSource[] in citation order + Query = "webhook signature verification", + Answer = "Read the raw body before you parse it [1].", + Duration = TimeSpan.FromSeconds(1.4), + Scenario = SourceCardsScenario.Live, +}; +``` + +### 流式输出带引用的回答 + + + +`SourceCitation` 是一个行内标签。用 `InlineUIContainer` 把它放进回答;在 `SourceCards` 内部,它会为屏幕阅读器说出自己的来源,在对应卡片高亮时变为强调色,按下它会把卡片高亮 2.6 秒(卡片在叠放中时会先展开)。 + +```xml + + + + + + + + + +``` + +自己设置 `Highlighted` 可以保持高亮(例如读者悬停在某句话上时),调用 `HighlightSource(n)` 则得到一次定时高亮。`HighlightChanged` 报告引用被按下。 + +### 叠放与窄容器 + + + +宽容器显示 `Max` + 1 个等宽列;窄于 540px 时网格变为两列,叠放是最后一格。叠放、Show all 开关以及指向叠放内部的引用都会把它展开(`IsExpanded`,双向绑定),并引发 `ExpandedChanged`。 + +### 自己的阅读器与失败 + + + +按下卡片会引发 `SourceInvoked`,然后在浏览器中打开地址。把事件标记为已处理,就能改在自己的阅读器里打开来源;或者设置 `OpensLinks="False"`,让卡片从不打开链接。搜索失败时保留已经到达的结果,去掉预留的位置,并显示 "Search failed" 和 `ErrorText`;`AllowsRetry` 会加上 Retry(没有找到任何结果时是 Search again),两者都引发 `RetryRequested`。 + +### 状态行 + +| 状态 | 图标 | 显示 | +|---|---|---| +| `Searching` | 加载圈 | "Searching 3 of 7" 和查询词 | +| `Done` | 地球 | "7 sources · 2.1s" | +| `Done`,没有来源 | 带叉的搜索 | "No sources found" 和查询词 | +| `Error` | 警示 | "Search failed" 和 `ErrorText` | + +当界面已经显示搜索进度时,设置 `IsStatusVisible="False"`;Show all 和 Retry 仍会保留。 + +### 网站标记 + +卡片根据域名绘制一个带字母的圆(webref.example 显示 W)。请把网站图标的形状作为 24 × 24 的描边几何图形传给 `SourceCardsSource.Icon`,而不是自行编造标志。 + +### 自定义文案 + +`Labels` 包含卡片及其引用显示或朗读的每一个词。替换整个对象即可本地化或改写: + +```csharp +cards.Labels = new SourceCardsLabels +{ + Searching = "Recherche", + Count = n => n == 1 ? "1 source" : $"{n} sources", + Progress = (n, total) => total is { } t ? $"{n} sur {t}" : $"{n}", +}; +``` + +### 最佳实践 + +- 让来源保持引用顺序,使 [n] 对应第 n 张卡片;追加,而不是重建列表。 +- 搜索时传入 `Total`,这样位置会被预留,什么都不会跳动。 +- 不要用自己的链接包裹卡片:在应用内阅读请处理 `SourceInvoked`。 + +## API 参考 + + + + + + + + + + + +## 无障碍 + +- 卡片是一个名为 "Sources" 的列表,搜索时报告自己处于忙碌状态;预留的位置对辅助技术隐藏。 +- 一个礼貌型实时区域对每种状态只播报一次:正在搜索某个查询、找到的数量、没有来源,或失败。不会在每张卡片到达时播报数量。模拟搜索会说明它是模拟的,其卡片不会打开任何内容。 +- 每张卡片的名称依次读出编号、标题和域名,并说明它会在浏览器中打开。预览打开期间,原文片段是这张卡片的帮助文本。 +- `SourceCitation` 是一个真正的按钮,名称如 "Source 2: Request: text() method, webref.example";它的高亮从不只靠颜色,因为对应卡片还会出现轮廓。 +- 状态行总是图标与文字并用。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过 Retry 或 Show all、引用、每张卡片和叠放。获得焦点的卡片会显示它的预览 | +| Enter, Space | 打开卡片(或引发 `SourceInvoked`)、展开叠放,或高亮引用对应的卡片 | +| Escape | 关闭打开的预览 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 卡片到达 | 在 180ms 内淡入并上升 4px;它的占位在 120ms 内淡出 | 只淡入,不上升 | 立即 | +| 预留的位置 | 条块脉动 | 静止 | 静止 | +| 叠放收起,或失败的搜索去掉占位 | 离开的格子在原地用 120ms 淡出,同时叠放淡入它的格子;网格高度平滑过渡一次 | 淡入淡出,高度立即变化 | 立即 | +| 数量变化 | 在预留宽度内滚动 5px(等宽数字) | 立即 | 立即 | +| 状态图标与文字 | 交叉淡化,淡出 100ms、淡入 160ms | 120ms 交叉淡化 | 立即 | +| 回答流式输出 | 高度以无回弹的弹簧变化,卡片平滑下移;宽度变化时直接跳到新高度 | 立即 | 立即 | +| 预览打开 | 悬停 320ms 后,在 160ms 内从 0.98 倍缩放淡入并上升 4px;之后在卡片之间立即切换 | 只淡入 | 立即 | + +没有任何回弹。模拟搜索在滚出屏幕时暂停。 + +## 相关组件 + +- `ToolCall`:智能体发起的工具调用。 +- `AgentPlan`:智能体搜索时遵循的计划。 +- `AgentStatus`:整个运行过程的实时状态。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/zh-TW.md new file mode 100644 index 00000000..6ce3fa4f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SourceCards/zh-TW.md @@ -0,0 +1,167 @@ +--- +title: Source cards +summary: AI 回答下方的一排引用來源:模型搜尋時卡片流入預留的位置,超出的部分收攏為一疊網站標記,懸停時浮出帶原文片段的預覽,內文中的 [n] 引用會醒目標示對應的卡片。 +source-hash: 5ae8e5408b8e +--- + + + +## 何時使用 + +`SourceCards` 呈現 AI 回答所依據的網頁。模型搜尋時,一個載入圈和閃爍的 "Searching 3 of 7" 位於預留的位置之上,每筆結果抵達時淡入自己的位置,因此這一排不會跳動。搜尋完成後,狀態列顯示 "7 sources · 2.1s",回答在卡片上方串流出現,回答中的 `SourceCitation` 標籤會醒目標示它所引用的卡片。超過 `Max` 的來源收攏為一疊相互重疊的標記("+4 more")。懸停或聚焦一張卡片時,會浮出一個預覽,顯示回答所用的原文片段。 + +- **AI 搜尋或文件助理回答下方的引用。** +- **即時的搜尋進度**("Searching 3 of 8"),在代理瀏覽網頁時顯示。 +- **把生成文字中的行內 [n] 引用**連結到它們出自的網頁。 + +## 何時不用 + +- **完整的搜尋結果頁**,包含大量結果和篩選條件:請改用清單版面。 +- **帶參數和輸出的工具呼叫**:使用 `ToolCall`。 +- **內文中的單一連結預覽**:使用工具提示或懸停卡片。 + +## 用法 + +依引用順序把來源交給卡片,這樣第 1 個來源就以 [1] 引用。`Status` 為 `Searching` 且 `Total` 說明將有多少筆結果時,隨著結果抵達向一個可觀察清單附加;模型開始作答時切換為 `Done`。把回答放進 `Content`:它位於狀態列和卡片之間,串流輸出時高度以彈簧變化,卡片隨之平滑下移。 + +```csharp +var sources = new AvaloniaList(); +cards.Sources = sources; +cards.Query = "webhook signature verification"; +cards.Total = 8; +cards.Status = SourceCardsStatus.Searching; + +// As each result arrives. +sources.Add(new SourceCardsSource(result.Title, result.Url) { Site = result.Site, Snippet = result.Passage }); + +// When the model starts answering. +cards.Duration = searchTime; +cards.Status = SourceCardsStatus.Done; +``` + +沒有 `Sources` 時,卡片顯示你設定的狀態;什麼都不設定時顯示 "No sources found"。用於沒有搜尋的展示或原型時,給卡片一個 `SourceCardsSimulation`,並傳入要播放的來源和回答(寫成 `[n]` 的詞會變成引用)。它在 `Sources` 為 null 且顯示在畫面上時播放,`Scenario` 可選 Live、Error 或 Empty,切換情境或按下重試時會從頭開始。模擬的卡片不會開啟任何連結;設定 `Sources` 後模擬即結束。 + +```csharp +cards.Simulation = new SourceCardsSimulation +{ + Sources = recordedResults, // SourceCardsSource[] in citation order + Query = "webhook signature verification", + Answer = "Read the raw body before you parse it [1].", + Duration = TimeSpan.FromSeconds(1.4), + Scenario = SourceCardsScenario.Live, +}; +``` + +### 串流輸出帶引用的回答 + + + +`SourceCitation` 是一個行內標籤。用 `InlineUIContainer` 把它放進回答;在 `SourceCards` 內部,它會為螢幕閱讀器說出自己的來源,在對應卡片醒目標示時變為強調色,按下它會把卡片醒目標示 2.6 秒(卡片在疊放中時會先展開)。 + +```xml + + + + + + + + + +``` + +自行設定 `Highlighted` 可以保持醒目標示(例如讀者懸停在某句話上時),呼叫 `HighlightSource(n)` 則得到一次定時的醒目標示。`HighlightChanged` 回報引用被按下。 + +### 疊放與窄容器 + + + +寬容器顯示 `Max` + 1 個等寬欄;窄於 540px 時網格變為兩欄,疊放是最後一格。疊放、Show all 開關以及指向疊放內部的引用都會把它展開(`IsExpanded`,雙向繫結),並引發 `ExpandedChanged`。 + +### 自己的閱讀器與失敗 + + + +按下卡片會引發 `SourceInvoked`,然後在瀏覽器中開啟網址。把事件標記為已處理,就能改在自己的閱讀器裡開啟來源;或者設定 `OpensLinks="False"`,讓卡片從不開啟連結。搜尋失敗時保留已經抵達的結果,移除預留的位置,並顯示 "Search failed" 和 `ErrorText`;`AllowsRetry` 會加上 Retry(找不到任何結果時是 Search again),兩者都引發 `RetryRequested`。 + +### 狀態列 + +| 狀態 | 圖示 | 顯示 | +|---|---|---| +| `Searching` | 載入圈 | "Searching 3 of 7" 和查詢詞 | +| `Done` | 地球 | "7 sources · 2.1s" | +| `Done`,沒有來源 | 帶叉的搜尋 | "No sources found" 和查詢詞 | +| `Error` | 警示 | "Search failed" 和 `ErrorText` | + +當介面已經顯示搜尋進度時,設定 `IsStatusVisible="False"`;Show all 和 Retry 仍會保留。 + +### 網站標記 + +卡片依網域繪製一個帶字母的圓(webref.example 顯示 W)。請把網站圖示的形狀作為 24 × 24 的描邊幾何圖形傳給 `SourceCardsSource.Icon`,而不是自行編造標誌。 + +### 自訂文案 + +`Labels` 包含卡片及其引用顯示或朗讀的每一個詞。替換整個物件即可在地化或改寫: + +```csharp +cards.Labels = new SourceCardsLabels +{ + Searching = "Recherche", + Count = n => n == 1 ? "1 source" : $"{n} sources", + Progress = (n, total) => total is { } t ? $"{n} sur {t}" : $"{n}", +}; +``` + +### 最佳實務 + +- 讓來源保持引用順序,使 [n] 對應第 n 張卡片;附加,而不是重建清單。 +- 搜尋時傳入 `Total`,這樣位置會被預留,什麼都不會跳動。 +- 不要用自己的連結包住卡片:在應用程式內閱讀請處理 `SourceInvoked`。 + +## API 參考 + + + + + + + + + + + +## 無障礙 + +- 卡片是一個名為 "Sources" 的清單,搜尋時回報自己處於忙碌狀態;預留的位置對輔助技術隱藏。 +- 一個禮貌型即時區域對每種狀態只播報一次:正在搜尋某個查詢、找到的數量、沒有來源,或失敗。不會在每張卡片抵達時播報數量。模擬搜尋會說明它是模擬的,其卡片不會開啟任何內容。 +- 每張卡片的名稱依序讀出編號、標題和網域,並說明它會在瀏覽器中開啟。預覽開啟期間,原文片段是這張卡片的說明文字。 +- `SourceCitation` 是一個真正的按鈕,名稱如 "Source 2: Request: text() method, webref.example";它的醒目標示從不只靠顏色,因為對應卡片還會出現外框。 +- 狀態列總是圖示與文字並用。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過 Retry 或 Show all、引用、每張卡片和疊放。取得焦點的卡片會顯示它的預覽 | +| Enter, Space | 開啟卡片(或引發 `SourceInvoked`)、展開疊放,或醒目標示引用對應的卡片 | +| Escape | 關閉開啟中的預覽 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 卡片抵達 | 在 180ms 內淡入並上升 4px;它的佔位在 120ms 內淡出 | 只淡入,不上升 | 立即 | +| 預留的位置 | 條塊脈動 | 靜止 | 靜止 | +| 疊放收起,或失敗的搜尋移除佔位 | 離開的格子在原地用 120ms 淡出,同時疊放淡入它的格子;網格高度平滑過渡一次 | 淡入淡出,高度立即變化 | 立即 | +| 數量變化 | 在預留寬度內捲動 5px(等寬數字) | 立即 | 立即 | +| 狀態圖示與文字 | 交叉淡化,淡出 100ms、淡入 160ms | 120ms 交叉淡化 | 立即 | +| 回答串流輸出 | 高度以無回彈的彈簧變化,卡片平滑下移;寬度變化時直接跳到新高度 | 立即 | 立即 | +| 預覽開啟 | 懸停 320ms 後,在 160ms 內從 0.98 倍縮放淡入並上升 4px;之後在卡片之間立即切換 | 只淡入 | 立即 | + +沒有任何回彈。模擬搜尋在捲出畫面時暫停。 + +## 相關元件 + +- `ToolCall`:代理發起的工具呼叫。 +- `AgentPlan`:代理搜尋時遵循的計畫。 +- `AgentStatus`:整個執行過程的即時狀態。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/en-US.md new file mode 100644 index 00000000..d0544524 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/en-US.md @@ -0,0 +1,239 @@ +--- +title: Speech player +summary: A read-aloud player for AI replies: Read aloud turns into a compact player with play, a scrubbable progress bar, speed and a voice picker, and a highlight follows the spoken word through the reply. +--- + + + +## When to use + +`SpeechPlayer` reads an AI reply out loud. The reply sits above its action row; Read aloud turns the row into a compact +player of the same height, with play and pause, the elapsed time, a thin progress bar you can scrub, the total time, a +speed button, a voice picker and Stop reading. While it plays, one rounded highlight follows the spoken word through +the text, karaoke style, and the words still to come stay dimmed. + +- **Chat and answer surfaces** where people want a reply read out and the spoken word followed in the text. +- **Accessibility and hands-free reading** of long answers, summaries or briefings. +- **Previewing text to speech voices and speeds** against real product copy. + +## When not to use + +- **Turning speech into text** in a composer: use a dictation control. +- **Podcasts or long recordings** with no transcript to highlight: use a plain audio player. +- **Text that is not exactly what is spoken**: strip Markdown and formatting first, or the highlight drifts. + +## Usage + +Set `Text` to the reply and `Synthesize` to a call to your text to speech service. It receives a `SpeechRequest` (the +text, the voice and a cancellation token) and returns a `SpeechAudio`: the playback as an `ISpeechAudio`, the word +timings and the length. The player drives the playback (play, pause, seek, rate) and reads its position on every frame, +so your audio is the clock. + +```csharp +player.Text = message.Text; +player.Synthesize = async request => +{ + var speech = await tts.SpeakAsync(request.Text, request.Voice.Id, request.CancellationToken); + return new SpeechAudio + { + Audio = new MyAudioAdapter(speech.Stream), // implements ISpeechAudio + Timings = speech.Words // one start and end per word, or null + .Select(w => new SpeechWordTiming(w.Start, w.End)) + .ToList(), + }; +}; +``` + +Each whitespace separated word of `Text` is one highlight step, and blank lines start new paragraphs, so a provider's +per-word alignment maps one to one. Without timings, the player estimates them from the text and fits them to the +audio's length (`SpeechSimulation.EstimateTimings`, `SpeechSimulation.FitTimings`). Without `Synthesize` the player is a +timed simulation that plays no sound. Without `Text` (and without a simulation, below) the player is empty: no reply, +and Read aloud waits disabled (the Empty tab above). Without `Voices` the picker offers one voice with an empty id, your +service's default, named by `SpeechPlayerLabels.DefaultVoice`. + +### Anatomy + +1. **Author line**: a 16px mark and the model's name (`AuthorName`, `AuthorMark`); hidden when there is no author. +2. **Reply**: the text in paragraphs. While reading, the spoken word sits on an accent highlight, spoken words are in + full color and the words to come are dimmed. +3. **Dock**: one 32px slot that holds either the action row (Read aloud, then Copy, Good response and Bad response, or + your `Actions`) or the player. +4. **Player**: play, elapsed time, progress bar, total time, speed, voice and Stop reading. + +### States + +| State | The player shows | +|---|---| +| `Idle` | The action row with Read aloud | +| `Preparing` | A spinner in the play button and a shimmering "Preparing Ava's voice" | +| `Playing` | Pause, the moving bar and the highlight following the words | +| `Paused` | Play; the highlight rests on the word | +| `Finished` | Play again; the whole reply in full color | +| `Failed` | Retry, an alert icon and "Couldn't generate audio" with the reason your service threw | + +Picking another voice while reading aborts the request for the old voice, fetches the new one and resumes from the word +being spoken. Speech is kept per voice, so going back to a voice, or reading again after Stop reading, starts at once. + +### In a chat column + + + +`Variant="Plain"` drops the border and the padding so the reply sits in the transcript. `Actions` replaces Copy, Good +response and Bad response with your own buttons; `ShowsDefaultActions="False"` leaves Read aloud alone. `Voices` sets +the picker's voices, each with an id, a name and one short line of description. + +### Events + + + +`Played`, `Paused`, `Ended` and `Seeked` carry the time and the length on the 1× timeline, the word index, the voice +and the speed. `WordChanged` adds the word, for a caption or analytics. `Seeked` is raised once when a drag on the bar +ends, not on every move. `VoiceChanged`, `SpeedChanged` and `Closed` complete the set. `SelectedVoiceId` and `Speed` +are two-way. + +```csharp +player.WordChanged += (_, e) => caption.Text = e.Word; +player.Played += (_, e) => analytics.Track("tts_play", e.Voice.Id, e.Speed); +``` + +### Driving it from code + +`Open()` does what Read aloud does, `Play()`, `Pause()`, `TogglePlayback()`, `Seek(TimeSpan)`, `SeekToWord(int)`, +`Retry()` and `Close()` do what their buttons do. `State`, `IsOpen`, `CurrentWordIndex`, `Position` and `Duration` say +where it is. + +### Simulated speech + +For a demo, leave `Text` unset and give the player a `SpeechSimulation` with your own reply, author and voices. It reads +once the player is in view and, with full motion, reads again with the other of its first two voices after a short +pause, until someone interacts. The app's own `Text` always wins, and setting `Simulation` to null stops it. The +preview's reply and voices are invented; its author is a real model, Claude Sonnet 5.5. + +```csharp +player.Simulation = new SpeechSimulation +{ + Text = "Lisbon in early May is a lovely choice. Mornings start near 16°C…", + AuthorName = "Claude Sonnet 5.5", + Voices = [new SpeechVoice("ava", "Ava", "Warm and conversational"), new SpeechVoice("leo", "Leo", "Calm, lower register")], +}; +``` + +`SpeechSimulation.SynthesizeAsync` is the stand-in behind every player without `Synthesize`: it waits like a network +round trip (640ms) and answers with estimated timings and no audio. Pass `Fail = true` to see the failed state, or a longer `Delay` to see +Preparing. Keep `IsSimulated` on while you use it, so the voice menu and the announcements say that no audio plays. + +```csharp +player.Synthesize = request => SpeechSimulation.SynthesizeAsync(request, + new SpeechSimulationOptions { Delay = TimeSpan.FromSeconds(2) }); +``` + +### Your own words + +`Labels` holds every word the player shows or says. Replace the object to localize or reword: + +```csharp +player.Labels = new SpeechPlayerLabels +{ + ReadAloud = "Vorlesen", + StopReading = "Vorlesen beenden", + PreparingVoice = voice => $"Stimme {voice} wird vorbereitet", +}; +``` + +### Narrow containers and touch + +The card fills its container up to 640px. Below 440px the total time steps aside so the bar keeps its room; the elapsed +time, speed, voice and Stop reading stay. After a touch, the dock grows to 36px so every control meets the touch target +size. + +### Best practices + +- Pass exactly the text that is spoken: the number of words must match the timings. +- Throw from `Synthesize` with a short, human message; it is shown next to "Couldn't generate audio". +- Honor the request's cancellation token: a new voice, Stop reading or new text cancels it. +- Use the plain variant inside a transcript and pass actions that match the rest of the message row. + +### Common questions + +**Does it play sound by itself?** No. `TKCode.Arc.Special` has no audio dependency; your `ISpeechAudio` plays the +sound. Without it the player keeps time on its own and plays nothing. + +**What if my provider has no word timings?** Return the audio alone. The timings are estimated from the text and +fitted to the audio's length. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The player is a labelled group. The play button's name follows its state: Play, Pause, Play again, Retry or + Preparing audio (it keeps focus while a request is in flight but does nothing). +- The progress bar is a slider whose value text reads "0:04 of 0:22", updated once a second rather than every frame. +- A polite status region says what changed: reading with Ava at 1× (simulated, no audio plays), paused at 0:12, + switching to Leo, finished, the failure, speed changes and feedback. +- Status never relies on color: a failure pairs an alert icon with "Couldn't generate audio" and the reason; dimmed + words to come keep body contrast and are only a progress cue. +- Read aloud is described by the reply. The voice menu has radio rows and opens on the selected voice. +- The hidden half of the dock leaves the tree once it has faded, so Tab never lands on a control you cannot see. + Pointer focus draws no ring; keyboard focus draws Arc's focus ring. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through Read aloud and the actions, or, while reading, through play, the progress bar, speed, voice and Stop reading | +| Enter, Space | On Read aloud, opens the player and starts reading with focus on play; on play, plays or pauses; on Stop reading, closes the player and returns focus to Read aloud | +| Arrow Left, Arrow Right | On the progress bar, 2 seconds back or forward | +| Page Down, Page Up | On the progress bar, 10 seconds back or forward | +| Home, End | On the progress bar, the start or the end | +| Space, K | On the progress bar, play or pause | +| Arrow Up, Arrow Down | In the voice menu, move between voices; Enter picks one and Escape closes the menu | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Read aloud and Stop reading | The row fades out over 100ms, the other fades in over 160ms and rises 2px, in the same 32px slot | 120ms crossfade | At once | +| The highlight moves along a line | Glides in 150ms at 1×, 100ms at 1.5×, 75ms at 2× | Moves at once | At once | +| The highlight reaches a new line | Cuts to it | Cuts to it | Cuts to it | +| A word is spoken | Fades from dimmed to full color as the highlight lands | Fades | At once | +| Reading starts or stops | The dimming and the highlight fade over 160ms | Fade | At once | +| Progress bar | Fill and thumb follow every frame; hover, focus and drag thicken the track and scale the thumb in over 160ms | Thickens at once | At once | +| Play, pause, play again, retry, spinner | Swap in place with a bounce-free scale (0.26s) and a 160ms fade | 120ms fade | At once | +| Speed and voice labels | Crossfade over hidden copies of every option, so the buttons never resize | 120ms fade | At once | +| Preparing label | A calm shimmer | Still | Still | +| A simulation | Loops through two voices until someone interacts | Reads once | Reads once | + +The clock runs at every motion level: reading is content, not decoration. A resize re-measures the highlight without +animating it. + +## Related + +- `AgentStatus` for the live status of an agent while it works on the reply. +- `ToolCall` for the tools an agent called while answering. +- `CopyButton` for the copy action in a message row. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/zh-CN.md new file mode 100644 index 00000000..3985c6fe --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/zh-CN.md @@ -0,0 +1,222 @@ +--- +title: Speech player +summary: 为 AI 回复朗读的播放器:Read aloud 变成一个紧凑的播放器,带播放、可拖动的进度条、语速和音色选择,高亮随朗读的词在回复中移动。 +source-hash: 0f58f8c877f9 +--- + + + +## 何时使用 + +`SpeechPlayer` 把 AI 的回复朗读出来。回复下方是它的操作行;Read aloud 会把这一行变成同样高度的紧凑播放器:播放与暂停、已播放时间、 +一条可以拖动的细进度条、总时长、语速按钮、音色选择和 Stop reading。播放时,一个圆角高亮像卡拉 OK 一样随朗读到的词在文本中移动, +尚未朗读的词保持变淡。 + +- **聊天与回答界面**:用户希望回复被读出来,并在文本中跟随正在朗读的词。 +- **无障碍与免看阅读**:朗读较长的回答、摘要或简报。 +- **试听文本转语音的音色与语速**:用真实的产品文案对比效果。 + +## 何时不用 + +- **把语音转成文字**写进输入框:使用听写控件。 +- **播客或长录音**,没有可高亮的文字稿:使用普通的音频播放器。 +- **文本与实际朗读内容不一致**:先去掉 Markdown 和格式,否则高亮会错位。 + +## 用法 + +把 `Text` 设为回复内容,把 `Synthesize` 设为调用你的文本转语音服务的函数。它收到一个 `SpeechRequest`(文本、音色和取消令牌), +返回一个 `SpeechAudio`:以 `ISpeechAudio` 表示的播放、逐词时间和总时长。播放器驱动播放(播放、暂停、跳转、倍速),并在每一帧读取 +它的位置,所以你的音频就是时钟。 + +```csharp +player.Text = message.Text; +player.Synthesize = async request => +{ + var speech = await tts.SpeakAsync(request.Text, request.Voice.Id, request.CancellationToken); + return new SpeechAudio + { + Audio = new MyAudioAdapter(speech.Stream), // implements ISpeechAudio + Timings = speech.Words // one start and end per word, or null + .Select(w => new SpeechWordTiming(w.Start, w.End)) + .ToList(), + }; +}; +``` + +`Text` 中每个以空白分隔的词是一个高亮步骤,空行开始新段落,因此服务商的逐词对齐数据可以一一对应。没有逐词时间时,播放器会根据 +文本估算,并按音频长度拉伸对齐(`SpeechSimulation.EstimateTimings`、`SpeechSimulation.FitTimings`)。不设 `Synthesize` 时, +播放器是一个不发声的定时模拟。不设 `Text`(也没有下文的模拟)时播放器为空:没有回复,Read aloud 保持禁用(上方的 Empty 选项卡)。不设 `Voices` 时,音色选择器只提供一个 id 为空的音色,即你的服务的默认音色,名称来自 `SpeechPlayerLabels.DefaultVoice`。 + +### 结构 + +1. **作者行**:16px 的标志和模型名称(`AuthorName`、`AuthorMark`);没有作者时隐藏。 +2. **回复**:分段的文本。朗读时,正在朗读的词落在强调色高亮上,已朗读的词为完整颜色,未朗读的词变淡。 +3. **底栏**:一个 32px 的位置,放操作行(Read aloud,然后是 Copy、Good response 和 Bad response,或你的 `Actions`)或者播放器。 +4. **播放器**:播放、已播放时间、进度条、总时长、语速、音色和 Stop reading。 + +### 状态 + +| 状态 | 播放器显示 | +|---|---| +| `Idle` | 带 Read aloud 的操作行 | +| `Preparing` | 播放按钮里的加载圈,以及带微光的 "Preparing Ava's voice" | +| `Playing` | 暂停按钮、移动的进度条,以及跟随词语的高亮 | +| `Paused` | 播放按钮;高亮停在当前词上 | +| `Finished` | Play again;整段回复为完整颜色 | +| `Failed` | Retry、警示图标,以及 "Couldn't generate audio" 和你的服务抛出的原因 | + +朗读中切换音色时,会中止旧音色的请求,获取新音色,并从正在朗读的词继续。语音按音色保留,所以切回某个音色、或在 Stop reading +之后再次朗读,都会立即开始。 + +### 在聊天栏中 + + + +`Variant="Plain"` 去掉边框和内边距,让回复融入对话记录。`Actions` 用你自己的按钮替换 Copy、Good response 和 Bad response; +`ShowsDefaultActions="False"` 只保留 Read aloud。`Voices` 设置音色选择中的音色,每个音色有 id、名称和一行简短说明。 + +### 事件 + + + +`Played`、`Paused`、`Ended` 和 `Seeked` 带有 1× 时间线上的时间与总时长、词的索引、音色和语速。`WordChanged` 额外带有这个词, +可用于字幕或统计。`Seeked` 在拖动进度条结束时只触发一次,而不是每次移动都触发。另有 `VoiceChanged`、`SpeedChanged` 和 +`Closed`。`SelectedVoiceId` 与 `Speed` 是双向的。 + +```csharp +player.WordChanged += (_, e) => caption.Text = e.Word; +player.Played += (_, e) => analytics.Track("tts_play", e.Voice.Id, e.Speed); +``` + +### 用代码控制 + +`Open()` 等同于 Read aloud;`Play()`、`Pause()`、`TogglePlayback()`、`Seek(TimeSpan)`、`SeekToWord(int)`、`Retry()` 和 +`Close()` 与对应按钮的作用相同。`State`、`IsOpen`、`CurrentWordIndex`、`Position` 和 `Duration` 说明它当前的状态。 + +### 模拟语音 + +用于演示时,不要设置 `Text`,而是给播放器一个 `SpeechSimulation`,并传入你自己的回复、作者和音色。播放器进入视口后它开始朗读,在完整动效下,短暂停顿后会换用前两个音色中的另一个再读一遍,直到有人操作。应用自己的 `Text` 总是优先,把 `Simulation` 设为 null 会停止它。预览中的回复和音色是虚构的;作者是真实模型 Claude Sonnet 5.5。 + +```csharp +player.Simulation = new SpeechSimulation +{ + Text = "Lisbon in early May is a lovely choice. Mornings start near 16°C…", + AuthorName = "Claude Sonnet 5.5", + Voices = [new SpeechVoice("ava", "Ava", "Warm and conversational"), new SpeechVoice("leo", "Leo", "Calm, lower register")], +}; +``` + +`SpeechSimulation.SynthesizeAsync` 是所有未设置 `Synthesize` 的播放器背后的替身:它像一次网络往返那样等待(640ms),然后返回估算的逐词时间,不带音频。传入 +`Fail = true` 可以查看失败状态,传入更长的 `Delay` 可以查看 Preparing。使用它时保持 `IsSimulated` 开启,让音色菜单和播报说明 +不会播放声音。 + +```csharp +player.Synthesize = request => SpeechSimulation.SynthesizeAsync(request, + new SpeechSimulationOptions { Delay = TimeSpan.FromSeconds(2) }); +``` + +### 自定义文案 + +`Labels` 包含播放器显示或播报的每一个词。替换整个对象即可本地化或改写: + +```csharp +player.Labels = new SpeechPlayerLabels +{ + ReadAloud = "Vorlesen", + StopReading = "Vorlesen beenden", + PreparingVoice = voice => $"Stimme {voice} wird vorbereitet", +}; +``` + +### 窄容器与触屏 + +卡片填满容器,最宽 640px。宽度小于 440px 时,总时长让位,进度条保留空间;已播放时间、语速、音色和 Stop reading 仍然显示。 +检测到触摸后,底栏增高到 36px,使每个控件都满足触控目标尺寸。 + +### 最佳实践 + +- 传入与朗读内容完全一致的文本:词数必须与逐词时间一致。 +- 在 `Synthesize` 中抛出带有简短、易懂信息的异常;它会显示在 "Couldn't generate audio" 旁边。 +- 遵守请求的取消令牌:切换音色、Stop reading 或新的文本都会取消它。 +- 在对话记录中使用 plain 变体,并传入与消息其他按钮一致的操作。 + +### 常见问题 + +**它自己会发声吗?** 不会。`TKCode.Arc.Special` 不依赖任何音频组件;声音由你的 `ISpeechAudio` 播放。没有它时,播放器自行计时, +不发出任何声音。 + +**服务商没有逐词时间怎么办?** 只返回音频即可。逐词时间会根据文本估算,并按音频长度拉伸对齐。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 播放器是一个带名称的分组。播放按钮的名称随状态变化:Play、Pause、Play again、Retry 或 Preparing audio(请求进行中时保留焦点, + 但按下不起作用)。 +- 进度条是一个滑块,其数值文本为 "0:04 of 0:22",每秒更新一次,而不是每帧更新。 +- 一个礼貌模式的状态区域播报变化:以 1× 用 Ava 朗读(模拟,不播放声音)、暂停在 0:12、切换到 Leo、朗读完毕、失败原因、语速变化 + 和反馈。 +- 状态从不只靠颜色表达:失败时警示图标与 "Couldn't generate audio" 及原因同时出现;变淡的未朗读词保持正文对比度,只作为进度提示。 +- Read aloud 以回复内容作为描述。音色菜单使用单选行,打开时定位在已选音色上。 +- 底栏中隐藏的一半在淡出后离开可视树,所以 Tab 不会落在看不见的控件上。指针获得焦点时不显示焦点环;键盘焦点显示 Arc 的焦点环。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 在 Read aloud 与各操作之间移动;朗读时在播放、进度条、语速、音色和 Stop reading 之间移动 | +| Enter、Space | 在 Read aloud 上打开播放器并开始朗读,焦点落在播放按钮上;在播放按钮上播放或暂停;在 Stop reading 上关闭播放器,焦点回到 Read aloud | +| 左方向键、右方向键 | 在进度条上后退或前进 2 秒 | +| Page Down、Page Up | 在进度条上后退或前进 10 秒 | +| Home、End | 在进度条上跳到开头或结尾 | +| Space、K | 在进度条上播放或暂停 | +| 上方向键、下方向键 | 在音色菜单中切换音色;Enter 选择,Escape 关闭菜单 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| Read aloud 与 Stop reading | 在同一个 32px 位置中,一行在 100ms 内淡出,另一行在 160ms 内淡入并上升 2px | 120ms 交叉淡化 | 立即 | +| 高亮在一行内移动 | 1× 时 150ms 滑过,1.5× 时 100ms,2× 时 75ms | 立即移动 | 立即 | +| 高亮到达新的一行 | 直接切过去 | 直接切过去 | 直接切过去 | +| 一个词被朗读 | 高亮落下时从变淡渐变为完整颜色 | 渐变 | 立即 | +| 开始或停止朗读 | 变淡效果和高亮在 160ms 内淡入淡出 | 淡入淡出 | 立即 | +| 进度条 | 填充与滑块逐帧跟随;悬停、聚焦和拖动时轨道在 160ms 内变粗、滑块放大出现 | 立即变粗 | 立即 | +| 播放、暂停、重播、重试、加载圈 | 原地以无回弹的缩放(0.26s)和 160ms 淡化切换 | 120ms 淡化 | 立即 | +| 语速与音色标签 | 在所有选项的隐藏副本上交叉淡化,按钮宽度不变 | 120ms 淡化 | 立即 | +| Preparing 标签 | 平缓的微光 | 静止 | 静止 | +| 模拟 | 在两个音色之间循环,直到有人操作 | 只读一遍 | 只读一遍 | + +时钟在所有动效等级下都运行:朗读是内容,不是装饰。尺寸变化时高亮重新测量,不播放动画。 + +## 相关组件 + +- `AgentStatus`:智能体处理回复时的实时状态。 +- `ToolCall`:智能体在回答时调用的工具。 +- `CopyButton`:消息操作行中的复制按钮。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/zh-TW.md new file mode 100644 index 00000000..3b41de2e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/SpeechPlayer/zh-TW.md @@ -0,0 +1,224 @@ +--- +title: Speech player +summary: 為 AI 回覆朗讀的播放器:Read aloud 會變成一個精簡的播放器,具備播放、可拖曳的進度列、語速與聲音選擇,醒目標示會隨著朗讀的字詞在回覆中移動。 +source-hash: 0f58f8c877f9 +--- + + + +## 何時使用 + +`SpeechPlayer` 會把 AI 的回覆唸出來。回覆下方是它的操作列;Read aloud 會把這一列變成同樣高度的精簡播放器:播放與暫停、已播放時間、 +一條可以拖曳的細進度列、總長度、語速按鈕、聲音選擇和 Stop reading。播放時,一個圓角醒目標示會像卡拉 OK 一樣隨著唸到的字詞在文字中 +移動,還沒唸到的字詞則維持淡化。 + +- **聊天與回答介面**:使用者希望回覆被唸出來,並在文字中跟著目前唸到的字詞。 +- **無障礙與免看閱讀**:朗讀較長的回答、摘要或簡報。 +- **試聽文字轉語音的聲音與語速**:用真實的產品文案比較效果。 + +## 何時不用 + +- **把語音轉成文字**寫進輸入框:請使用聽寫控制項。 +- **Podcast 或長錄音**,沒有可標示的逐字稿:請使用一般的音訊播放器。 +- **文字與實際朗讀內容不一致**:請先移除 Markdown 與格式,否則醒目標示會錯位。 + +## 用法 + +把 `Text` 設為回覆內容,把 `Synthesize` 設為呼叫你的文字轉語音服務的函式。它會收到一個 `SpeechRequest`(文字、聲音與取消權杖), +並傳回一個 `SpeechAudio`:以 `ISpeechAudio` 表示的播放、逐字時間與總長度。播放器會驅動播放(播放、暫停、跳轉、倍速),並在每一個 +影格讀取它的位置,所以你的音訊就是時鐘。 + +```csharp +player.Text = message.Text; +player.Synthesize = async request => +{ + var speech = await tts.SpeakAsync(request.Text, request.Voice.Id, request.CancellationToken); + return new SpeechAudio + { + Audio = new MyAudioAdapter(speech.Stream), // implements ISpeechAudio + Timings = speech.Words // one start and end per word, or null + .Select(w => new SpeechWordTiming(w.Start, w.End)) + .ToList(), + }; +}; +``` + +`Text` 中每個以空白分隔的字詞是一個標示步驟,空行開始新的段落,因此服務供應商的逐字對齊資料可以一一對應。沒有逐字時間時,播放器 +會根據文字估算,並依音訊長度伸縮對齊(`SpeechSimulation.EstimateTimings`、`SpeechSimulation.FitTimings`)。未設定 `Synthesize` +時,播放器是不發出聲音的定時模擬。未設定 `Text`(也沒有下文的模擬)時播放器為空白:沒有回覆,Read aloud 維持停用(上方的 Empty 分頁)。未設定 `Voices` 時,聲音選擇器只提供一個 id 為空的聲音,也就是你的服務的預設聲音,名稱來自 `SpeechPlayerLabels.DefaultVoice`。 + +### 結構 + +1. **作者列**:16px 的標誌與模型名稱(`AuthorName`、`AuthorMark`);沒有作者時隱藏。 +2. **回覆**:分段的文字。朗讀時,目前的字詞落在強調色的醒目標示上,已唸過的字詞為完整顏色,還沒唸到的字詞淡化。 +3. **底列**:一個 32px 的位置,放操作列(Read aloud,接著是 Copy、Good response 與 Bad response,或你的 `Actions`)或是播放器。 +4. **播放器**:播放、已播放時間、進度列、總長度、語速、聲音與 Stop reading。 + +### 狀態 + +| 狀態 | 播放器顯示 | +|---|---| +| `Idle` | 含 Read aloud 的操作列 | +| `Preparing` | 播放按鈕中的載入圈,以及帶有微光的 "Preparing Ava's voice" | +| `Playing` | 暫停按鈕、移動的進度列,以及跟著字詞移動的醒目標示 | +| `Paused` | 播放按鈕;醒目標示停在目前的字詞上 | +| `Finished` | Play again;整段回覆為完整顏色 | +| `Failed` | Retry、警示圖示,以及 "Couldn't generate audio" 與你的服務擲回的原因 | + +朗讀中切換聲音時,會中止舊聲音的請求、取得新聲音,並從目前唸到的字詞繼續。語音會依聲音保留,因此切回某個聲音、或在 Stop reading +之後再次朗讀,都會立即開始。 + +### 在聊天欄中 + + + +`Variant="Plain"` 會移除邊框與內距,讓回覆融入對話紀錄。`Actions` 以你自己的按鈕取代 Copy、Good response 與 Bad response; +`ShowsDefaultActions="False"` 只保留 Read aloud。`Voices` 設定聲音選擇中的聲音,每個聲音都有 id、名稱與一行簡短說明。 + +### 事件 + + + +`Played`、`Paused`、`Ended` 與 `Seeked` 帶有 1× 時間軸上的時間與總長度、字詞索引、聲音與語速。`WordChanged` 另外帶有該字詞,可用於 +字幕或分析。`Seeked` 會在拖曳進度列結束時觸發一次,而不是每次移動都觸發。另有 `VoiceChanged`、`SpeedChanged` 與 `Closed`。 +`SelectedVoiceId` 與 `Speed` 為雙向。 + +```csharp +player.WordChanged += (_, e) => caption.Text = e.Word; +player.Played += (_, e) => analytics.Track("tts_play", e.Voice.Id, e.Speed); +``` + +### 以程式碼控制 + +`Open()` 等同於 Read aloud;`Play()`、`Pause()`、`TogglePlayback()`、`Seek(TimeSpan)`、`SeekToWord(int)`、`Retry()` 與 `Close()` +的作用與對應的按鈕相同。`State`、`IsOpen`、`CurrentWordIndex`、`Position` 與 `Duration` 說明它目前的狀態。 + +### 模擬語音 + +用於展示時,不要設定 `Text`,而是給播放器一個 `SpeechSimulation`,並傳入你自己的回覆、作者與聲音。播放器進入可視範圍後它開始朗讀,在完整動效下,短暫停頓後會改用前兩個聲音中的另一個再唸一次,直到有人操作。應用程式自己的 `Text` 一律優先,把 `Simulation` 設為 null 會停止它。預覽中的回覆與聲音是虛構的;作者是真實模型 Claude Sonnet 5.5。 + +```csharp +player.Simulation = new SpeechSimulation +{ + Text = "Lisbon in early May is a lovely choice. Mornings start near 16°C…", + AuthorName = "Claude Sonnet 5.5", + Voices = [new SpeechVoice("ava", "Ava", "Warm and conversational"), new SpeechVoice("leo", "Leo", "Calm, lower register")], +}; +``` + +`SpeechSimulation.SynthesizeAsync` 是所有未設定 `Synthesize` 的播放器背後的替身:它會像一次網路往返那樣等待(640ms),然後傳回估算的逐字時間,不含音訊。 +傳入 `Fail = true` 可查看失敗狀態,傳入較長的 `Delay` 可查看 Preparing。使用它時請保持 `IsSimulated` 開啟,讓聲音選單與播報說明 +不會播放聲音。 + +```csharp +player.Synthesize = request => SpeechSimulation.SynthesizeAsync(request, + new SpeechSimulationOptions { Delay = TimeSpan.FromSeconds(2) }); +``` + +### 自訂文案 + +`Labels` 包含播放器顯示或播報的每一個字詞。替換整個物件即可在地化或改寫: + +```csharp +player.Labels = new SpeechPlayerLabels +{ + ReadAloud = "Vorlesen", + StopReading = "Vorlesen beenden", + PreparingVoice = voice => $"Stimme {voice} wird vorbereitet", +}; +``` + +### 窄容器與觸控 + +卡片會填滿容器,最寬 640px。寬度小於 440px 時,總長度會讓位,讓進度列保有空間;已播放時間、語速、聲音與 Stop reading 仍會顯示。 +偵測到觸控後,底列會增高到 36px,讓每個控制項都符合觸控目標尺寸。 + +### 最佳做法 + +- 傳入與朗讀內容完全一致的文字:字詞數量必須與逐字時間相符。 +- 在 `Synthesize` 中擲回帶有簡短、易懂訊息的例外;它會顯示在 "Couldn't generate audio" 旁邊。 +- 遵守請求的取消權杖:切換聲音、Stop reading 或新的文字都會取消它。 +- 在對話紀錄中使用 plain 變體,並傳入與訊息其他按鈕一致的操作。 + +### 常見問題 + +**它自己會發出聲音嗎?** 不會。`TKCode.Arc.Special` 不相依任何音訊元件;聲音由你的 `ISpeechAudio` 播放。沒有它時,播放器自行計時, +不會發出任何聲音。 + +**服務供應商沒有逐字時間怎麼辦?** 只傳回音訊即可。逐字時間會根據文字估算,並依音訊長度伸縮對齊。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 播放器是一個有名稱的群組。播放按鈕的名稱會隨狀態改變:Play、Pause、Play again、Retry 或 Preparing audio(請求進行中時保留焦點, + 但按下沒有作用)。 +- 進度列是一個滑桿,其數值文字為 "0:04 of 0:22",每秒更新一次,而不是每個影格更新。 +- 一個禮貌模式的狀態區域會播報變化:以 1× 使用 Ava 朗讀(模擬,不播放聲音)、暫停於 0:12、切換到 Leo、朗讀完畢、失敗原因、語速 + 變化與回饋。 +- 狀態從不只靠顏色表達:失敗時警示圖示會與 "Couldn't generate audio" 及原因一起出現;淡化的未朗讀字詞維持內文對比度,只作為進度 + 提示。 +- Read aloud 以回覆內容作為描述。聲音選單使用單選列,開啟時定位在已選的聲音上。 +- 底列中隱藏的一半在淡出後會離開視覺樹,因此 Tab 不會落在看不見的控制項上。指標取得焦點時不顯示焦點框;鍵盤焦點顯示 Arc 的 + 焦點框。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 在 Read aloud 與各操作之間移動;朗讀時在播放、進度列、語速、聲音與 Stop reading 之間移動 | +| Enter、Space | 在 Read aloud 上開啟播放器並開始朗讀,焦點落在播放按鈕;在播放按鈕上播放或暫停;在 Stop reading 上關閉播放器,焦點回到 Read aloud | +| 左方向鍵、右方向鍵 | 在進度列上倒退或前進 2 秒 | +| Page Down、Page Up | 在進度列上倒退或前進 10 秒 | +| Home、End | 在進度列上跳到開頭或結尾 | +| Space、K | 在進度列上播放或暫停 | +| 上方向鍵、下方向鍵 | 在聲音選單中切換聲音;Enter 選擇,Escape 關閉選單 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| Read aloud 與 Stop reading | 在同一個 32px 位置中,一列在 100ms 內淡出,另一列在 160ms 內淡入並上升 2px | 120ms 交叉淡化 | 立即 | +| 醒目標示在一行內移動 | 1× 時 150ms 滑過,1.5× 時 100ms,2× 時 75ms | 立即移動 | 立即 | +| 醒目標示到達新的一行 | 直接切過去 | 直接切過去 | 直接切過去 | +| 一個字詞被唸到 | 醒目標示落下時從淡化漸變為完整顏色 | 漸變 | 立即 | +| 開始或停止朗讀 | 淡化效果與醒目標示在 160ms 內淡入淡出 | 淡入淡出 | 立即 | +| 進度列 | 填色與拖曳點逐影格跟隨;滑鼠移入、取得焦點與拖曳時,軌道在 160ms 內變粗、拖曳點放大出現 | 立即變粗 | 立即 | +| 播放、暫停、重播、重試、載入圈 | 原地以無回彈的縮放(0.26s)與 160ms 淡化切換 | 120ms 淡化 | 立即 | +| 語速與聲音標籤 | 在所有選項的隱藏副本上交叉淡化,按鈕寬度不變 | 120ms 淡化 | 立即 | +| Preparing 標籤 | 平緩的微光 | 靜止 | 靜止 | +| 模擬 | 在兩個聲音之間循環,直到有人操作 | 只唸一次 | 只唸一次 | + +時鐘在所有動態效果等級下都會執行:朗讀是內容,不是裝飾。尺寸改變時醒目標示會重新測量,不播放動畫。 + +## 相關元件 + +- `AgentStatus`:代理程式處理回覆時的即時狀態。 +- `ToolCall`:代理程式在回答時呼叫的工具。 +- `CopyButton`:訊息操作列中的複製按鈕。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/en-US.md new file mode 100644 index 00000000..e3f184c0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/en-US.md @@ -0,0 +1,180 @@ +--- +title: Starter prompts +summary: A greeting and starter cards for the empty state of an AI app, with category tabs, a shuffle that crossfades the cards, and a composer that a picked card fills. +--- + + + +## When to use + +- The empty state of a chat, copilot or agent, before the first message. +- Teaching what an assistant can do with connected tools, such as mail, a code repository or a docs space. +- Onboarding a new workspace with a few tailored, ready to send prompts. + +## When not to use + +- Inside a running conversation; use follow-up suggestion chips in the thread instead. +- A command launcher with search; use a command palette. +- A single inline suggestion in a text field; use ghost text. + +## Usage + +### The empty state + +With no data the control shows the All tab and the composer, with no cards and no greeting. Give it a greeting, a quiet +line, tabs and starters (the preview's 24 are invented, over Write, Code, Analyze and Plan) and it shows four at a time. +Picking a card places its full `Prompt` in the composer, focuses it and raises +`PromptSelected`; Enter sends and raises `SubmitRequested`. Shuffle shows the next starters of the tab with a short +staggered crossfade, and always brings at least one card that was not on screen. + +```xml + +``` + +The card's title is what people scan; the prompt is what the composer shows. Keep titles short and verb first, and put +the full, specific instruction in `Prompt`. + +### Your own starters and tabs + + + +Add `StarterPromptCategory` items to `Categories` (an id, a label and an optional 24 × 24 stroked `Icon`) and +`StarterPrompt` items as the content (`Id`, `Category`, `Title`, `Description`, `Prompt`, `App`). All is added in front +and a tab with no starters is hidden. `Count` sets how many cards are on screen; the grid keeps that many slots, so a +tab with fewer starters leaves its empty slots in place and nothing below moves. + +`App` marks a card that uses a connected app: its mark is drawn in a tinted square and its name reads beside it and in +the card's accessible name. The presets (`Mail`, `Repository`, `Docs`, `Issues`, `Calendar`) are neutral glyphs; for a +real integration pass its own name and mark: + +```csharp +var gmail = new StarterPromptApp("Gmail", myGmailGeometry); +``` + +A `StarterPromptApp` with no `Mark` shows the first letter of its name. + +### Sending + + + +`SubmitRequested` hands over the trimmed `Message`, the `Prompt` it came from (null when the person wrote it) and a +`CancellationToken`. Put your request's task in `Sending`: the footer shows Sending and the send button turns into +Stop until the task completes. When it completes the composer empties and the footer shows Sent. When it throws, the +footer shows an alert icon, the exception's message and Retry, and the text stays. Stop and Escape cancel the token +and keep the text; so does the control leaving the window. + +```csharp +prompts.SubmitRequested += (_, e) => + e.Sending = chat.SendAsync(e.Message, e.CancellationToken); +``` + +Without a handler that sets `Sending`, the send is simulated and the footer says so. `SendStatus` and `SendError` +report the state, `Submit()` and `StopSending()` act like the keys, and `Text` is the composer's text (two-way). + +### Your own composer + + + +`ShowsComposer="False"` hides the built-in composer, so the cards can feed a composer your app already has. A pick +still sets `Text` and raises `PromptSelected` with the starter. + +### Loading + +`IsLoading` shows placeholder cards with the same size and rhythm as the real ones; they breathe slowly and cannot be +picked, and Shuffle is disabled. When it turns off, the starters fade in inside the same frames. + +### Narrow containers + + + +The cards sit in two columns from 560px of the control's width and in one column below. The tabs scroll inside +themselves with edge fades, Shuffle becomes an icon under 440px, and the padding, the corner radius and the greeting +step down under 480px. The control fills its container and its width never follows the cards or the text. + +### Your own words + +`Greeting`, `Description` (null hides it), `Placeholder` and `ModelName` with `ModelMark` are properties. Every other +word lives in `Labels`: + +```csharp +prompts.Labels = new StarterPromptsLabels +{ + All = "Alle", + Shuffle = "Mischen", + Send = "Nachricht senden", + CardName = (title, app, category) => app is null ? title : $"{title}, mit {app}", +}; +``` + +### Content guidelines + +- Titles are a few words and start with a verb ("Plan my week"); descriptions add the detail that makes it specific. +- Set `App` only when the prompt really uses that integration, so the mark means something. +- Keep greetings short and personal; one quiet line under it is enough. + +### Best practices + +- Use it as the first screen of a new chat or agent thread, then swap it for the conversation once a message is sent. +- Honor the cancellation token so Stop really stops the request. +- Keep the pool larger than `Count` so Shuffle has something to show; with fewer starters it is disabled. + +### Common questions + +**Does it render every starter?** No. Only the cards on screen exist; the pool can hold hundreds of starters. + +**Can I select a tab from code?** Yes. `SelectedCategory` is two-way; an id that names no visible tab shows All. + +## API reference + + + + + + + + + + + + + + + +## Accessibility + +- The greeting is a heading. Each card is a button whose name reads its title, the connected app and the category + ("Plan my week, uses Calendar, Plan"). +- A polite live region announces picks, shuffles, sending, sent and errors, and says when a send was simulated. +- Status is never color alone: Sent shows a check and a label, a failure an alert icon and the message. +- The send button is named Send message, and Stop sending while a message is in flight. +- Every word is in `StarterPromptsLabels` or a property. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the tabs, Shuffle, each card and the composer | +| Left, Right | Move between the category tabs | +| Enter, Space | On a card, fill the composer with its prompt and focus it | +| Enter | In the composer, send the message | +| Shift+Enter | In the composer, add a new line | +| Escape | While sending, stop the send and keep the text | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Shuffle or tab change | Each card's content fades out in 100ms, then the new content fades in over 160ms rising 2px; the cards start 35ms apart (about 100ms in all) inside fixed frames | At once | At once | +| Starters arrive after loading | The content fades in the same way, staggered | At once | At once | +| Picking a card | The composer is focused and the new text fades in over 160ms | At once | At once | +| Footer status | Crossfades in place: out 100ms, in 160ms | A 120ms fade | At once | +| Send and stop icon | Crossfade over 160ms; nothing moves or resizes | At once | At once | +| Loading placeholders | Breathe once every 1.6s | Still | Still | +| Tab selection | The pill glides on the tabs' spring | At once | At once | + +## Related + +- `AgentStatus` for the live status of an agent once a run starts. +- `ToolCall` for the tool calls an agent makes. +- `Tabs` and `SegmentedControl` for switching views elsewhere. +- `EmptyState` for a useful next step when there is nothing to show yet. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/zh-CN.md new file mode 100644 index 00000000..3ae78926 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/zh-CN.md @@ -0,0 +1,173 @@ +--- +title: Starter prompts +summary: AI 应用空状态里的问候语与起始卡片:带分类标签页、以交叉淡化切换卡片的“换一批”,以及由所选卡片填入的输入框。 +source-hash: 6969be5a1813 +--- + + + +## 何时使用 + +- 聊天、助手或智能体在第一条消息之前的空状态。 +- 介绍助手能借助已连接的工具做什么,例如邮件、代码仓库或文档空间。 +- 用几条量身定制、可以直接发送的提示词引导新的工作区。 + +## 何时不用 + +- 对话已经开始时;请改用会话里的后续建议标签。 +- 带搜索的命令启动器;请使用命令面板。 +- 文本框里的单条内联建议;请使用幽灵文本。 + +## 用法 + +### 空状态 + +没有数据时,控件只显示“全部”标签页和输入框,没有卡片,也没有问候语。为它提供问候语、一行说明、标签页和起始提示(预览中的 24 条分布在 Write、Code、Analyze、Plan 四个分类中,均为虚构),它每次显示 4 条。选择一张卡片会把它完整的 `Prompt` 填入输入框、聚焦输入框并引发 `PromptSelected`;按 Enter 发送并 +引发 `SubmitRequested`。“换一批”以短暂的错开交叉淡化显示该标签页的下一批起始提示,并且至少带来一张原本不在屏幕上的卡片。 + +```xml + +``` + +人们浏览的是卡片标题,输入框里显示的是提示词。标题要简短并以动词开头,把完整、具体的指令放进 `Prompt`。 + +### 你自己的起始提示与标签页 + + + +向 `Categories` 添加 `StarterPromptCategory`(一个 id、一个标签和可选的 24 × 24 描边 `Icon`),并把 `StarterPrompt` +作为内容添加(`Id`、`Category`、`Title`、`Description`、`Prompt`、`App`)。“全部”会自动加在最前面,没有起始提示的 +标签页会隐藏。`Count` 决定屏幕上同时有几张卡片;网格始终保留这么多个位置,所以起始提示较少的标签页会留出空位,下方 +内容不会移动。 + +`App` 标记使用了已连接应用的卡片:它的标志画在一个浅色底的方块里,名称显示在旁边,也会读入卡片的无障碍名称。预设 +(`Mail`、`Repository`、`Docs`、`Issues`、`Calendar`)是中性的图标;真实的集成请传入它自己的名称与标志: + +```csharp +var gmail = new StarterPromptApp("Gmail", myGmailGeometry); +``` + +没有 `Mark` 的 `StarterPromptApp` 显示其名称的首字母。 + +### 发送 + + + +`SubmitRequested` 交出去掉首尾空白的 `Message`、它来自的 `Prompt`(由人手写时为 null)以及一个 `CancellationToken`。 +把请求的任务放进 `Sending`:任务完成之前,底栏显示“发送中”,发送按钮变成停止按钮。任务完成后输入框清空,底栏显示 +“已发送”。任务抛出异常时,底栏显示警示图标、异常的消息和“重试”,文本保留。停止按钮与 Escape 会取消该令牌并保留文本; +控件离开窗口时也一样。 + +```csharp +prompts.SubmitRequested += (_, e) => + e.Sending = chat.SendAsync(e.Message, e.CancellationToken); +``` + +如果没有处理程序设置 `Sending`,发送是模拟的,底栏会注明这一点。`SendStatus` 与 `SendError` 报告状态,`Submit()` 与 +`StopSending()` 的作用与按键相同,`Text` 是输入框的文本(双向)。 + +### 你自己的输入框 + + + +`ShowsComposer="False"` 隐藏内置的输入框,让卡片为应用已有的输入框提供内容。选择卡片仍会设置 `Text`,并带着该起始 +提示引发 `PromptSelected`。 + +### 加载 + +`IsLoading` 显示与真实卡片尺寸、节奏相同的占位卡片;它们缓慢呼吸、不能被选择,“换一批”也被禁用。关闭后,起始提示在 +同样的框里淡入。 + +### 窄容器 + + + +控件宽度达到 560px 时卡片排成两列,更窄时排成一列。标签页在自身内部滚动并带有边缘渐隐,宽度小于 440px 时“换一批” +只显示图标,小于 480px 时内边距、圆角与问候语都会缩小一档。控件填满它的容器,宽度从不跟随卡片或文本变化。 + +### 你自己的文字 + +`Greeting`、`Description`(设为 null 即隐藏)、`Placeholder` 以及 `ModelName` 与 `ModelMark` 都是属性。其余所有文字 +都在 `Labels` 中: + +```csharp +prompts.Labels = new StarterPromptsLabels +{ + All = "Alle", + Shuffle = "Mischen", + Send = "Nachricht senden", + CardName = (title, app, category) => app is null ? title : $"{title}, mit {app}", +}; +``` + +### 内容准则 + +- 标题只有几个词,并以动词开头(“规划我的一周”);说明补充让它具体的细节。 +- 只有提示词确实用到某个集成时才设置 `App`,这样标志才有意义。 +- 问候语简短而亲切;下面一行安静的说明就够了。 + +### 最佳实践 + +- 把它用作新聊天或新智能体会话的第一屏,消息发出后换成对话本身。 +- 遵守取消令牌,让停止真正停止请求。 +- 让起始提示的总数多于 `Count`,这样“换一批”才有内容可换;数量不够时它会被禁用。 + +### 常见问题 + +**它会渲染每一条起始提示吗?** 不会。只有屏幕上的卡片存在;提示池可以容纳数百条起始提示。 + +**能用代码选择标签页吗?** 可以。`SelectedCategory` 是双向的;不对应任何可见标签页的 id 会显示“全部”。 + +## API 参考 + + + + + + + + + + + + + + + +## 无障碍 + +- 问候语是标题。每张卡片都是按钮,名称依次读出标题、已连接的应用与分类(“Plan my week, uses Calendar, Plan”)。 +- 一个礼貌(polite)的实时区域播报选择、换一批、发送中、已发送与错误,并说明发送是否为模拟。 +- 状态从不只靠颜色表达:已发送显示对勾与文字,失败显示警示图标与消息。 +- 发送按钮名为“Send message”,消息发送途中名为“Stop sending”。 +- 所有文字都在 `StarterPromptsLabels` 或属性中。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过标签页、换一批、每张卡片和输入框 | +| Left、Right | 在分类标签页之间移动 | +| Enter、Space | 在卡片上:用它的提示词填入输入框并聚焦输入框 | +| Enter | 在输入框中:发送消息 | +| Shift+Enter | 在输入框中:换行 | +| Escape | 发送途中:停止发送并保留文本 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 换一批或切换标签页 | 每张卡片的内容在 100ms 内淡出,随后新内容在 160ms 内淡入并上升 2px;卡片之间相隔 35ms 开始(总共约 100ms),都在固定的框内 | 立即 | 立即 | +| 加载结束、起始提示到达 | 内容以同样的方式错开淡入 | 立即 | 立即 | +| 选择卡片 | 输入框获得焦点,新文本在 160ms 内淡入 | 立即 | 立即 | +| 底栏状态 | 原地交叉淡化:淡出 100ms,淡入 160ms | 120ms 淡化 | 立即 | +| 发送与停止图标 | 在 160ms 内交叉淡化;没有移动或尺寸变化 | 立即 | 立即 | +| 加载占位 | 每 1.6 秒呼吸一次 | 静止 | 静止 | +| 标签页选择 | 选中块沿标签页的弹簧滑动 | 立即 | 立即 | + +## 相关组件 + +- `AgentStatus`:运行开始后智能体的实时状态。 +- `ToolCall`:智能体发起的工具调用。 +- `Tabs` 与 `SegmentedControl`:在其他地方切换视图。 +- `EmptyState`:没有内容可显示时给出有用的下一步。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/zh-TW.md new file mode 100644 index 00000000..19047402 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/StarterPrompts/zh-TW.md @@ -0,0 +1,174 @@ +--- +title: Starter prompts +summary: AI 應用程式空狀態裡的問候語與起始卡片:附分類分頁、以交叉淡化切換卡片的「換一批」,以及由所選卡片填入的輸入框。 +source-hash: 6969be5a1813 +--- + + + +## 何時使用 + +- 聊天、助理或代理程式在第一則訊息之前的空狀態。 +- 介紹助理能藉由已連結的工具做什麼,例如郵件、程式碼存放庫或文件空間。 +- 用幾則量身打造、可以直接傳送的提示詞引導新的工作區。 + +## 何時不用 + +- 對話已經開始時;請改用對話裡的後續建議標籤。 +- 附搜尋的指令啟動器;請使用指令面板。 +- 文字方塊裡的單一行內建議;請使用幽靈文字。 + +## 用法 + +### 空狀態 + +沒有資料時,控制項只顯示「全部」分頁和輸入框,沒有卡片,也沒有問候語。為它提供問候語、一行說明、分頁和起始提示(預覽中的 24 則分布在 Write、Code、Analyze、Plan 四個分類中,皆為虛構),它每次顯示 4 則。選擇一張卡片會把它完整的 `Prompt` 填入輸入框、讓輸入框取得焦點並引發 `PromptSelected`;按 Enter +傳送並引發 `SubmitRequested`。「換一批」以短暫的錯開交叉淡化顯示該分頁的下一批起始提示,而且至少帶來一張原本不在 +畫面上的卡片。 + +```xml + +``` + +使用者瀏覽的是卡片標題,輸入框裡顯示的是提示詞。標題要簡短並以動詞開頭,把完整、具體的指示放進 `Prompt`。 + +### 你自己的起始提示與分頁 + + + +在 `Categories` 加入 `StarterPromptCategory`(一個 id、一個標籤和選用的 24 × 24 描邊 `Icon`),並把 `StarterPrompt` +作為內容加入(`Id`、`Category`、`Title`、`Description`、`Prompt`、`App`)。「全部」會自動加在最前面,沒有起始提示的 +分頁會隱藏。`Count` 決定畫面上同時有幾張卡片;格線始終保留這麼多個位置,所以起始提示較少的分頁會留下空位,下方 +內容不會移動。 + +`App` 標示使用了已連結應用程式的卡片:它的標誌畫在一個淺色底的方塊裡,名稱顯示在旁邊,也會讀入卡片的無障礙名稱。 +預設值(`Mail`、`Repository`、`Docs`、`Issues`、`Calendar`)是中性的圖示;真實的整合請傳入它自己的名稱與標誌: + +```csharp +var gmail = new StarterPromptApp("Gmail", myGmailGeometry); +``` + +沒有 `Mark` 的 `StarterPromptApp` 會顯示其名稱的第一個字母。 + +### 傳送 + + + +`SubmitRequested` 交出去除前後空白的 `Message`、它來自的 `Prompt`(由使用者手寫時為 null)以及一個 `CancellationToken`。 +把請求的工作放進 `Sending`:工作完成之前,頁尾顯示「傳送中」,傳送按鈕變成停止按鈕。工作完成後輸入框清空,頁尾顯示 +「已傳送」。工作擲回例外時,頁尾顯示警示圖示、例外的訊息和「重試」,文字保留。停止按鈕與 Escape 會取消該權杖並保留 +文字;控制項離開視窗時也一樣。 + +```csharp +prompts.SubmitRequested += (_, e) => + e.Sending = chat.SendAsync(e.Message, e.CancellationToken); +``` + +如果沒有處理常式設定 `Sending`,傳送是模擬的,頁尾會註明這一點。`SendStatus` 與 `SendError` 回報狀態,`Submit()` 與 +`StopSending()` 的作用與按鍵相同,`Text` 是輸入框的文字(雙向)。 + +### 你自己的輸入框 + + + +`ShowsComposer="False"` 隱藏內建的輸入框,讓卡片為應用程式既有的輸入框提供內容。選擇卡片仍會設定 `Text`,並帶著該 +起始提示引發 `PromptSelected`。 + +### 載入 + +`IsLoading` 顯示與真實卡片尺寸、節奏相同的預留位置卡片;它們緩慢呼吸、無法選擇,「換一批」也會停用。關閉後,起始 +提示在同樣的框裡淡入。 + +### 窄容器 + + + +控制項寬度達到 560px 時卡片排成兩欄,更窄時排成一欄。分頁在自身內部捲動並帶有邊緣漸隱,寬度小於 440px 時「換一批」 +只顯示圖示,小於 480px 時內距、圓角與問候語都會縮小一級。控制項填滿它的容器,寬度從不跟著卡片或文字變化。 + +### 你自己的文字 + +`Greeting`、`Description`(設為 null 即隱藏)、`Placeholder` 以及 `ModelName` 與 `ModelMark` 都是屬性。其餘所有文字 +都在 `Labels` 中: + +```csharp +prompts.Labels = new StarterPromptsLabels +{ + All = "Alle", + Shuffle = "Mischen", + Send = "Nachricht senden", + CardName = (title, app, category) => app is null ? title : $"{title}, mit {app}", +}; +``` + +### 內容準則 + +- 標題只有幾個詞,並以動詞開頭(「規劃我的一週」);說明補上讓它具體的細節。 +- 只有提示詞確實用到某個整合時才設定 `App`,這樣標誌才有意義。 +- 問候語簡短而親切;下面一行安靜的說明就夠了。 + +### 最佳做法 + +- 把它當作新聊天或新代理程式對話的第一個畫面,訊息送出後換成對話本身。 +- 遵守取消權杖,讓停止真的停止請求。 +- 讓起始提示的總數多於 `Count`,「換一批」才有內容可換;數量不夠時它會停用。 + +### 常見問題 + +**它會轉譯每一則起始提示嗎?** 不會。只有畫面上的卡片存在;提示池可以容納數百則起始提示。 + +**能用程式碼選擇分頁嗎?** 可以。`SelectedCategory` 是雙向的;不對應任何可見分頁的 id 會顯示「全部」。 + +## API 參考 + + + + + + + + + + + + + + + +## 無障礙 + +- 問候語是標題。每張卡片都是按鈕,名稱依序讀出標題、已連結的應用程式與分類(「Plan my week, uses Calendar, Plan」)。 +- 一個禮貌(polite)的即時區域播報選擇、換一批、傳送中、已傳送與錯誤,並說明傳送是否為模擬。 +- 狀態從不只靠顏色表達:已傳送顯示勾號與文字,失敗顯示警示圖示與訊息。 +- 傳送按鈕名為「Send message」,訊息傳送途中名為「Stop sending」。 +- 所有文字都在 `StarterPromptsLabels` 或屬性中。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過分頁、換一批、每張卡片和輸入框 | +| Left、Right | 在分類分頁之間移動 | +| Enter、Space | 在卡片上:用它的提示詞填入輸入框並讓輸入框取得焦點 | +| Enter | 在輸入框中:傳送訊息 | +| Shift+Enter | 在輸入框中:換行 | +| Escape | 傳送途中:停止傳送並保留文字 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 換一批或切換分頁 | 每張卡片的內容在 100ms 內淡出,接著新內容在 160ms 內淡入並上升 2px;卡片之間相隔 35ms 開始(總共約 100ms),都在固定的框內 | 立即 | 立即 | +| 載入結束、起始提示到達 | 內容以同樣的方式錯開淡入 | 立即 | 立即 | +| 選擇卡片 | 輸入框取得焦點,新文字在 160ms 內淡入 | 立即 | 立即 | +| 頁尾狀態 | 原地交叉淡化:淡出 100ms,淡入 160ms | 120ms 淡化 | 立即 | +| 傳送與停止圖示 | 在 160ms 內交叉淡化;沒有移動或尺寸變化 | 立即 | 立即 | +| 載入預留位置 | 每 1.6 秒呼吸一次 | 靜止 | 靜止 | +| 分頁選取 | 選取塊沿著分頁的彈簧滑動 | 立即 | 立即 | + +## 相關元件 + +- `AgentStatus`:執行開始後代理程式的即時狀態。 +- `ToolCall`:代理程式發出的工具呼叫。 +- `Tabs` 與 `SegmentedControl`:在其他地方切換檢視。 +- `EmptyState`:沒有內容可顯示時提供有用的下一步。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/en-US.md new file mode 100644 index 00000000..5ac6715c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/en-US.md @@ -0,0 +1,227 @@ +--- +title: Team switcher +summary: A workspace switcher: the trigger shows the current workspace's logo and plan, and the menu searches your workspaces, shows your role and a Cmd/Ctrl+number shortcut for each, and creates or joins one inline. +--- + + + +## When to use + +`TeamSwitcher` sits at the top of an app sidebar or in a header and moves people between the organizations, teams, +projects or stores they belong to. The trigger shows where they are: the workspace's logo, its name and its plan. The +menu finds a workspace by name, plan or role, shows the role and a keyboard shortcut for each, and creates or joins a +workspace without leaving the page. Switching crossfades the trigger to the new workspace through a short loading state. + +- **Multi-tenant apps** where one account belongs to several organizations or teams. +- **Account, project or store pickers** that need search, roles and keyboard shortcuts. +- **Creating a workspace or accepting an invite** without sending people to another page. + +## When not to use + +- **A short static list in a form**: use `Select` or `SegmentedControl`. +- **Global navigation across pages and commands**: use a command palette. +- **Two or three views of the same data**: use `Tabs`. + +## Usage + +Add your workspaces and bind the active id to `Value`. Wire `SwitchHandler` to whatever loads a workspace (a session +switch, a route prefetch); the trigger shows the new workspace with a loading state until your task finishes. Every +delegate you leave out is simulated. With no workspaces (and no simulation, below) the trigger says "No workspace" and +the menu lists no workspace above Create workspace and Join with invite (the Empty tab above). + +```xml + + + + +``` + +```csharp +switcher.SwitchHandler = (workspace, token) => session.SwitchAsync(workspace.Id, token); +switcher.ValueChanged += (_, e) => navigation.GoHome(e.Id); +``` + +### Anatomy + +1. **Trigger**: the workspace's tile, its name over the plan line ("Enterprise · 214 members") and a chevron that becomes + a spinner while a workspace loads. It is 52px high and fills its container up to 360px. The menu under it is as wide + as the trigger, and at least 288px so names stay readable when the switcher sits in a narrow sidebar. +2. **Search**: filters the list by name, plan or role as you type. Focus stays here while you move through the list. +3. **Rows**: six 48px rows show at once and the list scrolls beyond that without a visible scrollbar. Each row has the + tile, the name and plan line (both truncate), your role, the shortcut and a check on the active workspace. +4. **Actions**: Create workspace and Join with invite, under a hairline. +5. **Highlight**: one quiet block that glides to the row under the pointer or the keyboard. + +### Your workspaces + + + +Each `TeamSwitcherWorkspace` has an `Id` and a `Name`, and optionally a `Plan`, `Members` and your `Role`. Without a mark +its tile shows the first letter of the name in a square tinted from the theme's accent and series colors (the tint is +picked from the name, so a workspace keeps its color). Give a tenant's brand mark as `Logo` (a filled geometry in a +24 × 24 box) on its `TileBackground`, or its logo image (a bitmap or a `DrawingImage`) as `LogoImage`, which fills the +tile. `TeamSwitcherTile` draws the same tile on its own, for example in a settings page. + +### Switching + +Picking a row, pressing Enter on it or pressing its shortcut closes the menu and starts the switch. The trigger shows +the new workspace at once with a shimmering "Switching…" and a spinner while `SwitchHandler` runs, then its plan once +the task finishes; `Value` changes and `ValueChanged` is raised only then. If the task throws, the trigger goes back to +the previous workspace and shows the exception's message for a few seconds, and `SwitchFailed` is raised. A newer +switch cancels the token of the older one, and setting `Value` yourself abandons a load in flight. + +Call `SwitchToAsync(id)` to switch from code the same way. + +### Creating and joining + +Create workspace and Join with invite open inline forms that crossfade in place of the list; the panel keeps its height. +When nothing matches the search, Enter opens Create workspace with the search text as the name. + +```csharp +switcher.CreateHandler = async (name, token) => +{ + var created = await api.Workspaces.CreateAsync(name, token); // throw to show a message under the field + return new TeamSwitcherWorkspace(created.Id, created.Name, "Free", 1, "Owner"); +}; +switcher.JoinHandler = (invite, token) => api.Invites.AcceptAsync(invite, token); +switcher.WorkspaceAdded += (_, e) => myWorkspaces.Add(e.Workspace); +``` + +Return the new workspace to add it and switch to it, or null to just close the menu. The switcher shows a workspace it +created or joined right away; add it to your own list in `WorkspaceAdded` too, the switcher de-duplicates by id. Without +`CreateHandler` the simulated create makes a one-member workspace from the name; without `JoinHandler` the simulated +join refuses every invite unless a simulation lists it. + +For a demo or a prototype, leave `Workspaces` empty and give the switcher a `TeamSwitcherSimulation`: its workspaces, +the invites its simulated join accepts (each a `TeamSwitcherSimulationInvite`: a code, matched on its own or at the end +of a link, and the workspace it joins; the join view lists the codes) and the plan and role its simulated create gives. +Your own `Workspaces` set it aside, and setting `Simulation` to null empties the switcher. The preview's workspaces and +invites (ADATUM-4821, TAILSPIN-0917) are invented demo data. + +```csharp +switcher.Simulation = new TeamSwitcherSimulation +{ + Workspaces = [new TeamSwitcherWorkspace("acme", "Acme", "Enterprise", 214, "Admin")], + Invites = [new TeamSwitcherSimulationInvite { Code = "ACME-1234", Workspace = new TeamSwitcherWorkspace("acme-labs", "Acme Labs") }], + CreatedPlan = "Free", + CreatedRole = "Owner", +}; +``` + +### Shortcuts + + + +The first nine workspaces get Cmd+1 to 9 on macOS and Ctrl+1 to 9 elsewhere. They work while focus is inside the +switcher; `UsesGlobalShortcuts="True"` makes them work anywhere in the window. + +### Disabled + + + +A disabled switcher keeps showing the workspace, does not open and ignores its shortcuts. + +### Your own words + +`Labels` holds every word the switcher shows or says. Replace the object to localize or reword: + +```csharp +switcher.Labels = new TeamSwitcherLabels +{ + Search = "Arbeitsbereich suchen…", + CreateWorkspace = "Arbeitsbereich erstellen", + PlanLine = (plan, members) => $"{plan} · {members} Mitglieder", +}; +``` + +### Reduced motion + +With reduced motion the menu, the trigger's parts and the forms trade places with plain fades, and the highlight moves +without gliding. With motion off everything changes at once. + +### Content guidelines + +- Name workspaces the way people know them, not by their id. +- Plans are one word ("Pro", "Enterprise"); roles too ("Owner", "Admin", "Member", "Guest"). +- Error messages say what happened and what to do: "That invite has expired. Ask for a new one." + +### Best practices + +- Keep the switch task short and let the trigger show the loading state; do not block the page. +- Put the switcher where the menu has about 450px below it, or where it can overflow. +- Keep the order of the list stable so the shortcuts stay the same. + +### Common questions + +**Can the app open the menu?** Set `IsOpen`; that opens it without moving focus. `Open()` opens it and focuses the search. + +**What does a simulated action do?** With no `SwitchHandler`, `CreateHandler` or `JoinHandler` it waits `SimulatedDelay` (650ms) +and announces that nothing was sent. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The trigger is a button that reports whether the menu is open. Its name reads the workspace and its plan ("Northwind, + Enterprise plan"), and its status says "Switching…" while a workspace loads. +- The search field keeps focus while the arrows move through the list; its help text names the highlighted row, so + screen readers hear it without focus leaving the field. +- The active workspace is marked with a check and "current" in its name, not by color alone. Errors pair an alert icon + with text. +- A polite live region announces a switch starting, its result, and when a simulated action did the work ("Simulated, + nothing was sent"). With no workspace the trigger is named "No workspace". +- Closing with Escape or by choosing gives focus back to the trigger. Tabbing out or clicking outside closes the menu. + +### Keyboard + +| Keys | Action | +|---|---| +| Enter, Space, Down arrow | On the trigger, opens the menu and moves focus to the search field | +| Typing | Filters by name, plan or role; with no match, Enter opens Create workspace with the name filled in | +| Down arrow, Up arrow | Move the highlight through the workspaces and the actions, wrapping at the ends | +| Home, End | Jump to the first or last item when the search is empty | +| Enter | Switches to the highlighted workspace, or opens Create workspace or Join with invite | +| Cmd/Ctrl+1 to 9 | Switches to the workspace at that place in the list (inside the switcher, or anywhere with global shortcuts) | +| Escape | Goes back from a form, then clears the search, then closes and returns focus to the trigger | +| Tab | Leaves the menu and closes it | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Menu opens | Fades and scales in from 98% over 180ms | Fades over 120ms | At once | +| Menu closes | Fades and scales to 98% over 120ms | Fades over 120ms | At once | +| Highlight moves | Glides on a bounce-free 0.24s spring; appears in place, never slides in from the top | Jumps | Jumps | +| Switching | The logo, name and plan line each crossfade: out over 100ms, in over 160ms rising 2px; then a shimmer and a spinner | Fades over 120ms, no shimmer | At once | +| Create or join opens | The views crossfade (100ms out, 160ms in) shifting 6px in the direction of travel | Fades over 120ms | At once | + +Nothing resizes: the trigger keeps its height through every state, and the menu's panel keeps its height when you search +or open a form. + +## Related + +- `DropdownMenu` for a focused list of actions anchored to a trigger. +- `Popover` for a small anchored surface. +- `Avatar` for a compact marker of a person. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/zh-CN.md new file mode 100644 index 00000000..224aefeb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/zh-CN.md @@ -0,0 +1,209 @@ +--- +title: Team switcher +summary: 工作区切换器:触发器显示当前工作区的标志和套餐,菜单可以搜索你的工作区,为每个工作区显示你的角色和 Cmd/Ctrl+数字快捷键,并能就地创建或加入工作区。 +source-hash: e7beb3710fa5 +--- + + + +## 何时使用 + +`TeamSwitcher` 放在应用侧边栏顶部或页眉中,让用户在自己所属的组织、团队、项目或店铺之间切换。触发器显示用户当前所在的位置: +工作区的标志、名称和套餐。菜单可以按名称、套餐或角色查找工作区,为每个工作区显示角色和键盘快捷键,并且无需离开页面就能创建或加入工作区。 +切换时,触发器会经过一个短暂的加载状态,交叉淡化到新的工作区。 + +- **多租户应用**:一个账号属于多个组织或团队。 +- **账号、项目或店铺选择器**:需要搜索、角色和键盘快捷键。 +- **创建工作区或接受邀请**:不必把用户带到另一个页面。 + +## 何时不用 + +- **表单中一个简短的固定列表**:使用 `Select` 或 `SegmentedControl`。 +- **跨页面和命令的全局导航**:使用命令面板。 +- **同一份数据的两三种视图**:使用 `Tabs`。 + +## 用法 + +添加你的工作区,并把当前工作区的 id 绑定到 `Value`。把 `SwitchHandler` 接到加载工作区的逻辑上(切换会话、预取路由);在你的任务完成之前, +触发器会以加载状态显示新的工作区。你没有提供的每个委托都会被模拟。没有工作区(也没有下文的模拟)时,触发器显示 "No workspace",菜单在 Create workspace 和 Join with invite 上方显示没有工作区(上方的 Empty 选项卡)。 + +```xml + + + + +``` + +```csharp +switcher.SwitchHandler = (workspace, token) => session.SwitchAsync(workspace.Id, token); +switcher.ValueChanged += (_, e) => navigation.GoHome(e.Id); +``` + +### 结构 + +1. **触发器**:工作区的图块、名称及其下方的套餐行(“Enterprise · 214 members”),以及一个在加载工作区时变成加载圈的箭头。 + 它高 52px,在不超过 360px 的范围内填满容器。其下的菜单与触发器同宽,但至少 288px,这样切换器放在窄侧边栏中时名称依然清晰可读。 +2. **搜索**:输入时按名称、套餐或角色筛选列表。在列表中移动时焦点一直留在这里。 +3. **行**:一次显示六行,每行 48px,超出后列表滚动,但不显示滚动条。每一行包含图块、名称和套餐行(两者都会截断)、你的角色、 + 快捷键,当前工作区还有一个对勾。 +4. **操作**:分隔线下方的“创建工作区”和“通过邀请加入”。 +5. **高亮**:一个安静的色块,滑动到指针或键盘所指的行。 + +### 你的工作区 + + + +每个 `TeamSwitcherWorkspace` 都有 `Id` 和 `Name`,还可以有 `Plan`、`Members` 和你的 `Role`。没有标志时,它的图块在一个方块中显示名称的 +首字母,方块的色调取自主题的强调色和系列色(色调由名称决定,所以同一个工作区始终保持同一种颜色)。租户的品牌标志用 `Logo` +提供(24 × 24 框内的填充几何图形),并放在 `TileBackground` 上;也可以把标志图片(位图或 `DrawingImage`)放进 `LogoImage`, +它会铺满整个图块。`TeamSwitcherTile` 可以单独绘制同样的图块,例如在设置页中。 + +### 切换 + +点选一行、在行上按 Enter 或按它的快捷键,都会关闭菜单并开始切换。`SwitchHandler` 运行期间,触发器立即显示新的工作区, +带有闪烁的“Switching…”和加载圈;任务完成后再显示它的套餐,此时 `Value` 才会改变并触发 `ValueChanged`。如果任务抛出异常, +触发器会回到之前的工作区并显示异常消息几秒钟,同时触发 `SwitchFailed`。较新的切换会取消较早切换的令牌;你自己设置 `Value` +会放弃正在进行的加载。 + +调用 `SwitchToAsync(id)` 可以用同样的方式在代码中切换。 + +### 创建与加入 + +“创建工作区”和“通过邀请加入”会打开内嵌表单,以交叉淡化的方式替换列表;面板保持高度不变。搜索没有匹配项时,按 Enter +会打开“创建工作区”,并把搜索文字作为名称填好。 + +```csharp +switcher.CreateHandler = async (name, token) => +{ + var created = await api.Workspaces.CreateAsync(name, token); // throw to show a message under the field + return new TeamSwitcherWorkspace(created.Id, created.Name, "Free", 1, "Owner"); +}; +switcher.JoinHandler = (invite, token) => api.Invites.AcceptAsync(invite, token); +switcher.WorkspaceAdded += (_, e) => myWorkspaces.Add(e.Workspace); +``` + +返回新的工作区即可添加并切换到它,返回 null 则只关闭菜单。切换器会立即显示它创建或加入的工作区;也请在 `WorkspaceAdded` +中把它加入你自己的列表,切换器会按 id 去重。没有 `CreateHandler` 时,模拟的创建会用这个名称创建一个只有一名成员的工作区;没有 `JoinHandler` 时,模拟的加入会拒绝所有邀请,除非模拟中列出了它。 + +用于演示或原型时,让 `Workspaces` 保持为空,并给切换器一个 `TeamSwitcherSimulation`:它的工作区、模拟加入所接受的邀请(每个都是一个 `TeamSwitcherSimulationInvite`:一个邀请码,可以单独匹配,也可以位于链接末尾,以及它加入的工作区;加入视图会列出这些邀请码),以及模拟创建时赋予的套餐和角色。你自己的 `Workspaces` 会让它让位,把 `Simulation` 设为 null 会清空切换器。预览中的工作区和邀请(ADATUM-4821、TAILSPIN-0917)是虚构的演示数据。 + +```csharp +switcher.Simulation = new TeamSwitcherSimulation +{ + Workspaces = [new TeamSwitcherWorkspace("acme", "Acme", "Enterprise", 214, "Admin")], + Invites = [new TeamSwitcherSimulationInvite { Code = "ACME-1234", Workspace = new TeamSwitcherWorkspace("acme-labs", "Acme Labs") }], + CreatedPlan = "Free", + CreatedRole = "Owner", +}; +``` + +### 快捷键 + + + +前九个工作区在 macOS 上对应 Cmd+1 到 9,在其他系统上对应 Ctrl+1 到 9。焦点在切换器内部时它们有效; +设置 `UsesGlobalShortcuts="True"` 后,在窗口的任何位置都有效。 + +### 禁用 + + + +禁用的切换器仍然显示工作区,但不会打开,也会忽略快捷键。 + +### 自定义文字 + +`Labels` 包含切换器显示或朗读的所有文字。替换整个对象即可本地化或改写: + +```csharp +switcher.Labels = new TeamSwitcherLabels +{ + Search = "Arbeitsbereich suchen…", + CreateWorkspace = "Arbeitsbereich erstellen", + PlanLine = (plan, members) => $"{plan} · {members} Mitglieder", +}; +``` + +### 减弱动效 + +减弱动效时,菜单、触发器的各部分和表单都以简单的淡入淡出切换,高亮直接移动而不滑动。关闭动效时一切立即改变。 + +### 内容准则 + +- 用大家熟悉的名称命名工作区,而不是它的 id。 +- 套餐用一个词(“Pro”“Enterprise”);角色也一样(“Owner”“Admin”“Member”“Guest”)。 +- 错误消息要说明发生了什么以及该怎么做:“That invite has expired. Ask for a new one.” + +### 最佳实践 + +- 让切换任务尽量短,交给触发器显示加载状态;不要阻塞页面。 +- 把切换器放在下方约有 450px 空间的位置,或者放在菜单可以溢出的地方。 +- 保持列表顺序稳定,让快捷键始终不变。 + +### 常见问题 + +**应用可以打开菜单吗?** 设置 `IsOpen`,这样打开不会移动焦点。`Open()` 会打开菜单并聚焦搜索框。 + +**模拟的操作会做什么?** 没有 `SwitchHandler`、`CreateHandler` 或 `JoinHandler` 时,它会等待 `SimulatedDelay`(650ms),并播报没有发送任何内容。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 触发器是一个按钮,会报告菜单是否打开。它的名称读出工作区及其套餐(“Northwind, Enterprise plan”),加载工作区时状态为“Switching…”。 +- 方向键在列表中移动时,搜索框一直保持焦点;它的帮助文本是高亮行的名称,因此屏幕阅读器无需让焦点离开输入框就能读到它。 +- 当前工作区用对勾以及名称中的“current”标出,而不仅仅依靠颜色。错误同时显示警告图标和文字。 +- 一个礼貌的实时区域会播报切换开始、切换结果,以及工作由模拟操作完成的情况(“Simulated, nothing was sent”)。没有工作区时,触发器的名称为 "No workspace"。 +- 用 Escape 关闭或选择后,焦点回到触发器。用 Tab 离开或点击外部会关闭菜单。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Enter、Space、下方向键 | 在触发器上打开菜单,并把焦点移到搜索框 | +| 输入 | 按名称、套餐或角色筛选;没有匹配时,Enter 打开“创建工作区”并填好名称 | +| 下方向键、上方向键 | 在工作区和操作之间移动高亮,到两端时循环 | +| Home、End | 搜索为空时跳到第一项或最后一项 | +| Enter | 切换到高亮的工作区,或打开“创建工作区”或“通过邀请加入” | +| Cmd/Ctrl+1 到 9 | 切换到列表中对应位置的工作区(在切换器内部,或开启全局快捷键后在任何位置) | +| Escape | 先从表单返回,再清空搜索,最后关闭菜单并把焦点还给触发器 | +| Tab | 离开菜单并将其关闭 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 菜单打开 | 在 180ms 内从 98% 淡入并放大 | 120ms 淡入 | 立即 | +| 菜单关闭 | 在 120ms 内淡出并缩小到 98% | 120ms 淡出 | 立即 | +| 高亮移动 | 沿 0.24s 无回弹弹簧滑动;在原位出现,绝不从顶部滑入 | 直接跳到位 | 直接跳到位 | +| 切换 | 标志、名称和套餐行各自交叉淡化:100ms 淡出,160ms 淡入并上升 2px;随后显示闪烁文字和加载圈 | 120ms 淡入淡出,无闪烁 | 立即 | +| 打开创建或加入 | 视图交叉淡化(100ms 淡出,160ms 淡入),并沿前进方向平移 6px | 120ms 淡入淡出 | 立即 | + +没有任何东西改变尺寸:触发器在所有状态下保持同样的高度,菜单面板在搜索或打开表单时也保持同样的高度。 + +## 相关组件 + +- `DropdownMenu`:锚定在触发器上的一组聚焦操作。 +- `Popover`:小型锚定浮层。 +- `Avatar`:表示一个人的紧凑标识。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/zh-TW.md new file mode 100644 index 00000000..d14ebb75 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TeamSwitcher/zh-TW.md @@ -0,0 +1,209 @@ +--- +title: Team switcher +summary: 工作區切換器:觸發器顯示目前工作區的標誌與方案,選單可以搜尋你的工作區,為每個工作區顯示你的角色與 Cmd/Ctrl+數字快速鍵,並能就地建立或加入工作區。 +source-hash: e7beb3710fa5 +--- + + + +## 何時使用 + +`TeamSwitcher` 放在應用程式側邊欄頂端或頁首,讓使用者在自己所屬的組織、團隊、專案或商店之間切換。觸發器顯示使用者目前所在的位置: +工作區的標誌、名稱與方案。選單可以依名稱、方案或角色尋找工作區,為每個工作區顯示角色與鍵盤快速鍵,並且不必離開頁面就能建立或加入工作區。 +切換時,觸發器會經過一段短暫的載入狀態,交叉淡化到新的工作區。 + +- **多租戶應用程式**:一個帳號屬於多個組織或團隊。 +- **帳號、專案或商店選擇器**:需要搜尋、角色與鍵盤快速鍵。 +- **建立工作區或接受邀請**:不必把使用者帶到另一個頁面。 + +## 何時不用 + +- **表單中一份簡短的固定清單**:使用 `Select` 或 `SegmentedControl`。 +- **跨頁面與指令的全域導覽**:使用指令面板。 +- **同一份資料的兩三種檢視**:使用 `Tabs`。 + +## 用法 + +加入你的工作區,並把目前工作區的 id 繫結到 `Value`。把 `SwitchHandler` 接到載入工作區的邏輯上(切換工作階段、預先擷取路由);在你的工作完成之前, +觸發器會以載入狀態顯示新的工作區。你沒有提供的每個委派都會被模擬。沒有工作區(也沒有下文的模擬)時,觸發器顯示 "No workspace",選單在 Create workspace 與 Join with invite 上方顯示沒有工作區(上方的 Empty 分頁)。 + +```xml + + + + +``` + +```csharp +switcher.SwitchHandler = (workspace, token) => session.SwitchAsync(workspace.Id, token); +switcher.ValueChanged += (_, e) => navigation.GoHome(e.Id); +``` + +### 結構 + +1. **觸發器**:工作區的圖塊、名稱與其下方的方案列(「Enterprise · 214 members」),以及一個在載入工作區時變成載入圈的箭頭。 + 它高 52px,在不超過 360px 的範圍內填滿容器。其下的選單與觸發器同寬,但至少 288px,這樣切換器放在窄側邊欄中時名稱依然清楚易讀。 +2. **搜尋**:輸入時依名稱、方案或角色篩選清單。在清單中移動時焦點一直留在這裡。 +3. **列**:一次顯示六列,每列 48px,超出後清單捲動,但不顯示捲軸。每一列包含圖塊、名稱與方案列(兩者都會截斷)、你的角色、 + 快速鍵,目前的工作區還有一個勾號。 +4. **動作**:分隔線下方的「建立工作區」與「透過邀請加入」。 +5. **醒目提示**:一塊低調的色塊,滑動到指標或鍵盤所指的那一列。 + +### 你的工作區 + + + +每個 `TeamSwitcherWorkspace` 都有 `Id` 與 `Name`,也可以有 `Plan`、`Members` 與你的 `Role`。沒有標誌時,它的圖塊在一個方塊中顯示名稱的 +第一個字母,方塊的色調取自主題的強調色與系列色(色調由名稱決定,所以同一個工作區永遠保持同一種顏色)。租戶的品牌標誌用 `Logo` +提供(24 × 24 方框內的填滿幾何圖形),並放在 `TileBackground` 上;也可以把標誌圖片(點陣圖或 `DrawingImage`)放進 `LogoImage`, +它會鋪滿整個圖塊。`TeamSwitcherTile` 可以單獨繪製同樣的圖塊,例如在設定頁中。 + +### 切換 + +點選一列、在列上按 Enter 或按它的快速鍵,都會關閉選單並開始切換。`SwitchHandler` 執行期間,觸發器立刻顯示新的工作區, +帶有閃爍的「Switching…」與載入圈;工作完成後再顯示它的方案,此時 `Value` 才會改變並引發 `ValueChanged`。如果工作擲回例外, +觸發器會回到先前的工作區並顯示例外訊息幾秒鐘,同時引發 `SwitchFailed`。較新的切換會取消較早切換的權杖;你自己設定 `Value` +會放棄進行中的載入。 + +呼叫 `SwitchToAsync(id)` 可以用同樣的方式從程式碼切換。 + +### 建立與加入 + +「建立工作區」與「透過邀請加入」會開啟內嵌表單,以交叉淡化的方式取代清單;面板高度維持不變。搜尋沒有相符項目時,按 Enter +會開啟「建立工作區」,並把搜尋文字當作名稱填好。 + +```csharp +switcher.CreateHandler = async (name, token) => +{ + var created = await api.Workspaces.CreateAsync(name, token); // throw to show a message under the field + return new TeamSwitcherWorkspace(created.Id, created.Name, "Free", 1, "Owner"); +}; +switcher.JoinHandler = (invite, token) => api.Invites.AcceptAsync(invite, token); +switcher.WorkspaceAdded += (_, e) => myWorkspaces.Add(e.Workspace); +``` + +傳回新的工作區即可加入並切換過去,傳回 null 則只關閉選單。切換器會立刻顯示它建立或加入的工作區;也請在 `WorkspaceAdded` +中把它加入你自己的清單,切換器會依 id 去除重複。沒有 `CreateHandler` 時,模擬的建立會用這個名稱建立一個只有一名成員的工作區;沒有 `JoinHandler` 時,模擬的加入會拒絕所有邀請,除非模擬中列出了它。 + +用於展示或原型時,讓 `Workspaces` 維持空白,並給切換器一個 `TeamSwitcherSimulation`:它的工作區、模擬加入所接受的邀請(每個都是一個 `TeamSwitcherSimulationInvite`:一個邀請碼,可以單獨比對,也可以位於連結結尾,以及它加入的工作區;加入檢視會列出這些邀請碼),以及模擬建立時給予的方案與角色。你自己的 `Workspaces` 會讓它讓位,把 `Simulation` 設為 null 會清空切換器。預覽中的工作區與邀請(ADATUM-4821、TAILSPIN-0917)是虛構的展示資料。 + +```csharp +switcher.Simulation = new TeamSwitcherSimulation +{ + Workspaces = [new TeamSwitcherWorkspace("acme", "Acme", "Enterprise", 214, "Admin")], + Invites = [new TeamSwitcherSimulationInvite { Code = "ACME-1234", Workspace = new TeamSwitcherWorkspace("acme-labs", "Acme Labs") }], + CreatedPlan = "Free", + CreatedRole = "Owner", +}; +``` + +### 快速鍵 + + + +前九個工作區在 macOS 上對應 Cmd+1 到 9,在其他系統上對應 Ctrl+1 到 9。焦點在切換器內部時它們有效; +設定 `UsesGlobalShortcuts="True"` 後,在視窗的任何位置都有效。 + +### 停用 + + + +停用的切換器仍然顯示工作區,但不會開啟,也會忽略快速鍵。 + +### 自訂文字 + +`Labels` 包含切換器顯示或朗讀的所有文字。取代整個物件即可在地化或改寫: + +```csharp +switcher.Labels = new TeamSwitcherLabels +{ + Search = "Arbeitsbereich suchen…", + CreateWorkspace = "Arbeitsbereich erstellen", + PlanLine = (plan, members) => $"{plan} · {members} Mitglieder", +}; +``` + +### 減少動態效果 + +減少動態效果時,選單、觸發器的各部分與表單都以單純的淡入淡出交換,醒目提示直接移動而不滑動。關閉動態效果時一切立即改變。 + +### 文案規範 + +- 用大家熟悉的名稱命名工作區,而不是它的 id。 +- 方案用一個詞(「Pro」「Enterprise」);角色也一樣(「Owner」「Admin」「Member」「Guest」)。 +- 錯誤訊息要說明發生了什麼事以及該怎麼做:「That invite has expired. Ask for a new one.」 + +### 最佳做法 + +- 讓切換工作盡量簡短,交給觸發器顯示載入狀態;不要卡住頁面。 +- 把切換器放在下方大約有 450px 空間的位置,或放在選單可以溢出的地方。 +- 維持清單順序穩定,讓快速鍵始終不變。 + +### 常見問題 + +**應用程式可以開啟選單嗎?** 設定 `IsOpen`,這樣開啟不會移動焦點。`Open()` 會開啟選單並聚焦搜尋欄位。 + +**模擬的操作會做什麼?** 沒有 `SwitchHandler`、`CreateHandler` 或 `JoinHandler` 時,它會等待 `SimulatedDelay`(650ms),並播報沒有傳送任何內容。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 觸發器是一個按鈕,會回報選單是否開啟。它的名稱讀出工作區與其方案(「Northwind, Enterprise plan」),載入工作區時狀態為「Switching…」。 +- 方向鍵在清單中移動時,搜尋欄位一直保有焦點;它的說明文字是醒目提示列的名稱,因此螢幕閱讀器不必讓焦點離開欄位就能讀到它。 +- 目前的工作區以勾號以及名稱中的「current」標示,而不只依靠顏色。錯誤同時顯示警告圖示與文字。 +- 一個禮貌的即時區域會播報切換開始、切換結果,以及工作由模擬操作完成的情況(「Simulated, nothing was sent」)。沒有工作區時,觸發器的名稱為 "No workspace"。 +- 用 Escape 關閉或選擇後,焦點回到觸發器。用 Tab 離開或點選外部會關閉選單。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Enter、Space、下方向鍵 | 在觸發器上開啟選單,並把焦點移到搜尋欄位 | +| 輸入 | 依名稱、方案或角色篩選;沒有相符項目時,Enter 開啟「建立工作區」並填好名稱 | +| 下方向鍵、上方向鍵 | 在工作區與動作之間移動醒目提示,到兩端時循環 | +| Home、End | 搜尋為空時跳到第一項或最後一項 | +| Enter | 切換到醒目提示的工作區,或開啟「建立工作區」或「透過邀請加入」 | +| Cmd/Ctrl+1 到 9 | 切換到清單中對應位置的工作區(在切換器內部,或開啟全域快速鍵後在任何位置) | +| Escape | 先從表單返回,再清除搜尋,最後關閉選單並把焦點還給觸發器 | +| Tab | 離開選單並將其關閉 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 選單開啟 | 在 180ms 內從 98% 淡入並放大 | 120ms 淡入 | 立即 | +| 選單關閉 | 在 120ms 內淡出並縮小到 98% | 120ms 淡出 | 立即 | +| 醒目提示移動 | 沿 0.24s 無回彈彈簧滑動;在原位出現,絕不從頂端滑入 | 直接跳到定位 | 直接跳到定位 | +| 切換 | 標誌、名稱與方案列各自交叉淡化:100ms 淡出,160ms 淡入並上升 2px;接著顯示閃爍文字與載入圈 | 120ms 淡入淡出,沒有閃爍 | 立即 | +| 開啟建立或加入 | 檢視交叉淡化(100ms 淡出,160ms 淡入),並沿前進方向平移 6px | 120ms 淡入淡出 | 立即 | + +沒有任何東西改變尺寸:觸發器在所有狀態下維持同樣的高度,選單面板在搜尋或開啟表單時也維持同樣的高度。 + +## 相關元件 + +- `DropdownMenu`:錨定在觸發器上的一組聚焦動作。 +- `Popover`:小型錨定浮層。 +- `Avatar`:代表一個人的精簡標識。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/en-US.md new file mode 100644 index 00000000..dc8395b4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/en-US.md @@ -0,0 +1,229 @@ +--- +title: Ticket router +summary: Support messages arrive one at a time, an AI classifier labels intent, sentiment and confidence, and each ticket travels into a team lane with an assignee or waits in a review lane for one click. +--- + + + +## When to use + +`TicketRouter` shows an AI classifier triaging inbound support messages. Messages from email, chat, a messaging +workspace and the help desk arrive one at a time. The classifier labels each one with an intent, a sentiment and a +confidence, a pulse runs along the connector to the target team, and the ticket travels into that team's lane, where +the next person in turn takes it. A ticket the classifier is unsure about waits in the review lane with its suggestion; +one click there sends it on. + +- **Helpdesk and ops dashboards**: a live triage widget, with a human review lane for low confidence. +- **Showing how AI routing works**: what the classifier read, how sure it was and where the ticket went. +- **Landing pages and onboarding** for support products, with a `Simulation` playing an inbox you supply on a loop. + +## When not to use + +- **A full ticket inbox** with search and bulk actions: use a table or a list. +- **A single tool step in a chat transcript**: use `ToolCall`. +- **Generic multi-step agent progress**: use `AgentStatus` or `AgentPlan`. + +## Usage + +```xml + +``` + +`TicketRouter` is a `TemplatedControl` in the Special package (register it with `UseArcSpecial()`). It fills its +container up to 840px; keep it in a content column of about 360 to 880px. With no tickets it rests on "No tickets yet" +in the default lanes (Billing, Engineering, Success and Needs review). Give it `Tickets` to route your own messages, or +a `Simulation` to play an inbox on its own. + +### Simulation + +`TicketRouterSimulation` is an inbox the router plays while `Tickets` is null: the classifier's `Model` name, the +`Lanes`, the `Tickets` in arrival order, each with the answer the simulated classifier gives in its `Sample`, and the +`Routed` tickets already in the lanes. It loops, waits while the card is off screen and pauses with `IsPaused`; its +announcements say the run is simulated and nothing was sent. Your own `Tickets`, `Lanes`, `InitialRouted` and `Model` +win over it, and setting `Simulation` to null starts over on the empty state. + +```csharp +router.Simulation = new TicketRouterSimulation +{ + Model = "Your classifier", + Tickets = + [ + new TicketMessage("t-1", TicketChannel.Email, "Hannah Brooks", "Lumen Bakery", "Charged twice for the March invoice", + "Our card shows two charges of $49 on the 3rd.") + { + Sample = new TicketClassification(TicketIntent.Billing, TicketSentiment.Negative, 0.94), + }, + ], +}; +``` + +The demos on this page take their inbox from `TicketRouterDemoData`: the people, companies and messages are invented, +and the classifier is named after a real model (checked on 2026-10-29). + +### Anatomy + +1. **Header**: the model mark on a tinted tile, the title, the model name, a status (Routing, Paused, All caught up) + and the Pause / Resume button. +2. **Incoming ticket**: the channel glyph and name, the customer and company, when it arrived, the subject and the first + words. Every line is one line high and ends with an ellipsis, so long subjects never change the card's height. +3. **Classifier strip**: "Classifying with Mistral Small 4" shimmers while the classifier reads, then the intent, the + sentiment and the confidence (a short meter and the percentage) fade in, with the route target at the end: the team, + or "Needs review · Suggested Billing". +4. **Connector**: a stem from the stage, a hairline across the lanes and a tick into each. +5. **Lanes**: each lane's name and count over its newest rows. A row shows the assignee's avatar (initials), the subject + and the intent with the customer. A review row shows the confidence and the lane it goes to, with an arrow. + +### Your classifier + +Set `Classifier` to your model call. It gets the message and a cancellation token and returns a +`TicketClassification` (intent, sentiment, confidence from 0 to 1). The token is cancelled when the flow pauses, when +the card scrolls off screen and when it is unloaded; the call runs again when the flow resumes. Throw to show the error +state: "Couldn't classify" with Retry and Send to review. + + + +```csharp +router.Classifier = async (ticket, token) => +{ + var reply = await http.PostAsJsonAsync("/api/classify", ticket, token); + return (await reply.Content.ReadFromJsonAsync(token))!; +}; +router.TicketRouted += (_, e) => helpdesk.Assign(e.Ticket.Message.Id, e.Ticket.LaneId, e.Ticket.Assignee); +router.ReviewConfirmed += (_, e) => helpdesk.Assign(e.Ticket.Message.Id, e.LaneId, e.Ticket.Assignee); +``` + +Without a classifier the card reads each message's `Sample`, and its announcements say the run is simulated and +nothing was sent. A message without a sample is read as an unsure question and goes to review. + +### Lanes and routing + +`Lanes` lists the lanes left to right. Each `TicketLane` has an id, a label, the assignees who take tickets in turn +(round robin), a `Count` of tickets routed earlier, and `IsReview` on the lane that collects low confidence tickets. +`Route` picks the lane id for a classification; the default, `TicketRouter.DefaultRoute`, sends billing and refunds to +`billing`, bugs to `engineering` and feature requests and questions to `success`. An id that matches no team lane falls +back to the first one. With no `Lanes` (and no simulation playing) the card uses the default lanes with those ids +(`TicketRouter.BillingLaneId`, `EngineeringLaneId`, `SuccessLaneId` and `ReviewLaneId`), named by the labels and +without assignees. + +A ticket below `ReviewThreshold` (0.7 by default) lands in the review lane with the picked lane as its suggestion and no +assignee. Clicking it, or pressing Enter or Space on it, sends it to that lane with the next assignee there and raises +`ReviewConfirmed`. `ConfirmReview(ticket)` does the same from code. + +### Your queue + +`Tickets` takes your messages in order. Your own queue does not loop unless you set `Loop`, so it ends in +"All caught up"; append to an `ObservableCollection` and the flow picks up the new tickets. An empty queue shows +"No tickets yet". `InitialRouted` fills the lanes on first show (newest first), `Restart()` starts over, and +`RoutedTickets` keeps the newest 80 tickets. + +### Pausing and speed + +`IsPaused` (two-way) pauses the flow where it is and cancels a running classifier call; the Pause / Resume button sets +it and every change raises `PausedChanged`. Set `IsPaused="True"` in markup to start paused. `Speed` multiplies the +pace (1 is about two seconds per ticket). One frame loop runs the flow, and it waits while the card is off screen. + +### Narrow containers + + + +Below 600px of content the four lanes fold into a 2 by 2 grid with at most two rows each (`MaxVisible` is 3 above), the +connector hides and the labels stack above the route target. Below 400px the confidence meter and the model name hide; +the percentage stays. + +### Your own words + +`Labels` holds every word the card shows or says, including the intents, sentiments and channels and every +announcement: + +```csharp +router.Labels = new TicketRouterLabels +{ + Routing = "Tri en cours", + Intent = intent => intent == TicketIntent.Billing ? "Facturation" : intent.ToString(), + AnnounceRouted = (subject, lane, assignee) => $"{subject} : {lane}", +}; +``` + +`Title`, `Model` and `ModelMark` (a 24 × 24 stroked glyph, the sparkle by default) set the header. Channels use neutral +glyphs (mail, chat bubble, message square, life buoy), never brand marks. + +### Content guidelines + +- Subjects are what the customer wrote; keep them as they are and let the card cut them. +- Keep lane names to one or two words ("Billing", "Needs review"). +- Keep the threshold where a wrong route costs more than a short wait. + +### Best practices + +- Wire `Classifier` to your model and `TicketRouted` to your help desk; the card never sends anything itself. +- Use the review lane: low confidence is the point where a person should decide. +- Pass `CancellationToken` through to your HTTP call so a pause really stops it. + +### Common questions + +**Can the same message land twice?** When a simulation loops, yes; each landing is its own `RoutedTicket`. + +**Does a ticket in the review lane count toward a team?** No. It counts in the review lane until someone confirms it. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is named by its title. Each lane is named with its label and ticket count ("Billing, 14 tickets"). +- A polite live region announces each routing ("routed to Billing, assigned to Andre Williams"), review tickets, + failures, pauses and confirmations, and adds that the run is simulated when no classifier is set. +- Intent, sentiment and review status always pair a glyph with a word; confidence reads as a percentage, never as + color alone. +- Review tickets are real buttons named with the subject, the confidence and the target lane. Team rows are not tab + stops. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves between Pause, Retry and Send to review when shown, and the review tickets | +| Enter, Space | Pauses or resumes, retries a failed classification, sends it to review, or confirms a review ticket's lane | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A ticket arrives | The old card fades out over 100ms, the new one fades in over 160ms and rises 4px | Fades, no rise | At once | +| Classifying | The label shimmers | Still | Still | +| Labels | Fade in over 160ms with a 50ms stagger and rise 2px | Fade, no rise | At once | +| Connector | A pulse draws to the target lane in 240ms, then fades | None | None | +| Landing | The row travels from the stage into its lane on a position spring (0.32s, no bounce) | Appears in place | Appears in place | +| Rows enter and leave | Their height springs (0.24s, no bounce) | At once | At once | +| Landing highlight | The row lights with the accent tint, which fades over 900ms | Kept | None | +| Lane counts | Roll in reserved tabular width | At once | At once | + +About two seconds per ticket at speed 1: arrive 220ms, classify, labels, the 240ms pulse, then the landing. The flow +pauses off screen. The card's height never changes while tickets flow. + +## Related + +- `AgentStatus` for the live status of one running agent. +- `ToolCall` for a single tool step in a transcript. +- `AgentPlan` for an agent's plan as a checklist. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/zh-CN.md new file mode 100644 index 00000000..589db145 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/zh-CN.md @@ -0,0 +1,188 @@ +--- +title: Ticket router +summary: 支持消息逐条到达,AI 分类器标注意图、情绪与置信度,每张工单都会移入带负责人的团队泳道,或在复核泳道中等待一次点击。 +source-hash: 7fc5577c3e60 +--- + + + +## 何时使用 + +`TicketRouter` 展示 AI 分类器如何分拣进来的支持消息。来自邮件、聊天、消息工作区和帮助台的消息逐条到达。分类器为每条消息标注意图、情绪和置信度,一道脉冲沿连接线跑向目标团队,工单随即移入该团队的泳道,由轮到的下一位成员接手。分类器拿不准的工单会带着它的建议停在复核泳道里,在那里点一下即可送走。 + +- **帮助台与运营仪表板**:一个实时分拣小部件,并为低置信度保留人工复核泳道。 +- **展示 AI 路由如何工作**:分类器读到了什么、有多确定、工单去了哪里。 +- **支持类产品的落地页与新手引导**,用 `Simulation` 循环播放一个由你提供的收件箱。 + +## 何时不用 + +- **完整的工单收件箱**(带搜索和批量操作):使用表格或列表。 +- **聊天记录中的单个工具步骤**:使用 `ToolCall`。 +- **通用的多步骤智能体进度**:使用 `AgentStatus` 或 `AgentPlan`。 + +## 用法 + +```xml + +``` + +`TicketRouter` 是 Special 包中的 `TemplatedControl`(用 `UseArcSpecial()` 注册)。它填满容器,最宽 840px;请放在约 360 到 880px 的内容栏中。没有工单时,它停在默认泳道(Billing、Engineering、Success 和 Needs review)中的 “No tickets yet”。设置 `Tickets` 来路由你自己的消息,或设置 `Simulation` 让它自行播放一个收件箱。 + +### 模拟 + +`TicketRouterSimulation` 是 `Tickets` 为 null 时路由器播放的收件箱:分类器的 `Model` 名称、`Lanes`、按到达顺序排列的 `Tickets`(每条的 `Sample` 中是模拟分类器给出的答案),以及已在泳道中的 `Routed` 工单。它会循环播放,卡片离开屏幕时等待,并随 `IsPaused` 暂停;它的播报会说明这是一次模拟运行、没有发送任何内容。你自己设置的 `Tickets`、`Lanes`、`InitialRouted` 和 `Model` 优先于它;把 `Simulation` 设为 null 会回到空状态重新开始。 + +```csharp +router.Simulation = new TicketRouterSimulation +{ + Model = "Your classifier", + Tickets = + [ + new TicketMessage("t-1", TicketChannel.Email, "Hannah Brooks", "Lumen Bakery", "Charged twice for the March invoice", + "Our card shows two charges of $49 on the 3rd.") + { + Sample = new TicketClassification(TicketIntent.Billing, TicketSentiment.Negative, 0.94), + }, + ], +}; +``` + +本页的演示从 `TicketRouterDemoData` 获取收件箱:人物、公司和消息都是虚构的,分类器使用真实模型的名称(核对于 2026-10-29)。 + +### 结构 + +1. **头部**:着色方块上的模型标记、标题、模型名称、状态(Routing、Paused、All caught up)以及暂停 / 继续按钮。 +2. **到达的工单**:渠道图标与名称、客户与公司、到达时间、主题和开头几句话。每一行都只有一行高,并以省略号结尾,所以长主题永远不会改变卡片高度。 +3. **分类条**:分类器读取时 "Classifying with Mistral Small 4" 闪烁,随后意图、情绪和置信度(一条短进度条和百分比)依次淡入,末尾是路由目标:团队,或 "Needs review · Suggested Billing"。 +4. **连接线**:从舞台引出的竖线、横跨各泳道的细线,以及伸入每条泳道的短线。 +5. **泳道**:每条泳道的名称和数量,下方是最新的几行。一行显示负责人的头像(姓名首字母)、主题,以及意图和客户。复核行显示置信度和它将去往的泳道,并带一个箭头。 + +### 你的分类器 + +把 `Classifier` 设为你的模型调用。它接收消息和取消令牌,返回一个 `TicketClassification`(意图、情绪、0 到 1 的置信度)。流程暂停、卡片滚出屏幕以及被卸载时,令牌都会被取消;流程恢复时会重新调用。抛出异常即显示错误状态:"Couldn't classify",并提供 Retry 和 Send to review。 + + + +```csharp +router.Classifier = async (ticket, token) => +{ + var reply = await http.PostAsJsonAsync("/api/classify", ticket, token); + return (await reply.Content.ReadFromJsonAsync(token))!; +}; +router.TicketRouted += (_, e) => helpdesk.Assign(e.Ticket.Message.Id, e.Ticket.LaneId, e.Ticket.Assignee); +router.ReviewConfirmed += (_, e) => helpdesk.Assign(e.Ticket.Message.Id, e.LaneId, e.Ticket.Assignee); +``` + +没有分类器时,卡片读取每条消息的 `Sample`,它的播报会说明这是一次模拟运行、没有发送任何内容。没有示例答案的消息会被当作一个不确定的问题,进入复核。 + +### 泳道与路由 + +`Lanes` 从左到右列出泳道。每个 `TicketLane` 有 id、标签、轮流接单的负责人(轮询)、此前已路由的工单数 `Count`,以及在收集低置信度工单的泳道上设置的 `IsReview`。`Route` 为一次分类挑选泳道 id;默认的 `TicketRouter.DefaultRoute` 把账单和退款送往 `billing`,缺陷送往 `engineering`,功能请求和问题送往 `success`。不匹配任何团队泳道的 id 会回退到第一条团队泳道。没有 `Lanes`(且没有播放模拟)时,卡片使用带有这些 id 的默认泳道(`TicketRouter.BillingLaneId`、`EngineeringLaneId`、`SuccessLaneId` 和 `ReviewLaneId`),名称来自标签文字,没有负责人。 + +低于 `ReviewThreshold`(默认 0.7)的工单会落入复核泳道,以挑选出的泳道作为建议,且没有负责人。点击它,或在它上面按 Enter 或 Space,就会把它送往该泳道、由那里的下一位负责人接手,并触发 `ReviewConfirmed`。`ConfirmReview(ticket)` 在代码中做同样的事。 + +### 你的队列 + +`Tickets` 按顺序接收你的消息。除非设置 `Loop`,你自己的队列不会循环,因此最后停在 "All caught up";向 `ObservableCollection` 追加消息,流程就会接着处理新工单。空队列显示 "No tickets yet"。`InitialRouted` 在首次显示时填充泳道(最新的在前),`Restart()` 从头开始,`RoutedTickets` 保留最新的 80 张工单。 + +### 暂停与速度 + +`IsPaused`(双向)让流程停在当前位置,并取消正在进行的分类器调用;暂停 / 继续按钮会设置它,每次变化都会触发 `PausedChanged`。在标记中设置 `IsPaused="True"` 即可以暂停状态启动。`Speed` 调整节奏的倍数(1 约为每张工单两秒)。一个帧循环驱动整个流程,卡片不在屏幕上时它会等待。 + +### 窄容器 + + + +内容宽度低于 600px 时,四条泳道折叠成 2 × 2 网格,每条最多两行(更宽时 `MaxVisible` 为 3),连接线隐藏,标签堆叠在路由目标上方。低于 400px 时,置信度进度条和模型名称隐藏,百分比保留。 + +### 你自己的文案 + +`Labels` 包含卡片显示或播报的每一个词,包括意图、情绪、渠道以及每一条播报: + +```csharp +router.Labels = new TicketRouterLabels +{ + Routing = "Tri en cours", + Intent = intent => intent == TicketIntent.Billing ? "Facturation" : intent.ToString(), + AnnounceRouted = (subject, lane, assignee) => $"{subject} : {lane}", +}; +``` + +`Title`、`Model` 和 `ModelMark`(24 × 24 描边图形,默认为闪光图标)设置头部。渠道使用中性的图标(信封、聊天气泡、消息方框、救生圈),从不使用品牌标志。 + +### 内容指南 + +- 主题是客户写下的原文;保持原样,让卡片去截断。 +- 泳道名称保持一两个词("Billing"、"Needs review")。 +- 把阈值设在"路由错误的代价高于短暂等待"的位置。 + +### 最佳实践 + +- 把 `Classifier` 接到你的模型,把 `TicketRouted` 接到你的帮助台;卡片本身从不发送任何内容。 +- 用好复核泳道:低置信度正是应该由人来决定的时刻。 +- 把 `CancellationToken` 一路传给你的 HTTP 调用,这样暂停才能真正停止它。 + +### 常见问题 + +**同一条消息会落地两次吗?** 模拟循环时会;每次落地都是一个独立的 `RoutedTicket`。 + +**复核泳道中的工单会计入某个团队吗?** 不会。在有人确认之前,它计入复核泳道。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片以其标题命名。每条泳道以其标签和工单数命名("Billing, 14 tickets")。 +- 一个礼貌的实时区域播报每次路由("routed to Billing, assigned to Andre Williams")、复核工单、失败、暂停和确认;没有设置分类器时,还会补充说明这是模拟运行。 +- 意图、情绪和复核状态总是图标与文字成对出现;置信度以百分比读出,从不只靠颜色。 +- 复核工单是真正的按钮,名称包含主题、置信度和目标泳道。团队行不是 Tab 停靠点。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 在暂停按钮、出现时的 Retry 与 Send to review,以及各复核工单之间移动 | +| Enter、Space | 暂停或继续、重试失败的分类、送入复核,或确认复核工单的泳道 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 工单到达 | 旧卡片在 100ms 内淡出,新卡片在 160ms 内淡入并上升 4px | 淡入淡出,不上升 | 立即 | +| 分类中 | 标签闪烁 | 静止 | 静止 | +| 标签 | 以 50ms 的间隔依次在 160ms 内淡入并上升 2px | 淡入,不上升 | 立即 | +| 连接线 | 一道脉冲在 240ms 内画向目标泳道,然后淡出 | 无 | 无 | +| 落地 | 行以位置弹簧(0.32s,无回弹)从舞台移入其泳道 | 原地出现 | 原地出现 | +| 行的进入与离开 | 高度以弹簧变化(0.24s,无回弹) | 立即 | 立即 | +| 落地高亮 | 行亮起强调色底色,并在 900ms 内淡去 | 保留 | 无 | +| 泳道计数 | 在预留的等宽位置中滚动 | 立即 | 立即 | + +速度为 1 时每张工单约两秒:到达 220ms、分类、标签、240ms 的脉冲,然后落地。卡片不在屏幕上时流程暂停。工单流动时卡片高度从不改变。 + +## 相关组件 + +- `AgentStatus`:单个运行中智能体的实时状态。 +- `ToolCall`:对话记录中的单个工具步骤。 +- `AgentPlan`:以清单形式呈现的智能体计划。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/zh-TW.md new file mode 100644 index 00000000..6a5ff195 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TicketRouter/zh-TW.md @@ -0,0 +1,188 @@ +--- +title: Ticket router +summary: 支援訊息逐則送達,AI 分類器標註意圖、情緒與信賴度,每張工單都會移入有負責人的團隊泳道,或在複核泳道中等待一次點擊。 +source-hash: 7fc5577c3e60 +--- + + + +## 何時使用 + +`TicketRouter` 展示 AI 分類器如何分流進來的支援訊息。來自電子郵件、聊天、訊息工作區與服務台的訊息逐則送達。分類器為每則訊息標註意圖、情緒與信賴度,一道脈衝沿著連接線跑向目標團隊,工單隨即移入該團隊的泳道,由輪到的下一位成員接手。分類器沒有把握的工單會帶著它的建議停在複核泳道,在那裡點一下即可送出。 + +- **服務台與營運儀表板**:一個即時分流小工具,並為低信賴度保留人工複核泳道。 +- **展示 AI 路由如何運作**:分類器讀到了什麼、有多確定、工單去了哪裡。 +- **客服類產品的形象頁與新手導覽**,以 `Simulation` 循環播放一個由你提供的收件匣。 + +## 何時不用 + +- **完整的工單收件匣**(含搜尋與批次操作):請使用表格或清單。 +- **對話紀錄中的單一工具步驟**:請使用 `ToolCall`。 +- **一般的多步驟代理進度**:請使用 `AgentStatus` 或 `AgentPlan`。 + +## 用法 + +```xml + +``` + +`TicketRouter` 是 Special 套件中的 `TemplatedControl`(以 `UseArcSpecial()` 註冊)。它會填滿容器,最寬 840px;請放在約 360 到 880px 的內容欄中。沒有工單時,它會停在預設泳道(Billing、Engineering、Success 與 Needs review)中的「No tickets yet」。設定 `Tickets` 來路由你自己的訊息,或設定 `Simulation` 讓它自行播放一個收件匣。 + +### 模擬 + +`TicketRouterSimulation` 是 `Tickets` 為 null 時路由器播放的收件匣:分類器的 `Model` 名稱、`Lanes`、依到達順序排列的 `Tickets`(每則的 `Sample` 中是模擬分類器給出的答案),以及已在泳道中的 `Routed` 工單。它會循環播放,卡片離開螢幕時等待,並隨 `IsPaused` 暫停;它的播報會說明這是一次模擬執行、沒有送出任何內容。你自己設定的 `Tickets`、`Lanes`、`InitialRouted` 與 `Model` 優先於它;把 `Simulation` 設為 null 會回到空狀態重新開始。 + +```csharp +router.Simulation = new TicketRouterSimulation +{ + Model = "Your classifier", + Tickets = + [ + new TicketMessage("t-1", TicketChannel.Email, "Hannah Brooks", "Lumen Bakery", "Charged twice for the March invoice", + "Our card shows two charges of $49 on the 3rd.") + { + Sample = new TicketClassification(TicketIntent.Billing, TicketSentiment.Negative, 0.94), + }, + ], +}; +``` + +本頁的示範從 `TicketRouterDemoData` 取得收件匣:人物、公司與訊息都是虛構的,分類器使用真實模型的名稱(核對於 2026-10-29)。 + +### 結構 + +1. **標頭**:上色方塊上的模型標記、標題、模型名稱、狀態(Routing、Paused、All caught up)以及暫停 / 繼續按鈕。 +2. **送達的工單**:管道圖示與名稱、客戶與公司、送達時間、主旨以及開頭幾句話。每一行都只有一行高,並以刪節號結尾,所以長主旨永遠不會改變卡片高度。 +3. **分類列**:分類器讀取時 "Classifying with Mistral Small 4" 閃爍,接著意圖、情緒與信賴度(一條短量條與百分比)依序淡入,末端是路由目標:團隊,或 "Needs review · Suggested Billing"。 +4. **連接線**:從舞台引出的直線、橫跨各泳道的細線,以及伸入每條泳道的短線。 +5. **泳道**:每條泳道的名稱與數量,下方是最新的幾列。一列顯示負責人的大頭貼(姓名縮寫)、主旨,以及意圖與客戶。複核列顯示信賴度與它將前往的泳道,並帶一個箭頭。 + +### 你的分類器 + +把 `Classifier` 設為你的模型呼叫。它接收訊息與取消權杖,傳回一個 `TicketClassification`(意圖、情緒、0 到 1 的信賴度)。流程暫停、卡片捲出畫面以及被卸載時,權杖都會被取消;流程恢復時會重新呼叫。擲出例外即顯示錯誤狀態:"Couldn't classify",並提供 Retry 與 Send to review。 + + + +```csharp +router.Classifier = async (ticket, token) => +{ + var reply = await http.PostAsJsonAsync("/api/classify", ticket, token); + return (await reply.Content.ReadFromJsonAsync(token))!; +}; +router.TicketRouted += (_, e) => helpdesk.Assign(e.Ticket.Message.Id, e.Ticket.LaneId, e.Ticket.Assignee); +router.ReviewConfirmed += (_, e) => helpdesk.Assign(e.Ticket.Message.Id, e.LaneId, e.Ticket.Assignee); +``` + +沒有分類器時,卡片會讀取每則訊息的 `Sample`,它的播報會說明這是一次模擬執行、沒有送出任何內容。沒有範例答案的訊息會被視為一個不確定的問題,進入複核。 + +### 泳道與路由 + +`Lanes` 由左到右列出泳道。每個 `TicketLane` 有 id、標籤、輪流接單的負責人(輪替)、先前已路由的工單數 `Count`,以及在收集低信賴度工單的泳道上設定的 `IsReview`。`Route` 為一次分類挑選泳道 id;預設的 `TicketRouter.DefaultRoute` 把帳務與退款送往 `billing`,缺陷送往 `engineering`,功能需求與問題送往 `success`。不符合任何團隊泳道的 id 會回退到第一條團隊泳道。沒有 `Lanes`(且沒有播放模擬)時,卡片會使用帶有這些 id 的預設泳道(`TicketRouter.BillingLaneId`、`EngineeringLaneId`、`SuccessLaneId` 與 `ReviewLaneId`),名稱來自標籤文字,沒有負責人。 + +低於 `ReviewThreshold`(預設 0.7)的工單會落入複核泳道,以挑選出的泳道作為建議,且沒有負責人。點擊它,或在它上面按 Enter 或 Space,就會把它送往該泳道、由那裡的下一位負責人接手,並觸發 `ReviewConfirmed`。`ConfirmReview(ticket)` 在程式碼中做同樣的事。 + +### 你的佇列 + +`Tickets` 依序接收你的訊息。除非設定 `Loop`,你自己的佇列不會循環,因此最後停在 "All caught up";向 `ObservableCollection` 追加訊息,流程就會接著處理新工單。空佇列顯示 "No tickets yet"。`InitialRouted` 在首次顯示時填入泳道(最新的在前),`Restart()` 從頭開始,`RoutedTickets` 保留最新的 80 張工單。 + +### 暫停與速度 + +`IsPaused`(雙向)讓流程停在目前位置,並取消進行中的分類器呼叫;暫停 / 繼續按鈕會設定它,每次變化都會觸發 `PausedChanged`。在標記中設定 `IsPaused="True"` 即可以暫停狀態啟動。`Speed` 調整節奏的倍數(1 約為每張工單兩秒)。一個影格迴圈驅動整個流程,卡片不在畫面上時它會等待。 + +### 窄容器 + + + +內容寬度低於 600px 時,四條泳道摺成 2 × 2 格線,每條最多兩列(較寬時 `MaxVisible` 為 3),連接線隱藏,標籤堆疊在路由目標上方。低於 400px 時,信賴度量條與模型名稱隱藏,百分比保留。 + +### 你自己的文字 + +`Labels` 包含卡片顯示或播報的每一個字詞,包括意圖、情緒、管道以及每一則播報: + +```csharp +router.Labels = new TicketRouterLabels +{ + Routing = "Tri en cours", + Intent = intent => intent == TicketIntent.Billing ? "Facturation" : intent.ToString(), + AnnounceRouted = (subject, lane, assignee) => $"{subject} : {lane}", +}; +``` + +`Title`、`Model` 與 `ModelMark`(24 × 24 描邊圖形,預設為閃光圖示)設定標頭。管道使用中性的圖示(信封、聊天泡泡、訊息方框、救生圈),從不使用品牌標誌。 + +### 內容指引 + +- 主旨是客戶寫下的原文;保持原樣,讓卡片去截斷。 +- 泳道名稱保持一兩個字詞("Billing"、"Needs review")。 +- 把門檻設在「路由錯誤的代價高於短暫等待」的位置。 + +### 最佳做法 + +- 把 `Classifier` 接到你的模型,把 `TicketRouted` 接到你的服務台;卡片本身從不送出任何內容。 +- 善用複核泳道:低信賴度正是應該由人來決定的時刻。 +- 把 `CancellationToken` 一路傳給你的 HTTP 呼叫,暫停才能真正停止它。 + +### 常見問題 + +**同一則訊息會落地兩次嗎?** 模擬循環時會;每次落地都是一個獨立的 `RoutedTicket`。 + +**複核泳道中的工單會計入某個團隊嗎?** 不會。在有人確認之前,它計入複核泳道。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片以其標題命名。每條泳道以其標籤與工單數命名("Billing, 14 tickets")。 +- 一個禮貌的即時區域會播報每次路由("routed to Billing, assigned to Andre Williams")、複核工單、失敗、暫停與確認;沒有設定分類器時,還會補充說明這是模擬執行。 +- 意圖、情緒與複核狀態一律圖示與文字成對出現;信賴度以百分比讀出,從不只靠顏色。 +- 複核工單是真正的按鈕,名稱包含主旨、信賴度與目標泳道。團隊列不是 Tab 停駐點。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 在暫停按鈕、出現時的 Retry 與 Send to review,以及各張複核工單之間移動 | +| Enter、Space | 暫停或繼續、重試失敗的分類、送入複核,或確認複核工單的泳道 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 工單送達 | 舊卡片在 100ms 內淡出,新卡片在 160ms 內淡入並上升 4px | 淡入淡出,不上升 | 立即 | +| 分類中 | 標籤閃爍 | 靜止 | 靜止 | +| 標籤 | 以 50ms 的間隔依序在 160ms 內淡入並上升 2px | 淡入,不上升 | 立即 | +| 連接線 | 一道脈衝在 240ms 內畫向目標泳道,然後淡出 | 無 | 無 | +| 落地 | 列以位置彈簧(0.32s,無回彈)從舞台移入其泳道 | 原地出現 | 原地出現 | +| 列的進入與離開 | 高度以彈簧變化(0.24s,無回彈) | 立即 | 立即 | +| 落地醒目提示 | 列亮起強調色底色,並在 900ms 內淡去 | 保留 | 無 | +| 泳道計數 | 在預留的等寬位置中滾動 | 立即 | 立即 | + +速度為 1 時每張工單約兩秒:送達 220ms、分類、標籤、240ms 的脈衝,然後落地。卡片不在畫面上時流程暫停。工單流動時卡片高度從不改變。 + +## 相關元件 + +- `AgentStatus`:單一執行中代理的即時狀態。 +- `ToolCall`:對話紀錄中的單一工具步驟。 +- `AgentPlan`:以清單呈現的代理計畫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/en-US.md new file mode 100644 index 00000000..4bd5d05c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/en-US.md @@ -0,0 +1,218 @@ +--- +title: TL;DR toggle +summary: An article card that streams an AI summary or key points in place of the article, with a length slider and key points that jump back to their paragraphs. +--- + + + +## When to use + +`TldrToggle` is an article card with a Full, Summary and Key points switch and a length slider. Switching streams an AI +summary in place of the article while the card's height springs to fit, and each key point links back to the paragraph +it came from, which is scrolled into view and highlighted. + +- **A quick summary or key points** above or in place of a long article, doc, ticket or report. +- **Letting readers check AI claims** against the source, through key points that link back to paragraphs. +- **Reading surfaces** where people choose how much detail they want with a length control. + +## When not to use + +- **Open-ended questions about content**: use an answer or chat component instead. +- **Short content** of a couple of paragraphs, where a summary saves no time. +- **Summaries of many documents at once**: use a list or a digest layout instead of one card per item. + +## Usage + +Give the card an `Article` whose paragraphs have stable ids, and a `Summarizer` that streams `TldrStreamEvent`s. Send +`TldrStreamEvent.OfText` for prose; for key points send `TldrStreamEvent.OfPoint(paragraphId)` before each point's +text, so its jump link lands on the right paragraph. Without a summarizer the card simulates summaries from your +article's lead sentences (`TldrStreams.Simulate`) and sends nothing. With no `Article` and no `Simulation` it shows its +quiet empty state: no header, "This article has no text to summarize yet." and the switch and slider off. + +```csharp +card.Article = new TldrArticle +{ + Title = post.Title, + SourceName = "Engineering blog", + AuthorName = post.Author, + Date = "Oct 21, 2026", + Paragraphs = post.Paragraphs.Select(p => new TldrParagraph(p.Id, p.Text)).ToList(), +}; +card.Model = new AgentModel("GPT-6 Luna", ArcIcons.Sparkles); +card.Summarizer = request => myService.StreamTldr(request); // IAsyncEnumerable +card.Jumping += (_, e) => analytics.Track("tldr_jump", e.ParagraphId); +``` + +The request carries `Mode` (`Summary` or `KeyPoints`), `Length` (1 to 5), `TargetWords` (35 to 130), `PointCount` +(3 to 7), the article and a `CancellationToken` that is cancelled on Stop, Escape, a mode or length change, a new +article, or when the card leaves the window. Pass the length to your model as `TargetWords` or `PointCount` rather than +a vague word like "short". + +### Simulation + +To show the card without a summarizer (a demo, a design review, an onboarding tour), give it a `TldrToggleSimulation`: +the article, the model to credit, a summary written for each length and key points ranked by weight. The card streams +the written text after a short thinking pause, says it is simulated and nothing was sent, and opens the summary by itself +once it has been half in view for a second (`AutoPlay="False"` keeps it on the article). A length without a written +summary gets the lead sentences. Your own `Article` wins over the simulation, and setting `Simulation` to null stops it +and shows the empty state. + +```csharp +card.Simulation = new TldrToggleSimulation +{ + Article = post, + Model = new AgentModel("GPT-6 Luna", ArcIcons.Sparkles), + Summaries = [shortest, shorter, medium, longer, longest], + KeyPoints = [new TldrKeyPoint("results", "Median cold start fell from 4.2 seconds to 0.9.")], +}; +``` + +The preview above plays a simulation whose post, summaries and key points are the gallery's own demo data. + +### Anatomy + +1. **Header**: the source's mark (or its first letter in a tinted square), name, date and reading time; the title; the + author with an avatar. +2. **Controls**: the mode switch (`SegmentedControl`) and the length slider (`Slider`), which reads "~60 words" or + "4 points" and is disabled in Full mode. +3. **Body**: the article, the streaming summary or the key point checklist. Its height springs to its content. +4. **Footer**: the state as an icon and a word, the model with its 14px mark, and Stop, Regenerate and Copy. + +### Key points + + + +Key points arrive as a checklist: each point ticks from a dashed circle to a check when it is written. Once the list +is done, each point's ¶ link switches back to the article, waits for the height to settle, scrolls the paragraph into +view, moves focus there and highlights it. `Jumping` is raised first with the paragraph's id. Call `JumpTo(id)` to do +the same from code. + +### Results, Stop and Regenerate + +Finished results are kept per mode and length, so switching back is instant and makes no request. `Regenerate` (the +footer's button) asks again. Stop or Escape ends a stream and keeps what arrived. `Completed` reports each finished +result with its text, points and word count, for analytics or a server-side cache; stopped and failed runs do not +raise it. `ModeChanged` and `LengthChanged` report changes the reader makes; the slider reports a length when a drag +is released or a key moves it, so each change makes one request. + +### Your own summarizer + + + +Stream when your provider streams. When it returns whole results, wrap them with `TldrStreams.FromResult(text, token)` +or `TldrStreams.FromResult(points, token)` and the card still streams them in chunks of 3 to 8 words. + +### Failures + + + +An exception from the summarizer shows its message as an alert with Try again. `TldrStreams.Simulate(request, fail: true)` +and `TldrToggleSimulation.Summarize(request, fail: true)` throw after the thinking pause. When you wrap a simulation +yourself, set `IsSimulated="True"` so announcements and error text say "Simulated, nothing was sent". + +### Narrow containers + + + +The card fills its container up to 680px. Below 520px the length slider moves under the mode switch at full width; +below 440px the padding steps from 24px to 16px. Footer labels stay on one line and the model's name truncates first. + +### Autoplay + +With no summarizer, the card opens the summary by itself once it has been half in view for a second, unless the reader +has already pressed or typed inside it. Set `AutoPlay` to turn it on or off explicitly. + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +card.Labels = new TldrToggleLabels +{ + Full = "Article", + KeyPoints = "Highlights", + Words = words => $"{words} words", +}; +``` + +### Best practices + +- Give paragraphs stable ids and have the model return the id each key point came from. +- Pass the length as `TargetWords` or `PointCount`. +- Without a summarizer the card runs a simulation and says so in its announcements; never ship that to production. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- The card is named after the article's title, and the title is a level 2 heading. +- The mode switch is a labelled group ("Reading mode"); the length slider's spoken value is the target ("~60 words", + "4 points"), and it is disabled in Full mode. +- A polite live region announces each run once ("Summarizing with GPT-6 Luna", "Summary ready, 58 words", "4 key points + ready", "Stopped"), never the streamed chunks. With the simulation it adds "Simulated, nothing was sent". +- Failures render as an alert with the message and a Try again button. +- Jump links are named after their point ("Jump to paragraph 3: Partial bootstrapping loads..."), and the jump moves + focus to the highlighted paragraph so keyboard and screen reader users land where the eye does. +- Status is never color alone: done, stopped and failed each pair an icon with a word, and key points tick from a dashed + circle to a check. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the mode switch, the length slider, the jump links and the footer actions | +| Arrow keys | On the mode switch, move between Full, Summary and Key points; on the slider, change the length by one step (one request per step) | +| Home, End | The first or last mode, or the shortest or longest length | +| Enter, Space | On a jump link, switch to the article, scroll to its paragraph, focus it and highlight it | +| Escape | Stops a running stream and keeps what arrived | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Body height (a view swaps or text streams in) | One spring, 0.26s, no bounce | At once | At once | +| Container resize | Cuts straight to the new height | At once | At once | +| Views swap | Old out over 100ms, new in over 160ms | 120ms crossfade | At once | +| Footer state and actions | The same crossfade | 120ms crossfade | At once | +| Streamed chunk (3 to 8 words, every 30 to 40ms) | Fades in over 140ms | Appears | Appears | +| Kept result | Appears whole, no fade | Appears | Appears | +| Thinking (about 0.7s) | Calm shimmer on the body's line; then the footer's Writing shimmers | Still | Still | +| Key point ticks | Dashed circle fades and shrinks, check grows in on the same spring | At once | At once | +| Jump | Waits for the height, scrolls smoothly, highlight in over 160ms and out over 600ms | Jumps, highlight fades | Jumps, highlight shows | + +Only one shimmer runs at a time. + +## Related + +- `ReasoningBlock` for the thinking of an AI reply. +- `SegmentedControl` and `Slider`, which the card uses for the mode and the length. +- `CopyButton` for copying a value with immediate confirmation. +- `ToolCall` for the tool calls of an agent transcript. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/zh-CN.md new file mode 100644 index 00000000..4c4c36da --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/zh-CN.md @@ -0,0 +1,187 @@ +--- +title: TL;DR toggle +summary: 一张文章卡片,可在原文位置流式呈现 AI 摘要或要点,带有长度滑块,每条要点都能跳回其出处段落。 +source-hash: 036af066c83d +--- + + + +## 何时使用 + +`TldrToggle` 是一张文章卡片,带有"全文""摘要""要点"切换和一个长度滑块。切换后,AI 摘要会在原文的位置流式出现,卡片高度随之以弹簧过渡到合适的大小;每条要点都链接回它所出自的段落,点击后该段落会滚动到视野中并高亮。 + +- **快速摘要或要点**:放在长文章、文档、工单或报告的上方,或直接替换原文。 +- **让读者核对 AI 的结论**:要点链接回段落,读者可以对照原文。 +- **阅读场景**:读者通过长度控件自行决定想看多少细节。 + +## 何时不用 + +- **针对内容的开放式提问**:改用问答或聊天组件。 +- **很短的内容**:只有一两段时,摘要省不了时间。 +- **一次汇总多篇文档**:改用列表或摘要汇编布局,而不是每项一张卡片。 + +## 用法 + +为卡片提供一个 `Article`,其段落带有稳定的 id,再提供一个流式返回 `TldrStreamEvent` 的 `Summarizer`。正文用 `TldrStreamEvent.OfText` 发送;要点则在每条要点的文字之前先发送 `TldrStreamEvent.OfPoint(paragraphId)`,这样它的跳转链接才会落在正确的段落上。没有摘要器时,卡片会用文章的首句模拟摘要(`TldrStreams.Simulate`),不会发送任何内容。既没有 `Article` 也没有 `Simulation` 时,卡片显示其安静的空状态:没有页眉,显示"This article has no text to summarize yet.",切换器和滑块处于关闭状态。 + +```csharp +card.Article = new TldrArticle +{ + Title = post.Title, + SourceName = "Engineering blog", + AuthorName = post.Author, + Date = "Oct 21, 2026", + Paragraphs = post.Paragraphs.Select(p => new TldrParagraph(p.Id, p.Text)).ToList(), +}; +card.Model = new AgentModel("GPT-6 Luna", ArcIcons.Sparkles); +card.Summarizer = request => myService.StreamTldr(request); // IAsyncEnumerable +card.Jumping += (_, e) => analytics.Track("tldr_jump", e.ParagraphId); +``` + +请求中包含 `Mode`(`Summary` 或 `KeyPoints`)、`Length`(1 到 5)、`TargetWords`(35 到 130)、`PointCount`(3 到 7)、文章本身,以及一个 `CancellationToken`:在停止、按 Escape、切换模式或长度、换了文章,或卡片离开窗口时取消。把长度以 `TargetWords` 或 `PointCount` 传给模型,而不是"简短"这类含糊的词。 + +### 模拟 + +想在没有摘要器的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `TldrToggleSimulation`:文章、要标注的模型、为每个长度写好的摘要,以及按重要性排序的要点。卡片会在短暂的思考停顿之后流式输出写好的文字,说明这是模拟、没有发送任何内容,并在卡片有一半处于可见区域一秒后自动打开摘要(`AutoPlay="False"` 会让它停留在文章上)。没有写好摘要的长度会使用文章的首句。你自己的 `Article` 优先于模拟;把 `Simulation` 设为 null 会停止模拟并显示空状态。 + +```csharp +card.Simulation = new TldrToggleSimulation +{ + Article = post, + Model = new AgentModel("GPT-6 Luna", ArcIcons.Sparkles), + Summaries = [shortest, shorter, medium, longer, longest], + KeyPoints = [new TldrKeyPoint("results", "Median cold start fell from 4.2 seconds to 0.9.")], +}; +``` + +上方的预览播放的就是一个模拟,其文章、摘要和要点来自图库自己的演示数据。 + +### 结构 + +1. **页眉**:来源的标志(或放在着色方块中的首字母)、名称、日期和阅读时间;标题;带头像的作者。 +2. **控件**:模式切换(`SegmentedControl`)与长度滑块(`Slider`),滑块显示"~60 words"或"4 points",在全文模式下禁用。 +3. **正文**:文章、流式摘要或要点清单,高度以弹簧跟随内容。 +4. **页脚**:以图标加文字表示的状态、带 14px 标志的模型,以及停止、重新生成和复制按钮。 + +### 要点 + + + +要点以清单形式出现:每条要点写完时,图标从虚线圆圈变为对勾。整张清单完成后,每条要点的 ¶ 链接会切回文章,等高度稳定后把段落滚动到视野中,把焦点移过去并高亮它。在此之前会先触发 `Jumping`,参数是段落的 id。在代码中调用 `JumpTo(id)` 也能做到同样的事。 + +### 结果、停止与重新生成 + +完成的结果按模式和长度保留,所以切回去是即时的,不会再发请求。`Regenerate`(页脚的按钮)会重新请求。停止或按 Escape 会结束流式输出,并保留已收到的内容。`Completed` 会报告每个完成的结果,附带文字、要点和字数,可用于统计或服务端缓存;被停止或失败的运行不会触发它。`ModeChanged` 与 `LengthChanged` 报告读者做出的更改;滑块在拖动松开或按键调整时才报告长度,所以每次更改只发一个请求。 + +### 自定义摘要器 + + + +服务能流式返回时就流式返回。如果它一次返回完整结果,用 `TldrStreams.FromResult(text, token)` 或 `TldrStreams.FromResult(points, token)` 包装,卡片仍会以每块 3 到 8 个词的节奏流式呈现。 + +### 失败 + + + +摘要器抛出的异常会以警示框显示其消息,并附带"Try again"按钮。`TldrStreams.Simulate(request, fail: true)` 和 `TldrToggleSimulation.Summarize(request, fail: true)` 会在思考停顿之后抛出异常。自行包装模拟时,请设置 `IsSimulated="True"`,让播报和错误文字都注明"Simulated, nothing was sent"。 + +### 窄容器 + + + +卡片会填满容器,最宽 680px。宽度小于 520px 时,长度滑块移到模式切换下方并占满整行;小于 440px 时,内边距从 24px 缩小到 16px。页脚文字保持一行,模型名称最先被截断。 + +### 自动播放 + +没有摘要器时,卡片在至少一半进入视野满一秒后会自动打开摘要,除非读者已经在卡片里按过鼠标或键盘。设置 `AutoPlay` 可以明确开启或关闭。 + +### 自定义文字 + +`Labels` 包含卡片显示或播报的所有文字。替换整个对象即可本地化或改写: + +```csharp +card.Labels = new TldrToggleLabels +{ + Full = "Article", + KeyPoints = "Highlights", + Words = words => $"{words} words", +}; +``` + +### 最佳实践 + +- 为段落提供稳定的 id,并让模型返回每条要点出自哪个 id。 +- 以 `TargetWords` 或 `PointCount` 传递长度。 +- 没有摘要器时卡片运行的是模拟,并会在播报中说明;切勿把它发布到生产环境。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 卡片以文章标题命名,标题是二级标题。 +- 模式切换是带标签的分组("Reading mode");长度滑块朗读的值是目标("~60 words"、"4 points"),在全文模式下禁用。 +- 一个礼貌型实时区域对每次运行只播报一次("Summarizing with GPT-6 Luna"、"Summary ready, 58 words"、"4 key points ready"、"Stopped"),从不朗读流式片段。使用模拟时会追加"Simulated, nothing was sent"。 +- 失败以警示框呈现,附带消息和"Try again"按钮。 +- 跳转链接以其要点命名("Jump to paragraph 3: Partial bootstrapping loads..."),跳转会把焦点移到高亮的段落,让键盘和读屏用户落在视线所在的位置。 +- 状态从不只靠颜色表达:完成、停止和失败都配有图标和文字,要点从虚线圆圈变为对勾。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过模式切换、长度滑块、跳转链接和页脚按钮 | +| 方向键 | 在模式切换上,于全文、摘要和要点之间移动;在滑块上,把长度改变一档(每档一个请求) | +| Home、End | 第一个或最后一个模式,或最短、最长的长度 | +| Enter、Space | 在跳转链接上,切回文章,滚动到对应段落,聚焦并高亮它 | +| Escape | 停止正在进行的流式输出,保留已收到的内容 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 正文高度(切换视图或流入文字) | 一个弹簧,0.26s,无回弹 | 立即 | 立即 | +| 容器尺寸变化 | 直接跳到新高度 | 立即 | 立即 | +| 视图切换 | 旧视图 100ms 淡出,新视图 160ms 淡入 | 120ms 交叉淡化 | 立即 | +| 页脚状态与按钮 | 同样的交叉淡化 | 120ms 交叉淡化 | 立即 | +| 流式片段(3 到 8 个词,每 30 到 40ms 一块) | 140ms 淡入 | 直接出现 | 直接出现 | +| 已保留的结果 | 整体出现,不淡入 | 直接出现 | 直接出现 | +| 思考(约 0.7s) | 正文那一行平静地闪烁;随后页脚的 Writing 闪烁 | 静止 | 静止 | +| 要点打勾 | 虚线圆圈淡出并缩小,对勾以同一弹簧放大出现 | 立即 | 立即 | +| 跳转 | 等待高度稳定,平滑滚动,高亮 160ms 淡入、600ms 淡出 | 直接跳到,高亮淡入淡出 | 直接跳到,高亮直接显示 | + +同一时间只有一处闪烁。 + +## 相关组件 + +- `ReasoningBlock`:AI 回复中的思考过程。 +- `SegmentedControl` 与 `Slider`:卡片用它们来切换模式和调整长度。 +- `CopyButton`:复制一个值并立即确认。 +- `ToolCall`:智能体记录中的工具调用。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/zh-TW.md new file mode 100644 index 00000000..5706ff4a --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TldrToggle/zh-TW.md @@ -0,0 +1,187 @@ +--- +title: TL;DR toggle +summary: 一張文章卡片,可在原文位置串流呈現 AI 摘要或重點,附有長度滑桿,每則重點都能跳回其出處段落。 +source-hash: 036af066c83d +--- + + + +## 何時使用 + +`TldrToggle` 是一張文章卡片,具有「全文」「摘要」「重點」切換和一個長度滑桿。切換後,AI 摘要會在原文的位置串流出現,卡片高度隨之以彈簧過渡到合適的大小;每則重點都連結回它所出自的段落,點選後該段落會捲動到視野中並醒目提示。 + +- **快速摘要或重點**:放在長篇文章、文件、工單或報告的上方,或直接取代原文。 +- **讓讀者核對 AI 的結論**:重點連結回段落,讀者可以對照原文。 +- **閱讀情境**:讀者透過長度控制項自行決定想看多少細節。 + +## 何時不用 + +- **針對內容的開放式提問**:改用問答或聊天元件。 +- **很短的內容**:只有一兩段時,摘要省不了時間。 +- **一次彙整多份文件**:改用清單或摘要彙編版面,而不是每項一張卡片。 + +## 用法 + +為卡片提供一個 `Article`,其段落帶有穩定的 id,再提供一個以串流傳回 `TldrStreamEvent` 的 `Summarizer`。內文用 `TldrStreamEvent.OfText` 傳送;重點則在每則重點的文字之前先傳送 `TldrStreamEvent.OfPoint(paragraphId)`,這樣它的跳轉連結才會落在正確的段落上。沒有摘要器時,卡片會以文章的首句模擬摘要(`TldrStreams.Simulate`),不會傳送任何內容。既沒有 `Article` 也沒有 `Simulation` 時,卡片顯示其安靜的空狀態:沒有頁首,顯示「This article has no text to summarize yet.」,切換器和滑桿處於關閉狀態。 + +```csharp +card.Article = new TldrArticle +{ + Title = post.Title, + SourceName = "Engineering blog", + AuthorName = post.Author, + Date = "Oct 21, 2026", + Paragraphs = post.Paragraphs.Select(p => new TldrParagraph(p.Id, p.Text)).ToList(), +}; +card.Model = new AgentModel("GPT-6 Luna", ArcIcons.Sparkles); +card.Summarizer = request => myService.StreamTldr(request); // IAsyncEnumerable +card.Jumping += (_, e) => analytics.Track("tldr_jump", e.ParagraphId); +``` + +請求中包含 `Mode`(`Summary` 或 `KeyPoints`)、`Length`(1 到 5)、`TargetWords`(35 到 130)、`PointCount`(3 到 7)、文章本身,以及一個 `CancellationToken`:在停止、按 Escape、切換模式或長度、換了文章,或卡片離開視窗時取消。把長度以 `TargetWords` 或 `PointCount` 傳給模型,而不是「簡短」這類含糊的字眼。 + +### 模擬 + +想在沒有摘要器的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `TldrToggleSimulation`:文章、要標示的模型、為每個長度寫好的摘要,以及依重要性排序的重點。卡片會在短暫的思考停頓之後串流輸出寫好的文字,說明這是模擬、沒有傳送任何內容,並在卡片有一半位於可見區域一秒後自動開啟摘要(`AutoPlay="False"` 會讓它停留在文章上)。沒有寫好摘要的長度會使用文章的首句。你自己的 `Article` 優先於模擬;把 `Simulation` 設為 null 會停止模擬並顯示空狀態。 + +```csharp +card.Simulation = new TldrToggleSimulation +{ + Article = post, + Model = new AgentModel("GPT-6 Luna", ArcIcons.Sparkles), + Summaries = [shortest, shorter, medium, longer, longest], + KeyPoints = [new TldrKeyPoint("results", "Median cold start fell from 4.2 seconds to 0.9.")], +}; +``` + +上方的預覽播放的就是一個模擬,其文章、摘要和重點來自圖庫自己的示範資料。 + +### 結構 + +1. **頁首**:來源的標誌(或放在著色方塊中的首字母)、名稱、日期和閱讀時間;標題;附大頭貼的作者。 +2. **控制項**:模式切換(`SegmentedControl`)與長度滑桿(`Slider`),滑桿顯示「~60 words」或「4 points」,在全文模式下停用。 +3. **內文**:文章、串流摘要或重點清單,高度以彈簧跟隨內容。 +4. **頁尾**:以圖示加文字表示的狀態、附 14px 標誌的模型,以及停止、重新產生和複製按鈕。 + +### 重點 + + + +重點以清單形式出現:每則重點寫完時,圖示從虛線圓圈變為勾號。整份清單完成後,每則重點的 ¶ 連結會切回文章,等高度穩定後把段落捲動到視野中,把焦點移過去並醒目提示它。在此之前會先觸發 `Jumping`,參數是段落的 id。在程式碼中呼叫 `JumpTo(id)` 也能做到同樣的事。 + +### 結果、停止與重新產生 + +完成的結果依模式和長度保留,所以切回去是即時的,不會再發出請求。`Regenerate`(頁尾的按鈕)會重新請求。停止或按 Escape 會結束串流,並保留已收到的內容。`Completed` 會回報每個完成的結果,附帶文字、重點和字數,可用於分析或伺服器端快取;被停止或失敗的執行不會觸發它。`ModeChanged` 與 `LengthChanged` 回報讀者做出的變更;滑桿在拖曳放開或按鍵調整時才回報長度,所以每次變更只發出一個請求。 + +### 自訂摘要器 + + + +服務能以串流傳回時就用串流。如果它一次傳回完整結果,用 `TldrStreams.FromResult(text, token)` 或 `TldrStreams.FromResult(points, token)` 包裝,卡片仍會以每塊 3 到 8 個字詞的節奏串流呈現。 + +### 失敗 + + + +摘要器擲出的例外會以警示框顯示其訊息,並附上「Try again」按鈕。`TldrStreams.Simulate(request, fail: true)` 和 `TldrToggleSimulation.Summarize(request, fail: true)` 會在思考停頓之後擲出例外。自行包裝模擬時,請設定 `IsSimulated="True"`,讓播報和錯誤文字都註明「Simulated, nothing was sent」。 + +### 窄容器 + + + +卡片會填滿容器,最寬 680px。寬度小於 520px 時,長度滑桿移到模式切換下方並占滿整列;小於 440px 時,內距從 24px 縮小到 16px。頁尾文字保持一行,模型名稱最先被截斷。 + +### 自動播放 + +沒有摘要器時,卡片在至少一半進入視野滿一秒後會自動開啟摘要,除非讀者已經在卡片裡按過滑鼠或鍵盤。設定 `AutoPlay` 可以明確開啟或關閉。 + +### 自訂文字 + +`Labels` 包含卡片顯示或播報的所有文字。取代整個物件即可在地化或改寫: + +```csharp +card.Labels = new TldrToggleLabels +{ + Full = "Article", + KeyPoints = "Highlights", + Words = words => $"{words} words", +}; +``` + +### 最佳做法 + +- 為段落提供穩定的 id,並讓模型傳回每則重點出自哪個 id。 +- 以 `TargetWords` 或 `PointCount` 傳遞長度。 +- 沒有摘要器時卡片執行的是模擬,並會在播報中說明;切勿把它發布到正式環境。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 卡片以文章標題命名,標題是第二層標題。 +- 模式切換是有標籤的群組(「Reading mode」);長度滑桿朗讀的值是目標(「~60 words」、「4 points」),在全文模式下停用。 +- 一個禮貌型即時區域對每次執行只播報一次(「Summarizing with GPT-6 Luna」、「Summary ready, 58 words」、「4 key points ready」、「Stopped」),從不朗讀串流片段。使用模擬時會加上「Simulated, nothing was sent」。 +- 失敗以警示框呈現,附上訊息和「Try again」按鈕。 +- 跳轉連結以其重點命名(「Jump to paragraph 3: Partial bootstrapping loads...」),跳轉會把焦點移到醒目提示的段落,讓鍵盤和螢幕閱讀器使用者落在視線所在的位置。 +- 狀態從不只靠顏色表達:完成、停止和失敗都配有圖示和文字,重點從虛線圓圈變為勾號。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過模式切換、長度滑桿、跳轉連結和頁尾按鈕 | +| 方向鍵 | 在模式切換上,於全文、摘要和重點之間移動;在滑桿上,把長度改變一格(每格一個請求) | +| Home、End | 第一個或最後一個模式,或最短、最長的長度 | +| Enter、Space | 在跳轉連結上,切回文章,捲動到對應段落,聚焦並醒目提示它 | +| Escape | 停止進行中的串流,保留已收到的內容 | + +## 動態效果 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 內文高度(切換檢視或流入文字) | 一個彈簧,0.26s,無回彈 | 立即 | 立即 | +| 容器尺寸變化 | 直接跳到新高度 | 立即 | 立即 | +| 檢視切換 | 舊檢視 100ms 淡出,新檢視 160ms 淡入 | 120ms 交叉淡化 | 立即 | +| 頁尾狀態與按鈕 | 同樣的交叉淡化 | 120ms 交叉淡化 | 立即 | +| 串流片段(3 到 8 個字詞,每 30 到 40ms 一塊) | 140ms 淡入 | 直接出現 | 直接出現 | +| 已保留的結果 | 整體出現,不淡入 | 直接出現 | 直接出現 | +| 思考(約 0.7s) | 內文那一行平靜地閃爍;隨後頁尾的 Writing 閃爍 | 靜止 | 靜止 | +| 重點打勾 | 虛線圓圈淡出並縮小,勾號以同一彈簧放大出現 | 立即 | 立即 | +| 跳轉 | 等待高度穩定,平滑捲動,醒目提示 160ms 淡入、600ms 淡出 | 直接跳到,醒目提示淡入淡出 | 直接跳到,醒目提示直接顯示 | + +同一時間只有一處閃爍。 + +## 相關元件 + +- `ReasoningBlock`:AI 回覆中的思考過程。 +- `SegmentedControl` 與 `Slider`:卡片用它們來切換模式和調整長度。 +- `CopyButton`:複製一個值並立即確認。 +- `ToolCall`:智慧代理紀錄中的工具呼叫。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/en-US.md new file mode 100644 index 00000000..4df93d24 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/en-US.md @@ -0,0 +1,188 @@ +--- +title: Tone tuner +summary: A brand voice card where two sliders (formality and detail) and voice chips rewrite a customer reply: each changed sentence crossfades in place, new words can be highlighted against the original, and Copy takes the current version. +--- + + + +## When to use + +`ToneTuner` puts a few dials on an AI rewrite. A customer reply sits in a card with Friendly, Confident and Empathetic +chips and two sliders, formality and detail. Each change asks your model for a new version after a short pause; only the +sentences that changed fade out and back in, the words that are new against the draft can be highlighted, and Copy takes +the version on screen. + +- **A composer or help desk reply** that offers AI tone controls before sending. +- **Brand voice tooling** where marketers compare versions of the same copy. +- **Any rewrite** where people would rather steer with a few dials than write a prompt. + +## When not to use + +- **Free form AI editing with a prompt box**: use a chat or a composer instead. +- **Long documents**: the card is sized for a message of a few paragraphs. +- **Exact line by line code or contract diffs**: use a dedicated diff view. + +## Usage + +Pass the draft in `Text` and your model call in `Rewrite`. The function gets a `ToneRewriteRequest` with the draft, the +tone (`Formality`, `Detail`, `Voice`) and a ready made `Instructions` string that already asks the model to keep facts, +numbers and names, and a `CancellationToken` that is cancelled when a newer change supersedes the request. Return plain +text with the same paragraph breaks so the sentences line up and only the changed ones animate. + +```csharp +tuner.Text = draft; +tuner.RecipientName = "Sofia Ramirez"; +tuner.RecipientSubject = "Re: Duplicate charge on INV-20481"; +tuner.Rewrite = async (request, token) => +{ + var reply = await model.CompleteAsync(request.Instructions, request.Text, token); + return reply.Text; +}; +tuner.Rewritten += (_, e) => Save(e.Text, e.Value); +``` + +With no `Text` and no `Simulation` the card is empty: no header, an empty message, and the voice chips, sliders, Changes +and Copy are turned off until a draft arrives. Without `Rewrite` a draft of your own is rewritten by the same plain rules +a simulation uses (see below), and nothing is sent. + +### Simulation + +To show the card without a model (a demo, a design review, an onboarding tour), give it a `ToneTunerSimulation`: the +draft, who it is for, the model to name and the versions you wrote for each tone (`ToneTunerVersion`). The card answers +each tone with its version after `Latency` (650ms), says the rewrite is simulated and nothing was sent, and tours the +voices by itself until someone presses or focuses anything inside it (`AutoPlay="False"` keeps it still). A tone without +a version is written by a few plain rules (contractions, greetings, fewer sentences, an opening line). Your own `Text` +wins over the simulation, and setting `Simulation` to null stops it and shows the empty card. + +```csharp +tuner.Simulation = new ToneTunerSimulation +{ + Text = draft, + RecipientName = "Sofia Ramirez", + RecipientSubject = "Re: Duplicate charge on INV-20481", + Model = "Claude Sonnet 5.5", + Versions = + [ + new ToneTunerVersion(new ToneSettings(2, 1, ToneVoice.Neutral), formalReply), + new ToneTunerVersion(new ToneSettings(0, 1, ToneVoice.Friendly), friendlyReply), + ], +}; +``` + +The preview above plays a simulation whose draft and versions are the gallery's own demo data. Keep the sentences a dial +does not touch identical between versions, so only the changed ones crossfade. + +### Your model + + + +The original shows at the starting tone (`ToneSettings.Default`: balanced, standard, neutral). Any other tone waits +`Debounce` (220ms) after the last change and then calls `Rewrite`, so dragging a slider sends at most one request after +it settles; a request still running when the tone changes again is cancelled and its answer ignored. `Rewritten` reports +each landed version with its tone and whether it was simulated. + +### Tone + +`Value` is the tone, two-way: `new ToneSettings(formality, detail, voice)` with levels 0 to 2. `ValueChanged` runs on every +slider step and chip press, before the debounce. A chip sets its voice and moves both sliders to its starting point; +pressing it again returns to the neutral voice. Replace the chips with `Presets`: + +```csharp +tuner.Presets = +[ + new TonePreset { Voice = ToneVoice.Friendly, Label = "Warm", Icon = ArcIcons.Smile, Formality = 0, Detail = 1 }, + new TonePreset { Voice = ToneVoice.Confident, Label = "Direct", Icon = ArcIcons.Zap, Formality = 1, Detail = 0 }, +]; +``` + +### Changes + +`ShowChanges` (two-way, the Changes switch) highlights the words that differ from the original draft with a rounded +accent background, and the header counts them. `ChangedWords` and `DisplayedText` read the same numbers and text from +code. Keep the original stable while the person tunes: the comparison is always against `Text`. + +### Failures + + + +When `Rewrite` throws, the text on screen stays, the status says "Couldn't rewrite" with a Retry button, and the live +region reads the error's message. `Retry()` runs the latest tone again from code. + +### Narrow + + + +The card is fluid up to 640px. Under 520px the sliders stack, the chips share one row in equal columns and the status +hides its settings. Under 400px the chip icons, the changed words count and Copy's label drop so nothing clips; it holds at +360px. + +### Your own words + +`Labels` holds every word the card shows or says, including the level names the sliders speak: + +```csharp +tuner.Labels = new ToneTunerLabels +{ + Formality = "Ton", + FormalityLevels = ["Familier", "Neutre", "Soutenu"], + Rewriting = model => $"Réécriture avec {model}", +}; +``` + +### Best practices + +- Send `Instructions` to the model as the system or user instruction; it already asks to keep facts, numbers and names. +- Return the same paragraph breaks as the draft, so unchanged sentences stay still. +- Name the model in `Model` so the status says who is rewriting. + +## API reference + + + + + + + + + + + + + + + +## Accessibility + +- The voice chips are toggle buttons that report pressed, inside a group named Voice. +- Each slider is named (Formality, Detail) and speaks its level name ("Formal"), not a number. +- A polite live region announces each landed rewrite with its settings, says when the rewrite is simulated (nothing was + sent), and reports failures. +- Highlighted words are counted in the header ("12 words changed"), so changes are not shown by color alone. +- The status pairs an icon with a short label: Original draft, Rewriting, Rewritten, or Couldn't rewrite with Retry. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through the voice chips, both sliders, Retry when shown, the Changes switch and Copy | +| Enter, Space | Presses a voice chip, flips Changes, retries or copies | +| Left, Right arrow | Steps a focused slider between its three levels | +| Home, End | Jumps a focused slider to its first or last level | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A rewrite lands | Each changed sentence fades out over 100ms, all of them swap on the same frame (one reflow, out of sight), the new ones fade in over 160ms; unchanged sentences never animate | Swaps at once | Swaps at once | +| Formality or detail changes | The text area eases to the new reserved height on a bounce free spring | At once | At once | +| Voice changes | Nothing moves: the text area already reserves the tallest voice at this formality and detail | Nothing moves | Nothing moves | +| Status | Flips to Rewriting on the same frame as the input with a calm shimmer, then crossfades to Rewritten | No shimmer, short fade | At once | +| Changes switch | Highlights fade over 160ms | At once | At once | +| Self playing tour | A new voice every 3.2s while on screen, until the first press or focus inside | Off | Off | + +## Related + +- `Slider` for the formality and detail dials on their own. +- `Switch` for settings that take effect immediately. +- `CopyButton` for copying a value with confirmation. +- `AgentStatus` and `ToolCall` for the rest of an AI conversation. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/zh-CN.md new file mode 100644 index 00000000..038df32b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/zh-CN.md @@ -0,0 +1,164 @@ +--- +title: Tone tuner +summary: 一张品牌语气卡片:两个滑块(正式程度与详略)和语气标签改写一封客户回复,每个改动过的句子原地交叉淡化,新词可以对照原稿高亮,"复制"取走当前版本。 +source-hash: 1659bebbd79f +--- + + + +## 何时使用 + +`ToneTuner` 为 AI 改写提供几个旋钮。一封客户回复放在卡片里,配有 Friendly、Confident、Empathetic 三个语气标签,以及正式程度与详略两个滑块。每次调整后稍候片刻,它会向你的模型请求新版本;只有改动过的句子会淡出再淡入,相对原稿新增的词可以高亮显示,"复制"取走屏幕上的版本。 + +- **撰写器或客服工单回复**:发送前提供 AI 语气控制。 +- **品牌语气工具**:营销人员比较同一段文案的不同版本。 +- **任何改写场景**:人们更愿意拧几个旋钮,而不是写提示词。 + +## 何时不用 + +- **带提示词输入框的自由 AI 编辑**:改用聊天或撰写器。 +- **长文档**:卡片的尺寸适合几段文字的消息。 +- **逐行精确的代码或合同差异**:使用专门的差异视图。 + +## 用法 + +把原稿传给 `Text`,把你的模型调用传给 `Rewrite`。该函数收到一个 `ToneRewriteRequest`,其中包含原稿、语气(`Formality`、`Detail`、`Voice`)以及现成的 `Instructions` 字符串(它已经要求模型保留事实、数字与姓名),还有一个 `CancellationToken`,当更新的调整取代这次请求时它会被取消。返回段落分隔与原稿相同的纯文本,这样句子才能对齐,只有改动过的句子会产生动画。 + +```csharp +tuner.Text = draft; +tuner.RecipientName = "Sofia Ramirez"; +tuner.RecipientSubject = "Re: Duplicate charge on INV-20481"; +tuner.Rewrite = async (request, token) => +{ + var reply = await model.CompleteAsync(request.Instructions, request.Text, token); + return reply.Text; +}; +tuner.Rewritten += (_, e) => Save(e.Text, e.Value); +``` + +既没有 `Text` 也没有 `Simulation` 时,卡片是空的:没有标题栏,消息为空,语气标签、滑块、"Changes"和"复制"都处于关闭状态,直到有原稿传入。你自己的原稿如果没有 `Rewrite`,会用与模拟相同的简单规则改写(见下文),不会发送任何内容。 + +### 模拟 + +想在没有模型的情况下展示卡片(演示、设计评审、新手引导),就给它一个 `ToneTunerSimulation`:原稿、收件人、要显示的模型名称,以及你为每种语气写好的版本(`ToneTunerVersion`)。卡片会在 `Latency`(650ms)之后用对应版本回应每种语气,说明这次改写是模拟的、没有发送任何内容,并自动轮流展示各种语气,直到有人在卡片内按下或聚焦任何元素(`AutoPlay="False"` 让它保持静止)。没有对应版本的语气会用几条简单规则改写(缩写、问候语、减少句子、加一句开场白)。你自己的 `Text` 优先于模拟;把 `Simulation` 设为 null 会停止模拟并显示空卡片。 + +```csharp +tuner.Simulation = new ToneTunerSimulation +{ + Text = draft, + RecipientName = "Sofia Ramirez", + RecipientSubject = "Re: Duplicate charge on INV-20481", + Model = "Claude Sonnet 5.5", + Versions = + [ + new ToneTunerVersion(new ToneSettings(2, 1, ToneVoice.Neutral), formalReply), + new ToneTunerVersion(new ToneSettings(0, 1, ToneVoice.Friendly), friendlyReply), + ], +}; +``` + +上方的预览播放的就是一个模拟,其原稿和各个版本来自图库自己的演示数据。让滑块不涉及的句子在各版本之间保持完全相同,这样只有改动过的句子会交叉淡化。 + +### 你的模型 + + + +原稿在起始语气(`ToneSettings.Default`:适中、标准、中性)下显示。其他任何语气会在最后一次调整后等待 `Debounce`(220ms),再调用 `Rewrite`,因此拖动滑块时,停下后最多只发送一次请求;语气再次变化时,仍在运行的请求会被取消,其结果被忽略。`Rewritten` 报告每个落定的版本、它的语气以及是否为模拟。 + +### 语气 + +`Value` 是语气,双向绑定:`new ToneSettings(formality, detail, voice)`,各档位为 0 到 2。`ValueChanged` 在每次滑块步进和标签按下时触发,早于防抖。标签会设置它的语气,并把两个滑块移到它的起点;再按一次则回到中性语气。用 `Presets` 替换这些标签: + +```csharp +tuner.Presets = +[ + new TonePreset { Voice = ToneVoice.Friendly, Label = "Warm", Icon = ArcIcons.Smile, Formality = 0, Detail = 1 }, + new TonePreset { Voice = ToneVoice.Confident, Label = "Direct", Icon = ArcIcons.Zap, Formality = 1, Detail = 0 }, +]; +``` + +### 改动 + +`ShowChanges`(双向,即 Changes 开关)用圆角强调色背景高亮与原稿不同的词,页眉会统计它们的数量。在代码中可以通过 `ChangedWords` 和 `DisplayedText` 读取同样的数量与文字。调整期间请保持原稿不变:比较始终针对 `Text`。 + +### 失败 + + + +当 `Rewrite` 抛出异常时,屏幕上的文字保持不变,状态显示"Couldn't rewrite"并附带重试按钮,实时区域会读出错误信息。在代码中可以用 `Retry()` 再次运行最近的语气。 + +### 窄屏 + + + +卡片在 640px 以内自适应宽度。窄于 520px 时,两个滑块上下堆叠,标签在一行内等宽排列,状态隐藏其设置说明。窄于 400px 时,标签图标、改动词数和"复制"的文字都会去掉,以免被截断;在 360px 下依然完整。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的所有文字,包括滑块朗读的档位名称: + +```csharp +tuner.Labels = new ToneTunerLabels +{ + Formality = "Ton", + FormalityLevels = ["Familier", "Neutre", "Soutenu"], + Rewriting = model => $"Réécriture avec {model}", +}; +``` + +### 最佳实践 + +- 把 `Instructions` 作为系统或用户指令发送给模型;它已经要求保留事实、数字与姓名。 +- 返回与原稿相同的段落分隔,让未改动的句子保持静止。 +- 在 `Model` 中写明模型名称,让状态说明是谁在改写。 + +## API 参考 + + + + + + + + + + + + + + + +## 无障碍 + +- 语气标签是会报告按下状态的切换按钮,位于名为 Voice 的分组中。 +- 每个滑块都有名称(Formality、Detail),朗读的是档位名称("Formal")而不是数字。 +- 一个礼貌型实时区域会播报每次落定的改写及其设置,说明改写是模拟的(没有发送任何内容),并报告失败。 +- 高亮的词会在页眉中计数("12 words changed"),因此改动不只靠颜色表达。 +- 状态把图标与简短标签配对:Original draft、Rewriting、Rewritten,或带重试按钮的 Couldn't rewrite。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过语气标签、两个滑块、显示时的重试按钮、Changes 开关和复制按钮 | +| Enter、Space | 按下语气标签、切换 Changes、重试或复制 | +| 左、右方向键 | 让获得焦点的滑块在三个档位之间步进 | +| Home、End | 让获得焦点的滑块跳到第一个或最后一个档位 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 改写落定 | 每个改动过的句子在 100ms 内淡出,所有句子在同一帧替换(只重排一次,且不可见),新句子在 160ms 内淡入;未改动的句子从不动画 | 立即替换 | 立即替换 | +| 正式程度或详略变化 | 文字区域以无回弹的弹簧过渡到新的预留高度 | 立即 | 立即 | +| 语气变化 | 没有任何移动:文字区域已经为当前正式程度与详略下最高的语气预留了高度 | 没有移动 | 没有移动 | +| 状态 | 与输入在同一帧切换为 Rewriting,带柔和的闪光,然后交叉淡化为 Rewritten | 无闪光,短暂淡化 | 立即 | +| Changes 开关 | 高亮在 160ms 内淡入淡出 | 立即 | 立即 | +| 自动轮播 | 在屏幕上时每 3.2s 换一种语气,直到卡片内第一次按下或获得焦点 | 关闭 | 关闭 | + +## 相关组件 + +- `Slider`:单独使用的正式程度与详略旋钮。 +- `Switch`:立即生效的设置。 +- `CopyButton`:带确认的复制。 +- `AgentStatus` 与 `ToolCall`:AI 对话的其他部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/zh-TW.md new file mode 100644 index 00000000..b953292e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToneTuner/zh-TW.md @@ -0,0 +1,164 @@ +--- +title: Tone tuner +summary: 一張品牌語氣卡片:兩個滑桿(正式程度與詳略)和語氣標籤改寫一封客戶回覆,每個改動過的句子原地交叉淡化,新詞可以對照原稿醒目提示,「複製」取走目前版本。 +source-hash: 1659bebbd79f +--- + + + +## 何時使用 + +`ToneTuner` 為 AI 改寫提供幾個旋鈕。一封客戶回覆放在卡片裡,配有 Friendly、Confident、Empathetic 三個語氣標籤,以及正式程度與詳略兩個滑桿。每次調整後稍候片刻,它會向你的模型要求新版本;只有改動過的句子會淡出再淡入,相對原稿新增的詞可以醒目提示,「複製」取走螢幕上的版本。 + +- **撰寫器或客服工單回覆**:送出前提供 AI 語氣控制。 +- **品牌語氣工具**:行銷人員比較同一段文案的不同版本。 +- **任何改寫情境**:大家寧可轉幾個旋鈕,也不想寫提示詞。 + +## 何時不用 + +- **附提示詞輸入框的自由 AI 編輯**:改用聊天或撰寫器。 +- **長篇文件**:卡片的尺寸適合幾段文字的訊息。 +- **逐行精確的程式碼或合約差異**:使用專門的差異檢視。 + +## 用法 + +把原稿傳給 `Text`,把你的模型呼叫傳給 `Rewrite`。該函式會收到一個 `ToneRewriteRequest`,其中包含原稿、語氣(`Formality`、`Detail`、`Voice`)以及現成的 `Instructions` 字串(它已經要求模型保留事實、數字與姓名),還有一個 `CancellationToken`,當較新的調整取代這次要求時它會被取消。回傳段落分隔與原稿相同的純文字,這樣句子才能對齊,只有改動過的句子會產生動畫。 + +```csharp +tuner.Text = draft; +tuner.RecipientName = "Sofia Ramirez"; +tuner.RecipientSubject = "Re: Duplicate charge on INV-20481"; +tuner.Rewrite = async (request, token) => +{ + var reply = await model.CompleteAsync(request.Instructions, request.Text, token); + return reply.Text; +}; +tuner.Rewritten += (_, e) => Save(e.Text, e.Value); +``` + +既沒有 `Text` 也沒有 `Simulation` 時,卡片是空的:沒有標題列,訊息為空,語氣標籤、滑桿、「Changes」和「複製」都處於關閉狀態,直到有原稿傳入。你自己的原稿如果沒有 `Rewrite`,會用與模擬相同的簡單規則改寫(見下文),不會送出任何內容。 + +### 模擬 + +想在沒有模型的情況下展示卡片(示範、設計審查、新手導覽),就給它一個 `ToneTunerSimulation`:原稿、收件人、要顯示的模型名稱,以及你為每種語氣寫好的版本(`ToneTunerVersion`)。卡片會在 `Latency`(650ms)之後用對應版本回應每種語氣,說明這次改寫是模擬的、沒有送出任何內容,並自動輪流展示各種語氣,直到有人在卡片內按下或聚焦任何元素(`AutoPlay="False"` 讓它保持靜止)。沒有對應版本的語氣會用幾條簡單規則改寫(縮寫、問候語、減少句子、加一句開場白)。你自己的 `Text` 優先於模擬;把 `Simulation` 設為 null 會停止模擬並顯示空卡片。 + +```csharp +tuner.Simulation = new ToneTunerSimulation +{ + Text = draft, + RecipientName = "Sofia Ramirez", + RecipientSubject = "Re: Duplicate charge on INV-20481", + Model = "Claude Sonnet 5.5", + Versions = + [ + new ToneTunerVersion(new ToneSettings(2, 1, ToneVoice.Neutral), formalReply), + new ToneTunerVersion(new ToneSettings(0, 1, ToneVoice.Friendly), friendlyReply), + ], +}; +``` + +上方的預覽播放的就是一個模擬,其原稿和各個版本來自圖庫自己的示範資料。讓滑桿不涉及的句子在各版本之間保持完全相同,這樣只有改動過的句子會交叉淡化。 + +### 你的模型 + + + +原稿在起始語氣(`ToneSettings.Default`:適中、標準、中性)下顯示。其他任何語氣會在最後一次調整後等待 `Debounce`(220ms),再呼叫 `Rewrite`,因此拖曳滑桿時,停下後最多只送出一次要求;語氣再次變化時,仍在執行的要求會被取消,其結果會被忽略。`Rewritten` 回報每個落定的版本、它的語氣以及是否為模擬。 + +### 語氣 + +`Value` 是語氣,雙向繫結:`new ToneSettings(formality, detail, voice)`,各檔位為 0 到 2。`ValueChanged` 在每次滑桿步進與標籤按下時觸發,早於防彈跳。標籤會設定它的語氣,並把兩個滑桿移到它的起點;再按一次則回到中性語氣。用 `Presets` 取代這些標籤: + +```csharp +tuner.Presets = +[ + new TonePreset { Voice = ToneVoice.Friendly, Label = "Warm", Icon = ArcIcons.Smile, Formality = 0, Detail = 1 }, + new TonePreset { Voice = ToneVoice.Confident, Label = "Direct", Icon = ArcIcons.Zap, Formality = 1, Detail = 0 }, +]; +``` + +### 改動 + +`ShowChanges`(雙向,即 Changes 開關)以圓角強調色背景醒目提示與原稿不同的詞,頁首會計算它們的數量。在程式碼中可以透過 `ChangedWords` 與 `DisplayedText` 讀取同樣的數量與文字。調整期間請保持原稿不變:比較一律針對 `Text`。 + +### 失敗 + + + +當 `Rewrite` 擲出例外時,螢幕上的文字維持不變,狀態顯示「Couldn't rewrite」並附上重試按鈕,即時區域會讀出錯誤訊息。在程式碼中可以用 `Retry()` 再次執行最近的語氣。 + +### 窄螢幕 + + + +卡片在 640px 以內自適應寬度。窄於 520px 時,兩個滑桿上下堆疊,標籤在一列內等寬排列,狀態隱藏其設定說明。窄於 400px 時,標籤圖示、改動字數與「複製」的文字都會拿掉,以免被截斷;在 360px 下依然完整。 + +### 自訂文案 + +`Labels` 包含卡片顯示或朗讀的所有文字,包括滑桿朗讀的檔位名稱: + +```csharp +tuner.Labels = new ToneTunerLabels +{ + Formality = "Ton", + FormalityLevels = ["Familier", "Neutre", "Soutenu"], + Rewriting = model => $"Réécriture avec {model}", +}; +``` + +### 最佳做法 + +- 把 `Instructions` 當作系統或使用者指示送給模型;它已經要求保留事實、數字與姓名。 +- 回傳與原稿相同的段落分隔,讓未改動的句子保持靜止。 +- 在 `Model` 中寫明模型名稱,讓狀態說明是誰在改寫。 + +## API 參考 + + + + + + + + + + + + + + + +## 無障礙 + +- 語氣標籤是會回報按下狀態的切換按鈕,位於名為 Voice 的群組中。 +- 每個滑桿都有名稱(Formality、Detail),朗讀的是檔位名稱(「Formal」)而不是數字。 +- 一個禮貌型即時區域會播報每次落定的改寫及其設定,說明改寫是模擬的(沒有送出任何內容),並回報失敗。 +- 醒目提示的詞會在頁首計數(「12 words changed」),因此改動不只靠顏色表達。 +- 狀態把圖示與簡短標籤配對:Original draft、Rewriting、Rewritten,或附重試按鈕的 Couldn't rewrite。 + +### 鍵盤 + +| 按鍵 | 動作 | +|---|---| +| Tab | 依序經過語氣標籤、兩個滑桿、顯示時的重試按鈕、Changes 開關與複製按鈕 | +| Enter、Space | 按下語氣標籤、切換 Changes、重試或複製 | +| 左、右方向鍵 | 讓取得焦點的滑桿在三個檔位之間步進 | +| Home、End | 讓取得焦點的滑桿跳到第一個或最後一個檔位 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 改寫落定 | 每個改動過的句子在 100ms 內淡出,所有句子在同一個影格替換(只重排一次,且看不見),新句子在 160ms 內淡入;未改動的句子從不產生動畫 | 立即替換 | 立即替換 | +| 正式程度或詳略變化 | 文字區域以無回彈的彈簧過渡到新的預留高度 | 立即 | 立即 | +| 語氣變化 | 沒有任何移動:文字區域已經為目前正式程度與詳略下最高的語氣預留了高度 | 沒有移動 | 沒有移動 | +| 狀態 | 與輸入在同一個影格切換為 Rewriting,帶柔和的閃光,然後交叉淡化為 Rewritten | 無閃光,短暫淡化 | 立即 | +| Changes 開關 | 醒目提示在 160ms 內淡入淡出 | 立即 | 立即 | +| 自動輪播 | 在螢幕上時每 3.2s 換一種語氣,直到卡片內第一次按下或取得焦點 | 關閉 | 關閉 | + +## 相關元件 + +- `Slider`:單獨使用的正式程度與詳略旋鈕。 +- `Switch`:立即生效的設定。 +- `CopyButton`:附確認的複製。 +- `AgentStatus` 與 `ToolCall`:AI 對話的其他部分。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/en-US.md index 181aed78..2abad245 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/en-US.md @@ -28,7 +28,8 @@ stays one click away. ## Usage Give a row a `ToolCallItem`. Tools are data, not brands: a name for screen readers, a 24 × 24 stroked mark (a Lucide -glyph) and the phrases for each status. With no item the row shows a sample web search. +glyph) and the phrases for each status. With no item the row is empty: a wrench and "No tool call" in the muted color, +with no status and nothing to open (the last row of the statuses demo below). A group with no calls takes no space. ```xml @@ -98,9 +99,9 @@ item). Update the call yourself, usually to running and then to success, and set a status you drive. When nobody handles an action, the control keeps a local behaviour so a page without wiring still works: **Deny** -marks the call denied; **Allow** and **Retry** only simulate a run, and only on the built-in sample data -(`ToolCallSamples`, `ToolCallSimulation`). On any other data nothing changes. A simulated run says so through the -row's live region ("This is sample data, so the call is only simulated."). +marks the call denied; **Allow** and **Retry** go to the `ToolCallSimulation` that appended the call, which runs it on. +On any other data they change nothing. A simulated run says so through the row's live region ("This is sample data, so +the call is only simulated."). ### Opening and folding @@ -114,9 +115,30 @@ group. `ExpandedChanged` tells whether the change was automatic. `ToolCallSimulation` plays a scripted turn into an observable list on a clock: calls are appended about 225ms apart, run for their time and settle; a failing call waits for `Retry`; a call that needs approval waits for `Approve` or -`Deny`. Bind a group's `ItemsSource` to `Items` and `IsActive` to `IsWorking`. The default script is -`ToolCallSamples.Script()`; pass a `TimeProvider` to step it in tests. The transcript around the group in the preview -(the question, the sentence, the answer and Run again) is demo content, not part of the control. +`Deny`. The package ships no script: you write one from `ToolCallSimulationStep`s, each a `ToolCallItem` (the run +appends a copy) with an outcome and its timing. Give the simulation to `ToolCallGroup.Simulation`: while the group has no +calls of its own, it shows the simulation's `Items`, says "Using" while it `IsWorking`, starts the run from the top when +it comes on screen and stops it when it leaves. Calls in `Items` or `ItemsSource` win over the simulation. + +```csharp +var lookup = new ToolCallItem("orders", "Orders", "Looked up order 1042", ToolCallStatus.Success, ArcIcons.Search) +{ + RunningTitle = "Looking up order 1042", + Summary = "1 result", + Duration = TimeSpan.FromMilliseconds(900) +}; +group.Simulation = new ToolCallSimulation( +[ + new ToolCallSimulationStep(lookup) { Delay = TimeSpan.FromMilliseconds(600) } +]); +group.Simulation.Start(); // Run again: plays from the top +``` + +In XAML, set `` to a `ToolCallSimulation` whose `Script` comes from `{x:Static}`, as the +preview's Code tab shows; give each group its own simulation (it keeps the playback state), the script can be shared. +Pass a `TimeProvider` to step it in tests. The transcript around the group in the preview (the question, the sentence, +the answer and Run again) is demo content written by the gallery, and so are the finance team's calls; none of it is +part of the control. ### Narrow containers @@ -148,7 +170,7 @@ statuses change at once. Labels, announcements, focus and all content stay. ### Common questions **Can I use a brand logo as the mark?** The mark is any 24 × 24 stroked geometry. Brand logos are yours to license; -the samples use Lucide glyphs only. +the demos use Lucide glyphs only. **Does Allow run the tool?** No. It raises `ApproveRequested`; your code runs the tool and updates the status. @@ -197,15 +219,13 @@ The enums: | `ToolCallSimulationState` | `Idle`, `Working`, `WaitingForRetry`, `WaitingForApproval`, `Finished` | | `ToolCallSimulationPhase` | `Started`, `CallAppended`, `CallSucceeded`, `CallFailed`, `WaitingForApproval`, `Retried`, `Allowed`, `Denied`, `Finished` | -`ToolCallSamples` holds the invented sample calls and the default script. - ## Accessibility - The group header and every row are buttons that report whether they are expanded. A row's name reads the tool, the phrase and the status: "Tracker: Searched tickets in finance/close Done, 4 results, 1.2s". - Status is never color alone: each status has its own icon, failed and waiting calls show a reason line, and the name spells the status out. -- Each row announces status changes through a polite live region, including that a sample call is only simulated. +- Each row announces status changes through a polite live region, including that a simulated call is only simulated. - Retry, Allow and Deny are named with the call they act on: "Allow: Post a status note to #finance-ops". When a button goes away focus moves to its row instead of dropping to the page. - Input and Output are named groups; the copy buttons confirm through the live region without changing width. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-CN.md index 4ba8e388..122f76b9 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-CN.md @@ -1,7 +1,7 @@ --- title: Tool call summary: 智能体对话记录中的一行工具调用:工具、它做了什么以及结果如何,点开即可看到调用的输入与输出,并在行内完成审批与重试。 -source-hash: 034f5a63d7bf +source-hash: 545a5441aa1e --- @@ -24,7 +24,7 @@ source-hash: 034f5a63d7bf ## 用法 -提供一个 `ToolCallItem` 给行。工具是数据,而非品牌:供屏幕阅读器使用的名称、一个 24 × 24 的描边标记(Lucide 图标)以及每种状态对应的短语。若不提供项,该行显示一次示例网络搜索。 +提供一个 `ToolCallItem` 给行。工具是数据,而非品牌:供屏幕阅读器使用的名称、一个 24 × 24 的描边标记(Lucide 图标)以及每种状态对应的短语。若不提供项,该行为空:以弱化颜色显示一个扳手和 "No tool call",没有状态,也无法展开(见下方状态演示的最后一行)。没有调用的分组不占空间。 ```xml @@ -80,7 +80,7 @@ JSON 基于 `System.Text.Json.Nodes` 构建,由我们自己的一个小型词 分组会引发 `ApproveRequested`、`DenyRequested` 和 `RetryRequested`(它们从行冒泡上来,并携带该项)。由你自己更新调用,通常先变为运行中,再变为成功,并设置 `Handled`。控件从不改变你驱动的状态。 -如果无人处理某个操作,控件会保留本地行为,使没有接线的页面也能工作:**拒绝**将调用标记为已拒绝;**允许**与**重试**只模拟一次运行,且仅限内置示例数据(`ToolCallSamples`、`ToolCallSimulation`)。对于其他数据,什么都不会改变。模拟运行会通过该行的实时区域说明这一点("This is sample data, so the call is only simulated.")。 +如果无人处理某个操作,控件会保留本地行为,使没有接线的页面也能工作:**拒绝**将调用标记为已拒绝;**允许**与**重试**会交给追加该调用的 `ToolCallSimulation`,由它继续运行。对于其他数据,它们什么都不改变。模拟运行会通过该行的实时区域说明这一点("This is sample data, so the call is only simulated.")。 ### 展开与折叠 @@ -88,7 +88,23 @@ JSON 基于 `System.Text.Json.Nodes` 构建,由我们自己的一个小型词 ### 模拟 -`ToolCallSimulation` 按时钟把一段预设的回合播放进一个可观察列表:调用约每 225ms 追加一次,运行其时长后结束;失败的调用等待 `Retry`;需要审批的调用等待 `Approve` 或 `Deny`。将分组的 `ItemsSource` 绑定到 `Items`,并将 `IsActive` 绑定到 `IsWorking`。默认脚本是 `ToolCallSamples.Script()`;传入 `TimeProvider` 即可在测试中逐步推进。预览中分组周围的对话记录(问题、句子、答案以及 "Run again" 按钮)是演示内容,不属于控件的一部分。 +`ToolCallSimulation` 按时钟把一段预设的回合播放进一个可观察列表:调用约每 225ms 追加一次,运行其时长后结束;失败的调用等待 `Retry`;需要审批的调用等待 `Approve` 或 `Deny`。包里不附带任何脚本:你用 `ToolCallSimulationStep` 编写脚本,每一步是一个 `ToolCallItem`(运行时追加它的副本),加上结果与时序。把模拟交给 `ToolCallGroup.Simulation`:分组没有自己的调用时,会显示模拟的 `Items`,在它 `IsWorking` 时显示 "Using",出现在屏幕上时从头开始运行,离开时停止。`Items` 或 `ItemsSource` 中的调用优先于模拟。 + +```csharp +var lookup = new ToolCallItem("orders", "Orders", "Looked up order 1042", ToolCallStatus.Success, ArcIcons.Search) +{ + RunningTitle = "Looking up order 1042", + Summary = "1 result", + Duration = TimeSpan.FromMilliseconds(900) +}; +group.Simulation = new ToolCallSimulation( +[ + new ToolCallSimulationStep(lookup) { Delay = TimeSpan.FromMilliseconds(600) } +]); +group.Simulation.Start(); // Run again: plays from the top +``` + +在 XAML 中,把 `` 设为一个 `ToolCallSimulation`,其 `Script` 来自 `{x:Static}`,写法见预览的 Code 标签;每个分组使用自己的模拟(它保存播放状态),脚本可以共享。传入 `TimeProvider` 即可在测试中逐步推进。预览中分组周围的对话记录(问题、句子、答案以及 "Run again" 按钮)是展示库编写的演示内容,财务团队的那些调用也是;它们都不属于控件。 ### 窄容器 @@ -116,7 +132,7 @@ JSON 基于 `System.Text.Json.Nodes` 构建,由我们自己的一个小型词 ### 常见问题 -**我可以把品牌标志用作标记吗?** 标记可以是任意 24 × 24 的描边几何图形。品牌标志需要你自行取得授权;示例只使用 Lucide 图标。 +**我可以把品牌标志用作标记吗?** 标记可以是任意 24 × 24 的描边几何图形。品牌标志需要你自行取得授权;演示只使用 Lucide 图标。 **允许会运行工具吗?** 不会。它会引发 `ApproveRequested`;由你的代码运行工具并更新状态。 @@ -165,13 +181,11 @@ JSON 基于 `System.Text.Json.Nodes` 构建,由我们自己的一个小型词 | `ToolCallSimulationState` | `Idle`, `Working`, `WaitingForRetry`, `WaitingForApproval`, `Finished` | | `ToolCallSimulationPhase` | `Started`, `CallAppended`, `CallSucceeded`, `CallFailed`, `WaitingForApproval`, `Retried`, `Allowed`, `Denied`, `Finished` | -`ToolCallSamples` 保存虚构的示例调用与默认脚本。 - ## 无障碍 - 分组标题与每一行都是按钮,并报告其是否展开。一行的名称会读出工具、短语与状态,例如:"Tracker: Searched tickets in finance/close Done, 4 results, 1.2s"。 - 状态从不只依赖颜色:每种状态都有各自的图标,失败与等待中的调用会显示原因行,名称中也把状态拼写出来。 -- 每一行都通过礼貌级别的实时区域播报状态变化,包括示例调用只是模拟这一点。 +- 每一行都通过礼貌级别的实时区域播报状态变化,包括模拟的调用只是模拟这一点。 - 重试、允许与拒绝都以其所作用的调用命名:"Allow: Post a status note to #finance-ops"。当按钮消失时,焦点会移到其所在的行,而不是落回页面。 - 输入与输出是有名称的分组;复制按钮通过实时区域确认,且不改变宽度。 - 指针焦点不显示焦点环;键盘焦点在行内显示 2px 的焦点环,因此不会被裁切。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-TW.md index 9af66aa3..cbd14f6c 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/ToolCall/zh-TW.md @@ -1,7 +1,7 @@ --- title: Tool call summary: 智能體逐字稿中的工具呼叫列,顯示工具、它做了什麼與結果如何;點開可看到呼叫的輸入與輸出,核准與重試也都在列中完成。 -source-hash: 034f5a63d7bf +source-hash: 545a5441aa1e --- @@ -24,7 +24,7 @@ source-hash: 034f5a63d7bf ## 用法 -為列提供一個 `ToolCallItem`。工具是資料,不是品牌:它包含供螢幕閱讀器使用的名稱、一個 24 × 24 的描邊標記(Lucide 字符),以及每個狀態的片語。沒有項目時,列會顯示一筆範例網路搜尋。 +為列提供一個 `ToolCallItem`。工具是資料,不是品牌:它包含供螢幕閱讀器使用的名稱、一個 24 × 24 的描邊標記(Lucide 字符),以及每個狀態的片語。沒有項目時,列為空:以淡化顏色顯示一個扳手與 "No tool call",沒有狀態,也無法展開(見下方狀態示範的最後一列)。沒有呼叫的群組不佔空間。 ```xml @@ -80,7 +80,7 @@ JSON 建立在 `System.Text.Json.Nodes` 之上,上色則由我們自己的小 群組會引發 `ApproveRequested`、`DenyRequested` 與 `RetryRequested`(它們從列冒泡而來,並帶著該項目)。由你自己更新呼叫,通常先改為執行中,再改為成功,並設定 `Handled`。控制項絕不會變更你所驅動的狀態。 -當沒有人處理某個動作時,控制項會保留本地行為,使沒有接線的頁面仍能運作:**Deny** 會把呼叫標記為已拒絕;**Allow** 與 **Retry** 只會模擬執行,而且只限於內建的範例資料(`ToolCallSamples`、`ToolCallSimulation`)。對於其他資料,什麼都不會改變。模擬執行時,列的即時區域會說明這一點("This is sample data, so the call is only simulated.")。 +當沒有人處理某個動作時,控制項會保留本地行為,使沒有接線的頁面仍能運作:**Deny** 會把呼叫標記為已拒絕;**Allow** 與 **Retry** 會交給加入該呼叫的 `ToolCallSimulation`,由它繼續執行。對於其他資料,它們什麼都不會改變。模擬執行時,列的即時區域會說明這一點("This is sample data, so the call is only simulated.")。 ### 展開與摺疊 @@ -88,7 +88,23 @@ JSON 建立在 `System.Text.Json.Nodes` 之上,上色則由我們自己的小 ### 模擬 -`ToolCallSimulation` 會依時鐘把一個預先寫好的輪次播放到一個可觀察的清單中:呼叫約每 225 ms 加入一次,執行其所需時間後落定;失敗的呼叫會等待 `Retry`;需要核准的呼叫則等待 `Approve` 或 `Deny`。把群組的 `ItemsSource` 綁定到 `Items`,並把 `IsActive` 綁定到 `IsWorking`。預設腳本是 `ToolCallSamples.Script()`;傳入 `TimeProvider` 即可在測試中逐步執行它。預覽中群組周圍的逐字稿(問題、句子、答案與 "Run again")屬於示範內容,不是控制項的一部分。 +`ToolCallSimulation` 會依時鐘把一個預先寫好的輪次播放到一個可觀察的清單中:呼叫約每 225 ms 加入一次,執行其所需時間後落定;失敗的呼叫會等待 `Retry`;需要核准的呼叫則等待 `Approve` 或 `Deny`。套件不附帶任何腳本:你用 `ToolCallSimulationStep` 撰寫腳本,每一步是一個 `ToolCallItem`(執行時加入它的副本),加上結果與時序。把模擬交給 `ToolCallGroup.Simulation`:群組沒有自己的呼叫時,會顯示模擬的 `Items`,在它 `IsWorking` 時顯示 "Using",出現在畫面上時從頭開始執行,離開時停止。`Items` 或 `ItemsSource` 中的呼叫優先於模擬。 + +```csharp +var lookup = new ToolCallItem("orders", "Orders", "Looked up order 1042", ToolCallStatus.Success, ArcIcons.Search) +{ + RunningTitle = "Looking up order 1042", + Summary = "1 result", + Duration = TimeSpan.FromMilliseconds(900) +}; +group.Simulation = new ToolCallSimulation( +[ + new ToolCallSimulationStep(lookup) { Delay = TimeSpan.FromMilliseconds(600) } +]); +group.Simulation.Start(); // Run again: plays from the top +``` + +在 XAML 中,把 `` 設為一個 `ToolCallSimulation`,其 `Script` 來自 `{x:Static}`,寫法見預覽的 Code 分頁;每個群組使用自己的模擬(它保存播放狀態),腳本可以共用。傳入 `TimeProvider` 即可在測試中逐步執行它。預覽中群組周圍的逐字稿(問題、句子、答案與 "Run again")是展示庫撰寫的示範內容,財務團隊的那些呼叫也是;它們都不屬於控制項。 ### 窄容器 @@ -116,7 +132,7 @@ JSON 建立在 `System.Text.Json.Nodes` 之上,上色則由我們自己的小 ### 常見問題 -**我能使用品牌標誌作為標記嗎?** 標記可以是任何 24 × 24 的描邊幾何圖形。品牌標誌須由你自行取得授權;範例只使用 Lucide 字符。 +**我能使用品牌標誌作為標記嗎?** 標記可以是任何 24 × 24 的描邊幾何圖形。品牌標誌須由你自行取得授權;示範只使用 Lucide 字符。 **Allow 會執行工具嗎?** 不會。它會引發 `ApproveRequested`;由你的程式碼執行工具並更新狀態。 @@ -165,13 +181,11 @@ JSON 建立在 `System.Text.Json.Nodes` 之上,上色則由我們自己的小 | `ToolCallSimulationState` | `Idle`, `Working`, `WaitingForRetry`, `WaitingForApproval`, `Finished` | | `ToolCallSimulationPhase` | `Started`, `CallAppended`, `CallSucceeded`, `CallFailed`, `WaitingForApproval`, `Retried`, `Allowed`, `Denied`, `Finished` | -`ToolCallSamples` 保存虛構的範例呼叫與預設腳本。 - ## 無障礙 - 群組標頭與每一列都是按鈕,會回報自己是否已展開。列的名稱會朗讀工具、片語與狀態:"Tracker: Searched tickets in finance/close Done, 4 results, 1.2s"。 - 狀態絕不只靠顏色傳達:每個狀態都有自己的圖示,失敗與等待中的呼叫會顯示原因行,名稱也會把狀態拼寫出來。 -- 每一列都透過禮貌的即時區域通報狀態變化,包括範例呼叫只是被模擬這件事。 +- 每一列都透過禮貌的即時區域通報狀態變化,包括模擬的呼叫只是被模擬這件事。 - Retry、Allow 與 Deny 會以它們所作用的呼叫來命名:"Allow: Post a status note to #finance-ops"。按鈕消失時,焦點會移到其所屬的列,而不會掉到頁面上。 - Input 與 Output 是有名稱的群組;複製按鈕會透過即時區域確認,且不改變寬度。 - 指標焦點不畫焦點環;鍵盤焦點會在列內顯示 2 px 的焦點環,因此不會被裁切。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/en-US.md new file mode 100644 index 00000000..39a6fbfb --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/en-US.md @@ -0,0 +1,221 @@ +--- +title: Training run +summary: A fine-tuning job card where a live train and validation loss curve draws across a fixed step axis that doubles as the epoch progress bar, with the learning rate, elapsed time, ETA, recent events, pause and cancel, and results with Deploy. +--- + + + +## When to use + +`TrainingRun` is one fine-tuning or training job on a card. The loss curve draws live across a step axis that spans the +whole run and doubles as the epoch progress bar, so nothing rescales while it trains. Beside it sit the step, the learning +rate, the elapsed time and the ETA, the newest three events, and Pause and Cancel. When the run completes, the results +replace the live stats and Deploy takes the model to an endpoint. + +- **A fine-tuning or training job page** in an AI platform, where people watch the loss, pause, cancel and deploy. +- **An eval or distillation run** that reports a loss curve, checkpoints and a final score. +- **An onboarding step** that trains a custom model on the customer's data and ends in a deploy. + +## When not to use + +- **A chart with many series and no job lifecycle**: use `LineChart`. +- **A short agent task made of steps** rather than a loss curve: use `AgentStatus`. +- **A list of jobs**: render one compact row per job and open this card for the selected one. + +## Usage + +Set `Status`, `TotalSteps` and `Epochs` from the job config so the axis is right from the first sample. Then keep `Step`, +`LearningRate`, `Elapsed` and `Eta` current, and append to `Loss` and `Events` as they stream. There is no need to throttle: +the curve eases toward each new sample, so bursts still draw at an even pace. + +```csharp +run.TotalSteps = job.TotalSteps; +run.Epochs = job.Epochs; +run.Status = TrainingRunStatus.Running; + +// On every report from the job: +run.Loss.Add(new TrainingRunLossPoint(report.Step, report.TrainLoss, report.ValidationLoss)); +run.Step = report.Step; +run.LearningRate = report.LearningRate; +run.Elapsed = report.Elapsed; +run.Eta = report.Eta; +``` + +`Apply` takes one `TrainingRunProgress` with whatever changed, and `FollowAsync` applies a whole stream of them on the UI +thread: + +```csharp +await run.FollowAsync(jobs.WatchAsync(job.Id, token), token); +``` + +With no `Status` and no `Simulation` the card shows a queued job with no data yet: the name "Fine-tuning job", empty +stats and an empty chart. With no `Status` it plays its `Simulation` instead, which is handy while you wire up a real +job API: a `TrainingRunSimulation` names the job (`JobName`, `BaseModel`, `Details`, `Dataset`), the `Hardware` it +waits for and trains on, and the `Endpoint` a simulated deploy returns. The card queues it, starts it up and trains it +over 2,400 steps in three epochs with a procedural loss curve, checkpoints and evals. `Scenario="Oom"` makes it fail +once at step 940 so the retry path can be seen. The simulation freezes while the card is off screen, your own `Status` +wins over it, and setting `Simulation` to null stops it. + +```csharp +run.Simulation = new TrainingRunSimulation +{ + JobName = "support-v4", + BaseModel = new AgentModel("Base model"), + Details = ["LoRA r16", "8× H200"], + Dataset = new TrainingRunDataset("your-org/support-tickets", 48210), + Hardware = "8× H200", + Endpoint = "https://inference.example.com/v1/support-v4", +}; +``` + +The demos on this page take their job from `TrainingRunDemoData`. The base model keeps a neutral label; the job, the +dataset and every loss are invented. + +### Anatomy + +1. **Head**: the base model's 24px mark (a monogram when `BaseModel` has none), the job name, the model with its + `Details`, and the status as an icon and a word. +2. **Legend**: the train and validation loss of the latest sample, or of the inspected one, and the current epoch. +3. **Chart**: every other epoch as a faint band, the raw train loss (faint), the smoothed train loss and the validation + loss with a dot per sample. Under the plot the step axis is the epoch progress bar, one segment per epoch. +4. **Stats**: step, learning rate, elapsed time and ETA on rolling digits. On completion the results take their place. +5. **Events**: the newest three, with an icon per kind and the step. +6. **Footer**: the dataset, the failure reason or the deployed endpoint, and the actions. + +### Driven by your app + + + +The buttons raise `PauseRequested`, `ResumeRequested`, `CancelRequested`, `RetryRequested` and `RestartRequested`. Act on +your job, then set `Status`. Cancel takes two presses: the first turns it into Confirm cancel for 3 seconds. + +`Actions` decides which buttons may show (all by default). A read-only monitor sets `Actions="None"`. Each button also needs +the right status: Pause while running, Resume while paused, Cancel while queued, running or paused, Retry from checkpoint +after a failure, and Run again after the run ends. + +### Results and deploy + + + +On completion the results replace the live stats. By default they are the final train and validation loss, the steps and +the duration. `Metrics` sets up to four of your own. Deploy shows when `DeployHandler` is set: + +```csharp +run.DeployHandler = async token => +{ + var deployment = await api.DeployAsync(job.Id, token); + return deployment.Endpoint; +}; +``` + +The button loads while the task runs and keeps focus. Then the footer shows the endpoint with a copy button. If the task +throws, its message shows with Retry deploy. The token is cancelled when the card unloads or the run leaves Completed. + +### Narrow + + + +The card fills its container up to 640px. Under 440px the padding drops to 16px, the stats go to two columns, the legend +shortens Validation to Val and the footer stacks the dataset over the actions. + +### Your own words + +`Labels` holds every word the card shows or says. Replace the object to localize or reword: + +```csharp +run.Labels = new TrainingRunLabels +{ + Training = "Entrenando", + Pause = "Pausar", + StepAt = step => $"paso {step:N0}", +}; +``` + +### Content guidelines + +- Name the job after the model it produces, such as `support-v4`. +- Keep `Details` to two or three short facts: the method and the hardware. +- Write event labels as short past-tense facts: "Checkpoint saved", "Loss spike, gradients clipped". +- Give `ErrorText` the cause and the next step, not a stack trace. + +### Best practices + +- Pass `TotalSteps` and `Epochs` before the first sample so the axis never moves. +- For runs longer than a few thousand samples, downsample `Loss` to about 500 points. +- Remove the samples after a checkpoint when you roll back to it; the curve jumps back instead of easing. + +### Common questions + +**Why does the y axis not grow when the loss spikes?** It is fixed from the first samples so the curve never rescales +mid-run. A higher value is drawn at the top edge. + +**What does the legend show?** The latest smoothed train loss and the latest validation loss, or the values of the sample +you inspect with the pointer or the arrow keys. + +## API reference + + + + + + + + + + + + + + + + + + + + + + + + + +## Accessibility + +- A polite live region announces status changes (Queued, Training, Paused, Completed, Failed, Cancelled, Deployed) and + `Notice`, never every step. +- The status is an icon and a word, never color alone. A failure reason is text in the footer and in the event log. +- The chart is a labelled group whose name carries the step and both losses. The arrow keys inspect samples and the + legend shows them. +- Cancel is a two-step action, so a stray press never stops a long run. Deploy keeps focus while it loads. +- The stats are a list of terms with their values. Steps and numbers use tabular figures. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves to the loss chart, then to the footer actions | +| Left, Right | On the chart, steps through samples; the legend shows that step's losses | +| Shift + Left, Shift + Right | Jumps ten samples | +| Home, End | Jumps to the first or the latest sample | +| Escape | Clears the inspected sample | +| Enter, Space | Presses the focused action; Cancel needs a second press on Confirm cancel | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| A new sample | The curve and the progress bar ease toward it (exponential, 120ms) | At once | At once | +| A rollback or reset | At once | At once | At once | +| A new event | The list slides down on a bounce-free 0.24s spring, the new row fades in | At once | At once | +| Status and footer text | Old fades out over 100ms, new fades in over 160ms rising 2px | At once | At once | +| Counting values | Digits roll on a spring | At once | At once | +| Spinner and activity shimmer | Turning, shimmering | Still | Still | + +The x axis spans the whole run and the y axis is fixed from the first samples, so nothing rescales mid-run. A +simulation keeps running under reduced motion (it is content) but freezes while the card is off screen. + +## Related + +- `AgentStatus` for the live status of an agent run. +- `ToolCall` for one tool call in an agent transcript. +- `LineChart` for a general multi-series chart. +- `CopyButton` for copying a value with confirmation. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/zh-CN.md new file mode 100644 index 00000000..18221834 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/zh-CN.md @@ -0,0 +1,196 @@ +--- +title: Training run +summary: 微调任务卡片:训练与验证损失曲线沿着固定的步数轴实时绘制,这条轴同时也是轮次进度条;卡片还显示学习率、已用时间、预计剩余时间、最近事件、暂停与取消,以及带 Deploy 的结果。 +source-hash: c7fd2a8ac687 +--- + + + +## 何时使用 + +`TrainingRun` 把一个微调或训练任务放在一张卡片上。损失曲线沿着覆盖整个运行的步数轴实时绘制,这条轴同时也是轮次进度条,所以训练过程中什么都不会重新缩放。旁边是步数、学习率、已用时间和预计剩余时间、最新的三条事件,以及暂停和取消。运行完成后,结果取代实时统计,Deploy 把模型部署到一个端点。 + +- **AI 平台里的微调或训练任务页**:人们在这里查看损失、暂停、取消和部署。 +- **评测或蒸馏运行**:报告损失曲线、检查点和最终分数。 +- **新手引导中的一步**:用客户的数据训练自定义模型,最后部署。 + +## 何时不用 + +- **有多条序列、没有任务生命周期的图表**:使用 `LineChart`。 +- **由步骤组成的简短代理任务**,而不是损失曲线:使用 `AgentStatus`。 +- **任务列表**:每个任务渲染一行紧凑的条目,选中时再打开这张卡片。 + +## 用法 + +根据任务配置设置 `Status`、`TotalSteps` 和 `Epochs`,这样从第一个样本起坐标轴就是正确的。然后持续更新 `Step`、`LearningRate`、`Elapsed` 和 `Eta`,并在数据流入时追加到 `Loss` 和 `Events`。无需节流:曲线会缓动到每个新样本,所以成批到达的数据也会匀速绘制。 + +```csharp +run.TotalSteps = job.TotalSteps; +run.Epochs = job.Epochs; +run.Status = TrainingRunStatus.Running; + +// On every report from the job: +run.Loss.Add(new TrainingRunLossPoint(report.Step, report.TrainLoss, report.ValidationLoss)); +run.Step = report.Step; +run.LearningRate = report.LearningRate; +run.Elapsed = report.Elapsed; +run.Eta = report.Eta; +``` + +`Apply` 接受一个只包含变化内容的 `TrainingRunProgress`,`FollowAsync` 则在 UI 线程上依次应用整条更新流: + +```csharp +await run.FollowAsync(jobs.WatchAsync(job.Id, token), token); +``` + +既不设置 `Status` 也没有 `Simulation` 时,卡片显示一个排队中、尚无数据的任务:名称为 “Fine-tuning job”,统计为空,图表为空。不设置 `Status` 时,卡片会改为播放它的 `Simulation`,方便在接入真实任务 API 之前先行开发:`TrainingRunSimulation` 为任务命名(`JobName`、`BaseModel`、`Details`、`Dataset`),给出它等待和训练所用的 `Hardware`,以及模拟部署返回的 `Endpoint`。卡片让它排队、启动,并以程序生成的损失曲线、检查点和评估,在三个 epoch 中训练 2,400 步。`Scenario="Oom"` 让它在第 940 步失败一次,以便查看重试流程。卡片离开屏幕时模拟会暂停,你自己设置的 `Status` 优先于它,把 `Simulation` 设为 null 会停止它。 + +```csharp +run.Simulation = new TrainingRunSimulation +{ + JobName = "support-v4", + BaseModel = new AgentModel("Base model"), + Details = ["LoRA r16", "8× H200"], + Dataset = new TrainingRunDataset("your-org/support-tickets", 48210), + Hardware = "8× H200", + Endpoint = "https://inference.example.com/v1/support-v4", +}; +``` + +本页的演示从 `TrainingRunDemoData` 获取任务。基础模型使用中性的标签;任务、数据集和所有损失值都是虚构的。 + +### 结构 + +1. **头部**:基础模型 24px 的标识(`BaseModel` 没有标识时显示首字母),任务名称,模型及其 `Details`,以及用图标加文字表示的状态。 +2. **图例**:最新样本(或正在查看的样本)的训练损失和验证损失,以及当前轮次。 +3. **图表**:每隔一个轮次一条浅色背景带,原始训练损失(浅色)、平滑后的训练损失,以及每个样本带一个圆点的验证损失。绘图区下方的步数轴就是轮次进度条,每个轮次一段。 +4. **统计**:步数、学习率、已用时间和预计剩余时间,数字滚动变化。完成后由结果取代。 +5. **事件**:最新的三条,每种类型一个图标,并标出步数。 +6. **底部**:数据集、失败原因或已部署的端点,以及操作按钮。 + +### 由应用驱动 + + + +按钮会引发 `PauseRequested`、`ResumeRequested`、`CancelRequested`、`RetryRequested` 和 `RestartRequested`。在你的任务上执行相应操作,然后设置 `Status`。取消需要按两次:第一次按下后它会变成 Confirm cancel,持续 3 秒。 + +`Actions` 决定哪些按钮可以显示(默认全部)。只读监控设置 `Actions="None"`。每个按钮还需要相应的状态:运行中显示 Pause,暂停时显示 Resume,排队、运行或暂停时显示 Cancel,失败后显示 Retry from checkpoint,运行结束后显示 Run again。 + +### 结果与部署 + + + +完成后,结果取代实时统计。默认是最终训练损失和验证损失、步数以及时长。`Metrics` 可以设置最多四项自定义结果。设置了 `DeployHandler` 时显示 Deploy: + +```csharp +run.DeployHandler = async token => +{ + var deployment = await api.DeployAsync(job.Id, token); + return deployment.Endpoint; +}; +``` + +任务运行期间按钮显示加载状态并保持焦点。之后底部显示端点和一个复制按钮。如果任务抛出异常,则显示异常消息和 Retry deploy。卡片卸载或运行离开 Completed 时,令牌会被取消。 + +### 窄布局 + + + +卡片填满容器,最宽 640px。宽度小于 440px 时,内边距降为 16px,统计改为两列,图例把 Validation 缩写为 Val,底部把数据集放在操作按钮上方。 + +### 自定义文案 + +`Labels` 包含卡片显示或朗读的所有文字。替换整个对象即可本地化或改写: + +```csharp +run.Labels = new TrainingRunLabels +{ + Training = "Entrenando", + Pause = "Pausar", + StepAt = step => $"paso {step:N0}", +}; +``` + +### 内容准则 + +- 用任务产出的模型来命名任务,例如 `support-v4`。 +- `Details` 只放两三个简短事实:方法和硬件。 +- 事件标签写成简短的已发生事实:"Checkpoint saved"、"Loss spike, gradients clipped"。 +- `ErrorText` 写明原因和下一步,而不是堆栈跟踪。 + +### 最佳实践 + +- 在第一个样本之前传入 `TotalSteps` 和 `Epochs`,坐标轴就永远不会移动。 +- 超过几千个样本的运行,把 `Loss` 降采样到约 500 个点。 +- 回滚到某个检查点时,删除该检查点之后的样本;曲线会直接跳回,而不是缓动。 + +### 常见问题 + +**损失突然升高时,为什么 y 轴不扩大?** 它根据最初的几个样本固定下来,所以曲线在运行中途不会重新缩放。更高的值画在顶边。 + +**图例显示什么?** 最新的平滑训练损失和最新的验证损失,或者你用指针或方向键查看的那个样本的值。 + +## API 参考 + + + + + + + + + + + + + + + + + + + + + + + + + +## 无障碍 + +- 一个礼貌的实时区域播报状态变化(Queued、Training、Paused、Completed、Failed、Cancelled、Deployed)和 `Notice`,从不逐步播报。 +- 状态由图标和文字共同表示,绝不只靠颜色。失败原因以文字形式出现在底部和事件日志中。 +- 图表是一个带标签的分组,其名称包含步数和两个损失。方向键可逐个查看样本,图例显示其数值。 +- 取消是两步操作,误按一次不会停止耗时很长的运行。Deploy 在加载期间保持焦点。 +- 统计是一组术语及其数值。步数和数字使用等宽数字。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 移到损失图表,再到底部的操作按钮 | +| Left、Right | 在图表上逐个查看样本;图例显示该步的损失 | +| Shift + Left、Shift + Right | 跳过十个样本 | +| Home、End | 跳到第一个或最新的样本 | +| Escape | 清除正在查看的样本 | +| Enter、Space | 按下获得焦点的操作;Cancel 需要再按一次 Confirm cancel | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 新样本 | 曲线和进度条缓动到它(指数,120ms) | 立即 | 立即 | +| 回滚或重置 | 立即 | 立即 | 立即 | +| 新事件 | 列表以 0.24s 无回弹弹簧下移,新行淡入 | 立即 | 立即 | +| 状态与底部文字 | 旧文字 100ms 淡出,新文字 160ms 淡入并上升 2px | 立即 | 立即 | +| 计数数值 | 数字以弹簧滚动 | 立即 | 立即 | +| 加载圈与活动文字微光 | 旋转、闪烁 | 静止 | 静止 | + +x 轴覆盖整个运行,y 轴根据最初的样本固定,所以运行中途什么都不会重新缩放。模拟在减弱动效下继续运行(它是内容),但卡片离开屏幕时会暂停。 + +## 相关组件 + +- `AgentStatus`:代理运行的实时状态。 +- `ToolCall`:代理记录中的一次工具调用。 +- `LineChart`:通用的多序列图表。 +- `CopyButton`:复制一个值并给出确认。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/zh-TW.md new file mode 100644 index 00000000..828442df --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrainingRun/zh-TW.md @@ -0,0 +1,196 @@ +--- +title: Training run +summary: 微調任務卡片:訓練與驗證損失曲線沿著固定的步數軸即時繪製,這條軸同時也是訓練週期進度列;卡片還顯示學習率、已用時間、預估剩餘時間、最近事件、暫停與取消,以及附 Deploy 的結果。 +source-hash: c7fd2a8ac687 +--- + + + +## 何時使用 + +`TrainingRun` 把一個微調或訓練任務放在一張卡片上。損失曲線沿著涵蓋整個執行的步數軸即時繪製,這條軸同時也是訓練週期進度列,所以訓練過程中什麼都不會重新縮放。旁邊是步數、學習率、已用時間與預估剩餘時間、最新的三則事件,以及暫停和取消。執行完成後,結果取代即時統計,Deploy 把模型部署到一個端點。 + +- **AI 平台中的微調或訓練任務頁面**:使用者在這裡查看損失、暫停、取消與部署。 +- **評測或蒸餾執行**:回報損失曲線、檢查點和最終分數。 +- **新手引導中的一個步驟**:以客戶的資料訓練自訂模型,最後部署。 + +## 何時不用 + +- **有多條序列、沒有任務生命週期的圖表**:使用 `LineChart`。 +- **由步驟組成的簡短代理任務**,而不是損失曲線:使用 `AgentStatus`。 +- **任務清單**:每個任務呈現一列精簡的項目,選取時再開啟這張卡片。 + +## 用法 + +依任務設定設置 `Status`、`TotalSteps` 和 `Epochs`,這樣從第一個樣本起座標軸就是正確的。接著持續更新 `Step`、`LearningRate`、`Elapsed` 和 `Eta`,並在資料串流進來時附加到 `Loss` 和 `Events`。不需要節流:曲線會緩動到每個新樣本,所以成批抵達的資料也會以均勻的速度繪製。 + +```csharp +run.TotalSteps = job.TotalSteps; +run.Epochs = job.Epochs; +run.Status = TrainingRunStatus.Running; + +// On every report from the job: +run.Loss.Add(new TrainingRunLossPoint(report.Step, report.TrainLoss, report.ValidationLoss)); +run.Step = report.Step; +run.LearningRate = report.LearningRate; +run.Elapsed = report.Elapsed; +run.Eta = report.Eta; +``` + +`Apply` 接受一個只包含變動內容的 `TrainingRunProgress`,`FollowAsync` 則在 UI 執行緒上依序套用整個更新串流: + +```csharp +await run.FollowAsync(jobs.WatchAsync(job.Id, token), token); +``` + +既未設定 `Status` 也沒有 `Simulation` 時,卡片顯示一個排隊中、尚無資料的任務:名稱為「Fine-tuning job」,統計為空,圖表為空。未設定 `Status` 時,卡片會改為播放它的 `Simulation`,方便在串接真正的任務 API 之前先行開發:`TrainingRunSimulation` 為任務命名(`JobName`、`BaseModel`、`Details`、`Dataset`),給出它等候與訓練所用的 `Hardware`,以及模擬部署傳回的 `Endpoint`。卡片讓它排隊、啟動,並以程式產生的損失曲線、檢查點與評估,在三個 epoch 中訓練 2,400 步。`Scenario="Oom"` 讓它在第 940 步失敗一次,以便查看重試流程。卡片離開畫面時模擬會暫停,你自己設定的 `Status` 優先於它,把 `Simulation` 設為 null 會停止它。 + +```csharp +run.Simulation = new TrainingRunSimulation +{ + JobName = "support-v4", + BaseModel = new AgentModel("Base model"), + Details = ["LoRA r16", "8× H200"], + Dataset = new TrainingRunDataset("your-org/support-tickets", 48210), + Hardware = "8× H200", + Endpoint = "https://inference.example.com/v1/support-v4", +}; +``` + +本頁的示範從 `TrainingRunDemoData` 取得任務。基礎模型使用中性的標籤;任務、資料集與所有損失值都是虛構的。 + +### 結構 + +1. **標頭**:基礎模型 24px 的標誌(`BaseModel` 沒有標誌時顯示首字母)、任務名稱、模型及其 `Details`,以及以圖示加文字表示的狀態。 +2. **圖例**:最新樣本(或正在檢視的樣本)的訓練損失與驗證損失,以及目前的訓練週期。 +3. **圖表**:每隔一個訓練週期一條淺色背景帶、原始訓練損失(淺色)、平滑後的訓練損失,以及每個樣本帶一個圓點的驗證損失。繪圖區下方的步數軸就是訓練週期進度列,每個週期一段。 +4. **統計**:步數、學習率、已用時間和預估剩餘時間,數字以滾動方式變化。完成後由結果取代。 +5. **事件**:最新的三則,每種類型一個圖示,並標出步數。 +6. **頁尾**:資料集、失敗原因或已部署的端點,以及操作按鈕。 + +### 由應用程式驅動 + + + +按鈕會引發 `PauseRequested`、`ResumeRequested`、`CancelRequested`、`RetryRequested` 和 `RestartRequested`。在你的任務上執行對應操作,然後設定 `Status`。取消需要按兩次:第一次按下後它會變成 Confirm cancel,持續 3 秒。 + +`Actions` 決定哪些按鈕可以顯示(預設全部)。唯讀監控設定 `Actions="None"`。每個按鈕還需要對應的狀態:執行中顯示 Pause,暫停時顯示 Resume,排隊、執行或暫停時顯示 Cancel,失敗後顯示 Retry from checkpoint,執行結束後顯示 Run again。 + +### 結果與部署 + + + +完成後,結果取代即時統計。預設是最終訓練損失與驗證損失、步數以及耗時。`Metrics` 可以設定最多四項自訂結果。設定了 `DeployHandler` 時顯示 Deploy: + +```csharp +run.DeployHandler = async token => +{ + var deployment = await api.DeployAsync(job.Id, token); + return deployment.Endpoint; +}; +``` + +任務執行期間按鈕顯示載入狀態並保持焦點。之後頁尾顯示端點和一個複製按鈕。若任務擲回例外,則顯示例外訊息和 Retry deploy。卡片卸載或執行離開 Completed 時,權杖會被取消。 + +### 窄版面 + + + +卡片填滿容器,最寬 640px。寬度小於 440px 時,內距降為 16px,統計改為兩欄,圖例把 Validation 縮寫為 Val,頁尾把資料集放在操作按鈕上方。 + +### 自訂文字 + +`Labels` 包含卡片顯示或朗讀的所有文字。替換整個物件即可在地化或改寫: + +```csharp +run.Labels = new TrainingRunLabels +{ + Training = "Entrenando", + Pause = "Pausar", + StepAt = step => $"paso {step:N0}", +}; +``` + +### 內容準則 + +- 以任務產出的模型為任務命名,例如 `support-v4`。 +- `Details` 只放兩三個簡短事實:方法和硬體。 +- 事件標籤寫成簡短的已發生事實:"Checkpoint saved"、"Loss spike, gradients clipped"。 +- `ErrorText` 寫明原因和下一步,而不是堆疊追蹤。 + +### 最佳做法 + +- 在第一個樣本之前傳入 `TotalSteps` 和 `Epochs`,座標軸就永遠不會移動。 +- 超過數千個樣本的執行,把 `Loss` 降取樣到約 500 個點。 +- 回滾到某個檢查點時,移除該檢查點之後的樣本;曲線會直接跳回,而不是緩動。 + +### 常見問題 + +**損失突然升高時,為什麼 y 軸不擴大?** 它依最初的幾個樣本固定下來,所以曲線在執行中途不會重新縮放。更高的值畫在頂端邊緣。 + +**圖例顯示什麼?** 最新的平滑訓練損失與最新的驗證損失,或是你以指標或方向鍵檢視的那個樣本的值。 + +## API 參考 + + + + + + + + + + + + + + + + + + + + + + + + + +## 無障礙 + +- 一個禮貌的即時區域播報狀態變化(Queued、Training、Paused、Completed、Failed、Cancelled、Deployed)和 `Notice`,從不逐步播報。 +- 狀態以圖示和文字共同表示,絕不只靠顏色。失敗原因以文字形式出現在頁尾和事件記錄中。 +- 圖表是一個有標籤的群組,其名稱包含步數和兩個損失。方向鍵可逐一檢視樣本,圖例顯示其數值。 +- 取消是兩步驟操作,誤按一次不會停止耗時很長的執行。Deploy 在載入期間保持焦點。 +- 統計是一組術語及其數值。步數和數字使用等寬數字。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 移到損失圖表,再到頁尾的操作按鈕 | +| Left、Right | 在圖表上逐一檢視樣本;圖例顯示該步的損失 | +| Shift + Left、Shift + Right | 跳過十個樣本 | +| Home、End | 跳到第一個或最新的樣本 | +| Escape | 清除正在檢視的樣本 | +| Enter、Space | 按下取得焦點的操作;Cancel 需要再按一次 Confirm cancel | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 新樣本 | 曲線和進度列緩動到它(指數,120ms) | 立即 | 立即 | +| 回滾或重設 | 立即 | 立即 | 立即 | +| 新事件 | 清單以 0.24s 無回彈彈簧下移,新列淡入 | 立即 | 立即 | +| 狀態與頁尾文字 | 舊文字 100ms 淡出,新文字 160ms 淡入並上升 2px | 立即 | 立即 | +| 計數數值 | 數字以彈簧滾動 | 立即 | 立即 | +| 載入圈與活動文字微光 | 旋轉、閃爍 | 靜止 | 靜止 | + +x 軸涵蓋整個執行,y 軸依最初的樣本固定,所以執行中途什麼都不會重新縮放。模擬在減弱動態效果下仍會繼續執行(它是內容),但卡片離開畫面時會暫停。 + +## 相關元件 + +- `AgentStatus`:代理執行的即時狀態。 +- `ToolCall`:代理記錄中的一次工具呼叫。 +- `LineChart`:通用的多序列圖表。 +- `CopyButton`:複製一個值並給予確認。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/en-US.md new file mode 100644 index 00000000..c0fb3736 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/en-US.md @@ -0,0 +1,162 @@ +--- +title: Trial banner +summary: A trial status banner where a ring counts down the days left, the copy names what the workspace loses, and the upgrade path is one click away, as a slim banner, a sidebar card or a header pill. +--- + + + +## When to use + +`TrialBanner` tells a workspace on a free trial how long it has, what it loses when the trial ends and how to keep it. +A ring counts down the days, the copy names what goes away ("SAML SSO and 12 seats") and Add payment method is one click +away. In the last days it shifts to a warning tone and names the end day; once the trial has ended it shows a lock and +what the workspace lost; once a payment method is on file it shows a check. + +- **A SaaS app with a free trial** that needs a quiet, always visible upgrade path. +- **Telling a workspace what it loses** when the trial ends, and nudging harder in the last days. +- **Showing an expired trial** without blocking the app. + +## When not to use + +- **A hard paywall** that blocks a feature: use a dialog or an upgrade screen. +- **Generic announcements or release notes**: use a plain banner or a toast. +- **Usage limits** (credits, seats): use a usage meter. + +## Usage + +Pass the days left from your billing provider, or the end date, the plan, the workspace, what it would lose and your +checkout. With neither `DaysLeft` nor `EndsAt` there is no trial to show: the banner reads "No trial in progress" over an +empty ring, without actions (the preview's "No trial" option). `Plan`, `Workspace`, `LossSummary` and `Features` start +empty; the package ships no sample trial. The previews pass the gallery's invented one: a Pro trial of the Northwind +workspace that would lose SAML SSO and 12 seats. + +```csharp +banner.DaysLeft = trial.DaysLeft; // or: banner.EndsAt = trial.EndsAt; +banner.EndsAt = trial.EndsAt; // for the date in the copy +banner.AddPaymentHandler = async cancellation => +{ + // Open checkout or the billing portal; return once a payment method is on file, throw if it failed. + await billing.AddPaymentMethodAsync(cancellation); +}; +banner.ComparePlansRequested += (_, e) => { e.Handled = true; OpenPlansPage(); }; +banner.Snoozed += (_, e) => settings.TrialBannerSnoozedUntil = e.Until; +banner.Dismissed += (_, _) => settings.TrialBannerDismissed = true; +``` + +### Days and dates + +`DaysLeft` wins when you set it: the banner then never reads the clock for the days, so it shows exactly what your billing +provider says (0 or less is the expired state). Leave it null and set `EndsAt` to have the banner count the local calendar +days from today; it counts again at local midnight. `TimeProvider` is where "today" comes from (the system clock by +default); pass your own in tests. `EndsAt` also gives the date in the copy; without it the date is today plus the days +left. + +`WarnAt` (3) is how many of the last days take the warning tone. `TrialDays` (14) is the trial's length: the ring shows the +days left of it. + +### Variants + + + +`Variant` picks the shape. `Banner` is a slim row for the top of an app. `Card` is a sidebar card, up to 340px wide, +with the list of what the workspace loses: pass three or four `TrialFeature`s in `Features` (a name, an optional muted +detail and a 14px mark). `Pill` is a compact header pill that opens the card in a popover. + + + +### Checkout + + + +`AddPaymentHandler` opens your checkout. While it runs the button shows a spinner and the copy reads "Opening checkout…"; +when it returns the banner shows the upgraded state and sets `IsUpgraded`; when it throws the copy says checkout did not +open and the button reads Try again. The token is cancelled if the banner leaves the window. Without a handler the call is +simulated (900ms). + +Compare plans raises `ComparePlansRequested` and, in the upgraded state, Manage billing raises `ManageBillingRequested`. +Set `Handled`; an unhandled request shows a short simulated note in place of the copy. + +### Dismiss and snooze + +Dismiss raises `Dismissed`; the snooze menu offers until tomorrow, for three days, or until the warning starts, and raises +`Snoozed` with the option and `Until`, the start of that local day. Persist both yourself and stop rendering the banner +until the snooze ends. Meanwhile the banner folds into the trial pill (`IsCollapsed` turns true); the pill opens it again. +`CollapseTo="None"` renders nothing instead. `IsDismissible` and `IsSnoozable` hide the buttons. + +### Narrow containers + + + +The banner reads its own width: under 600px the actions move under the copy, aligned with its left edge, and the second +line may take two lines (both are reserved, so nothing shifts). It holds up at 360px without horizontal scroll. + +### Your own words + +`Labels` holds every word the banner shows or says. Lines that carry values are format functions: + +```csharp +banner.Labels = new TrialBannerLabels +{ + Days = days => days == 1 ? "1 Tag" : $"{days} Tage", + ActiveTitle = (days, plan) => $"Noch {days} in Ihrer {plan}-Testphase", + AddPayment = "Zahlungsmethode hinzufügen", +}; +``` + +### Content guidelines + +- Keep `LossSummary` to a few words ("SAML SSO and 12 seats") so the banner stays on one line. +- Keep `Features` to three or four rows: these are the reasons to pay, not a feature table. +- Name the plan the workspace is trying (`Plan`) and the workspace (`Workspace`); both appear in the copy. + +### Best practices + +- Put the banner above the app content, the card at the bottom of a sidebar, the pill in a dense header. +- Pass the days or the end date from your billing provider (for example the subscription's trial end). +- Persist dismiss and snooze, then stop rendering the banner until the snooze ends. + +## API reference + + + + + + + + + +## Accessibility + +- The banner and the card are a named region ("Pro trial"); the ring is decorative and the days are always in the text. +- A polite live region announces billing, snooze and dismiss results, and says when billing was simulated. +- Status never relies on color: the warning tone also names the end day in the copy, an ended trial shows a lock and an + upgraded one a check. +- After dismiss or snooze, focus moves to the trial pill; opening the banner again moves focus to the primary action. + +### Keyboard + +| Keys | Action | +|---|---| +| Tab | Moves through Compare plans, Add payment method, Snooze and Dismiss | +| Enter, Space | Runs the focused action, opens the snooze menu or opens the pill's card | +| Arrow Up, Arrow Down | Move through the snooze options | +| Escape | Closes the snooze menu or the pill's popover | + +## Motion + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Days change | The ring's arc springs to its new length with no bounce (0.24s) | At once | At once | +| Tone changes | The ring's color follows in 160ms | At once | At once | +| Copy, days and icons change | One crossfade in a shared cell: old out in 100ms, new in over 160ms with a 2px rise | At once | At once | +| A button label changes | The width morphs without bounce | At once | At once | +| The pill's label changes | Its width springs to the measured text | At once | At once | +| Dismiss, snooze, reopen | The surface springs between the banner and the pill (0.33s, no bounce); the faces crossfade | At once | At once | + +No timers or loops run while the banner is idle: only the simulated billing call and the short simulated note use a +timeout. + +## Related + +- `ContextWindow` for showing how much of a budget is used. +- `AgentStatus` for a live status pill. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/zh-CN.md new file mode 100644 index 00000000..2b69a061 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/zh-CN.md @@ -0,0 +1,139 @@ +--- +title: Trial banner +summary: 试用状态横幅:圆环倒数剩余天数,文案说明工作区将失去什么,升级只需点击一次;可作为细横幅、侧边栏卡片或标题栏胶囊。 +source-hash: 42c8d885a742 +--- + + + +## 何时使用 + +`TrialBanner` 告诉处于免费试用期的工作区还剩多久、试用结束时会失去什么,以及如何保留。圆环倒数天数,文案说明会失去的内容("SAML SSO and 12 seats"),"添加付款方式"只需点击一次。在最后几天它切换为警告色调并说出结束的那一天;试用结束后显示锁形图标和工作区失去的内容;付款方式登记后显示对勾。 + +- **提供免费试用的 SaaS 应用**,需要一条安静且始终可见的升级途径。 +- **告诉工作区试用结束时会失去什么**,并在最后几天更积极地提醒。 +- **显示已结束的试用**,而不阻挡应用。 + +## 何时不用 + +- **阻挡某项功能的硬性付费墙**:使用对话框或升级页面。 +- **一般公告或发布说明**:使用普通横幅或 Toast。 +- **用量限制**(额度、席位):使用用量仪表。 + +## 用法 + +传入来自计费服务的剩余天数或结束日期、方案、工作区、它将失去的内容,以及你的结账流程。`DaysLeft` 和 `EndsAt` 都没有时,没有可显示的试用:横幅在空的圆环上方显示 "No trial in progress",没有操作按钮(即预览中的 "No trial" 选项)。`Plan`、`Workspace`、`LossSummary` 和 `Features` 默认为空;包里不附带示例试用。预览传入的是展示库虚构的试用:Northwind 工作区的 Pro 试用,到期后将失去 SAML SSO 和 12 个席位。 + +```csharp +banner.DaysLeft = trial.DaysLeft; // or: banner.EndsAt = trial.EndsAt; +banner.EndsAt = trial.EndsAt; // for the date in the copy +banner.AddPaymentHandler = async cancellation => +{ + // Open checkout or the billing portal; return once a payment method is on file, throw if it failed. + await billing.AddPaymentMethodAsync(cancellation); +}; +banner.ComparePlansRequested += (_, e) => { e.Handled = true; OpenPlansPage(); }; +banner.Snoozed += (_, e) => settings.TrialBannerSnoozedUntil = e.Until; +banner.Dismissed += (_, _) => settings.TrialBannerDismissed = true; +``` + +### 天数与日期 + +设置了 `DaysLeft` 时以它为准:横幅不会为天数读取时钟,因此显示的正是计费服务给出的值(0 或更小即为已结束状态)。将它保留为 null 并设置 `EndsAt`,横幅会从今天起按本地日历日计数,并在本地午夜重新计数。`TimeProvider` 决定"今天"从何而来(默认为系统时钟);测试中可传入自己的实现。`EndsAt` 也提供文案中的日期;没有它时,日期为今天加上剩余天数。 + +`WarnAt`(3)表示最后几天使用警告色调。`TrialDays`(14)是试用期的长度:圆环显示其中剩余的天数。 + +### 变体 + + + +`Variant` 选择形态。`Banner` 是放在应用顶部的细横幅。`Card` 是侧边栏卡片,最宽 340px,列出工作区会失去的内容:在 `Features` 中传入三到四个 `TrialFeature`(名称、可选的次要说明和 14px 图标)。`Pill` 是紧凑的标题栏胶囊,在弹出层中打开卡片。 + + + +### 结账 + + + +`AddPaymentHandler` 打开你的结账流程。运行期间按钮显示加载指示,文案为"Opening checkout…";返回后横幅显示已升级状态并设置 `IsUpgraded`;抛出异常时,文案说明结账未能打开,按钮变为 Try again。如果横幅离开窗口,取消令牌会被触发。没有处理程序时,该调用为模拟(900ms)。 + +"比较方案"引发 `ComparePlansRequested`;在已升级状态下,"管理账单"引发 `ManageBillingRequested`。请设置 `Handled`;未处理的请求会在文案位置显示一条简短的模拟提示。 + +### 关闭与稍后提醒 + +关闭会引发 `Dismissed`;稍后提醒菜单提供"直到明天"、"三天"或"直到警告开始",并引发 `Snoozed`,附带所选选项和 `Until`(那一天本地时间的开始)。请自行保存这两者,并在稍后提醒结束前不再显示横幅。在此期间,横幅折叠为试用胶囊(`IsCollapsed` 变为 true);点击胶囊会再次展开。`CollapseTo="None"` 则什么也不显示。`IsDismissible` 和 `IsSnoozable` 用于隐藏对应按钮。 + +### 窄容器 + + + +横幅读取自身宽度:小于 600px 时,操作按钮移到文案下方,与文案左边缘对齐,第二行文案可以占两行(两行空间都会预留,因此不会跳动)。在 360px 宽度下也不会出现水平滚动。 + +### 自定义文字 + +`Labels` 包含横幅显示或朗读的所有文字。带有数值的行是格式化函数: + +```csharp +banner.Labels = new TrialBannerLabels +{ + Days = days => days == 1 ? "1 Tag" : $"{days} Tage", + ActiveTitle = (days, plan) => $"Noch {days} in Ihrer {plan}-Testphase", + AddPayment = "Zahlungsmethode hinzufügen", +}; +``` + +### 内容准则 + +- `LossSummary` 保持在几个词以内("SAML SSO and 12 seats"),让横幅保持一行。 +- `Features` 保持三到四行:这些是付费的理由,而不是功能对照表。 +- 写明工作区正在试用的方案(`Plan`)和工作区名称(`Workspace`);两者都会出现在文案中。 + +### 最佳实践 + +- 横幅放在应用内容上方,卡片放在侧边栏底部,胶囊放在紧凑的标题栏中。 +- 从计费服务传入天数或结束日期(例如订阅的试用结束时间)。 +- 自行保存关闭与稍后提醒,并在稍后提醒结束前不再显示横幅。 + +## API 参考 + + + + + + + + + +## 无障碍 + +- 横幅和卡片是带名称的区域("Pro trial");圆环仅作装饰,天数始终写在文字中。 +- 礼貌模式的实时区域会播报计费、稍后提醒和关闭的结果,并说明计费是否为模拟。 +- 状态从不只依赖颜色:警告色调同时会在文案中说出结束的那一天,已结束的试用显示锁形图标,已升级的显示对勾。 +- 关闭或稍后提醒后,焦点移到试用胶囊;再次展开横幅时,焦点移到主要操作。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 依次经过比较方案、添加付款方式、稍后提醒和关闭 | +| Enter、Space | 执行获得焦点的操作、打开稍后提醒菜单或打开胶囊的卡片 | +| 上方向键、下方向键 | 在稍后提醒选项之间移动 | +| Escape | 关闭稍后提醒菜单或胶囊的弹出层 | + +## 动效 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 天数变化 | 圆环的弧以无回弹的弹簧(0.24s)变到新长度 | 立即 | 立即 | +| 色调变化 | 圆环的颜色在 160ms 内跟随 | 立即 | 立即 | +| 文案、天数与图标变化 | 在同一格中交叉淡化一次:旧内容 100ms 淡出,新内容 160ms 淡入并上升 2px | 立即 | 立即 | +| 按钮文字变化 | 宽度无回弹地变形 | 立即 | 立即 | +| 胶囊文字变化 | 宽度以弹簧变到测得的文字宽度 | 立即 | 立即 | +| 关闭、稍后提醒、重新展开 | 表面在横幅与胶囊之间以弹簧过渡(0.33s,无回弹);两种形态交叉淡化 | 立即 | 立即 | + +横幅空闲时不运行任何计时器或循环:只有模拟的计费调用和简短的模拟提示使用超时。 + +## 相关组件 + +- `ContextWindow`:显示预算已用了多少。 +- `AgentStatus`:实时状态胶囊。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/zh-TW.md new file mode 100644 index 00000000..0e3dfe41 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/TrialBanner/zh-TW.md @@ -0,0 +1,139 @@ +--- +title: Trial banner +summary: 試用狀態橫幅:圓環倒數剩餘天數,文案說明工作區將失去什麼,升級只需點擊一次;可作為細橫幅、側邊欄卡片或標題列膠囊。 +source-hash: 42c8d885a742 +--- + + + +## 何時使用 + +`TrialBanner` 告訴處於免費試用期的工作區還剩多久、試用結束時會失去什麼,以及如何保留。圓環倒數天數,文案說明會失去的內容("SAML SSO and 12 seats"),「新增付款方式」只需點擊一次。在最後幾天它切換為警告色調並說出結束的那一天;試用結束後顯示鎖頭圖示和工作區失去的內容;付款方式登錄後顯示勾號。 + +- **提供免費試用的 SaaS 應用程式**,需要一條安靜且始終可見的升級途徑。 +- **告訴工作區試用結束時會失去什麼**,並在最後幾天更積極地提醒。 +- **顯示已結束的試用**,而不阻擋應用程式。 + +## 何時不用 + +- **阻擋某項功能的硬性付費牆**:使用對話方塊或升級頁面。 +- **一般公告或版本說明**:使用一般橫幅或 Toast。 +- **用量限制**(額度、席位):使用用量儀表。 + +## 用法 + +傳入來自計費服務的剩餘天數或結束日期、方案、工作區、它將失去的內容,以及你的結帳流程。`DaysLeft` 與 `EndsAt` 都沒有時,沒有可顯示的試用:橫幅在空的圓環上方顯示 "No trial in progress",沒有操作按鈕(即預覽中的 "No trial" 選項)。`Plan`、`Workspace`、`LossSummary` 與 `Features` 預設為空;套件不附帶範例試用。預覽傳入的是展示庫虛構的試用:Northwind 工作區的 Pro 試用,到期後將失去 SAML SSO 與 12 個席位。 + +```csharp +banner.DaysLeft = trial.DaysLeft; // or: banner.EndsAt = trial.EndsAt; +banner.EndsAt = trial.EndsAt; // for the date in the copy +banner.AddPaymentHandler = async cancellation => +{ + // Open checkout or the billing portal; return once a payment method is on file, throw if it failed. + await billing.AddPaymentMethodAsync(cancellation); +}; +banner.ComparePlansRequested += (_, e) => { e.Handled = true; OpenPlansPage(); }; +banner.Snoozed += (_, e) => settings.TrialBannerSnoozedUntil = e.Until; +banner.Dismissed += (_, _) => settings.TrialBannerDismissed = true; +``` + +### 天數與日期 + +設定了 `DaysLeft` 時以它為準:橫幅不會為天數讀取時鐘,因此顯示的正是計費服務給出的值(0 或更小即為已結束狀態)。將它保留為 null 並設定 `EndsAt`,橫幅會從今天起按本地日曆日計數,並在本地午夜重新計數。`TimeProvider` 決定「今天」從何而來(預設為系統時鐘);測試中可傳入自己的實作。`EndsAt` 也提供文案中的日期;沒有它時,日期為今天加上剩餘天數。 + +`WarnAt`(3)表示最後幾天使用警告色調。`TrialDays`(14)是試用期的長度:圓環顯示其中剩餘的天數。 + +### 變體 + + + +`Variant` 選擇形態。`Banner` 是放在應用程式頂端的細橫幅。`Card` 是側邊欄卡片,最寬 340px,列出工作區會失去的內容:在 `Features` 中傳入三到四個 `TrialFeature`(名稱、選用的次要說明和 14px 圖示)。`Pill` 是精簡的標題列膠囊,在彈出層中開啟卡片。 + + + +### 結帳 + + + +`AddPaymentHandler` 開啟你的結帳流程。執行期間按鈕顯示載入指示,文案為 "Opening checkout…";傳回後橫幅顯示已升級狀態並設定 `IsUpgraded`;擲回例外時,文案說明結帳未能開啟,按鈕變為 Try again。如果橫幅離開視窗,取消權杖會被觸發。沒有處理常式時,該呼叫為模擬(900ms)。 + +「比較方案」引發 `ComparePlansRequested`;在已升級狀態下,「管理帳單」引發 `ManageBillingRequested`。請設定 `Handled`;未處理的請求會在文案位置顯示一則簡短的模擬提示。 + +### 關閉與稍後提醒 + +關閉會引發 `Dismissed`;稍後提醒選單提供「直到明天」、「三天」或「直到警告開始」,並引發 `Snoozed`,附帶所選選項和 `Until`(那一天本地時間的開始)。請自行儲存這兩者,並在稍後提醒結束前不再顯示橫幅。在此期間,橫幅收合為試用膠囊(`IsCollapsed` 變為 true);點擊膠囊會再次展開。`CollapseTo="None"` 則什麼也不顯示。`IsDismissible` 和 `IsSnoozable` 用於隱藏對應按鈕。 + +### 窄容器 + + + +橫幅讀取自身寬度:小於 600px 時,操作按鈕移到文案下方,與文案左緣對齊,第二行文案可以佔兩行(兩行空間都會預留,因此不會跳動)。在 360px 寬度下也不會出現水平捲動。 + +### 自訂文字 + +`Labels` 包含橫幅顯示或朗讀的所有文字。帶有數值的行是格式化函式: + +```csharp +banner.Labels = new TrialBannerLabels +{ + Days = days => days == 1 ? "1 Tag" : $"{days} Tage", + ActiveTitle = (days, plan) => $"Noch {days} in Ihrer {plan}-Testphase", + AddPayment = "Zahlungsmethode hinzufügen", +}; +``` + +### 內容準則 + +- `LossSummary` 保持在幾個字以內("SAML SSO and 12 seats"),讓橫幅保持一行。 +- `Features` 保持三到四行:這些是付費的理由,而不是功能對照表。 +- 寫明工作區正在試用的方案(`Plan`)和工作區名稱(`Workspace`);兩者都會出現在文案中。 + +### 最佳實務 + +- 橫幅放在應用程式內容上方,卡片放在側邊欄底部,膠囊放在精簡的標題列中。 +- 從計費服務傳入天數或結束日期(例如訂閱的試用結束時間)。 +- 自行儲存關閉與稍後提醒,並在稍後提醒結束前不再顯示橫幅。 + +## API 參考 + + + + + + + + + +## 無障礙 + +- 橫幅和卡片是具名稱的區域("Pro trial");圓環僅作裝飾,天數始終寫在文字中。 +- 禮貌模式的即時區域會播報計費、稍後提醒和關閉的結果,並說明計費是否為模擬。 +- 狀態從不只依賴顏色:警告色調同時會在文案中說出結束的那一天,已結束的試用顯示鎖頭圖示,已升級的顯示勾號。 +- 關閉或稍後提醒後,焦點移到試用膠囊;再次展開橫幅時,焦點移到主要操作。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 依序經過比較方案、新增付款方式、稍後提醒和關閉 | +| Enter、Space | 執行取得焦點的操作、開啟稍後提醒選單或開啟膠囊的卡片 | +| 上方向鍵、下方向鍵 | 在稍後提醒選項之間移動 | +| Escape | 關閉稍後提醒選單或膠囊的彈出層 | + +## 動態效果 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 天數變化 | 圓環的弧以無回彈的彈簧(0.24s)變到新長度 | 立即 | 立即 | +| 色調變化 | 圓環的顏色在 160ms 內跟隨 | 立即 | 立即 | +| 文案、天數與圖示變化 | 在同一格中交叉淡化一次:舊內容 100ms 淡出,新內容 160ms 淡入並上升 2px | 立即 | 立即 | +| 按鈕文字變化 | 寬度無回彈地變形 | 立即 | 立即 | +| 膠囊文字變化 | 寬度以彈簧變到測得的文字寬度 | 立即 | 立即 | +| 關閉、稍後提醒、重新展開 | 表面在橫幅與膠囊之間以彈簧過渡(0.33s,無回彈);兩種形態交叉淡化 | 立即 | 立即 | + +橫幅閒置時不執行任何計時器或迴圈:只有模擬的計費呼叫和簡短的模擬提示使用逾時。 + +## 相關元件 + +- `ContextWindow`:顯示預算已用了多少。 +- `AgentStatus`:即時狀態膠囊。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/en-US.md new file mode 100644 index 00000000..3c3001d9 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/en-US.md @@ -0,0 +1,268 @@ +--- +title: Upgrade prompt +summary: An upgrade nudge for the moment someone hits a limit, with a usage meter, a rolling price with a monthly and yearly toggle, three concrete unlocks and a success state, in a card that never changes size. +--- + + + +## When to use + +`UpgradePrompt` is the card an AI product shows the moment someone runs out: it says what was used, shows a usage meter +at the limit, offers one next plan with its price and what it unlocks, and lets the person upgrade or decide later. +Upgrading rolls into a success state with the new limits while the meter settles to the new limit. + +- **A usage limit in an AI product**: credits, generations or seats run out and an action is blocked. Show the prompt in + a dialog or popover anchored to the blocked control, or inline in place of the blocked result. +- **A paywall moment**: one next plan with a monthly and a yearly price, where the yearly saving is part of the pitch. +- **A conversion focused nudge that stays calm**: Maybe later leads to a quiet reminder with a way back, not a second + pitch. + +## When not to use + +- **Comparing several plans side by side**: use a pricing table or a plan picker. The prompt offers exactly one plan. +- **Buying prepaid credits without a plan change**: use a credit top-up card. +- **Passive usage reporting on a billing page**: use a usage meter or `Progress`. The prompt is a nudge, not a report. +- **On page load**: show it when an action is actually blocked by the limit, not before. + +## Usage + +```xml + + + +``` + +`UpgradePrompt` is a `TemplatedControl` in the Special package. Register the package with `UseArcSpecial()` next to +`UseArcControls()`. The card fills its container up to 440px and follows its own width, never its content. + +Your app gives the plan (`Plan`) and the usage (`Used`, `Limit`, `Unit`, `ResetsOn`). With no plan the card is empty: +"No plan to offer" in the header, no meter and no offer, and Upgrade and Maybe later turned off; the package ships no +sample plan. The previews pass the gallery's own plan: an invented product's Pro plan at $20 a month or $16 a month billed +yearly, with 2,000 AI credits, Claude Opus 5.5 and a priority queue. The models are real; the plan, its prices and limits +are the invented product's, not a model vendor's prices. With no `UpgradeHandler` Upgrade runs a 1.1s simulation that +charges nothing, and the success state says so. + +### Anatomy + +1. **Header**: a tinted tile with a glyph (a bolt in the offer, a check after the upgrade, a clock in the reminder), the + title as a heading and a two line description. +2. **Usage meter**: "AI credits this month" with the counts ("50 / 50") on the right, a pill bar, and a status row with + an icon and a label ("Limit reached", or "1,950 AI credits left") and the reset date or the plan's limit on the right. +3. **Price**: the plan's name over the rolling per month price and "/ month", with the billing toggle (Monthly, and + Yearly with "Save 20%") on the right. Under it, one slot for the billing note ("$192 billed yearly") or an error. +4. **Unlocks**: "Pro unlocks" and three rows, each with a mark in a small tile and one line of text. +5. **Footer**: Maybe later at its natural width and Upgrade filling the rest. Continue in the success state, See Pro + again in the reminder. + +### States + +| State | Shows | Buttons | +|---|---|---| +| `Offer` | The limit, the price with the toggle and the unlocks | Maybe later, Upgrade to Pro | +| `Processing` | The same, with the toggle off | Maybe later (off), Upgrading… with a spinner | +| `Error` | The error message under the price | Maybe later, Try again | +| `Success` | "You're on Pro", the meter at the new limit and the new limits | Continue | +| `Reminder` | "No problem", when the limit resets and a hint | See Pro again | + +All states share one cell: the body panels and the footers are stacked and crossfade, so the card keeps the size of the +tallest state and the buttons stay in place. `State` is read only and bindable; `ErrorMessage` holds the failure's +message while the card is in `Error`. + +### Your plan and unit + +`Plan` takes an `UpgradePromptPlan` with a name, the monthly price, the per month price when billed yearly, a currency, +the new limit, the unlocks (`UpgradePromptFeature`: an id, a label and an optional mark with its color) and the new +limits (`UpgradePromptLimit`: a label, a value and an optional mark). The saving badge on Yearly and the yearly charge +come from the two prices. `Unit` names what is counted, and `Title` and `Description` replace the offer's words. + + + +```xml + + + + + +``` + +Marks are 24 × 24 stroked glyphs such as `ArcIcons` members. Use neutral glyphs or your own product's mark; the previews +use a sparkle beside the model's name rather than any vendor's logo. + +### Running the upgrade + +`UpgradeHandler` receives an `UpgradePromptSelection` (the plan, the interval, the amount charged today and the +currency) and a cancellation token. Complete the task once the plan is active; throw to show the exception's message +under the price with Try again. The token is cancelled when the prompt leaves the visual tree, and the prompt then +returns to the offer. + + + +```csharp +prompt.UpgradeHandler = async (selection, cancellationToken) => +{ + var response = await http.PostAsJsonAsync("/api/billing/upgrade", + new { plan = selection.Plan.Name, interval = selection.Interval.ToString() }, cancellationToken); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Your card was declined. Try another card."); + } +}; +prompt.Upgraded += (_, e) => workspace.Refresh(); +prompt.Dismissed += (_, _) => nudges.Snooze("upgrade"); +prompt.ContinueRequested += (_, e) => +{ + e.Handled = true; + dialog.Close(); + RetryBlockedAction(); +}; +``` + +`Upgraded` follows a successful upgrade, after the success state shows. `UpgradeFailed` carries the exception. +`Dismissed` follows Maybe later; record the snooze there. `ContinueRequested` follows Continue: set `Handled` to keep the +success state while you close the prompt; left unhandled, the prompt resets to the offer. `Interval` is two way and +`IntervalChanged` follows the toggle. `UpgradeAsync()`, `Dismiss()`, `ShowOffer()` and `Reset()` drive the same steps +from code. + +The amount in the selection is for confirmation copy only. Charge the plan and interval on the server. + +### Narrow containers + +The card is fluid up to 440px. Below 420px of card width the padding drops from 24 to 20. When the price and the toggle +no longer fit side by side, the toggle moves under the price. Feature lines stay on one line and end in an ellipsis; +the buttons stay side by side at every width. + + + +### Reduced motion + +Under reduced motion the bar and the price jump to their values and the crossfades become plain fades. + + + +### Content guidelines + +Keep the three unlocks concrete and tied to what the person was just doing: more of the thing they ran out of, the +better model, no waiting. + +| Instead of | Write | +|---|---| +| Unlock premium features | 2,000 AI credits every month | +| Access to advanced AI | Claude Opus 5.5 for your hardest tasks | +| Enhanced performance | Priority queue, no waiting at peak times | + +`Labels` takes an `UpgradePromptLabels` object with every word the card shows or announces. Sentences with values are +functions, so the word order can change in other languages: + +```csharp +prompt.Labels = new UpgradePromptLabels +{ + MaybeLater = "Not now", + TitleAtLimit = (limit, unit) => $"All {limit} {unit} used" +}; +``` + +### Best practices + +**Do** + +- Show the prompt when an action is blocked by the limit, anchored to the control that was blocked. +- Snooze the nudge in `Dismissed` so it does not reappear on every blocked action. +- Handle `ContinueRequested` to close the prompt and retry the blocked action, so upgrading feels like it unblocked them. + +**Avoid** + +- Presenting a demo's prices as real ones. The previews' plan prices are made up for an invented product. +- Charging from the amount in the selection. Charge the plan and interval on the server. +- Listing more than three unlocks; the card reserves room for three. + +### Common questions + +**Why does the card not shrink after Maybe later?** Every state shares one cell, so the card keeps the size of its +tallest state and nothing around it moves. + +**Can the meter show usage below the limit?** Yes. Below the limit the title says how many are left and the status +reads "5 AI credits left" with a gauge icon. + +**What happens when the prompt is closed during an upgrade?** The handler's token is cancelled and the prompt returns to +the offer. + +## API reference + + + + + + + + + + + + + + + + + + + +The enums: + +| Type | Values | +|---|---| +| `UpgradePromptInterval` | `Monthly`, `Yearly` | +| `UpgradePromptState` | `Offer`, `Processing`, `Error`, `Success`, `Reminder` | + +## Accessibility + +- The card is a group named "Upgrade to Pro", and its title is a heading, so it reads well inside a dialog or popover. +- The usage meter is a progress bar named with its value ("50 of 50 AI credits used"); its status is the help text. +- Status is an icon plus a label ("Limit reached", "1,950 AI credits left"), never color alone. +- A polite live region announces upgrading, success, errors and the reminder. Without an `UpgradeHandler` it says the + upgrade is simulated and nothing is charged. +- The rolling price is hidden from assistive technology; the price group is named in plain text ("$16 a month, $192 + billed yearly"). +- Inactive panels are inert and hidden: not hit-testable, skipped by Tab and not exposed, so focus never lands on a + faded panel. When a state change hides the focused button, focus moves to the new state's main button. +- Upgrade stays focusable while the upgrade runs, so keyboard focus is not lost. + +### Keyboard + +| Keys | Action | +|---|---| +| `Tab` / `Shift+Tab` | Move through the billing toggle, Maybe later and Upgrade; Continue or See Pro again in the other states | +| `Left` / `Right` | Switch between Monthly and Yearly on the billing toggle | +| `Enter` / `Space` | Activate the focused button | + +## Motion + +- The usage bar fills once when the card scrolls into view, and settles to the new limit after the upgrade, on a 0.5s + bounce-free spring. The fill is a pill moved by a translation and clipped to the track, so its rounded end never + stretches. +- The price rolls digit by digit between monthly and yearly on a bounce-free 0.28s spring, in tabular figures, so + nothing beside it shifts. +- The title, the description, the meter label, the counts, the status and the billing note crossfade: 100ms out, 160ms + in with a 2px rise. +- The body panels and the footers fade the same way with a 4px rise; the new one starts as the old one clears. +- The billing toggle's thumb glides on its own spring and its "Save 20%" badge takes the success tint once Yearly is + chosen. + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Bar | Springs over 0.5s | Jumps | Jumps | +| Price | Rolls on a 0.28s spring | Jumps | Jumps | +| Texts | Crossfade with a 2px rise | Fade | Replaced | +| Panels and footers | Fade with a 4px rise | Fade | Replaced | + +## Related + +- `BillingToggle` and `BillingPrice` for a billing period switch on a pricing page. +- `Progress` for a single allowance with a label. +- `ContextWindow` for what fills a model's context window. + +| Component | Choose it when | +|---|---| +| `BillingToggle` | You build a pricing page with several plans | +| `Progress` | You only report usage | +| `ContextWindow` | The limit is a model's context, not a plan | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/zh-CN.md new file mode 100644 index 00000000..9f0aa84f --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/zh-CN.md @@ -0,0 +1,239 @@ +--- +title: Upgrade prompt +summary: 在用户用完额度的那一刻出现的升级提示:用量条、可在月付与年付之间切换并滚动变化的价格、三项具体的解锁内容和成功状态,卡片尺寸始终不变。 +source-hash: 4c42633c3bda +--- + + + +## 何时使用 + +`UpgradePrompt` 是 AI 产品在用户用完额度的那一刻显示的卡片:它说明用掉了什么,显示已到上限的用量条,提供下一档方案的价格和解锁内容, +并让用户立即升级或稍后再说。升级后卡片滚入成功状态,列出新的额度,用量条也随之落到新的上限。 + +- **AI 产品的用量上限**:点数、生成次数或席位用完,某个操作被拦下。把提示放在锚定于被拦下控件的对话框或弹出层中,或直接替换被拦下的结果。 +- **付费墙时刻**:只有一档下一级方案,带月付和年付价格,年付节省的金额是卖点的一部分。 +- **注重转化但保持克制的提示**:Maybe later 之后显示一条安静的提醒并留有返回入口,而不是再推销一次。 + +## 何时不用 + +- **并排比较多个方案**:使用定价表或方案选择器。这个提示只提供一档方案。 +- **不改方案、只购买预付点数**:使用点数充值卡片。 +- **在账单页被动展示用量**:使用用量条或 `Progress`。这个提示是一次推动,不是报表。 +- **页面一加载就显示**:只在某个操作真的因上限被拦下时显示。 + +## 用法 + +```xml + + + +``` + +`UpgradePrompt` 是 Special 包中的 `TemplatedControl`。在 `UseArcControls()` 旁边用 `UseArcSpecial()` 注册这个包。卡片最多 440px +宽,随自身宽度布局,从不随内容变宽。 + +方案(`Plan`)和用量(`Used`、`Limit`、`Unit`、`ResetsOn`)由你的应用提供。没有设置方案时,卡片为空:标题显示 +"No plan to offer",没有用量条也没有报价,Upgrade 和 Maybe later 都不可用;包里不附带示例方案。预览传入的是展示库自己的方案: +一个虚构产品的 Pro 方案,每月 $20 或按年付费每月 $16,含 2,000 个 AI 点数、Claude Opus 5.5 和优先队列。模型是真实的;方案、 +价格与额度属于这个虚构产品,并非模型厂商的价格。没有 `UpgradeHandler` 时,Upgrade 运行一个 1.1 秒、不收取任何费用的模拟,成功状态也会如实说明。 + +### 结构 + +1. **标题区**:带图形的着色小方块(报价时是闪电,升级后是对勾,提醒时是时钟)、作为标题的标题文字和两行说明。 +2. **用量条**:“AI credits this month”,右侧是计数(“50 / 50”),下面是胶囊形进度条,再下面是状态行:图标加标签(“Limit reached”或“1,950 AI credits left”),右侧是重置日期或方案的额度。 +3. **价格**:方案名称在上,下面是滚动的每月价格和“/ month”,右侧是计费切换(Monthly,以及带“Save 20%”的 Yearly)。下方一个位置显示计费说明(“$192 billed yearly”)或错误。 +4. **解锁内容**:“Pro unlocks”和三行内容,每行一个放在小方块里的标记和一行文字。 +5. **底部**:Maybe later 保持自然宽度,Upgrade 填满其余空间。成功状态是 Continue,提醒状态是 See Pro again。 + +### 状态 + +| 状态 | 显示 | 按钮 | +|---|---|---| +| `Offer` | 上限、带切换的价格和解锁内容 | Maybe later、Upgrade to Pro | +| `Processing` | 同上,切换不可用 | Maybe later(不可用)、带加载圈的 Upgrading… | +| `Error` | 价格下方的错误信息 | Maybe later、Try again | +| `Success` | “You're on Pro”、落到新上限的用量条和新的额度 | Continue | +| `Reminder` | “No problem”、额度何时重置以及一条提示 | See Pro again | + +所有状态共用一个单元格:正文面板和底部都叠放在一起并交叉淡化,因此卡片保持最高那个状态的尺寸,按钮也待在原位。`State` 只读且可绑定;卡片处于 +`Error` 时,`ErrorMessage` 保存失败的信息。 + +### 你的方案和单位 + +`Plan` 接受一个 `UpgradePromptPlan`:名称、月付价格、按年付费时的每月价格、货币、新的上限、解锁内容(`UpgradePromptFeature`:id、标签,以及可选的标记和颜色) +和新的额度(`UpgradePromptLimit`:标签、值和可选的标记)。Yearly 上的节省徽标和年付金额都由这两个价格算出。`Unit` 说明计数的是什么,`Title` 和 +`Description` 替换报价状态的文字。 + + + +```xml + + + + + +``` + +标记是 24 × 24 的描边图形,例如 `ArcIcons` 的成员。请使用中性图形或你自己产品的标记;预览在模型名称旁使用闪光图形,而不是任何厂商的标志。 + +### 执行升级 + +`UpgradeHandler` 收到一个 `UpgradePromptSelection`(方案、计费周期、今天收取的金额和货币)以及一个取消令牌。方案生效后完成任务;抛出异常则在价格下方 +显示异常信息并提供 Try again。提示离开可视树时令牌会被取消,提示随后回到报价状态。 + + + +```csharp +prompt.UpgradeHandler = async (selection, cancellationToken) => +{ + var response = await http.PostAsJsonAsync("/api/billing/upgrade", + new { plan = selection.Plan.Name, interval = selection.Interval.ToString() }, cancellationToken); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Your card was declined. Try another card."); + } +}; +prompt.Upgraded += (_, e) => workspace.Refresh(); +prompt.Dismissed += (_, _) => nudges.Snooze("upgrade"); +prompt.ContinueRequested += (_, e) => +{ + e.Handled = true; + dialog.Close(); + RetryBlockedAction(); +}; +``` + +升级成功、成功状态显示之后引发 `Upgraded`。`UpgradeFailed` 带有异常。`Dismissed` 在 Maybe later 之后引发,请在这里记录暂缓。`ContinueRequested` 在 +Continue 之后引发:关闭提示时把 `Handled` 设为 true 可保留成功状态;未处理时,提示重置回报价状态。`Interval` 是双向的,`IntervalChanged` 跟随切换。 +`UpgradeAsync()`、`Dismiss()`、`ShowOffer()` 和 `Reset()` 可以从代码执行同样的步骤。 + +选择中的金额只用于确认文案。请在服务器上按方案和计费周期收费。 + +### 窄容器 + +卡片在 440px 以内随容器伸缩。卡片宽度小于 420px 时,内边距从 24 降到 20。价格和切换无法并排放下时,切换移到价格下方。解锁内容始终保持一行, +超出部分以省略号结尾;按钮在任何宽度下都并排显示。 + + + +### 减弱动效 + +减弱动效时,进度条和价格直接跳到目标值,交叉淡化变为普通淡入淡出。 + + + +### 内容准则 + +三项解锁内容要具体,并与用户刚才在做的事有关:更多他们刚用完的东西、更好的模型、无需等待。 + +| 不要写 | 改写为 | +|---|---| +| Unlock premium features | 2,000 AI credits every month | +| Access to advanced AI | Claude Opus 5.5 for your hardest tasks | +| Enhanced performance | Priority queue, no waiting at peak times | + +`Labels` 接受一个 `UpgradePromptLabels` 对象,包含卡片显示或播报的每个词。带值的句子是函数,因此其他语言可以调整语序: + +```csharp +prompt.Labels = new UpgradePromptLabels +{ + MaybeLater = "Not now", + TitleAtLimit = (limit, unit) => $"All {limit} {unit} used" +}; +``` + +### 最佳实践 + +**建议** + +- 在某个操作因上限被拦下时显示提示,并锚定在被拦下的控件上。 +- 在 `Dismissed` 中暂缓这条提示,避免每次被拦下都再次出现。 +- 处理 `ContinueRequested` 来关闭提示并重试被拦下的操作,让升级感觉像是替用户解除了阻碍。 + +**避免** + +- 把演示中的价格当作真实价格展示。预览中的方案价格是为一个虚构产品编造的。 +- 按选择中的金额收费。请在服务器上按方案和计费周期收费。 +- 列出超过三项解锁内容;卡片只为三项预留了空间。 + +### 常见问题 + +**为什么 Maybe later 之后卡片没有变小?** 所有状态共用一个单元格,因此卡片保持最高那个状态的尺寸,周围的内容都不会移动。 + +**用量条能显示未到上限的用量吗?** 能。未到上限时,标题说明还剩多少,状态显示带仪表图标的“5 AI credits left”。 + +**升级进行中关闭提示会怎样?** 处理程序的令牌被取消,提示回到报价状态。 + +## API 参考 + + + + + + + + + + + + + + + + + + + +枚举: + +| 类型 | 值 | +|---|---| +| `UpgradePromptInterval` | `Monthly`、`Yearly` | +| `UpgradePromptState` | `Offer`、`Processing`、`Error`、`Success`、`Reminder` | + +## 无障碍 + +- 卡片是名为“Upgrade to Pro”的分组,标题是一个标题元素,因此放在对话框或弹出层中也易于朗读。 +- 用量条是一个进度条,名称就是它的值(“50 of 50 AI credits used”),状态作为帮助文本。 +- 状态始终是图标加标签(“Limit reached”、“1,950 AI credits left”),从不只靠颜色。 +- 一个礼貌的实时区域播报升级中、成功、错误和提醒。没有 `UpgradeHandler` 时,它会说明升级是模拟的,不会收取任何费用。 +- 滚动的价格对辅助技术隐藏;价格组以纯文本命名(“$16 a month, $192 billed yearly”)。 +- 未激活的面板是惰性且隐藏的:不可命中、Tab 会跳过、也不会公开,因此焦点永远不会落在淡出的面板上。状态变化隐藏了获得焦点的按钮时,焦点移到新状态的主按钮。 +- 升级进行中 Upgrade 仍可获得焦点,因此键盘焦点不会丢失。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| `Tab` / `Shift+Tab` | 在计费切换、Maybe later 和 Upgrade 之间移动;其他状态下是 Continue 或 See Pro again | +| `Left` / `Right` | 在计费切换上切换 Monthly 和 Yearly | +| `Enter` / `Space` | 激活获得焦点的按钮 | + +## 动效 + +- 卡片第一次滚入视口时用量条填充一次,升级后以 0.5 秒无回弹的弹簧落到新的上限。填充是一个通过平移移动并裁剪在轨道内的胶囊,所以它的圆角端从不被拉伸。 +- 价格在月付和年付之间以 0.28 秒无回弹的弹簧逐位滚动,使用等宽数字,旁边的内容不会移动。 +- 标题、说明、用量条标签、计数、状态和计费说明交叉淡化:100ms 淡出,160ms 淡入并上升 2px。 +- 正文面板和底部以同样方式淡化并上升 4px;旧面板淡出后新面板才开始出现。 +- 计费切换的滑块以自身的弹簧滑动,选中 Yearly 后“Save 20%”徽标换上成功色调。 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 进度条 | 0.5 秒弹簧 | 直接跳转 | 直接跳转 | +| 价格 | 以 0.28 秒弹簧滚动 | 直接跳转 | 直接跳转 | +| 文字 | 交叉淡化并上升 2px | 淡入淡出 | 直接替换 | +| 面板与底部 | 淡化并上升 4px | 淡入淡出 | 直接替换 | + +## 相关组件 + +- `BillingToggle` 和 `BillingPrice`:定价页上的计费周期切换。 +- `Progress`:带标签的单一额度。 +- `ContextWindow`:模型上下文窗口里装了什么。 + +| 组件 | 何时选择 | +|---|---| +| `BillingToggle` | 你在做包含多个方案的定价页 | +| `Progress` | 你只需要报告用量 | +| `ContextWindow` | 上限是模型的上下文,而不是方案 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/zh-TW.md new file mode 100644 index 00000000..95c7b4c0 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Ai/UpgradePrompt/zh-TW.md @@ -0,0 +1,239 @@ +--- +title: Upgrade prompt +summary: 在使用者用完額度的那一刻出現的升級提示:用量條、可在月繳與年繳之間切換並滾動變化的價格、三項具體的解鎖內容和成功狀態,卡片尺寸始終不變。 +source-hash: 4c42633c3bda +--- + + + +## 何時使用 + +`UpgradePrompt` 是 AI 產品在使用者用完額度的那一刻顯示的卡片:它說明用掉了什麼,顯示已達上限的用量條,提供下一級方案的價格和解鎖內容, +並讓使用者立即升級或稍後再說。升級後卡片滾入成功狀態,列出新的額度,用量條也隨之落到新的上限。 + +- **AI 產品的用量上限**:點數、生成次數或席次用完,某個操作被擋下。把提示放在錨定於被擋下控制項的對話框或彈出層中,或直接取代被擋下的結果。 +- **付費牆時刻**:只有一級下一階方案,附月繳和年繳價格,年繳省下的金額是賣點的一部分。 +- **重視轉換但保持克制的提示**:Maybe later 之後顯示一則安靜的提醒並留有返回入口,而不是再推銷一次。 + +## 何時不用 + +- **並排比較多個方案**:使用價目表或方案選擇器。這個提示只提供一級方案。 +- **不改方案、只購買預付點數**:使用點數儲值卡片。 +- **在帳單頁被動呈現用量**:使用用量條或 `Progress`。這個提示是一次推動,不是報表。 +- **頁面一載入就顯示**:只在某個操作真的因上限被擋下時顯示。 + +## 用法 + +```xml + + + +``` + +`UpgradePrompt` 是 Special 套件中的 `TemplatedControl`。在 `UseArcControls()` 旁邊用 `UseArcSpecial()` 註冊這個套件。卡片最多 440px +寬,隨自身寬度版面配置,從不隨內容變寬。 + +方案(`Plan`)與用量(`Used`、`Limit`、`Unit`、`ResetsOn`)由你的應用程式提供。沒有設定方案時,卡片為空:標題顯示 +"No plan to offer",沒有用量條也沒有報價,Upgrade 與 Maybe later 都無法使用;套件不附帶範例方案。預覽傳入的是展示庫自己的方案: +一個虛構產品的 Pro 方案,每月 $20 或按年繳費每月 $16,含 2,000 個 AI 點數、Claude Opus 5.5 和優先佇列。模型是真實的;方案、 +價格與額度屬於這個虛構產品,並非模型廠商的價格。沒有 `UpgradeHandler` 時,Upgrade 會執行一個 1.1 秒、不收取任何費用的模擬,成功狀態也會如實說明。 + +### 結構 + +1. **標題區**:帶圖形的著色小方塊(報價時是閃電,升級後是勾號,提醒時是時鐘)、作為標題的標題文字和兩行說明。 +2. **用量條**:「AI credits this month」,右側是計數(「50 / 50」),下面是膠囊形進度條,再下面是狀態列:圖示加標籤(「Limit reached」或「1,950 AI credits left」),右側是重設日期或方案的額度。 +3. **價格**:方案名稱在上,下面是滾動的每月價格和「/ month」,右側是計費切換(Monthly,以及附「Save 20%」的 Yearly)。下方一個位置顯示計費說明(「$192 billed yearly」)或錯誤。 +4. **解鎖內容**:「Pro unlocks」和三列內容,每列一個放在小方塊裡的標記和一行文字。 +5. **底部**:Maybe later 維持自然寬度,Upgrade 填滿其餘空間。成功狀態是 Continue,提醒狀態是 See Pro again。 + +### 狀態 + +| 狀態 | 顯示 | 按鈕 | +|---|---|---| +| `Offer` | 上限、附切換的價格和解鎖內容 | Maybe later、Upgrade to Pro | +| `Processing` | 同上,切換無法使用 | Maybe later(無法使用)、附載入圈的 Upgrading… | +| `Error` | 價格下方的錯誤訊息 | Maybe later、Try again | +| `Success` | 「You're on Pro」、落到新上限的用量條和新的額度 | Continue | +| `Reminder` | 「No problem」、額度何時重設以及一則提示 | See Pro again | + +所有狀態共用一個儲存格:內文面板和底部都疊放在一起並交叉淡化,因此卡片維持最高那個狀態的尺寸,按鈕也待在原位。`State` 唯讀且可繫結;卡片處於 +`Error` 時,`ErrorMessage` 保存失敗的訊息。 + +### 你的方案和單位 + +`Plan` 接受一個 `UpgradePromptPlan`:名稱、月繳價格、按年繳費時的每月價格、幣別、新的上限、解鎖內容(`UpgradePromptFeature`:id、標籤,以及選用的標記和顏色) +和新的額度(`UpgradePromptLimit`:標籤、值和選用的標記)。Yearly 上的節省徽章和年繳金額都由這兩個價格算出。`Unit` 說明計數的是什麼,`Title` 和 +`Description` 取代報價狀態的文字。 + + + +```xml + + + + + +``` + +標記是 24 × 24 的描邊圖形,例如 `ArcIcons` 的成員。請使用中性圖形或你自己產品的標記;預覽在模型名稱旁使用閃光圖形,而不是任何廠商的標誌。 + +### 執行升級 + +`UpgradeHandler` 收到一個 `UpgradePromptSelection`(方案、計費週期、今天收取的金額和幣別)以及一個取消權杖。方案生效後完成工作;擲回例外則在價格下方 +顯示例外訊息並提供 Try again。提示離開視覺樹時權杖會被取消,提示隨後回到報價狀態。 + + + +```csharp +prompt.UpgradeHandler = async (selection, cancellationToken) => +{ + var response = await http.PostAsJsonAsync("/api/billing/upgrade", + new { plan = selection.Plan.Name, interval = selection.Interval.ToString() }, cancellationToken); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException("Your card was declined. Try another card."); + } +}; +prompt.Upgraded += (_, e) => workspace.Refresh(); +prompt.Dismissed += (_, _) => nudges.Snooze("upgrade"); +prompt.ContinueRequested += (_, e) => +{ + e.Handled = true; + dialog.Close(); + RetryBlockedAction(); +}; +``` + +升級成功、成功狀態顯示之後引發 `Upgraded`。`UpgradeFailed` 帶有例外。`Dismissed` 在 Maybe later 之後引發,請在這裡記錄暫緩。`ContinueRequested` 在 +Continue 之後引發:關閉提示時把 `Handled` 設為 true 可保留成功狀態;未處理時,提示重設回報價狀態。`Interval` 是雙向的,`IntervalChanged` 跟隨切換。 +`UpgradeAsync()`、`Dismiss()`、`ShowOffer()` 和 `Reset()` 可以從程式碼執行同樣的步驟。 + +選擇中的金額只用於確認文案。請在伺服器上按方案和計費週期收費。 + +### 窄容器 + +卡片在 440px 以內隨容器伸縮。卡片寬度小於 420px 時,內距從 24 降到 20。價格和切換無法並排放下時,切換移到價格下方。解鎖內容始終維持一行, +超出部分以刪節號結尾;按鈕在任何寬度下都並排顯示。 + + + +### 減少動態效果 + +減少動態效果時,進度條和價格直接跳到目標值,交叉淡化變為一般的淡入淡出。 + + + +### 文案規範 + +三項解鎖內容要具體,並與使用者剛才在做的事有關:更多他們剛用完的東西、更好的模型、不必等待。 + +| 不要寫 | 改寫為 | +|---|---| +| Unlock premium features | 2,000 AI credits every month | +| Access to advanced AI | Claude Opus 5.5 for your hardest tasks | +| Enhanced performance | Priority queue, no waiting at peak times | + +`Labels` 接受一個 `UpgradePromptLabels` 物件,包含卡片顯示或朗讀的每個詞。帶值的句子是函式,因此其他語言可以調整語序: + +```csharp +prompt.Labels = new UpgradePromptLabels +{ + MaybeLater = "Not now", + TitleAtLimit = (limit, unit) => $"All {limit} {unit} used" +}; +``` + +### 最佳做法 + +**建議** + +- 在某個操作因上限被擋下時顯示提示,並錨定在被擋下的控制項上。 +- 在 `Dismissed` 中暫緩這則提示,避免每次被擋下都再次出現。 +- 處理 `ContinueRequested` 來關閉提示並重試被擋下的操作,讓升級感覺像是替使用者解除了阻礙。 + +**避免** + +- 把示範中的價格當作真實價格呈現。預覽中的方案價格是為一個虛構產品編造的。 +- 按選擇中的金額收費。請在伺服器上按方案和計費週期收費。 +- 列出超過三項解鎖內容;卡片只為三項預留了空間。 + +### 常見問題 + +**為什麼 Maybe later 之後卡片沒有變小?** 所有狀態共用一個儲存格,因此卡片維持最高那個狀態的尺寸,周圍的內容都不會移動。 + +**用量條能顯示未達上限的用量嗎?** 能。未達上限時,標題說明還剩多少,狀態顯示附儀表圖示的「5 AI credits left」。 + +**升級進行中關閉提示會怎樣?** 處理常式的權杖被取消,提示回到報價狀態。 + +## API 參考 + + + + + + + + + + + + + + + + + + + +列舉: + +| 型別 | 值 | +|---|---| +| `UpgradePromptInterval` | `Monthly`、`Yearly` | +| `UpgradePromptState` | `Offer`、`Processing`、`Error`、`Success`、`Reminder` | + +## 無障礙 + +- 卡片是名為「Upgrade to Pro」的群組,標題是一個標題元素,因此放在對話框或彈出層中也容易朗讀。 +- 用量條是一個進度條,名稱就是它的值(「50 of 50 AI credits used」),狀態作為說明文字。 +- 狀態始終是圖示加標籤(「Limit reached」、「1,950 AI credits left」),從不只靠顏色。 +- 一個禮貌的即時區域朗讀升級中、成功、錯誤和提醒。沒有 `UpgradeHandler` 時,它會說明升級是模擬的,不會收取任何費用。 +- 滾動的價格對輔助技術隱藏;價格群組以純文字命名(「$16 a month, $192 billed yearly」)。 +- 未啟用的面板是惰性且隱藏的:無法點擊、Tab 會略過、也不會公開,因此焦點永遠不會落在淡出的面板上。狀態變化隱藏了取得焦點的按鈕時,焦點移到新狀態的主要按鈕。 +- 升級進行中 Upgrade 仍可取得焦點,因此鍵盤焦點不會遺失。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| `Tab` / `Shift+Tab` | 在計費切換、Maybe later 和 Upgrade 之間移動;其他狀態下是 Continue 或 See Pro again | +| `Left` / `Right` | 在計費切換上切換 Monthly 和 Yearly | +| `Enter` / `Space` | 啟用取得焦點的按鈕 | + +## 動態效果 + +- 卡片第一次捲入檢視區時用量條填滿一次,升級後以 0.5 秒無回彈的彈簧落到新的上限。填色是一個透過平移移動並裁切在軌道內的膠囊,所以它的圓角端從不被拉長。 +- 價格在月繳和年繳之間以 0.28 秒無回彈的彈簧逐位滾動,使用等寬數字,旁邊的內容不會移動。 +- 標題、說明、用量條標籤、計數、狀態和計費說明交叉淡化:100ms 淡出,160ms 淡入並上升 2px。 +- 內文面板和底部以同樣方式淡化並上升 4px;舊面板淡出後新面板才開始出現。 +- 計費切換的滑塊以自身的彈簧滑動,選取 Yearly 後「Save 20%」徽章換上成功色調。 + +| 變化 | 完整 | 減少 | 關閉 | +|---|---|---|---| +| 進度條 | 0.5 秒彈簧 | 直接跳轉 | 直接跳轉 | +| 價格 | 以 0.28 秒彈簧滾動 | 直接跳轉 | 直接跳轉 | +| 文字 | 交叉淡化並上升 2px | 淡入淡出 | 直接取代 | +| 面板與底部 | 淡化並上升 4px | 淡入淡出 | 直接取代 | + +## 相關元件 + +- `BillingToggle` 和 `BillingPrice`:價目頁上的計費週期切換。 +- `Progress`:附標籤的單一額度。 +- `ContextWindow`:模型上下文視窗裡裝了什麼。 + +| 元件 | 何時選擇 | +|---|---| +| `BillingToggle` | 你在做包含多個方案的價目頁 | +| `Progress` | 你只需要回報用量 | +| `ContextWindow` | 上限是模型的上下文,而不是方案 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Api/zh-CN.xlf b/gallery/TKCode.Arc.Gallery/Docs/Api/zh-CN.xlf index 4b86cf85..a63fe043 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Api/zh-CN.xlf +++ b/gallery/TKCode.Arc.Gallery/Docs/Api/zh-CN.xlf @@ -420,6 +420,266 @@ This week's 168 hourly values, Monday 00:00 first (day × 24 + hour). 本周 168 个小时数值,从周一 00:00 开始(天序号 × 24 + 小时)。 + + A live document where an AI agent edits like a collaborator (Arc Agent cursor, design reference). The agent's labeled cursor comes out of its avatar, selects a paragraph and streams in a rewrite, adds a heading and fixes a typo, while a teammate's cursor reads along. The changes stay tinted in the agent's color until someone accepts or undoes them from a floating review bar. Give it the document as `Blocks` (title, heading and paragraph blocks with stable ids) and the edits as `Edits`, or fetch them from your agent with `Run` (cancelled when someone presses Stop). Store the document from `Accepted` and pass it back as `Blocks`. With none of them it plays a `Simulation` (a document, its edits, an agent and a teammate you pass), looping; with no data at all it shows an empty document. + 一份实时文档,AI 智能体像协作者一样在其中编辑(Arc Agent cursor,设计参考)。智能体带名字标签的光标从它的头像中出来,选中一个段落并流式写入改写,添加一个标题并修正一个拼写错误,同时一位同事的光标跟着阅读。改动以智能体的颜色着色,直到有人在浮动的审阅栏中接受或撤销。把文档作为 `Blocks`(带稳定 id 的标题、小标题和段落块)交给它,把编辑作为 `Edits`,或用 `Run` 从你的智能体获取(有人按下 Stop 时取消)。从 `Accepted` 保存文档,再作为 `Blocks` 传回。三者都没有时,它循环播放 `Simulation`(你传入的文档、编辑、智能体和同事);完全没有数据时显示一份空文档。 + + + Raised with the edited document when someone accepts the changes. Bubbles. + 有人接受改动时引发,参数是编辑后的文档。冒泡。 + + + The agent: its name on the cursor, its 24 × 24 stroked mark in the avatar and, from a solid `MarkBrush`, the color of its cursor, selection and change tint (the accent otherwise). Null is the `Simulation`'s agent while one plays, or an unnamed one. + 智能体:光标上的名字、头像中 24 × 24 的描边标记,以及(来自纯色的 `MarkBrush`)其光标、选区和改动着色的颜色(否则为强调色)。为 null 时,播放 `Simulation` 期间使用其智能体,否则使用一个未命名的智能体。 + + + Starts a run as soon as the card is on screen. Off, the header shows an Ask button instead. + 卡片一出现在屏幕上就开始运行。关闭时,页头显示 Ask 按钮。 + + + The document as title, heading and paragraph blocks with stable ids; null shows the `Simulation`'s while one plays, or an empty document. Compared by content, so passing back an equal list does nothing. + 文档,由带稳定 id 的标题、小标题和段落块组成;为 null 时,播放 `Simulation` 期间显示其文档,否则显示空文档。按内容比较,所以传回相同的列表不会有任何变化。 + + + A person in the document, drawn as a second cursor with their initials and first name. Null is the `Simulation`'s teammate while one plays, or nobody. + 文档中的一个人,绘制为第二个光标,带有其姓名首字母和名字。为 null 时,播放 `Simulation` 期间使用其同事,否则没有同事。 + + + The edits to play in order (rewrite, insert, fix); ignored when `Run` is set. Null plays the `Simulation`'s while one plays, or none. + 按顺序播放的编辑(改写、插入、修正);设置了 `Run` 时忽略。为 null 时,播放 `Simulation` 期间播放其编辑,否则没有编辑。 + + + Whether the collaborator is shown; false hides their cursor and avatar. + 是否显示协作者;false 会隐藏其光标和头像。 + + + Adds "Simulated, nothing was saved" to every announcement. Null: only while a `Simulation` plays. + 在每次播报后附加 "Simulated, nothing was saved"。为 null 时只在播放 `Simulation` 时附加。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + Replays the run: the collaborator accepts after a short pause (never while the pointer is over the card or focus is in it), then the document resets. Null loops only while a `Simulation` plays. Never loops below full motion. + 重放运行:协作者在短暂停顿后接受(指针停在卡片上或焦点在卡片内时从不接受),然后文档重置。为 null 时只在播放 `Simulation` 时循环。低于完整动效时从不循环。 + + + A muted line under the title, such as the owner and a target date. + 标题下方的一行弱化文字,例如负责人和目标日期。 + + + How many edits are waiting for review. + 等待审阅的编辑数量。 + + + Where the run is: idle, thinking, editing, review, accepted, reverted, stopped, empty or error. + 运行所处的阶段:空闲、思考、编辑、审阅、已接受、已撤销、已停止、无改动或错误。 + + + Raised when `Phase` changes. Bubbles. + `Phase` 变化时引发。冒泡。 + + + Fetches the edits from your agent. The token is cancelled when someone presses Stop; a failed task shows the error with Retry. + 从你的智能体获取编辑。有人按下 Stop 时令牌被取消;失败的任务会显示错误和 Retry。 + + + A session to play while the card has no `Blocks`, `Edits` or `Run`: its document, edits, agent, teammate and meta line, looping with full motion (nothing is saved). Your own data wins over it; null stops it and shows the empty document. + 卡片没有 `Blocks`、`Edits` 或 `Run` 时要播放的会话:它的文档、编辑、智能体、同事和元信息行,在完整动效下循环播放(不会保存任何内容)。你自己的数据优先于它;为 null 时停止播放并显示空文档。 + + + Playback speed multiplier for cursor travel, streaming and pauses (above zero). + 光标移动、流式写入和停顿的播放速度倍数(大于零)。 + + + Raised with the restored document when someone undoes the changes. Bubbles. + 有人撤销改动时引发,参数是恢复后的文档。冒泡。 + + + One block of the document an `AgentCursor` shows: a title, heading or paragraph with a stable id. + `AgentCursor` 所显示文档中的一个块:带稳定 id 的标题、小标题或段落。 + + + A person in an `AgentCursor` document, drawn as a second cursor with their initials and first name. + `AgentCursor` 文档中的一个人,绘制为第二个光标,带有其姓名首字母和名字。 + + + Arguments of `Accepted` and `Undone`: the document after the change. + `Accepted` 和 `Undone` 的参数:变化后的文档。 + + + One edit the agent makes in an `AgentCursor` document. Edits address blocks by id: a rewrite replaces a block's text, an insert adds a block after one, a fix replaces an exact word inside a block. + 智能体在 `AgentCursor` 文档中进行的一次编辑。编辑按 id 指向块:改写替换一个块的文字,插入在某个块之后添加一个块,修正替换块中一个确切的词。 + + + Every word `AgentCursor` shows or says, in English by default. Replace the whole object to localize or reword; texts that hold a name or a count are format functions. + `AgentCursor` 显示或朗读的每一个词,默认是英文。替换整个对象即可本地化或改写措辞;包含名字或数量的文字是格式化函数。 + + + Arguments of `PhaseChanged`. + `PhaseChanged` 的参数。 + + + A document session an `AgentCursor` plays without an agent, from data you give it: the document, the edits the agent makes, the agent and a teammate who reads along, and the muted line under the title. Set it as `Simulation` while the card has no `Blocks`, `Edits` or `Run`: the card shows the document, plays the edits, says nothing was saved and, with full motion, loops (the teammate accepts after a short pause, then it starts over). + `AgentCursor` 在没有智能体时按你提供的数据播放的文档会话:文档、智能体所做的编辑、智能体和一位跟着阅读的同事,以及标题下方的浅色行。在卡片没有 `Blocks`、`Edits` 或 `Run` 时把它设为 `Simulation`:卡片显示文档、播放编辑、说明什么都没有保存,并在完整动效下循环(同事在短暂停顿后接受,然后重新开始)。 + + + A support conversation where an AI assistant hands off to a person (Arc Agent handoff, design reference). The assistant answers first; when it transfers, a card in the thread shows the summary it passes to the team and the typical wait. The person then joins and takes over, and a thin timeline on the left marks each step: the assistant, the handoff (with the help desk's mark), the person joining and the resolution. Drive it from your support backend: set `Stage`, keep the assistant's messages in `Messages` and everything after the handoff in `AgentMessages`, set `Typing`, `Summary` and `Agent`, and handle the events. Stream assistant text by updating a message's `Text` with `IsStreaming` set; each appended chunk fades in. Leave `Stage` unset for an empty chat, or to play a `Simulation` you pass in (`AgentHandoffSimulation`). The card fills its container up to 560px and has a fixed height (600 by default, set `Height`); the thread scrolls inside and stays pinned to the newest message, so the page never shifts and the width never follows the text. + AI 助手把对话转交给真人的客服对话(Arc Agent handoff,设计参考)。助手先回答;转接时,对话中的一张卡片显示它交给团队的摘要和通常的等待时间。随后真人加入并接手,左侧一条细时间线标出每一步:助手、转接(带客服系统的标识)、真人加入和问题解决。由你的客服后端驱动:设置 `Stage`,助手的消息放在 `Messages`,转接之后的一切放在 `AgentMessages`,设置 `Typing`、`Summary` 和 `Agent`,并处理事件。要流式输出助手的文字,在设置了 `IsStreaming` 的消息上更新 `Text`;每次追加的片段都会淡入。不设置 `Stage` 时显示一个空对话,或播放你传入的 `Simulation`(`AgentHandoffSimulation`)。卡片填满容器,最宽 560px,高度固定(默认 600,可设置 `Height`);对话在内部滚动并固定在最新消息处,所以页面不会跳动,宽度也不会随文字变化。 + + + The person who takes over: shown on the join step, in the card and in the header from connected on. + 接手的人:从已接通起显示在加入步骤、卡片和标题栏中。 + + + Shows Cancel on the handoff card while transferring; it raises `CancelTransferRequested`. + 转接中在转接卡片上显示 Cancel;它触发 `CancelTransferRequested`。 + + + Shows a Talk to a person reply under the assistant's last answer while the stage is Assistant; it raises `HumanRequested`. + 阶段为 Assistant 时,在助手最后一条回答下方显示 Talk to a person 快捷回复;它触发 `HumanRequested`。 + + + Shows Try again on the handoff card when the stage is Unavailable; it raises `RetryRequested`. + 阶段为 Unavailable 时在转接卡片上显示 Try again;它触发 `RetryRequested`。 + + + The assistant's mark: a 24 × 24 stroked glyph drawn at 14px (the sparkle by default). Pass your model's own mark. + 助手的标识:一个 24 × 24 的描边图形,以 14px 绘制(默认是星芒)。传入你的模型自己的标识。 + + + The assistant mark's color; the accent by default. + 助手标识的颜色;默认为强调色。 + + + The model behind the assistant, shown muted after its name; null (the default) hides it. + 助手背后的模型,以弱化颜色显示在名字之后;为 null(默认)时隐藏。 + + + The assistant's name on the first step. + 第一步上的助手名字。 + + + Raised when the customer presses Cancel on the handoff card. Bubbles. Cancel the escalation, then set `Stage` back to Assistant. + 客户在转接卡片上按下 Cancel 时触发。冒泡。取消升级,然后把 `Stage` 设回 Assistant。 + + + Raised when the customer presses Talk to a person. Bubbles. Escalate, then set `Stage` to Transferring. + 客户按下 Talk to a person 时触发。冒泡。升级给人工,然后把 `Stage` 设为 Transferring。 + + + Whether the summary in the handoff card is open (two-way). + 转接卡片中的摘要是否展开(双向)。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + A one-off message for screen readers, such as "Refund issued"; announced once each time it changes. + 给屏幕阅读器的一次性消息,例如 “Refund issued”;每次变化时朗读一次。 + + + The composer placeholder; null follows the stage ("Ask a question", "Message Jordan"). + 输入框的占位文字;为 null 时随阶段变化(“Ask a question”、“Message Jordan”)。 + + + The help desk's mark on the timeline next to the handoff card: a 24 × 24 stroked glyph (an inbox by default). + 时间线上转接卡片旁的客服系统标识:一个 24 × 24 的描边图形(默认是收件箱)。 + + + The provider mark's color; the secondary text color by default. + 客服系统标识的颜色;默认为次要文字颜色。 + + + The name of the help desk the conversation moves to. + 对话转入的客服系统名称。 + + + The typical wait shown while transferring, such as "2 min"; null (the default) shows none. + 转接中显示的通常等待时间,例如 "2 min";为 null(默认)时不显示。 + + + Raised when the customer presses Try again on the handoff card. Bubbles. Escalate again, then set `Stage` to Transferring. + 客户在转接卡片上按下 Try again 时触发。冒泡。再次升级,然后把 `Stage` 设为 Transferring。 + + + Raised when the customer sends a message, with the trimmed text. Bubbles. Append it to `Messages` or `AgentMessages` yourself. + 客户发送消息时触发,携带去掉首尾空白的文本。冒泡。由你自己把它追加到 `Messages` 或 `AgentMessages`。 + + + Shows the composer; sending raises `SendRequested` with the trimmed text. + 显示输入框;发送会触发 `SendRequested`,携带去掉首尾空白的文本。 + + + A simulated chat to play while `Stage` is unset (demos and prototypes). The card plays it from the top each time it is shown, with the composer and the Talk to a person, Cancel and Try again actions; null shows the card's own state. + 未设置 `Stage` 时要播放的模拟对话(演示与原型)。卡片每次显示时从头播放它,输入框以及 Talk to a person、Cancel 和 Try again 操作都可使用;为 null 时显示卡片自己的状态。 + + + Where the conversation is. Leave it unset (null) to play the `Simulation`, if any. + 对话进行到哪一步。不设置(null)时播放 `Simulation`(如果有)。 + + + A muted line under the title. + 标题下方一行弱化的文字。 + + + What the assistant passes to the person; shown in the handoff card from transferring on, collapsible. + 助手交给真人的内容;从转接中起显示在转接卡片里,可以收起。 + + + The header title, such as the order or ticket number; null shows "Support chat". + 标题栏标题,例如订单号或工单号;为 null 时显示 “Support chat”。 + + + Who is typing: the assistant shows a short shimmering Thinking line, a person shows typing dots. + 谁在输入:助手显示一行简短的闪光 Thinking,真人显示正在输入的圆点。 + + + Every word `AgentHandoff` shows or says, in English by default. Replace the whole object to localize or reword; texts that name the person are format functions of their first name. + `AgentHandoff` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;提到那个人的文字是以其名字为参数的格式化函数。 + + + One message of an `AgentHandoff` conversation. Change `Text` in place while it streams (with `IsStreaming` set); the conversation fades each appended chunk in. + `AgentHandoff` 对话中的一条消息。流式输出时原地修改 `Text`(并设置 `IsStreaming`);对话会让每次追加的片段淡入。 + + + Who wrote the message. + 消息由谁写的。 + + + Whether the text is still streaming in; each appended chunk fades in while it is. + 文字是否仍在流式输出;在此期间每次追加的片段都会淡入。 + + + The message text; it wraps. + 消息文字;会换行。 + + + An optional time shown under the bubble, such as "14:05". + 显示在气泡下方的可选时间,例如 “14:05”。 + + + The words of the support chat an `AgentHandoffSimulation` plays, in the order they come: the customer opens, the assistant answers, the customer asks for a person, the assistant hands off with a `Summary`, the customer adds a note, the person (`Agent`) joins, greets, resolves and closes. The replies at the end answer the customer's own actions (Cancel, a typed message). A line left null is skipped. Share one script between simulations; it is only read. + `AgentHandoffSimulation` 播放的客服对话台词,按出现顺序排列:客户开口,助手回答,客户要求找真人,助手带着 `Summary` 转接,客户补充一条备注,真人(`Agent`)加入、问候、解决问题并结束。最后几条回复用来回应客户自己的操作(取消、输入的消息)。为 null 的台词会被跳过。多个模拟可以共用同一份剧本;它只会被读取。 + + + A simulated support chat for `AgentHandoff` (Arc's `useHandoffSimulation`), for demos, prototypes and tests: it plays the `Script` you pass and exposes the stage, the messages, who is typing, the summary, the person and a notice, and takes the same actions as the view (talk to a person, cancel, try again, send). Set it as the card's `Simulation`: the card plays it while its own `Stage` is unset, from the top each time it is shown. The customer opens, the assistant streams its answers in chunks of 3 to 8 words about every 34ms, the customer asks for a person and the assistant hands off with a summary. In `Handoff` a person joins, resolves the issue and the chat, and the story starts over a few seconds later; in `Unavailable` nobody picks the transfer up and it waits for `Retry`. Reduced motion: replies appear whole and the story plays once. The card's clock runs at every motion level but only while the card is on screen, so the story pauses off screen. Use one simulation per card. + 为 `AgentHandoff` 提供的模拟客服对话(Arc 的 `useHandoffSimulation`),用于演示、原型和测试:它播放你传入的 `Script`,公开阶段、消息、谁在输入、摘要、接手的人和通知,并接受与视图相同的操作(找真人、取消、重试、发送)。把它设为卡片的 `Simulation`:卡片在自己的 `Stage` 未设置时播放它,每次显示时从头开始。客户先开口,助手每隔约 34ms 以 3 到 8 个词的片段流式输出回答,客户要求找真人,助手带着摘要转接。在 `Handoff` 中,真人加入、解决问题并结束对话,几秒后故事重新开始;在 `Unavailable` 中,无人接手转接,故事等待 `Retry`。减弱动效时,回复整段出现,故事只播放一次。卡片的时钟在所有动效等级下运行,但只在卡片处于屏幕上时运行,所以离开屏幕时故事暂停。每张卡片使用一个模拟。 + + + Raised when the stage, the typing state, the summary, the person or the notice changes. Messages are observable on their own. + 阶段、输入状态、摘要、接手的人或通知变化时触发。消息本身可以单独观察。 + + + Which story to play: a handoff a person picks up, or one nobody picks up. A change starts the story over. + 要播放的故事:有人接手的转接,或无人接手的转接。修改会让故事从头开始。 + + + The words of the chat; null plays an empty chat that still moves through the stages. + 对话的台词;为 null 时播放一段没有消息、但仍会依次经过各阶段的对话。 + An animated explainer of the agent loop: a bright head with a gradient trail circles a ring past three stations (Gather context, Take action, Verify) around the model. At Gather a context block joins the stack beside the ring and the token total counts up; at Take action one to three tool calls drop out of the ring on a pin and resolve; at Verify a check passes or sends the loop round again. The run ends done (the ring draws in, a check at the center, the summary as the caption) or stopped when the last check fails. Play, pause, step, restart and a 0.5× / 1× / 2× speed control drive it; under reduced motion it opens on its end. The run is sample data: nothing calls a real service. 智能体循环的动画讲解:一个明亮的光点带着渐变拖尾绕环而行,依次经过三个站点(收集上下文、执行操作、验证)围绕模型。在收集上下文时,一个上下文块加入环旁的堆叠,token 总数逐步增加;在执行操作时,一到三个工具调用通过图钉从环中落下并得出结果;在验证时,检查通过,或让循环再次运行。运行结束时显示完成(环线绘制收拢,中心出现对勾,摘要作为说明)或在最后一次检查失败时停止。播放、暂停、单步、重新开始以及 0.5×/1×/2× 速度控件驱动演示;在减少动效下直接打开结束状态。此运行使用示例数据,不调用任何真实服务。 @@ -649,12 +909,12 @@ `PauseChanged` 的参数:暂停(true)或恢复(false)。 - A step with its simulation script: what the sample draft, the suggestions and the inserted step are made of. - 带有模拟脚本的步骤:示例草稿、建议和插入步骤的内容均由它构成。 + A step with its simulation script: what a simulation's draft, its suggestions and the steps it inserts are made of. + 带有模拟脚本的步骤:模拟的草稿、它的建议以及它插入的步骤均由它构成。 - The built-in simulated agent of an `AgentPlan` that has no `Status`: it answers the plan's buttons (Approve plan, Keep planning, Pause, Resume, Retry, Skip, Start over) and plays a scripted run, one step after the other, with the beats measured on the reference. Scripts are found by step id; a step without one runs `GenericScript`. It is event-driven: exactly one timer waits per state change, nothing runs while the plan is idle, and Pause keeps the remaining time of that timer so Resume continues where the run stopped. - `AgentPlan` 的内置模拟智能体,适用于没有设置 `Status` 的情况:它响应计划的按钮(批准计划、继续规划、暂停、继续、重试、跳过、重新开始),并按照参考实现测得的节拍,逐步播放预设的运行过程。脚本通过步骤 id 查找;没有脚本的步骤运行 `GenericScript`。它由事件驱动:每次状态变化只等待一个计时器,计划空闲时不运行任何内容;暂停(Pause)会保留该计时器剩余的时间,因此继续(Resume)会从停止处接着运行。 + A simulated agent for an `AgentPlan` that has no `Status`: give it to `Simulation` and it answers the plan's buttons (Approve plan, Keep planning, Pause, Resume, Retry, Skip, Start over) and plays a scripted run, one step after the other, with the beats measured on the reference. Your app writes the content: the draft (`Steps`), what Keep planning suggests (`Suggestions`), the goal, the model and the summary; nothing ships with the package. Scripts are found by step id; a step without one runs `GenericScript`. It is event-driven: exactly one timer waits per state change, nothing runs while the plan is idle, and Pause keeps the remaining time of that timer so Resume continues where the run stopped. The data is read when the plan attaches the simulation; create one per plan, since it keeps the run's state. + 用于未设置 `Status` 的 `AgentPlan` 的模拟智能体:把它交给 `Simulation`,它会响应计划的按钮(批准计划、继续规划、暂停、继续、重试、跳过、重新开始),并按照参考实现测得的节拍,逐步播放预设的运行过程。内容由你的应用编写:草稿(`Steps`)、继续规划给出的建议(`Suggestions`)、目标、模型和摘要;包里不附带任何内容。脚本通过步骤 id 查找;没有脚本的步骤运行 `GenericScript`。它由事件驱动:每次状态变化只等待一个计时器,计划空闲时不运行任何内容;暂停(Pause)会保留该计时器剩余的时间,因此继续(Resume)会从停止处接着运行。计划挂接模拟时读取这些数据;每个计划各建一个,因为它保存运行状态。 One step of an `AgentPlan`: an id, an instruction-style title, and while the plan runs its state, the activity lines the agent reports, an optional note (a suggested or agent-inserted step), the reason of a failure and the time a finished step took. Every property is observed, so changing a step updates the plan on screen. @@ -689,12 +949,12 @@ `StepAction` 的参数:对失败的步骤执行重试(Retry)或跳过(Skip)。 - How the built-in simulation runs one step: the activity lines it reports, the time it took, and optionally a failure (after the last line) with the lines and time of the retry, and a step the agent inserts after one of the lines. - 内置模拟如何运行一个步骤:它报告的活动行和耗时;可选地在最后一行之后出现失败,以及重试所用的行和耗时,还有智能体在某一行之后插入的步骤。 + How an `AgentPlanSimulation` runs one step: the activity lines it reports, the time it took, and optionally a failure (after the last line) with the lines and time of the retry, and a step the agent inserts after one of the lines. Your app writes it; nothing ships with the package. + `AgentPlanSimulation` 如何运行一个步骤:它报告的活动行和耗时;可选地在最后一行之后出现失败,以及重试所用的行和耗时,还有智能体在某一行之后插入的步骤。由你的应用编写,包里不附带任何脚本。 - A scripted agent run for `AgentRunSimulation`: the agent, its steps with their timing, the outcome and the notices the simulation announces for each action. `AgentStatusSamples` has two invented runs. Data only. - 用于 `AgentRunSimulation` 的脚本化智能体运行:智能体、其带时序的步骤、结果,以及模拟为每个操作宣布的通知。`AgentStatusSamples` 提供两个虚构的运行。仅为数据。 + A scripted agent run for `AgentRunSimulation`: the agent, its steps with their timing, the outcome and the notices the simulation announces for each action. Your app writes the run; nothing ships with the package. Data only. + 用于 `AgentRunSimulation` 的脚本化智能体运行:智能体、其带时序的步骤、结果,以及模拟为每个操作宣布的通知。运行由你的应用编写,包里不附带任何运行。仅为数据。 One step of an `AgentRunScenario`: its present- and past-tense labels, icon and active duration, an optional count that ticks in place, an optional approval gate before it runs and an optional failure on its first attempt. @@ -709,8 +969,8 @@ 在状态每次变化之后引发(一个节拍、一个操作或一次重新开始)。 - A compact live status pill for a running agent (Arc Agent status, design reference): the agent's mark inside a thin live ring, the current activity with a matching icon, and an elapsed timer in tabular figures. A new activity rolls in from below while the pill's width follows on a spring, and the label fades at its right edge while it is wider than its room. Pressing the pill opens it in place into a small floating panel with the latest steps (icon, text, duration), a segmented step progress bar and Pause, Stop and Open run; Escape or a press outside closes it and returns focus to the pill. It covers idle (Ready), working, needs input (an accent dot and inline Approve and Deny), paused, done (the ring closes and the mark becomes a drawn check, then it settles back to Ready), failed (with Retry) and stopped. Drive it from your agent's events: set `Status`, keep `Activity` to the latest tool call in the present tense, add `Steps`, give a fixed `StartedAt` (the timer leaves out paused and failed time itself) and handle the action events; `Actions` chooses which buttons are offered, the counterpart of showing a control only when its handler exists. With nothing set, or with a `Simulation`, it plays an invented sample run on its own. - 运行中智能体的紧凑实时状态胶囊(Arc 智能体状态,设计参考):智能体标记位于细的实时环内,显示当前活动及与之匹配的图标,并以表格数字显示已用时间。新活动从下方滚入,胶囊宽度以弹簧方式跟随变化,标签宽于可用空间时在右侧边缘淡出。按下胶囊会就地展开为一个小型浮动面板,列出最新步骤(图标、文字、耗时)、分段式步骤进度条,以及暂停(Pause)、停止(Stop)和打开运行(Open run)按钮;按下 Escape 或在外部按下即可关闭面板,并将焦点返回胶囊。它涵盖空闲(就绪,Ready)、工作中、需要输入(强调色圆点,并内联批准(Approve)和拒绝(Deny))、已暂停、已完成(环闭合,标记变为绘制的对勾,随后回落到就绪)、失败(带重试(Retry))和已停止。请通过智能体的事件驱动它:设置 `Status`,将 `Activity` 保持为最新的工具调用(现在时态),添加 `Steps`,给出固定的 `StartedAt`(计时器自行排除暂停和失败的时间),并处理操作事件;`Actions` 选择提供哪些按钮,这相当于仅在存在处理程序时才显示控件。若两者均未设置,或设置了 `Simulation`,则它会自行播放一段虚构的示例运行。 + A compact live status pill for a running agent (Arc Agent status, design reference): the agent's mark inside a thin live ring, the current activity with a matching icon, and an elapsed timer in tabular figures. A new activity rolls in from below while the pill's width follows on a spring, and the label fades at its right edge while it is wider than its room. Pressing the pill opens it in place into a small floating panel with the latest steps (icon, text, duration), a segmented step progress bar and Pause, Stop and Open run; Escape or a press outside closes it and returns focus to the pill. It covers idle (Ready), working, needs input (an accent dot and inline Approve and Deny), paused, done (the ring closes and the mark becomes a drawn check, then it settles back to Ready), failed (with Retry) and stopped. Drive it from your agent's events: set `Status`, keep `Activity` to the latest tool call in the present tense, add `Steps`, give a fixed `StartedAt` (the timer leaves out paused and failed time itself) and handle the action events; `Actions` chooses which buttons are offered, the counterpart of showing a control only when its handler exists. Without run data of its own it plays a `Simulation` when one is set (your scripted run), and otherwise rests at Ready. + 运行中智能体的紧凑实时状态胶囊(Arc 智能体状态,设计参考):智能体标记位于细的实时环内,显示当前活动及与之匹配的图标,并以表格数字显示已用时间。新活动从下方滚入,胶囊宽度以弹簧方式跟随变化,标签宽于可用空间时在右侧边缘淡出。按下胶囊会就地展开为一个小型浮动面板,列出最新步骤(图标、文字、耗时)、分段式步骤进度条,以及暂停(Pause)、停止(Stop)和打开运行(Open run)按钮;按下 Escape 或在外部按下即可关闭面板,并将焦点返回胶囊。它涵盖空闲(就绪,Ready)、工作中、需要输入(强调色圆点,并内联批准(Approve)和拒绝(Deny))、已暂停、已完成(环闭合,标记变为绘制的对勾,随后回落到就绪)、失败(带重试(Retry))和已停止。请通过智能体的事件驱动它:设置 `Status`,将 `Activity` 保持为最新的工具调用(现在时态),添加 `Steps`,给出固定的 `StartedAt`(计时器自行排除暂停和失败的时间),并处理操作事件;`Actions` 选择提供哪些按钮,这相当于仅在存在处理程序时才显示控件。没有自己的运行数据时,若设置了 `Simulation`(你编写的脚本化运行)便播放它,否则停在就绪状态。 Which action buttons may show (all by default); a button also needs the right state. @@ -721,8 +981,8 @@ 智能体当前正在做什么,使用现在时态。需要输入时为请求内容("Needs your approval to run a command");完成时为结果,接在 "Done in …" 之后;失败后为失败原因。 - The agent and its mark (drawn at 12px inside the ring); null is a plain "Agent" with a default mark. - 智能体及其标记(在环内以 12px 绘制);为 null 时显示普通的 "Agent",并使用默认标记。 + The agent and its mark (drawn at 12px inside the ring); null takes the simulated run's agent, or a plain "Agent" with a default mark. + 智能体及其标记(在环内以 12px 绘制);为 null 时使用模拟运行的智能体,否则显示普通的 "Agent",并使用默认标记。 Raised when Approve is pressed. @@ -785,16 +1045,16 @@ 运行完成或停止后回到就绪状态时触发。 - A simulated run that drives the pill (its status, activity, steps and times; its buttons are routed back to it). With no data set at all the pill plays the first sample run on its own. - 模拟运行,用来驱动状态胶囊(其状态、活动、步骤和时间;其按钮会转回给它)。若完全未设置数据,胶囊会自行播放第一个示例运行。 + A simulated run that drives the pill while the app sets no `Status`, `Activity`, `StartedAt` or steps: the pill starts it once attached, shows its status, activity, steps and times, and routes its buttons back to it. Null (the default) leaves a pill without data at Ready. Give each pill its own. + 模拟运行,在应用未设置 `Status`、`Activity`、`StartedAt` 或步骤时驱动胶囊:胶囊挂载后启动它,显示其状态、活动、步骤和时间,并将按钮转回给它。为 null(默认)时,没有数据的胶囊停在就绪状态。每个胶囊各用一个。 When the run started: the timer counts from here and leaves out time spent paused or failed. 运行开始的时间:计时器从此处开始计时,并且不计入暂停或失败期间所花的时间。 - Where the run is. Ignored while a `Simulation` drives the pill. - 运行所处的状态。在 `Simulation` 驱动胶囊时会被忽略。 + Where the run is. Setting it (or `Activity`, `StartedAt` or a step) shows your data instead of a `Simulation`. + 运行所处的状态。设置它(或 `Activity`、`StartedAt`、某个步骤)后,胶囊显示你的数据,而不是 `Simulation`。 The steps list: every step of the run, pending ones included, so the progress bar knows the total. @@ -824,10 +1084,6 @@ Every word an `AgentStatus` shows or announces, in English by default. Replace the instance (or set its properties) to translate or reword; texts with numbers or names are functions so that the word order can change. The durations they receive are already formatted by `FormatTimer`, `FormatTotal` and the other format functions. `AgentStatus` 显示或播报的每一个文字,默认为英文。替换该实例(或设置其属性)即可翻译或改写;含数字或名称的文字为函数,以便调整词序。它们接收的时长已由 `FormatTimer`、`FormatTotal` 等格式化函数处理完毕。 - - Two invented runs for `AgentRunSimulation` (the agents, file names and texts are made up). They keep the reference's rhythm: the coding run takes six steps of 1.9, 1.5, 2.5, 1.9 and 3.3 seconds (the fifth counting 12 specs in place) and then waits for an approval for as long as it takes; the research run takes five steps, and its third fails once after five of nine pages so Retry can be seen. - 为 `AgentRunSimulation` 准备的两个虚构运行(智能体、文件名和文字均为虚构)。它们保留了参考设计的节奏:编码运行共六个步骤,耗时分别为 1.9、1.5、2.5、1.9 和 3.3 秒(第五步原地计数 12 个规格),之后等待批准,等待多久都可以;研究运行共五个步骤,其中第三步在处理完九页中的第五页后失败一次,以便看到重试。 - One step of an agent's run in an `AgentStatus`. List every step, pending ones included, so the progress bar knows the total; the panel lists the latest finished and running ones. Write a running step's label in the present tense and a finished one's in the past tense ("Running tests", then "Passed 12 of 12 tests"), so the words carry the state. Changes reach the screen at once (each property is observed). `AgentStatus` 中智能体运行的一个步骤。应列出每一个步骤(含尚未开始的步骤),以便进度条知道总数;面板会列出最近完成的和正在进行的步骤。正在进行步骤的标签使用现在时,已完成步骤的标签使用过去时("Running tests",随后为 "Passed 12 of 12 tests"),让文字本身表达状态。变更会立即反映到界面上(每个属性都被监听)。 @@ -1492,6 +1748,258 @@ The name without the slash; the person filters by its start, ignoring case. 不含斜杠的名称;人员按开头字符筛选,忽略大小写。 + + A Cmd+K command palette with an AI mode (Arc AI command, design reference). Search pages and actions, or press Tab to ask a question: the answer streams inside the palette with numbered sources, and the next steps it suggests run straight from the list. The palette never changes size: the results area keeps `ResultsHeight` while typing, switching modes and streaming, and its views crossfade in place. Give it pages and actions in `Items` (or `ItemsSource`) and answer questions with `Responder`: stream text with `Append`, land the sources early with `SetSources`, and resolve with the suggested actions. With no data it is an empty palette; a `Simulation` you pass in fills it with a workspace and a simulated model, and plays a looping walkthrough while it is on screen until someone uses it. Focus stays in the input: Up and Down move the highlight, Enter opens, asks, runs or retries, Tab asks the model about the query (or opens Ask AI with suggested questions, and goes back from there), and Escape goes back to search, clears the query, then raises `CloseRequested`. Put it in an `AiCommandPaletteDialog` to open it over the page with Cmd+K or Ctrl+K. + 带 AI 模式的 Cmd+K 命令面板(Arc AI command,设计参考)。搜索页面与操作,或按 Tab 提问:回答在面板内流式呈现并附带编号来源,模型建议的后续步骤可直接在列表中执行。面板的尺寸从不改变:在输入、切换模式和流式输出时,结果区域始终保持 `ResultsHeight`,各视图就地交叉淡化。通过 `Items`(或 `ItemsSource`)提供页面与操作,并用 `Responder` 回答问题:用 `Append` 流式输出文字,用 `SetSources` 提前放出来源,最后返回建议的操作。没有数据时它是一个空面板;你传入的 `Simulation` 会为它补上工作区和模拟模型,并在可见时循环播放演示,直到有人开始使用。焦点始终留在输入框中:Up 和 Down 移动高亮,Enter 打开、提问、执行或重试,Tab 就查询询问模型(查询为空时打开带建议问题的 Ask AI,并可从那里返回),Escape 返回搜索、清空查询,然后引发 `CloseRequested`。把它放进 `AiCommandPaletteDialog`,即可用 Cmd+K 或 Ctrl+K 在页面上方打开。 + + + Raised after a suggested action finishes, with the label it ended on ("Created ENG-2481"). Bubbles. + 建议的操作完成后引发,附带它最终显示的标签("Created ENG-2481")。冒泡。 + + + Plays a looping walkthrough while on screen. It stops for good on the first real pointer or key press inside, pauses off screen and starts over from a clean state, and never runs with reduced motion. Null plays it only when the palette has a `Simulation` and neither items nor a responder of its own. + 可见时循环播放演示。在面板内第一次真实的指针或按键操作后永久停止,不可见时暂停并从干净状态重新开始,减弱动效时从不播放。为 null 时,仅在面板有 `Simulation` 且自己既没有条目也没有回答器时播放。 + + + Raised by Escape in an empty search: close the surrounding dialog here. Bubbles. + 在空搜索中按 Escape 时引发:在这里关闭外层对话框。冒泡。 + + + Adds the floating shadow, for use inside a dialog or a popover. + 添加浮层阴影,用于对话框或弹出框中。 + + + Listens for Cmd+K or Ctrl+K in the window and focuses the input. + 在窗口中监听 Cmd+K 或 Ctrl+K,并聚焦输入框。 + + + Raised when a page or action is chosen with Enter or a click. Close your dialog or navigate here. Bubbles. + 用 Enter 或点击选择页面或操作时引发。在这里关闭对话框或进行导航。冒泡。 + + + The pages and actions to search; when set it takes the place of `Items`. + 要搜索的页面与操作;设置后取代 `Items`。 + + + Every word the palette shows or says. + 面板显示或朗读的所有文字。 + + + Search or Ask AI (two-way). Switching back to search cancels a streaming answer. + 搜索或 Ask AI(双向)。切回搜索会取消正在流式输出的回答。 + + + Raised when the palette switches between Search and Ask AI. Bubbles. + 面板在搜索与 Ask AI 之间切换时引发。冒泡。 + + + The model's mark (a 24 × 24 stroked glyph) in the footer, the input and beside each answer; the sparkle by default. + 模型的标记(24 × 24 描边图形),显示在页脚、输入框和每条回答旁边;默认为闪光图标。 + + + The model that answers, shown in the footer and beside each answer; null (the default) shows the mark alone. + 回答问题的模型,显示在页脚和每条回答旁边;为 null(默认)时只显示标识。 + + + The text in the input (two-way): the search query, or the question being typed in Ask AI. + 输入框中的文字(双向):搜索查询,或在 Ask AI 中正在输入的问题。 + + + Answers a question: stream text with `Append`, show sources early with `SetSources`, and resolve with the actions (and the text, if it was not streamed). Throw to show the failed state with a retry row. Null uses the `Simulation`'s model; with neither, a question gets no answer. + 回答问题:用 `Append` 流式输出文字,用 `SetSources` 提前显示来源,最后返回操作(如果没有流式输出,还要返回文字)。抛出异常会显示带重试行的失败状态。为 null 时使用 `Simulation` 的模型;两者都没有时,提问不会得到回答。 + + + The height of the results area in px; the palette keeps this size while typing, switching modes and streaming. + 结果区域的高度(px);在输入、切换模式和流式输出时,面板都保持这个尺寸。 + + + A simulated workspace (demos and prototypes): its items, questions and model fill in what the palette lacks, and with no items and no responder of its own the palette plays its walkthrough. Null leaves the palette to its own data. + 模拟工作区(演示与原型):它的条目、问题和模型会补上面板缺少的部分;面板自己既没有条目也没有回答器时,会播放它的演示。为 null 时面板只使用自己的数据。 + + + Raised when a source chip or a citation is chosen. Bubbles. Unless handled, a source with an address opens it with the system launcher. + 选择来源标签或引用时引发。冒泡。除非已处理,带地址的来源会用系统启动器打开。 + + + The questions offered while Ask AI is open and empty; null offers the `Simulation`'s, if any. + Ask AI 打开且为空时提供的问题;为 null 时提供 `Simulation` 的问题(如果有)。 + + + A suggested next step returned with an answer and run in place: a row with a mark, a label and quiet meta text that turns into a spinner while `Run` works, then a check and the done label (or an alert, and Enter retries). + 随回答一起返回并就地执行的建议下一步:一行带有标记、标签和低调附注文字,在 `Run` 运行时变为加载圈,然后显示对勾和完成标签(或显示警示图标,按 Enter 重试)。 + + + The label once done when `Run` returns nothing; null uses `Done`. + `Run` 未返回任何内容时完成后的标签;为 null 时使用 `Done`。 + + + The meta text once done; a simulated action says here that nothing was sent. + 完成后的附注文字;模拟操作在这里说明没有发送任何内容。 + + + The label after a failure; null uses `ActionFailed`. + 失败后的标签;为 null 时使用 `ActionFailed`。 + + + A stable id. + 稳定的 ID。 + + + The row label ("Create issue in Linear"). + 行标签("Create issue in Linear")。 + + + The 16px mark of the tool it runs in, a 24 × 24 stroked glyph; null shows an arrow. + 执行该操作的工具的 16px 标记,24 × 24 描边图形;为 null 时显示箭头。 + + + Quiet detail after the label ("ENG · Billing"). + 标签后面的低调细节("ENG · Billing")。 + + + Does the work. Resolve with a done label ("Created ENG-2481") or null; throw to show the failed state (Enter retries). The token is cancelled when the person asks again, goes back to search or the palette leaves the window. + 执行工作。返回完成标签("Created ENG-2481")或 null;抛出异常会显示失败状态(Enter 重试)。当用户再次提问、返回搜索或面板离开窗口时,令牌会被取消。 + + + The label while it runs; null uses `Running`. + 执行期间的标签;为 null 时使用 `Running`。 + + + What a `Responder` resolves with: the whole text (when it did not stream it with `Append`), the sources (when it did not set them early) and the suggested actions. + `Responder` 返回的内容:完整文字(未用 `Append` 流式输出时)、来源(未提前设置时)以及建议的操作。 + + + The question a `Responder` answers, with a token that is cancelled when the person asks again or goes back, and the two ways to fill the answer while it works: `Append` streams text, `SetSources` lands the source chips before the text. + `Responder` 要回答的问题,附带一个在用户再次提问或返回时取消的令牌,以及工作期间填充回答的两种方式:`Append` 流式输出文字,`SetSources` 让来源标签先于文字出现。 + + + An `AiCommandPalette` as a dialog over the page: declare it once and Cmd+K (Ctrl+K elsewhere) opens it near the top of the window over a dimmed scrim, or bind `IsOpen`. It renders nothing in place. The palette gets the floating shadow and the focus when it opens. Escape in an empty search, a press on the scrim and choosing a page or action (`CloseOnSelect`) close it, and the focus returns to what had it before. Without a palette of its own it shows the sample one. + 以对话框形式显示在页面上方的 `AiCommandPalette`:声明一次后,Cmd+K(其他平台为 Ctrl+K)会在窗口顶部附近、变暗的遮罩上方打开它,也可以绑定 `IsOpen`。它在原位不渲染任何内容。面板打开时获得浮层阴影和焦点。在空搜索中按 Escape、点击遮罩以及选择页面或操作(`CloseOnSelect`)都会关闭它,焦点会回到之前拥有焦点的元素。没有自己的面板时显示示例面板。 + + + Choosing a page or action closes the dialog (default true). + 选择页面或操作会关闭对话框(默认为 true)。 + + + Shown while true; set false (or press Escape in an empty search) to close it. Two-way by default. + 为 true 时显示;设为 false(或在空搜索中按 Escape)即可关闭。默认双向。 + + + Turns the window-level open gesture on (default) or off. + 开启(默认)或关闭窗口级的打开手势。 + + + The key combination that toggles the dialog from anywhere in the window; null is Cmd+K or Ctrl+K. + 在窗口任意位置切换对话框的组合键;为 null 时为 Cmd+K 或 Ctrl+K。 + + + Set on a button: clicking it opens that dialog. + 设置在按钮上:点击按钮会打开该对话框。 + + + The palette to show; null shows a sample palette. + 要显示的面板;为 null 时显示示例面板。 + + + A page or an action an `AiCommandPalette` searches: a label, an optional group, a 16px icon, quiet meta text, a shortcut and extra keywords. Rows marked `IsSuggested` show while the query is empty. + `AiCommandPalette` 搜索的页面或操作:一个标签、可选的分组、16px 图标、低调的附注文字、快捷键和额外的关键词。标记为 `IsSuggested` 的行在查询为空时显示。 + + + Runs when the row is chosen, with `CommandParameter` (or the item when that is null). + 选择该行时执行,参数为 `CommandParameter`(为 null 时为该条目本身)。 + + + The parameter passed to `Command`. + 传给 `Command` 的参数。 + + + The section the row is listed under ("Pages", "Actions"); rows without one share an untitled section. + 该行所在的分区("Pages"、"Actions");没有分组的行共用一个无标题分区。 + + + The 16px icon, a 24 × 24 stroked glyph (`ArcIcons`); null shows a neutral dot. + 16px 图标,24 × 24 描边图形(`ArcIcons`);为 null 时显示一个中性圆点。 + + + A stable id (a route, a command name). + 稳定的 ID(路由、命令名称)。 + + + Lists the row while the query is empty. + 查询为空时列出该行。 + + + More words that find the row, separated by spaces or commas ("invoice payment card"). + 能找到该行的更多词语,以空格或逗号分隔("invoice payment card")。 + + + The row label, kept short ("Billing"). + 行标签,保持简短("Billing")。 + + + Quiet detail after the label ("Settings", "12 unread"); it truncates before the label does. + 标签后面的低调细节("Settings"、"12 unread");它会先于标签被截断。 + + + A shortcut shown as key caps at the end of the row, keys separated by spaces ("G I"). + 在行末以键帽显示的快捷键,按键以空格分隔("G I")。 + + + Every word `AiCommandPalette` shows or says, in English by default. Replace the whole object to localize or reword; the texts that carry a value are format functions. + `AiCommandPalette` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;带有数值的文字是格式化函数。 + + + A simulated workspace for an `AiCommandPalette`, for demos, prototypes and tests: the pages and actions to search (`Items`), the questions offered in Ask AI (`Questions`), and a simulated model that answers from `Answers`. It "searches" for 700ms, lands the sources, waits 250ms and streams the text in small uneven pieces every 45ms, then offers the actions, which pretend to run and say "Simulated, nothing was sent". Nothing talks to any service. Set it as the palette's `Simulation`. Each part fills in only what the palette lacks: `Items` while the palette has no items, `Questions` while it has no suggested questions, and the answers while it has no responder. While the palette has neither items nor a responder of its own it also plays the walkthrough (unless `AutoPlay` says otherwise): it searches for `SearchQuery`, opens Ask AI, asks the first question, runs its first action and starts over. + 为 `AiCommandPalette` 提供的模拟工作区,用于演示、原型和测试:要搜索的页面和操作(`Items`)、Ask AI 中提供的问题(`Questions`),以及根据 `Answers` 作答的模拟模型。它“搜索” 700ms,放出来源,等待 250ms,再每 45ms 以大小不一的小段流式输出文字,然后提供操作;这些操作只是假装运行,并显示 “Simulated, nothing was sent”。不会连接任何服务。把它设为面板的 `Simulation`。每一部分只补上面板缺少的内容:面板没有条目时用 `Items`,没有建议问题时用 `Questions`,没有回答器时用其中的回答。面板自己既没有条目也没有回答器时,它还会播放演示(除非 `AutoPlay` 另有设置):搜索 `SearchQuery`,打开 Ask AI,提出第一个问题,运行其第一个操作,然后从头开始。 + + + The canned answers the simulated model picks from. + 模拟模型从中挑选的预设回答。 + + + The pages and actions to search. + 要搜索的页面和操作。 + + + The questions offered while Ask AI is open and empty; the walkthrough asks the first. + Ask AI 打开且为空时提供的问题;演示会提出第一个。 + + + What the walkthrough types into the search before it looks through the results; null skips the search. + 演示在浏览结果之前输入到搜索中的内容;为 null 时跳过搜索。 + + + An action an `AiCommandPaletteSimulationAnswer` offers: pressing it pretends to run for 1.1s, then reports `Result` with "Simulated, nothing was sent". + `AiCommandPaletteSimulationAnswer` 提供的操作:按下后假装运行 1.1 秒,然后报告 `Result`,并附上 "Simulated, nothing was sent"。 + + + A canned answer of an `AiCommandPaletteSimulation`: the question words it answers, its sources, its text with [n] citations and its actions. + `AiCommandPaletteSimulation` 的一条预设回答:它回答的问题词语、来源、带 [n] 引用的文字以及操作。 + + + A document an answer cites as [1], [2] in its text, shown as a numbered chip above the answer. + 回答在文字中以 [1]、[2] 引用的文档,以编号标签显示在回答上方。 + + + Longer text for the tooltip and the accessible name of the chip and its citations. + 用于工具提示以及标签和其引用的无障碍名称的较长文字。 + + + An address opened with the system launcher when the source is chosen (unless the event is handled). + 选择该来源时用系统启动器打开的地址(除非事件已被处理)。 + + + A stable id. + 稳定的 ID。 + + + The 14px mark in the chip, a 24 × 24 stroked glyph; null shows a document glyph. + 标签中的 14px 标记,24 × 24 描边图形;为 null 时显示文档图形。 + + + The short chip title ("billing-api #1893"). + 简短的标签标题("billing-api #1893")。 + The prompt box of an AI app: a text area that grows with its text, "/" commands and "@" mentions that become tokens, attachments that upload in place (picked, pasted or dropped on the box), tool toggles, a model and effort menu, a character counter and a send button that turns into a stop button while a reply is generated. The composer reports a send through `Sent` (or `SendCommand`); your code starts the reply and sets `IsGenerating` while it runs, and the send button becomes a stop button that raises `StopRequested`. While a reply is generated the box stays editable but Enter does not send. AI 应用的提示输入框:一个随文本增高的文本区域,"/" 命令和 "@" 提及会变为令牌,附件可在原处上传(选取、粘贴或拖放到输入框上),还有工具开关、模型和推理强度菜单、字符计数器,以及一个在生成回复时变为停止按钮的发送按钮。输入框通过 `Sent`(或 `SendCommand`)报告发送;你的代码负责开始回复,并在运行时设置 `IsGenerating`,此时发送按钮变为停止按钮,并引发 `StopRequested`。生成回复期间输入框仍可编辑,但按 Enter 不会发送。 @@ -1688,6 +2196,86 @@ The name the token shows; the person filters by it (and by `Hint`), ignoring case. 令牌上显示的名称;人员按它(以及 `Hint`)筛选,忽略大小写。 + + A small assistant character that shows what an AI is doing (Arc AI mascot, design reference): a soft pebble with pill eyes, a quiet form shade and a companion dot. Idle breathes, floats, blinks and glances around. Listening leans in with wider eyes while the dot pulses like a voice level. Thinking tilts the head and looks up as the dot orbits it, passing behind and in front. Writing looks down and reads along a line the dot writes, then the pen lifts and returns. Done squints happily as the dot turns into a sparkle. Error shakes once and shows worried brows. A status change blends from the pose on screen over about 420ms, and the eyes follow the pointer on hover. Set `Status` from your assistant's phases; leave it unset to play through every state on a loop (a showcase). The mascot is a fixed square of `Size` px, from 24 (inline) to 160 (empty state); below 56 a compact drawing takes over. It pauses by itself off screen, and reduced motion keeps a still pose for each state. + 一个表现 AI 正在做什么的小助手角色(Arc AI mascot,设计参考):一块柔和的鹅卵石,有胶囊形的眼睛、一道安静的形体阴影和一个伴随圆点。空闲时呼吸、浮动、眨眼并四处张望。倾听时向前凑近、眼睛睁大,圆点像音量一样跳动。思考时歪头向上看,圆点绕头旋转,从后方和前方经过。书写时向下看,跟着圆点写下的线读,然后抬笔返回。完成时开心地眯眼,圆点变成闪光。出错时摇晃一次并露出担忧的眉毛。状态切换在约 420ms 内从屏幕上的姿势混合过去,悬停时眼睛跟随指针。根据助手的阶段设置 `Status`;不设置时循环播放所有状态(用于展示)。吉祥物是边长为 `Size` px 的固定正方形,从 24(行内)到 160(空状态);小于 56 时改用紧凑画法。离开屏幕时自行暂停,减弱动效时每个状态保持静止姿势。 + + + The accessible label in use, for example "Assistant, thinking". + 当前使用的无障碍标签,例如 "Assistant, thinking"。 + + + Announces status changes in a polite live region. Turn it on for the one mascot that carries status, not for decorative copies. + 在礼貌型实时区域中朗读状态变化。只为承载状态的那一个吉祥物打开,不要为装饰性的副本打开。 + + + The assistant's name in the accessible label ("Assistant, thinking"); `DefaultName` when null. + 无障碍标签中的助手名字("Assistant, thinking");为 null 时使用 `DefaultName`。 + + + Any color for the body. Overrides `Tone`; the eyes become a dark mix of it unless `EyeColor` is set. + 身体的任意颜色。覆盖 `Tone`;除非设置了 `EyeColor`,眼睛会变成它的深色混合。 + + + The state shown now: `Status`, or the self-playing tour's state while it is null. + 当前显示的状态:`Status`,或在它为 null 时自动轮播所处的状态。 + + + Any color for the companion dot and the sparkle; the body color (the foreground for the soft tone) by default. + 伴随圆点和闪光的任意颜色;默认为身体颜色(`Soft` 色调为前景色)。 + + + Any color for the eyes, the squint and the brows. + 眼睛、眯眼和眉毛的任意颜色。 + + + When the eyes follow the pointer: over the mascot, anywhere in the window or never. Touch is ignored; thinking and writing keep their own gaze. + 眼睛何时跟随指针:在吉祥物上方时、在窗口内任意位置或从不。忽略触摸;思考和书写保持自己的视线。 + + + Freezes the mascot on the current frame. It also pauses by itself off screen and while hidden, and resumes without a jump. + 让吉祥物停在当前帧。它在离开屏幕或被隐藏时也会自行暂停,恢复时不会跳变。 + + + Overrides the whole accessible label. + 替换整个无障碍标签。 + + + Every word the mascot says. + 吉祥物使用的所有文字。 + + + Shows the companion dot that floats, pulses, orbits, writes and becomes the done sparkle. + 显示伴随圆点:它会漂浮、跳动、绕头旋转、书写,并变成完成时的闪光。 + + + Shows the soft contact shadow and the writing line under the mascot (drawn only at 56px and up). + 显示吉祥物下方柔和的接触阴影和书写线(只在 56px 及以上绘制)。 + + + Width and height in px, from 24 (inline next to a message) to 160 (empty state). Below 56 the compact drawing takes over. + 以 px 为单位的宽和高,从 24(消息旁的行内)到 160(空状态)。小于 56 时改用紧凑画法。 + + + What the assistant is doing. Leave it null to play through every state on a loop (a showcase). + 助手正在做什么。保持为 null 时循环播放所有状态(用于展示)。 + + + Raised when the self-playing tour (`Status` null) moves to the next state. Bubbles. Use it to sync a caption or a control. + 自动轮播(`Status` 为 null)进入下一个状态时引发。冒泡。可用于同步说明文字或控件。 + + + Where the body color comes from: the accent, the foreground ink or a muted surface with a border. + 身体颜色的来源:强调色、前景墨色,或带边框的柔和表面色。 + + + Every word an `AiMascot` says, in English by default. Replace the instance (or set its properties) to translate or reword; the accessible label is a function so that the word order can change. + `AiMascot` 使用的所有文字,默认为英文。替换实例(或设置其属性)即可翻译或改写;无障碍标签是一个函数,以便调整语序。 + + + Arguments of `StatusChanged`: the state the self-playing tour moved to and the one it left. + `StatusChanged` 的参数:自动轮播进入的状态和它离开的状态。 + A model a person can choose in the AI blocks: the model picker, the model pill and menu in a composer, a chat header or a prompt studio. Data only: an `AvaloniaObject` so the properties bind and a change (a new description, a price update) reaches what is on screen. AI 区块中人可以选择的模型:模型选择器、输入框、聊天标题或提示工作室中的模型胶囊与菜单。仅为数据:它是一个 `AvaloniaObject`,因此属性可以绑定,其变化(新的描述、价格更新)会反映到屏幕上。 @@ -2008,6 +2596,282 @@ Where the source can be opened; null makes the citation plain. 来源可以打开的位置;为 null 时引用仅为纯文本。 + + An animated spark mark that shows an AI at work (Arc AI spark): four tapered primary rays and four lighter diagonal rays around a soft core. Each status is its own motion: a slow breath at rest, a ripple while thinking, a heartbeat while writing, a gather when done and a small shake on an error, and every change blends from the pose the mark is already in, so states never hard cut. Set `Variant` to Pill for a status label or to Avatar for a round tile. Leave `Status` unset and the spark plays through idle, thinking, writing and done on its own (for demos). + 表示 AI 正在工作的动画火花标记(Arc AI spark):四条渐细的主光线和四条较浅的斜向光线围绕柔和的核心。每种状态都有自己的动效:静止时缓慢呼吸,思考时涟漪流转,书写时像心跳一样搏动,完成时向中心收拢,出错时轻微摇晃;每次切换都从标记当前所处的姿态过渡,状态之间从不生硬切换。把 `Variant` 设为 Pill 可加上状态标签,设为 Avatar 则放进圆形底块。不设置 `Status` 时,火花会自行依次播放空闲、思考、书写和完成(用于演示)。 + + + The accessible name; null (default) uses a phrase for the status ("AI is thinking"), or the label in the pill. + 可访问名称;为 null(默认)时使用状态短语("AI is thinking"),胶囊中则使用标签文字。 + + + Announces status changes as a polite live region. Turn it off for decorative copies so only one spark speaks; they then present as an image with an accessible name. + 以礼貌型实时区域播报状态变化。在装饰性副本上关闭它,只让一个火花发声;关闭后它们以带可访问名称的图像呈现。 + + + The status the spark shows: `Status`, or the current self-play step while it is unset. + 火花正在显示的状态:`Status`,未设置时为当前自行播放的那一步。 + + + Freezes the animation in place; resuming continues from the same phase. + 让动画原地定格;恢复后从同一相位继续。 + + + The pill's text, overriding the built-in label for the current status ("Reading 12 files"). + 胶囊的文字,覆盖当前状态的内置标签("Reading 12 files")。 + + + Every word the spark shows or announces. + 火花显示或播报的所有文字。 + + + A color for the mark that overrides `Tone` (a solid brush; a gradient uses its first stop). + 覆盖 `Tone` 的标记颜色(纯色画刷;渐变画刷取其第一个色标)。 + + + The size in px of the mark, of the avatar tile, or of the mark inside the pill; NaN (default) gives 24, 32 for the avatar and 16 in the pill. Set it to the surrounding font size to drop the mark into a line of text. Small marks get heavier rays and under 18px the diagonals become dots. + 标记、头像底块或胶囊内标记的尺寸(px);NaN(默认)时为 24,头像为 32,胶囊内为 16。设为周围文字的字号即可把标记嵌入一行文字。小标记的光线更粗,小于 18px 时斜线变成圆点。 + + + Multiplies every loop and spin speed (1 is the designed pace). + 让所有循环与旋转速度成倍变化(1 为设计速度)。 + + + What the AI is doing. Leave it unset (null) and the spark plays through idle, thinking, generating and done on its own, which is meant for demos and marketing pages. + AI 正在做什么。不设置(null)时,火花会自行依次播放空闲、思考、书写和完成,这只用于演示和营销页面。 + + + The semantic color of the mark; the diagonals and the core use a lighter tone of it. An error always eases to a muted danger tone. + 标记的语义颜色;斜线和核心使用其较浅色调。出错时总会过渡到柔和的危险色。 + + + A very slow breathing twinkle while idle. Turn it off for a fully still idle mark. + 空闲时非常缓慢的呼吸闪烁。关闭后空闲标记完全静止。 + + + The spark alone, with a status label in a capsule, or in a round tinted tile. + 单独的火花、带状态标签的胶囊,或放在圆形着色底块中。 + + + Every word an `AiSpark` shows or announces, in English by default: the pill's status labels and the spoken phrases the mark uses as its accessible name. Replace the instance (or set its properties) to translate or reword. + `AiSpark` 显示或播报的所有文字,默认为英文:胶囊的状态标签,以及标记用作可访问名称的播报短语。替换实例(或设置其属性)即可翻译或改写。 + + + An inline translator for one message or paragraph (Arc AI translate, design reference). A byline with the author and a detected-language chip sits above the text; below it a Translate picker, a Show original toggle and a status line led by the model's mark. Picking a language dims the text on screen and streams the translation in place in fast chunks, each new sentence retiring one sentence of the old text, while the paragraph's height springs to fit. Finished translations are cached per text, so Show original and switching back crossfade at once. A failure puts the previous text back and shows Couldn't translate with Retry. Wire `Translator` to your model: it gets an `AiTranslateRequest` and returns the translation as an async stream of text deltas (yield the whole text once to have the card stream it in chunks itself). Without one the card simulates the stream from `Translations`. Without `Text` it plays a `Simulation` (a message and its translations you pass, toured by itself); with neither it shows an empty card whose picker is off. + 针对一条消息或一段文字的内联翻译器(Arc AI translate,设计参考)。文字上方是带作者和检测到的语言标签的署名行;下方是 Translate 选择器、Show original 开关,以及以模型标志开头的状态行。选择一种语言后,屏幕上的文字变暗,译文以快速的小块在原处流式出现,每开始一个新句子就撤下一句旧文字,同时正文高度以弹簧动画适配。完成的译文按文字缓存,因此“显示原文”和切换回来时会立即交叉淡化。失败时恢复之前的文字,并显示 Couldn't translate 和 Retry。把 `Translator` 接到你的模型:它接收一个 `AiTranslateRequest`,并以文本增量的异步流返回译文(一次 yield 整段文字时,卡片会自行分块流式输出)。没有它时,卡片从 `Translations` 模拟流式输出。没有 `Text` 时它播放 `Simulation`(你传入的消息及其译文,自动轮播);两者都没有时显示一张空卡片,其中的选择器处于关闭状态。 + + + Who wrote the text, shown with an avatar. Null hides the byline (the language chip stays), or shows the `Simulation`'s author while one plays. + 文字的作者,带头像显示。为 null 时隐藏署名行(语言标签保留),播放 `Simulation` 时则显示其作者。 + + + Tours the languages (the `Simulation`'s `Tour`, or every other language in the picker), then the original, and loops. It pauses off screen and stops for good on the first pointer or key press inside the card; with reduced motion it translates once and stops. Null tours only while a `Simulation` plays. + 依次切换语言(`Simulation` 的 `Tour`,或选择器中的其他每种语言),再回到原文,并循环播放。离开屏幕时暂停,在卡片内第一次按下指针或按键后永久停止;减少动态效果时只翻译一次就停止。为 null 时只在播放 `Simulation` 时轮播。 + + + The language of the text: `SourceLanguage`, or the one detected from the text. + 文字的语言:`SourceLanguage`,或从文字中检测到的语言。 + + + Adds "Simulated, nothing was sent" to the announcements. Null: true without a `Translator` or while a `Simulation` plays. + 在播报中加上“Simulated, nothing was sent”。为 null 时:没有 `Translator` 或正在播放 `Simulation` 时为 true。 + + + Every word the card shows or says. + 卡片显示或朗读的每个词。 + + + The languages offered in the picker, in order; null offers all six. The source language is always listed, as Original. + 选择器中按顺序提供的语言;为 null 时提供全部六种。源语言总会列出,标为 Original。 + + + The model's mark (a 24 × 24 stroked glyph) that leads every status; a sparkle by default. Pass your provider's mark. + 每个状态开头的模型标志(24 × 24 描边图形);默认是火花图标。传入你的服务商的标志。 + + + The model credited in the status line: its name is the idle label. Null credits the `Simulation`'s model while one plays, or none. + 状态行中标注的模型:其名称是空闲时的标签。为 null 时,播放 `Simulation` 期间标注其模型,否则不标注。 + + + A message to play while the card has no `Text`: its text, byline, model and ready translations, streamed as a simulation (nothing is sent). Your own `Text` wins over it; null stops it and shows the empty card. + 卡片没有 `Text` 时要播放的消息:它的文字、署名、模型和现成译文,以模拟方式流式输出(不会发送任何内容)。你自己的 `Text` 优先于它;为 null 时停止播放并显示空卡片。 + + + The language of `Text`. Null detects it from the text with a light check and the chip reads Detected; set, the chip reads Written in. + `Text` 的语言。为 null 时通过简单检查从文字中检测,标签显示 Detected;设置后标签显示 Written in。 + + + What the card shows: the original, a translation streaming in, a translation, or a failure. + 卡片显示的内容:原文、正在流式输出的译文、译文,或失败。 + + + The text to translate, in its original language. Changing it clears the cached translations and shows the new original. Null plays `Simulation`, or shows the empty card without one. + 要翻译的文字,使用其原始语言。更改它会清除缓存的译文并显示新的原文。为 null 时播放 `Simulation`,没有模拟时显示空卡片。 + + + Shown after the role in the byline, for example "Oct 27, 09:14". + 显示在署名行中角色之后,例如 "Oct 27, 09:14"。 + + + Raised once a translation has fully arrived, before it is cached. Persist it here and pass it back in `Translations`. Bubbles. + 译文完整到达后、缓存之前引发。在此保存译文,并通过 `Translations` 传回。冒泡。 + + + Raised when a translation fails. Cancelled requests never raise it. Bubbles. + 翻译失败时引发。被取消的请求从不引发它。冒泡。 + + + Ready translations by language, streamed by the simulation (content you translated ahead of time). A missing language shows the failure. + 按语言提供的现成译文,由模拟流式输出(预先翻译好的内容)。缺少的语言会显示失败状态。 + + + Your translation call: returns the translation as an async stream of text deltas. Pass the request's cancellation token on to your network call; it is cancelled when the reader picks another language, shows the original or the card leaves the window. A thrown exception's message is announced and kept as the status tooltip. Null simulates the stream. + 你的翻译调用:以文本增量的异步流返回译文。把请求的取消令牌传给网络调用;读者选择其他语言、显示原文或卡片离开窗口时它会被取消。抛出的异常消息会被播报,并保留为状态行的提示。为 null 时模拟流式输出。 + + + The language on screen (two-way); null or the source language means the original. Set it to translate, for example to a reader's preferred language: a language other than the source translates as soon as the card is shown. + 屏幕上的语言(双向);null 或源语言表示原文。设置它即可翻译,例如设为读者的偏好语言:源语言以外的语言会在卡片一显示时就开始翻译。 + + + Raised when the reader picks a language or toggles Show original (not when the app sets `Value` or autoplay moves). Bubbles. + 读者选择语言或切换 Show original 时引发(应用设置 `Value` 或自动播放切换时不引发)。冒泡。 + + + Who wrote the text of an `AiTranslate` card, shown in its byline. + `AiTranslate` 卡片文字的作者,显示在其署名行中。 + + + Every word `AiTranslate` shows or says, in English by default. Replace the whole object to localize or reword; the phrases that name a language are format functions. + `AiTranslate` 显示或朗读的每个词,默认为英文。替换整个对象即可本地化或改写;包含语言名称的短语是格式化函数。 + + + One translation the card asks your `Translator` for. + 卡片向你的 `Translator` 请求的一次翻译。 + + + A message an `AiTranslate` plays without a translator, from data you give it: the text, its byline, the model credited and translations made ahead of time. Set it as `Simulation` while the card has no `Text`: picking a language streams the ready translation in place after a short wait (a language without one shows the failure), the status says it is simulated and nothing was sent, and unless `AutoPlay` is false the card tours `Tour` by itself until someone presses a pointer or a key inside it. + `AiTranslate` 在没有翻译函数时按你提供的数据播放的消息:文字、署名、标注的模型以及预先做好的译文。在卡片没有 `Text` 时把它设为 `Simulation`:选择一种语言后,卡片会在短暂等待后原地流式输出现成译文(没有译文的语言会显示失败状态),状态会说明这是模拟、没有发送任何内容,并且除非 `AutoPlay` 为 false,卡片会自动依次播放 `Tour`,直到有人在卡片内按下指针或按键。 + + + A compact automation builder card with an AI step (Arc AI workflow, design reference). Steps are 32px rows on one 1px hairline rail: a trigger, an AI step, a branch whose paths switch with small tabs, and actions. Test run glides a small accent dot down each connector while the line behind it fills; each step crossfades to Running with a shimmering activity line, then shows its duration and an inline output preview in small muted text (the email, the label with a streamed reason, the posted message, the created issue). The path the run does not take is marked Skipped. A failing step shows its error and Retry, which resumes the run from that step. Steps are added from a compact command menu (search, rows with app marks and number shortcuts) and removed with a small button. + 一张带 AI 步骤的紧凑自动化构建卡片(Arc AI workflow,设计参考)。步骤是排在同一条 1px 细线轨道上的 32px 行:触发器、AI 步骤、可用小标签页切换路径的分支,以及动作。Test run 让一个强调色小圆点沿每段连接线滑下,身后的线随之填满;每个步骤交叉淡化为 Running 并显示闪烁的活动行,然后以小号弱化文字显示用时和内嵌的输出预览(邮件、带流式理由的标签、发出的消息、创建的工单)。运行未走的路径标为 Skipped。失败的步骤显示错误和 Retry,Retry 会从该步骤继续运行。步骤从紧凑的命令菜单(搜索、带应用标记和数字快捷键的行)中添加,用一个小按钮删除。 + + + Plays a test run once the card is on screen and repeats it after a short pause, until someone presses or types inside the card. Nothing is scheduled while the card is off screen. Keep it off in real products. + 卡片出现在屏幕上后播放一次测试运行,短暂停顿后重复,直到有人在卡片内按下鼠标或按键。卡片不在屏幕上时不安排任何播放。正式产品中请关闭。 + + + The entries of the add step menu. Triggers are offered in the empty state, everything else in the menu. Null uses the simulation's catalog while its flow is shown, and no entries otherwise; an empty list hides the menu. + 添加步骤菜单的条目。触发器在空状态中提供,其余都在菜单中。为 null 时,显示模拟的流程期间使用模拟的目录,否则没有条目;空列表会隐藏菜单。 + + + One line of context under the name. Null shows the simulation's line while its flow is shown. + 名称下方的一行说明。为 null 时,显示模拟的流程期间显示模拟的说明。 + + + Shows the add step menu and the remove buttons. Triggers cannot be removed. + 显示添加步骤菜单和删除按钮。触发器不能删除。 + + + Shows three skeleton steps while your flow loads; Test run and the add menu are unavailable. + 在流程加载时显示三个骨架步骤;此时 Test run 和添加菜单不可用。 + + + True while a test run is in progress. + 测试运行进行中时为 true。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + Raised when a test run passes or stops on a failed step (not after Stop). Bubbles. + 测试运行通过或停在失败步骤上时触发(Stop 之后不触发)。冒泡。 + + + Runs one step for real and returns its output; throw to fail the step with the message. Null simulates each step from its `Output` and `Duration` (the card then says "Simulated"). + 真实运行一个步骤并返回其输出;抛出异常则以该消息让步骤失败。为 null 时,每一步都按其 `Output` 和 `Duration` 模拟(卡片会显示 "Simulated")。 + + + The flow the card shows and test-runs on its own while `Steps` is null: your name, line, steps and catalog, played exactly like a flow of your own (autoplay, pausing off screen, stopping for good after a press). Null shows the empty state instead. + `Steps` 为 null 时卡片自行显示并测试运行的流程:你的名称、说明、步骤和目录,播放方式与你自己的流程完全相同(自动播放、离开屏幕时暂停、按下后永久停止)。为 null 时改为显示空状态。 + + + The steps, in order (two-way). A branch carries its paths and must be the last top-level step. Null plays the `Simulation`'s flow, or shows the empty state without one; an empty list shows the empty state. Adding or removing a step puts a new list here. + 按顺序排列的步骤(双向)。分支带有自己的路径,且必须是最后一个顶层步骤。为 null 时播放 `Simulation` 的流程,没有模拟时显示空状态;空列表显示空状态。添加或删除步骤时会在这里放入一个新列表。 + + + Raised after a step is added or removed, with the new list (already in `Steps`). Bubbles. + 添加或删除步骤后触发,附带新列表(已写入 `Steps`)。冒泡。 + + + The workflow's name in the header. Null shows the simulation's name while its flow is shown. + 标题栏中的工作流名称。为 null 时,显示模拟的流程期间显示模拟的名称。 + + + The app a step of an `AiWorkflow` belongs to: a name read to screen readers and shown in the add step menu, and a mark (a 24 × 24 stroked glyph) drawn in a small hairline square on the rail. The built-in apps are generic (Mail, AI, Chat, Tracker, ...) with Lucide glyphs; pass your own name and mark for real products (for a model step, the model's vendor with `isAccent: true`). + `AiWorkflow` 中步骤所属的应用:一个供屏幕阅读器朗读、并显示在添加步骤菜单中的名称,以及一个画在轨道上小细线方框里的标记(24 × 24 描边图形)。内置应用都是通用的(Mail、AI、Chat、Tracker 等),使用 Lucide 图标;真实产品请传入你自己的名称和标记(模型步骤请使用模型厂商,并设置 `isAccent: true`)。 + + + One path of a branch step: a label for its tab and the steps that run when the branch picks it. + 分支步骤的一条路径:标签页上的文字,以及分支选中它时运行的步骤。 + + + An entry of the add step menu of an `AiWorkflow`: the same shape as a step without an id. Triggers are offered in the empty state; everything else is in the menu. + `AiWorkflow` 添加步骤菜单中的一个条目:与步骤形状相同,只是没有 id。触发器在空状态中提供;其余都在菜单中。 + + + An email: the sender and subject on one line and a two line preview. + 一封邮件:一行发件人和主题,以及两行预览。 + + + A label a model chose: a hairline label with a status dot, the confidence in monospace and a streamed reason. + 模型选定的标签:一个带状态圆点的细线标签、等宽字体的置信度,以及流式出现的理由。 + + + Every word `AiWorkflow` shows or says, in English by default. Replace the whole object to localize or reword; the sentences that carry a value are format functions. + `AiWorkflow` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;带有数值的句子是格式化函数。 + + + A posted chat message. + 一条已发出的聊天消息。 + + + The output preview of a step of an `AiWorkflow`, rendered inline in small muted text under the step, aligned with its title. Use one of `AiWorkflowEmailOutput`, `AiWorkflowLabelOutput`, `AiWorkflowRouteOutput`, `AiWorkflowMessageOutput`, `AiWorkflowRecordOutput` or `AiWorkflowTextOutput`. + `AiWorkflow` 中步骤的输出预览,以小号弱化文字内嵌在步骤下方,与标题对齐。使用 `AiWorkflowEmailOutput`、`AiWorkflowLabelOutput`、`AiWorkflowRouteOutput`、`AiWorkflowMessageOutput`、`AiWorkflowRecordOutput` 或 `AiWorkflowTextOutput` 之一。 + + + A created or updated record: an issue key, a row, a ticket or a page. + 一条创建或更新的记录:工单编号、表格行、客服工单或页面。 + + + The path a branch took. Return it from a branch step to pick the path. + 分支走的路径。从分支步骤返回它即可选择路径。 + + + Arguments of `RunCompleted`. + `RunCompleted` 的参数。 + + + What `RunStep` gets with each step. + `RunStep` 随每个步骤收到的内容。 + + + A flow an `AiWorkflow` shows and test-runs on its own while the app sets no `Steps`: a name, a line of context, the steps and the add step catalog, all supplied by the caller. Each simulated run plays the steps' `Output`, `Duration` and `Error`; with `AutoPlay` the card repeats the run while it is on screen until someone presses or types inside it. Nothing is sent. Set the data before handing the simulation to the card; to show another flow, give the card a new simulation. + 在应用没有设置 `Steps` 时,`AiWorkflow` 自行显示并测试运行的流程:名称、一行说明、步骤和添加步骤目录,全部由调用方提供。每次模拟运行都播放步骤的 `Output`、`Duration` 和 `Error`;开启 `AutoPlay` 时,只要卡片在屏幕上就会重复运行,直到有人在卡片内按下鼠标或按键。不会发送任何内容。请在把模拟交给卡片之前设置好数据;要显示另一个流程,就给卡片一个新的模拟。 + + + One step of an `AiWorkflow`. The workflow treats steps as values: to change one, put a new list in `Steps`. `Output`, `Duration` and `Error` are what a simulated run shows; a run through `RunStep` shows what your code returns. + `AiWorkflow` 的一个步骤。工作流把步骤当作值:要修改某一步,就在 `Steps` 中放入一个新列表。`Output`、`Duration` 和 `Error` 是模拟运行显示的内容;通过 `RunStep` 运行时显示你的代码返回的内容。 + + + Arguments of `StepsChanged`. + `StepsChanged` 的参数。 + + + Free text, such as a drafted reply; it streams in fast chunks. + 自由文本,例如起草的回复;以快速的分块流式出现。 + Arc alert: a persistent message that helps people recover or continue. Keep one instance and change `Tone`, `Title` and the content to morph it between states: the icon pops into the new tone, new text rises out of a soft blur and the height springs to fit. Hiding it (`IsOpen`) collapses the height while it fades, so the content below closes the gap. A string `Content` is the description line; any other content is shown as is. Arc 警示:持久显示的消息,帮助使用者恢复或继续操作。保持同一实例,改变 `Tone`、`Title` 和内容即可在各状态间变形:图标以弹出方式切换为新色调,新文字从柔和模糊中升起,高度以弹簧适配内容。隐藏(`IsOpen`)时高度在淡出的同时收缩,使下方内容填补空隙。字符串类型的 `Content` 为描述行;其他类型的内容按原样显示。 @@ -2424,6 +3288,110 @@ The whole prompt sent to both models. 发送给两个模型的完整提示。 + + Natural language to chart in one card (Arc Ask your data, design reference). A question field with the data source; a collapsible Generated SQL panel where the query streams in with syntax tints; a running state with a rolling count of the rows scanned over a breathing skeleton; then a result that builds as stacked bars, lines or a table, with a legend, a pointer and keyboard readout, and a one line insight streamed underneath. Failures show the database's message with Try again; a query that matches nothing offers the suggestions. Wire `GenerateSql` (a whole string, revealed four tokens a tick at a fixed final size) or `StreamSql` (chunks from your model; the panel grows with them) to your model, and `RunQuery` to a read only database role. Without them the card plays its `Simulation`, answers you supply, and asks its question once it is 35% on screen; with neither, it waits in its empty state and Ask stays unavailable. + 在一张卡片里把自然语言变成图表(Arc Ask your data,设计参考)。带数据源的问题输入框;可折叠的 Generated SQL 面板,查询带着语法着色流式写入;运行状态中,已扫描行数在呼吸的骨架上方滚动计数;然后结果以堆叠柱形、折线或表格逐步呈现,附带图例、指针与键盘读数,以及在下方流式出现的一行洞察。失败时显示数据库的消息和“重试”;没有匹配结果的查询会给出建议问题。把 `GenerateSql`(整段字符串,以固定的最终尺寸每个节拍显示四个记号)或 `StreamSql`(来自模型的分块,面板随之增高)接入你的模型,把 `RunQuery` 接入只读数据库角色。都不接入时,卡片播放它的 `Simulation`(由你提供的答案),并在有 35% 出现在屏幕上时提出它的问题;两者都没有时,卡片停在空状态,提问按钮不可用。 + + + Asks the question once the card is 35% on screen. Null: on when a `Simulation` writes the SQL, off when you wire a real model. + 卡片有 35% 出现在屏幕上时提出问题。为 null 时:由 `Simulation` 编写 SQL 则开启,接入真实模型则关闭。 + + + The view on show (two-way). Null leaves it to the card: each result opens on its own view or `DefaultChart`, and the switch changes it until the next result. + 当前显示的视图(双向)。为 null 时交给卡片决定:每个结果按自己的视图或 `DefaultChart` 打开,切换器在下一个结果到来前可改变它。 + + + Raised when the person switches the view with the chart switch. Bubbles. + 用户通过图表切换器切换视图时引发。冒泡。 + + + The view for results that do not name one. + 未指定视图的结果所使用的视图。 + + + Raised when writing or running fails; not when the person stops a run. Bubbles. + 编写或运行失败时引发;用户停止运行时不引发。冒泡。 + + + Writes the SQL for a question as one string. The card reveals it four tokens every 34ms at its final size. Honour the request's cancellation token: Stop cancels it. Without this and `StreamSql` the `Simulation` writes the SQL. + 以一整段字符串为问题编写 SQL。卡片以其最终尺寸每 34ms 显示四个记号。请遵守请求的取消令牌:“停止”会取消它。此项和 `StreamSql` 都未设置时,由 `Simulation` 编写 SQL。 + + + Keeps the SQL panel open after the query starts instead of folding it away. Once the person opens or closes the panel during a run, their choice wins. + 查询开始后让 SQL 面板保持展开,而不是折叠起来。一旦用户在运行中展开或折叠了面板,就以用户的选择为准。 + + + Every word the card shows or says. + 卡片显示或朗读的所有文字。 + + + The placeholder of the empty field. + 输入框为空时的占位文字。 + + + The question (two-way); the field writes it as the person types. Null shows the simulation's question, or an empty field without one. + 问题(双向);用户输入时由输入框写入。为 null 时显示模拟的问题,没有模拟时输入框为空。 + + + The result on show, or null. + 当前显示的结果,或 null。 + + + Raised once a result is charted (for saving the question to a dashboard or logging). Bubbles. + 结果绘制成图表后引发(用于把问题保存到仪表板或记录日志)。冒泡。 + + + Runs the SQL (read only) and returns a chart ready result. Call `ReportProgress` to roll the count in the running state; throw to show the database error. Without it the `Simulation` answers. + 运行 SQL(只读)并返回可直接绘图的结果。调用 `ReportProgress` 让运行状态中的计数滚动;抛出异常以显示数据库错误。未设置时由 `Simulation` 回答。 + + + The model and database the card plays while you wire none: its question (asked on its own once the card is on screen), suggestions, source and answers, run exactly like your own delegates (the SQL revealed, the rows scanned rolling, the chart building, the insight streaming). It stands in only for what you have not wired. Null leaves the card in its empty state. + 你一个委托都没有接入时卡片播放的模型和数据库:它的问题(卡片出现在屏幕上后自行提出)、建议、数据源和答案,运行方式与你自己的委托完全相同(SQL 逐步显示、扫描行数滚动、图表构建、洞察逐字出现)。它只替代你没有接入的部分。为 null 时卡片停在空状态。 + + + The database the question runs against: its name shows in the field and the empty state, its dialect leads the SQL meta. Null uses the simulation's source, or names none. + 问题所针对的数据库:其名称显示在输入框和空状态中,其方言位于 SQL 元信息的开头。为 null 时使用模拟的数据源,否则不显示名称。 + + + The SQL of the current run, as written so far. + 本次运行的 SQL,即目前已写出的部分。 + + + Where the run is: idle, writing, running, done, empty, failed or stopped. + 运行所处的阶段:空闲、编写中、运行中、完成、无数据、失败或已停止。 + + + Streams the SQL for a question as chunks from your model; each chunk fades in and the panel grows with them. Takes precedence over `GenerateSql`. Honour the cancellation token. + 以来自模型的分块流式输出问题的 SQL;每个分块淡入出现,面板随之增高。优先于 `GenerateSql`。请遵守取消令牌。 + + + Questions offered in the empty state and after a question that matches no data. Null offers the simulation's suggestions, or none; an empty list hides them. + 在空状态以及问题没有匹配数据之后提供的问题。为 null 时提供模拟的建议,没有模拟时不提供;空列表会隐藏它们。 + + + Every word `AskYourData` shows or says, in English by default. Replace the whole object to localize or reword; counts and times are format functions. + `AskYourData` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;计数与时间是格式化函数。 + + + What `RunQuery` returns, shaped for the chart. + `RunQuery` 返回的内容,结构为图表而设计。 + + + One series of an `AskYourDataResult`: a plan, a region or a metric with one value per category. + `AskYourDataResult` 中的一个系列:一个套餐、地区或指标,每个类别一个值。 + + + A model and a database an `AskYourData` plays while the app wires neither: the question it asks on its own, the suggestions, the source, and the answers it knows. A question that contains one of an answer's `Keywords` gets that answer (the first match in order); any other gets `Fallback`. The SQL is revealed at the card's own pace after a short pause, the rows scanned roll up in ten steps, then the result builds and its insight streams in. Nothing is queried, and the live region says so. Set the data before handing the simulation to the card; to play other answers, give the card a new simulation. + 应用一个都没有接入时 `AskYourData` 播放的模型和数据库:它自行提出的问题、建议、数据源以及它知道的答案。问题包含某个答案的 `Keywords` 之一时得到该答案(按顺序取第一个匹配);其他问题得到 `Fallback`。SQL 在短暂停顿后按卡片自己的节奏逐步显示,扫描行数分十步滚动上升,然后结果构建、洞察逐字出现。不会查询任何内容,实时区域会说明这一点。请在把模拟交给卡片之前设置好数据;要播放其他答案,就给卡片一个新的模拟。 + + + One answer of an `AskYourDataSimulation`: the words that pick it, the SQL, the rows scanned and the result. + `AskYourDataSimulation` 的一个答案:选中它的词、SQL、扫描的行数和结果。 + + + The database an `AskYourData` question runs against. + `AskYourData` 的问题所针对的数据库。 + Arguments of `ConfirmRequested`: set `Cancel` to keep the selection unconfirmed. `ConfirmRequested` 的参数:设置 `Cancel` 可使选择保持未确认。 @@ -3088,6 +4056,70 @@ The built-in text of `BudgetVariance`. Replace the whole object to localize the block; parameterized entries are functions that receive already formatted amounts. `BudgetVariance` 的内置文案。替换整个对象即可本地化区块;带参数的条目是接收已格式化金额的函数。 + + An animated "AI is building your app" visual (Arc Building blocks, design reference). Rounded blocks for a nav bar, a hero card, a chart, an avatar row, a settings row and a button fly in and snap into a tiny app layout with no bounce, land as tinted placeholders, fill with real-looking content and pick up the tint in a styling pass, while the status line steps through Laying out, Adding components, Styling and Ready. Map your request to `Status`: `Building` while it runs, `Done` when the result arrives (the rest fast forwards), `Idle` before the first prompt. Leave `Status` unset to play a simulated build. The frame grows to `Size` (or `StageWidth`) and never past its container; everything inside scales with the width. + “AI 正在搭建你的应用”的动画画面(Arc Building blocks,设计参考)。导航栏、主卡片、图表、头像行、设置行和按钮这些圆角积木飞入,无回弹地吸附进一个小小的应用布局,先以着色的占位块落位,再填上逼真的内容,并在样式阶段染上主题色;状态行依次显示 Laying out、Adding components、Styling 和 Ready。把请求映射到 `Status`:进行中为 `Building`,结果到达为 `Done`(其余阶段快进),第一次提示之前为 `Idle`。不设置 `Status` 时播放模拟构建。框架长到 `Size`(或 `StageWidth`)的宽度且不超出容器;内部一切随宽度缩放。 + + + Shows Replay on Ready; it raises `ReplayRequested`. Start the generation again and set `Status` back to building. + 在 Ready 时显示 Replay;按下会触发 `ReplayRequested`。请重新开始生成,并把 `Status` 设回 building。 + + + One build, from the first block to the end of styling, split evenly over the three phases; never under 2.4 seconds. + 一次构建的时长,从第一块积木到样式结束,平均分给三个阶段;最少 2.4 秒。 + + + Shows the status line under the frame: the mark, the phase label and Replay. Turn it off when the surrounding UI already says what happens. + 在框架下方显示状态行:标记、阶段标签和 Replay。周围界面已经说明正在发生什么时可以关闭。 + + + Without a status: replay the build after a short hold on Ready (off plays once and shows Replay). Building: re-assemble until the status turns done (off holds on Styling). Reduced motion always plays once. + 没有状态时:在 Ready 上短暂停留后重播构建(关闭则只播放一次并显示 Replay)。building 时:持续重新拼装直到状态变为 done(关闭则停在 Styling)。减少动效时总是只播放一次。 + + + Every word the component shows or says. + 组件显示或朗读的所有文字。 + + + The phase the status line shows. + 状态行显示的阶段。 + + + Raised when the visible phase changes, for analytics or to keep other UI in step. Bubbles. + 可见阶段变化时触发,可用于统计或让其他界面保持同步。冒泡。 + + + Raised when the person presses Replay. Bubbles. Without a status the simulated build also starts again. + 用户按下 Replay 时触发。冒泡。没有状态时模拟构建也会重新开始。 + + + The width the frame grows to: Small 280, Medium 400, Large 560 (never past the container). + 框架能长到的宽度:Small 280、Medium 400、Large 560(不超出容器)。 + + + A width in pixels that takes the place of `Size`; NaN (the default) uses `Size`. + 以像素指定的宽度,取代 `Size`;NaN(默认)时使用 `Size`。 + + + What the app says about its generation: idle clears the frame to dashed slots, building runs the phases (and re-assembles while `IsLooping`), done fast forwards and settles on Ready. Null plays the built-in simulated build. + 应用对生成过程的说明:idle 把框架清空为虚线槽位,building 播放各个阶段(`IsLooping` 时重新拼装),done 快进并停在 Ready。为 null 时播放内置的模拟构建。 + + + Any color, taking the place of `Tone`, for a brand or per-product tint. + 任意颜色,取代 `Tone`,用于品牌色或按产品着色。 + + + The tint of the placeholders, the button, the chart, the switch and the status mark: the accent or the foreground. + 占位块、按钮、图表、开关和状态标记的颜色:强调色或前景色。 + + + Every word `BuildingBlocks` shows or says, in English by default. Replace the whole object to localize or to name the thing being built (`Ready = "Your dashboard is ready"`). + `BuildingBlocks` 显示或朗读的所有文字,默认为英文。替换整个对象来本地化,或写明正在构建的东西(`Ready = "Your dashboard is ready"`)。 + + + Arguments of `PhaseChanged`. + `PhaseChanged` 的参数。 + Arc button: a clear, responsive action with quiet secondary states. Presses scale on a snappy spring, label changes crossfade with a short blur while the width springs to fit, and `IsLoading` shows a spinner while keeping keyboard focus (clicks are swallowed instead of disabling the button). Arc 按钮:清晰、灵敏的操作,次要状态安静低调。按下时以轻快的弹簧缩放;文字变化时以短暂模糊交叉淡化,宽度以弹簧适配内容。`IsLoading` 显示加载指示器,同时保留键盘焦点(点击会被忽略,而不是禁用按钮)。 @@ -3100,6 +4132,14 @@ Anchors of popups and menus must not scale on press (the popup measures the pressed size); they answer with color only. Defaults to true. 弹层与菜单的锚点按钮在按下时不得缩放(弹层需测量按下时的尺寸);它们仅以颜色作出反馈。默认为 true。 + + Spring the button's width follows when its label changes: Morph (default, a slight bounce), or Quick for a bounce-free 0.25s glide that pairs with the Label swap. + 标签变化时按钮宽度跟随的弹簧:Morph(默认,带轻微回弹),或 Quick,即 0.25 秒无回弹的滑动,与 Label 切换搭配使用。 + + + How a new label replaces the old one: BlurSwap (default, a short blur), or Label for the quiet crossfade of Arc's AI blocks (100ms out, 160ms in rising 2px), or any other content swap mode. + 新标签替换旧标签的方式:BlurSwap(默认,短暂模糊),或 Label,即 Arc AI 区块的安静交叉淡化(淡出 100ms,淡入 160ms 并上升 2px),也可以是其他任意内容切换模式。 + Button height: Small, Medium (default) or Large. 按钮高度:Small、Medium(默认)或 Large。 @@ -4136,6 +5176,142 @@ Header text shown when `FileName` is empty (default "Source code"). `FileName` 为空时显示的标题文字(默认 “源代码”)。 + + An AI code edit suggestion in a small editor panel, like an inline edit in an AI code editor. The header shows the model's mark, the instruction and the model; the panel shows a file tab with its language mark, a running +/− count, and the code with each proposed change as an inline diff hunk: removed lines tinted red with a minus, new lines tinted green with a plus. New lines stream in, then every hunk gets Reject and Accept under it, and the footer offers Reject all and Accept all (Cmd or Ctrl + Enter). Accepting a hunk folds its removed lines away and settles the new lines into the file; rejecting folds the new lines away and restores the original. Give it the file in `Code`, the changes in `Hunks` and set `Status`; write the file in `Resolved` (or progressively in `DecisionMade`): the control never writes anything itself. With no code and no hunks it shows an empty file, or plays a `Simulation` you pass in, which reviews itself and replays until someone touches it. + 在小型编辑器面板中呈现的 AI 代码修改建议,类似 AI 代码编辑器中的行内修改。标题栏显示模型标记、用户要求和模型;面板显示带语言标记的文件标签、实时的 +/− 计数,以及代码,其中每处建议的改动都是一个行内差异块:删除的行以红色底并带减号,新增的行以绿色底并带加号。新行逐行流入,随后每个差异块下方出现“拒绝”和“接受”,底栏提供“全部拒绝”和“全部接受”(Cmd 或 Ctrl + Enter)。接受一个差异块会折叠其删除行并让新行并入文件;拒绝则折叠新行并恢复原文。把文件放进 `Code`、改动放进 `Hunks` 并设置 `Status`;在 `Resolved` 中(或在 `DecisionMade` 中逐步)写入文件:控件自己从不写入任何内容。既没有代码也没有差异块时,它显示一个空文件,或播放你传入的 `Simulation`,后者会自行审阅并重播,直到有人操作它。 + + + Shows Regenerate once the review is done (or there were no changes) and Retry on error; both raise `RegenerateRequested`. A simulation always has them and replays on its own. + 审阅完成后(或没有改动时)显示“重新生成”,出错时显示“重试”;两者都会引发 `RegenerateRequested`。模拟始终带有它们,并会自行重播。 + + + Shows Stop while thinking or streaming; it raises `StopRequested`. A simulation always has it. + 思考或流式时显示“停止”;它会引发 `StopRequested`。模拟始终带有它。 + + + Cmd or Ctrl + Enter accepts all and Cmd or Ctrl + Backspace rejects all while focus is inside the card; Accept all shows the hint. + 焦点在卡片内时,Cmd 或 Ctrl + Enter 接受全部,Cmd 或 Ctrl + Backspace 拒绝全部;“全部接受”显示提示。 + + + Simulation only: after streaming, it accepts and rejects hunks on its own (one by one and Accept all on alternate runs) and replays about 2.6s after it finishes. Any pointer, key or focus inside the card hands control to the person for that run. Off under reduced motion. + 仅用于模拟:流式结束后自行接受和拒绝差异块(隔轮交替为逐个决定和全部接受),结束约 2.6 秒后重播。卡片内的任何指针、按键或焦点都会把这一轮交给用户控制。减少动态效果时关闭。 + + + The original file contents, lines split on `\n`. Leave it and `Hunks` unset to play the `Simulation`. + 文件的原始内容,按 `\n` 分行。与 `Hunks` 都不设置时播放 `Simulation`。 + + + Raised for every hunk accepted or rejected (Accept all raises it once per hunk), with the file as it now stands. Bubbles. + 每接受或拒绝一个差异块时引发(“全部接受”对每个差异块各引发一次),并附带文件当前的内容。冒泡。 + + + The message of the error status. + 错误状态显示的消息。 + + + Your own language mark for the tab (any content); null shows two letters from the file's extension. + 标签中你自己的语言标记(任意内容);为 null 时显示取自文件扩展名的两个字母。 + + + The file name in the tab (empty by default); its extension picks the highlighting language and the language mark. + 标签中的文件名(默认为空);其扩展名决定高亮语言和语言标记。 + + + The folder shown after the file name from a 520px card, such as "src/billing". + 卡片宽度达到 520px 时显示在文件名之后的文件夹,例如 "src/billing"。 + + + What the person asked for, shown as the header title (empty by default). + 用户提出的要求,显示为标题栏的标题(默认为空)。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + Caps the code area's height; longer files scroll inside it. Infinity (the default) never scrolls. + 限制代码区域的高度;更长的文件在其中滚动。默认值为无穷大,从不滚动。 + + + The model that wrote the suggestion: its name under the title and its mark (a 24 × 24 stroked glyph, in `MarkBrush` or the text color) in the header. Null (the default) hides the model line. + 写出这个建议的模型:其名称显示在标题下方,其标记(24 × 24 的描边图形,颜色为 `MarkBrush` 或文字颜色)显示在标题栏中。为 null(默认)时隐藏模型那一行。 + + + How many hunks still wait for a decision. + 还在等待决定的差异块数量。 + + + Raised when the person presses Regenerate or Retry. Bubbles. + 用户按下“重新生成”或“重试”时引发。冒泡。 + + + Raised once every hunk has a decision, with the final code and the hunk ids. Write the file here. Bubbles. + 每个差异块都有决定后引发一次,附带最终代码和差异块的 id。在这里写入文件。冒泡。 + + + Whether the tab shows a language mark at all. + 标签是否显示语言标记。 + + + A simulated suggestion to play while the card has no `Code` and no `Hunks` (demos and prototypes): it streams its hunks over its file and, with `AutoPlay`, reviews itself and replays. Null shows the card's own state. + 卡片没有 `Code` 和 `Hunks` 时要播放的模拟建议(演示与原型):它在自己的文件上流入差异块,并在 `AutoPlay` 下自行审阅并重播。为 null 时显示卡片自己的状态。 + + + Where the suggestion is: thinking shows a shimmering label over the plain file, streaming shows hunks as they grow, ready shows Accept and Reject, error shows the message and Retry. Going back to thinking clears earlier decisions. + 建议所处的阶段:thinking 在原始文件上显示闪烁的标签,streaming 显示正在增长的差异块,ready 显示“接受”和“拒绝”,error 显示消息和“重试”。回到 thinking 会清除之前的决定。 + + + Raised when the person presses Stop. Abort your request, then set `Status`. Bubbles. + 用户按下“停止”时引发。中止你的请求,然后设置 `Status`。冒泡。 + + + Data of `DecisionMade`. + `DecisionMade` 的事件数据。 + + + One proposed change of a `CodeSuggestion`: it replaces `Remove` lines starting at the zero based line `Start` with the `Insert` lines (remove 0 inserts before `Start`; an empty insert deletes). To stream, replace `Insert` with a longer list (or call `AppendLine`) as tokens arrive. + `CodeSuggestion` 中的一处建议改动:从从零开始计数的第 `Start` 行起,用 `Insert` 行替换 `Remove` 行(remove 为 0 时在 `Start` 之前插入;insert 为空时表示删除)。流式时,随着令牌到达把 `Insert` 替换为更长的列表(或调用 `AppendLine`)。 + + + The hunk's id, unique within the suggestion; decisions are kept by it. + 差异块的 id,在建议中唯一;决定按它保存。 + + + The new lines (empty for a pure deletion). Replace the list to stream more lines in. + 新增的行(纯删除时为空)。替换列表即可流入更多行。 + + + How many original lines the hunk removes (0 for a pure insertion). + 差异块删除的原始行数(纯插入时为 0)。 + + + The zero based line of the first removed line, or the line the new lines go before. + 第一行删除行从零开始的行号,或新行插入位置之后的那一行。 + + + A two to six word summary shown in the review bar and in the names of its buttons. + 两到六个词的摘要,显示在审阅栏中,也用于其按钮的名称。 + + + Every word `CodeSuggestion` shows or says, in English by default. Replace the whole object to localize or reword; texts that carry a name or a count are format functions. + `CodeSuggestion` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;带名称或数量的文本是格式化函数。 + + + Data of `Resolved`. + `Resolved` 的事件数据。 + + + A simulated suggestion for a `CodeSuggestion`, for demos, prototypes and tests: the card reads `Code` for 0.8s, then each of `Hunks` opens (its removed lines tint red in 240ms) and its new lines stream in one every 38ms. With `AutoPlay` the card then reviews it itself (one hunk at a time and Accept all on alternate runs) and replays 2.6s after it finishes. Any pointer, key or focus inside the card hands control to the person for that run; Stop keeps the hunks that finished and Regenerate replays. Reduced motion and Off open on the finished suggestion with no streaming and no autoplay. Nothing is sent to a model. Set it as the card's `Simulation`: it plays while the card has no `Code` and no `Hunks` of its own, from the top each time the card comes into a window. The file name, the instruction and the model stay on the card. Its values take effect at the next run; the hunks are copied, so one list can be shared between simulations. Use one simulation per card. + 为 `CodeSuggestion` 提供的模拟建议,用于演示、原型和测试:卡片读取 `Code` 0.8 秒,然后 `Hunks` 中的每个差异块依次展开(其删除行在 240ms 内染成红色),新行每 38ms 流入一行。在 `AutoPlay` 下,卡片随后自行审阅(隔轮交替为逐个决定和全部接受),结束 2.6 秒后重播。卡片内的任何指针、按键或焦点都会把这一轮交给用户控制;“停止”保留已完成的差异块,“重新生成”会重播。减弱动效和关闭动效时直接显示完成的建议,不流式也不自动播放。不会向任何模型发送内容。把它设为卡片的 `Simulation`:卡片没有自己的 `Code` 和 `Hunks` 时播放它,每次卡片进入窗口时从头开始。文件名、指令和模型仍设置在卡片上。它的值在下一轮生效;差异块会被复制,所以多个模拟可以共用一份列表。每张卡片使用一个模拟。 + + + The original file contents, lines split on `\n`. + 文件的原始内容,按 `\n` 分行。 + + + The complete hunks the simulated model streams, in file order. + 模拟模型要流式输出的完整差异块,按文件中的顺序排列。 + One cell of a `CohortRetention`: a cohort (or the average row) and a month since the cohort started. `CohortRetention` 的一个单元格:一个队列(或平均行)和自该队列开始以来的某个月。 @@ -5076,6 +6252,130 @@ The legend text of the healthy zone. 健康区域的图例文本。 + + One app a `ConnectorSetup` lists. Write the scopes as plain-language capabilities ("Post replies as your assistant"), not raw OAuth scope strings. + `ConnectorSetup` 列出的一个应用。用通俗的能力描述来写权限范围("Post replies as your assistant"),不要写原始的 OAuth scope 字符串。 + + + What a connect handler gets besides the app: a token that is cancelled when the person presses Cancel or the card leaves the window, and the report calls that move the row through its steps. The report calls may come from any thread. + 连接处理程序除应用之外得到的内容:一个在用户按下 Cancel 或卡片离开窗口时取消的令牌,以及推动这一行经过各个步骤的报告调用。报告调用可以来自任何线程。 + + + What a finished connect reports back: the account, the granted scopes and how many items the first sync read. + 连接完成后报告的内容:账户、获得的权限范围,以及首次同步读取了多少项。 + + + An app's mark as `ConnectorSetup` draws it: a neutral Lucide glyph for the kind of app (or your own glyph, or the first letter of the name) in a quiet rounded tile. No brand logos are drawn; pass the app's own mark as a 24 × 24 stroked `Glyph`. Use it anywhere else in your settings UI to match the card. + `ConnectorSetup` 绘制的应用标记:在一个安静的圆角图块中,显示表示应用类型的中性 Lucide 图标(或你自己的图标,或名称的首字母)。不绘制品牌标志;应用自己的标记以 24 × 24 描边 `Glyph` 传入。可在设置界面的其他位置使用,与卡片保持一致。 + + + Your own 24 × 24 stroked glyph; it takes the place of the built-in one. + 你自己的 24 × 24 描边图标;它取代内置图标。 + + + The glyph's color; the foreground by default. + 图标的颜色;默认为前景色。 + + + The built-in glyph for the kind of app. + 表示应用类型的内置图标。 + + + The name whose first letter is shown when there is no glyph. + 没有图标时显示其首字母的名称。 + + + The glyph's size in pixels; the tile adds its padding around it. + 图标的大小(像素);图块在其周围加上内边距。 + + + A "Connect your tools" card for an AI assistant (Arc Connector setup, design reference). Each app connects inline: its row opens the sign-in, waits for the person to approve and runs a first sync with a progress hairline and a rolling count, then lists the scopes it was granted behind a Connected button and offers Disconnect with an inline confirmation. A search field filters the list by name, description, account and keywords. The OAuth flow is the app's: set `ConnectHandler` to run it, report the steps through the `ConnectorConnectContext` and return the account, scopes and sync count (throw to show the reason with Retry); set `DisconnectHandler` to revoke. Without handlers the flow is simulated and nothing is sent. With no `Connectors` the card lists its `Simulation`'s apps and, once on screen, plays its scripted run; with neither it lists no apps. + AI 助手的"Connect your tools"卡片(Arc Connector setup,设计参考)。每个应用都在行内连接:它的行打开登录页,等待用户批准,并运行首次同步,同时显示进度细线和滚动的计数;随后在 Connected 按钮后列出获得的权限范围,并提供带行内确认的 Disconnect。搜索框按名称、描述、账户和关键词筛选列表。OAuth 流程由应用负责:设置 `ConnectHandler` 来运行它,通过 `ConnectorConnectContext` 报告各步骤,并返回账户、权限范围和同步数量(抛出异常即可显示原因和 Retry);设置 `DisconnectHandler` 来撤销访问。没有处理程序时流程是模拟的,不会发送任何内容。没有 `Connectors` 时,卡片列出它的 `Simulation` 中的应用,并在出现在屏幕上后播放它的脚本演示;两者都没有时不列出任何应用。 + + + Raised once a connection completes, with the account, scopes and sync count. Bubbles. + 连接完成后引发,带有账户、权限范围和同步数量。冒泡。 + + + Raised once access to an app was revoked. Bubbles. + 对某个应用的访问被撤销后引发。冒泡。 + + + The assistant's name, used in the description, the scopes heading, the confirmation and the footer. Null uses the simulation's while it plays; with no name the texts say "your assistant". + 助手的名称,用于描述、权限范围标题、确认问题和页脚。为 null 时,播放模拟期间使用模拟中的名称;没有名称时文字会写成“your assistant”。 + + + Plays the `Simulation`'s scripted run once the card is on screen. It stops at the first pointer or key press. Null plays it while the simulation's apps show and there is no connect handler. + 卡片出现在屏幕上后播放 `Simulation` 的脚本演示。首次指针或按键操作时停止。为 null 时,在显示模拟的应用且没有连接处理程序时播放。 + + + Runs your OAuth flow for an app. Null simulates it (nothing is sent). + 为某个应用运行你的 OAuth 流程。为 null 时进行模拟(不发送任何内容)。 + + + The ids of the connected apps (two-way). The card writes the new list after each connect or disconnect finishes. Null at first connects nothing, or the simulation's connected apps while it plays. + 已连接应用的 id(双向)。每次连接或断开完成后,卡片写回新的列表。初始为 null 时不连接任何应用;播放模拟期间则连接模拟中已连接的应用。 + + + Raised with the new list of connected ids after a connect or disconnect finishes. Bubbles. + 连接或断开完成后,以新的已连接 id 列表引发。冒泡。 + + + The apps to list. Null lists the `Simulation`'s apps, or none without one. + 要列出的应用。为 null 时列出 `Simulation` 中的应用,没有模拟时不列出任何应用。 + + + The line under the heading; built from the assistant's name when null. + 标题下的一行文字;为 null 时根据助手名称生成。 + + + Revokes access after the inline confirmation. Null simulates it. + 在行内确认之后撤销访问。为 null 时进行模拟。 + + + The footer under the list (text or any content); a short privacy note when null. + 列表下方的页脚(文字或任意内容);为 null 时显示一段简短的隐私说明。 + + + Shows the footer. + 显示页脚。 + + + Shows the search field. + 显示搜索框。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + The search field's placeholder; "Search apps" when null. + 搜索框的占位文字;为 null 时为 "Search apps"。 + + + The search text (two-way); it matches name, description, account and keywords. + 搜索文字(双向);匹配名称、描述、账户和关键词。 + + + The apps the card lists and plays on its own while `Connectors` is null: the assistant's name, the apps, the ones already connected and the scripted run, played exactly like your own apps would connect (simulated without a connect handler). Null lists no apps instead. + `Connectors` 为 null 时卡片自行列出并播放的应用:助手的名称、应用、已连接的应用和脚本演示,播放方式与你自己的应用连接完全相同(没有连接处理程序时为模拟)。为 null 时改为不列出任何应用。 + + + The card's heading; "Connect your tools" when null. + 卡片的标题;为 null 时为 "Connect your tools"。 + + + Every word `ConnectorSetup` shows or says, in English by default. Replace the whole object to localize or reword; texts that hold a name, the assistant or a count are format functions. + `ConnectorSetup` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;包含名称、助手或数量的文字是格式化函数。 + + + The apps a `ConnectorSetup` lists and plays on its own while the app sets no `Connectors`: the assistant's name, the apps, the ones already connected and a scripted run of connects (one of them can fail first, then succeed on retry). The run starts once the card is on screen and stops for good at the first pointer or key press inside it; each connect is simulated (sign-in, approval, the first sync counting up) and nothing is sent. Set the data before handing the simulation to the card; to show other apps, give the card a new simulation. + 应用没有设置 `Connectors` 时 `ConnectorSetup` 自行列出并播放的应用:助手的名称、应用、已连接的应用,以及一段连接的脚本演示(其中一次可以先失败,重试后成功)。演示在卡片出现在屏幕上后开始,并在卡片内第一次出现指针按下或按键时永久停止;每次连接都是模拟的(登录、批准、首次同步计数上升),不会发送任何内容。请在把模拟交给卡片之前设置好数据;要显示其他应用,就给卡片一个新的模拟。 + + + One step of the scripted run of a `ConnectorSetupSimulation`. + `ConnectorSetupSimulation` 脚本演示中的一步。 + Secondary actions kept close to the selected object (Arc context menu). Wraps its `Content` and opens a menu at the pointer on right-click, or under the target (12px in, 8px below) on a plain click, a tap, the ContextMenu key or Shift+F10. The menu grows from the pointer on a snappy spring, rows press with a tint, and it closes on any scroll, resize or outside press instead of repositioning. Avoid it on targets that need their own click. To give any control a right-click menu without wrapping it, attach a surface: `<Border><arc:ContextMenu.Menu><arc:MenuSurface>…</arc:MenuSurface></arc:ContextMenu.Menu></Border>`. 靠近所选对象的次要操作(Arc 上下文菜单)。包裹其 `Content`,在右键时于指针处打开菜单,或在普通点击、轻触、ContextMenu 键或 Shift+F10 时在目标下方(向内 12px、向下 8px)打开。菜单以轻快的弹簧从指针处展开,菜单项按下时会着色,滚动、调整大小或外部按压时会关闭而不是重新定位。避免用于本身需要单击功能的目标。若要让任意控件拥有右键菜单而无需包裹,可附加一个表面:`<Border><arc:ContextMenu.Menu><arc:MenuSurface>…</arc:MenuSurface></arc:ContextMenu.Menu></Border>`。 @@ -5117,8 +6417,8 @@ 在目标上普通点击或轻触是否打开菜单(Arc 为 true)。 - A model's context window as a fixed budget of 200 cells (20 × 10) that fills in reading order: the system prompt, tool definitions, files and memory, the summary and the conversation, then free space, with the output reserve held as outlined cells at the end. A dashed line marks the compaction threshold. The header reads the tokens used, the window, the percentage and the free tokens, with a pill to switch models; a legend lists every part with its tokens and share; Send message, Attach file and Compact now change the window, and a status line says what happened. Crossing the threshold after a send or an attachment, or switching to a model the window overfills, folds every message but the newest two into one summary (about 6% of them, between 4K and 24K), and the space comes back. The card rewrites `Items` as it goes and raises `ItemsChanged`, `Compacted` and `ModelChanged`. Given no data at all it shows an invented sample session with three sample models (`ContextWindowSamples`). - 以 200 个单元格(20 × 10)组成的固定预算表示模型的上下文窗口,按阅读顺序填充:系统提示、工具定义、文件与记忆、摘要与对话,然后是剩余空间,输出预留部分以轮廓单元格保留在末尾。虚线标记压缩阈值。标题栏显示已用令牌、窗口大小、百分比和剩余令牌,并带有切换模型的胶囊;图例列出每个部分的令牌数和占比;发送消息、附加文件和立即压缩会改变窗口,状态行说明发生了什么。发送或附加后超过阈值,或切换到窗口容纳不下的模型时,除最新两条之外的所有消息会合并为一条摘要(约占其中的 6%,介于 4K 与 24K 之间),空间随之释放。该卡片会在运行中改写 `Items`,并引发 `ItemsChanged`、`Compacted` 和 `ModelChanged`。若完全没有数据,它会显示内置的虚构示例会话和三个示例模型(`ContextWindowSamples`)。 + A model's context window as a fixed budget of 200 cells (20 × 10) that fills in reading order: the system prompt, tool definitions, files and memory, the summary and the conversation, then free space, with the output reserve held as outlined cells at the end. A dashed line marks the compaction threshold. The header reads the tokens used, the window, the percentage and the free tokens, with a pill to switch models; a legend lists every part with its tokens and share; Send message, Attach file and Compact now change the window, and a status line says what happened. Crossing the threshold after a send or an attachment, or switching to a model the window overfills, folds every message but the newest two into one summary (about 6% of them, between 4K and 24K), and the space comes back. The card rewrites `Items` as it goes and raises `ItemsChanged`, `Compacted` and `ModelChanged`. Without models or items the card is empty: no model pill, every cell free, and no Send message or Attach file without `Turns` and `Files`. + 以 200 个单元格(20 × 10)组成的固定预算表示模型的上下文窗口,按阅读顺序填充:系统提示、工具定义、文件与记忆、摘要与对话,然后是剩余空间,输出预留部分以轮廓单元格保留在末尾。虚线标记压缩阈值。标题栏显示已用令牌、窗口大小、百分比和剩余令牌,并带有切换模型的胶囊;图例列出每个部分的令牌数和占比;发送消息、附加文件和立即压缩会改变窗口,状态行说明发生了什么。发送或附加后超过阈值,或切换到窗口容纳不下的模型时,除最新两条之外的所有消息会合并为一条摘要(约占其中的 6%,介于 4K 与 24K 之间),空间随之释放。该卡片会在运行中改写 `Items`,并引发 `ItemsChanged`、`Compacted` 和 `ModelChanged`。没有模型或项目时,卡片为空:没有模型胶囊,所有格子都空闲;没有 `Turns` 和 `Files` 时也没有发送消息和附加文件。 Defines the `AutoCompact` property. @@ -5480,6 +6780,94 @@ Replaces the Split shape's setup card (any control or content); null shows the setup card. 替换分栏形态的设置卡片(可以是任意控件或内容);为 null 时显示设置卡片。 + + An isometric cube character that shows what an assistant is doing (Arc cube mascot), drawn from real 3D math: a rounded silhouette, three faces shaded by how they face the light, and a face painted on the front side through its projected plane. Idle tilts, floats and blinks; thinking turns the cube a quarter turn in eased steps and the face fades in on the side that arrives at the front; writing tips the face down while lines print onto the ground in front of it; done settles with a happy squint and a sparkle; error shakes once and shows worried brows. Status changes blend from the pose on screen over 420ms, never a hard cut. Leave `Status` unset and the cube plays through every state on a loop (for showcases). + 一个表示助手正在做什么的等轴测立方体角色(Arc 立方体吉祥物),由真实的三维数学绘制:圆润的轮廓、按朝向光线的程度着色的三个面,以及通过投影平面画在正面上的一张脸。空闲时倾斜、浮动和眨眼;思考时立方体以缓动的步子转四分之一圈,脸在转到正面的那一面上淡入;书写时脸向下低,线条印在它前方的地面上;完成时带着开心的眯眼和闪光安顿下来;出错时摇晃一次并露出担忧的眉毛。状态切换从屏幕上的姿态在 420ms 内过渡,从不生硬切换。不设置 `Status` 时,立方体会循环播放每种状态(用于展示)。 + + + Overrides the whole accessible label; null (default) builds it from the name and the status. + 替换整个无障碍标签;null(默认)时由名字和状态组成。 + + + Announces status changes in a polite live region. Turn it on for the one mascot that carries status, not for decorative copies. + 在礼貌(polite)实时区域中朗读状态变化。只为承载状态的那个吉祥物打开,装饰性的副本不要打开。 + + + The assistant's name, used in the accessible label ("Assistant, thinking"). + 助手的名字,用于无障碍标签("Assistant, thinking")。 + + + The cube's body color; the theme sets the accent. The top and the shaded side are mixed from it. + 立方体主体的颜色;主题设为强调色。顶面和背光侧面由它混合得到。 + + + The status the cube shows: `Status`, or the current self-play step while it is unset. + 立方体显示的状态:`Status`,未设置时为自动播放的当前步骤。 + + + The color of the eyes, brows and mouth; the theme sets the accent foreground. + 眼睛、眉毛和嘴巴的颜色;主题设为强调色上的前景色。 + + + Overrides the front face's shading anchor; null uses `BodyBrush`. + 覆盖正面的着色锚点;null 时使用 `BodyBrush`。 + + + Freezes the cube on the current frame. It also pauses by itself off screen and while hidden, and resumes without a jump. + 把立方体冻结在当前帧。它在屏幕外或被隐藏时也会自动暂停,并无跳变地继续。 + + + Every word the cube speaks. + 立方体朗读的所有文字。 + + + The color of the lines printed while writing; the theme sets the muted text color. + 书写时印出的线条的颜色;主题设为弱化文字色。 + + + The color of the contact shadow (drawn translucent); null (default) uses the theme's shadow tone. + 接触阴影的颜色(半透明绘制);null(默认)时使用主题的阴影色调。 + + + Draws the contact shadow and the lines printed while writing (at 56px and up). + 绘制接触阴影和书写时印出的线条(56px 及以上)。 + + + Overrides the shaded side's anchor; null mixes it from `BodyBrush` toward black. + 覆盖背光侧面的锚点;null 时由 `BodyBrush` 向黑色混合得到。 + + + Width and height in px, from 24 (inline next to a message) to 160 (empty state). Below 56 a compact drawing takes over: a larger cube in the frame, bolder eyes, no mouth, shadow or printed lines. + 宽和高(px),从 24(内联在消息旁)到 160(空状态)。小于 56 时改用紧凑画法:立方体在画框中更大,眼睛更粗,没有嘴巴、阴影和印出的线条。 + + + The color of the done sparkle; the theme sets the accent. + 完成时闪光的颜色;主题设为强调色。 + + + What the assistant is doing. Leave it unset (null) and the cube plays through idle, thinking, writing, done and error on a loop, which is meant for showcases; `StatusChanged` reports each step. + 助手正在做什么。不设置(null)时立方体会循环播放空闲、思考、书写、完成和出错,用于展示;`StatusChanged` 报告每一步。 + + + Raised when the self-playing loop (status unset) moves to the next state. + 自动播放的循环(未设置状态)进入下一个状态时触发。 + + + Overrides the top face's shading anchor; null mixes it from `BodyBrush` toward white. + 覆盖顶面的着色锚点;null 时由 `BodyBrush` 向白色混合得到。 + + + Every word a `CubeMascot` speaks, in English by default: the status words and the pattern that joins them with the assistant's name into the accessible label ("Assistant, thinking"). Replace the instance (or set its properties) to translate or reword. + `CubeMascot` 朗读的所有文字,默认是英文:状态词,以及把状态词与助手名字连成无障碍标签的格式("Assistant, thinking")。替换这个实例(或设置它的属性)即可翻译或改写。 + + + What the assistant behind a `CubeMascot` is doing; each status has its own pose, face and motion. + `CubeMascot` 背后的助手正在做什么;每种状态都有自己的姿态、表情和动效。 + + + Arguments of `StatusChanged`: the status the self-playing loop moved to. + `StatusChanged` 的参数:自动播放的循环进入的状态。 + A customer galaxy: every account a dot, its area by monthly recurring revenue and its color by health, slowly turning as a tilted two-armed spiral. Group the accounts by any dimension and the dots spring into one packed cluster per value, labelled with its MRR and size; pan, zoom about the pointer, double-click a cluster to fly into it, hover a dot for its card and click it to select it. A totals panel beside the stage (under it in narrow containers) shows the MRR, accounts, growing and at-risk MRR of everything or of the focused cluster, and a list of the clusters by MRR or by accounts whose rows fly to their cluster. 客户星系:每个账户是一个点,面积由月度经常性收入(MRR)决定,颜色由健康度决定,以倾斜的双臂螺旋缓慢旋转。按任意维度对账户分组后,这些点会弹跳聚合成每个分组值对应的一个紧凑团簇,并标注其 MRR 与规模;可平移、以指针为中心缩放、双击团簇飞入其中,悬停点可查看其卡片,点击即可选中。舞台旁(窄容器中在其下方)是汇总面板,显示全部或所选团簇的 MRR、账户数、增长与风险 MRR,以及按 MRR 或账户数排序的团簇列表,列表中各行可飞至对应团簇。 @@ -6380,6 +7768,114 @@ Raised when a range is applied. 应用范围时引发。 + + A screenshot-to-code card (Arc Design to code, design reference). The person drops a UI screenshot or imports a frame from a design file; a scan line crosses it and the layout regions it finds appear as boxes with tags; then code streams in beside it, linked region by region (pointing at a box or a tag highlights its lines and the other way round), with a working live preview one tab away. Generation comes from `Generator`, an async delegate that adds regions and writes code chunks through a `DesignToCodeContext`. Without one the card plays a built-in simulation on a sample pricing card and, while it is on screen, loops it (generate, hold on the code, show the preview, start again) until the first pointer or key press inside the card. Dropping a file uses Avalonia drag and drop; Choose file raises `ChooseFileRequested` for your picker (hand the file to `LoadFileAsync`). + 截图转代码卡片(Arc Design to code,设计参考)。用户拖入界面截图或从设计文件导入一个画框;扫描线扫过它,找到的布局区域以带标签的方框出现;随后代码在旁边流式写出,并与区域一一对应(指向方框或标签会高亮它的代码行,反之亦然),切换一个标签页即可看到可实际操作的实时预览。生成来自 `Generator`,这是一个通过 `DesignToCodeContext` 添加区域并写入代码块的异步委托。没有生成器时,卡片在示例价格卡片上播放内置模拟,并在可见期间循环播放(生成、停留在代码上、显示预览、重新开始),直到卡片内第一次出现指针按下或按键。拖入文件使用 Avalonia 拖放;Choose file 会引发 `ChooseFileRequested`,供你打开文件选择器(把文件交给 `LoadFileAsync`)。 + + + The file types accepted, as in an input's accept attribute (MIME types, `image/*` or extensions). Other files show an error. + 接受的文件类型,写法同输入框的 accept 属性(MIME 类型、`image/*` 或扩展名)。其他文件会显示错误。 + + + Plays the `Simulation` on a loop while the card is on screen (generate, hold on the code, show the preview, start again), and stops at the first pointer or key press inside the card. Null plays it when there is no `Generator`. Under reduced motion it plays once. + 卡片在屏幕上时循环播放 `Simulation`(生成、停留在代码上、显示预览、重新开始),并在卡片内第一次出现指针按下或按键时停止。为 null 时,在没有 `Generator` 的情况下播放。在减少动态效果下只播放一次。 + + + Raised when the person presses Choose file. Bubbles. Open your file picker, pass the file to `LoadFileAsync` and set Handled; when nobody handles it the card uses the sample and says nothing was opened. + 用户按下 Choose file 时引发。会冒泡。打开你的文件选择器,把文件交给 `LoadFileAsync` 并设置 Handled;没有人处理时,卡片改用示例,并说明没有打开任何文件。 + + + The code written so far (the finished code once `Phase` is Done). + 目前已写出的代码(`Phase` 为 Done 时即完成的代码)。 + + + Raised with the finished code when a run completes. Bubbles. + 一次运行完成时引发,附带完成的代码。会冒泡。 + + + Runs your design file picker or link flow for Import design file: return the frame (with its rendered image) or null when cancelled. Null simulates an import of the simulation's design, if any. + 为“导入设计文件”运行你的设计文件选择器或链接流程:返回画框(带渲染后的图像),取消时返回 null。为 null 时模拟导入模拟的设计(如果有)。 + + + The problem the status names (a rejected file, a failed import or run), or null. + 状态中指出的问题(被拒绝的文件、失败的导入或运行),没有时为 null。 + + + The file name shown in the code header, in the status while writing and in the copy button's name. Empty uses the simulation's while it plays. + 显示在代码标题栏、编写时的状态以及复制按钮名称中的文件名。为空时,播放模拟期间使用模拟的文件名。 + + + Turns a source into code: call `AddRegion` as parts are detected, then `Write` as code streams, with a region id to link a chunk to its box. Complete the task when done, throw to show the error, and stop when the context's token is cancelled. Null plays the `Simulation`, if any. + 把来源转换为代码:识别出部分时调用 `AddRegion`,代码流式到达时调用 `Write`,并用区域 id 把代码块与它的方框关联。完成时结束任务,抛出异常以显示错误,并在上下文的令牌取消时停止。为 null 时播放 `Simulation`(如果有)。 + + + Every word the card shows or announces. + 卡片显示或朗读的每一个词。 + + + The language the code is highlighted as (a TextMate grammar name or alias such as tsx, jsx, html or vue). + 代码高亮所用的语言(TextMate 语法名称或别名,例如 tsx、jsx、html 或 vue)。 + + + The largest file accepted, in bytes (10 MB by default). + 接受的最大文件,单位为字节(默认 10 MB)。 + + + The model credited in the code header: its mark and name. Null credits the simulation's model while it plays, and none otherwise. + 在代码标题栏中注明的模型:它的标记和名称。为 null 时,播放模拟期间注明模拟的模型,否则不注明。 + + + Where the card is in its run. + 卡片在本次运行中所处的阶段。 + + + Renders the finished code in the Preview tab (for example in a sandboxed web view; never run generated code in your app). Return null for no preview. Null uses the simulation's preview after a simulated run, and a short note otherwise. + 在“预览”标签页中渲染完成的代码(例如放在沙箱化的网页视图中;切勿在你的应用内运行生成的代码)。返回 null 表示没有预览。为 null 时,模拟运行之后使用模拟的预览,其他情况显示一条简短说明。 + + + The design and code the card plays while there is no `Generator`: its design shows while `Source` is unset, Use sample brings it back, and Generate (or the autoplay loop) scans it and streams its code exactly like a generator would. Null leaves the card on its drop zone. + 没有 `Generator` 时卡片播放的设计和代码:`Source` 未设置期间显示它的设计,“Use sample” 会把它带回来,“生成”(或自动播放循环)会扫描它并流式输出它的代码,方式与生成器完全相同。为 null 时卡片停在拖放区。 + + + The design shown (two-way). Leave it unset to show the `Simulation`'s design (or the drop zone without one, or when `Generator` is set); set it to null to open on the drop zone. A source without an image draws its `Sketch`. + 显示的设计(双向)。不设置时显示 `Simulation` 的设计(没有模拟或设置了 `Generator` 时显示拖放区);设为 null 则以拖放区打开。没有图像的来源绘制它的 `Sketch`。 + + + The layer open on the output side (two-way). Each new run returns to Code so the stream is visible. + 输出一侧打开的图层(双向)。每次新的运行都会回到“代码”,让流式输出可见。 + + + Arguments of `Completed`: the finished code and what it was made from. + `Completed` 的参数:完成的代码以及它的来源。 + + + What a `Generator` gets besides the source: a token that is cancelled when the person stops the run (Escape or Stop) or replaces the design, `AddRegion` for each layout region as it is detected, and `Write` for each chunk of code as it streams. Both may be called from any thread; calls after the run ended are ignored. + `Generator` 除来源之外得到的内容:一个令牌,在用户停止运行(Escape 或 Stop)或替换设计时取消;每识别出一个布局区域时调用的 `AddRegion`;以及代码流式到达时为每个代码块调用的 `Write`。两者都可以在任意线程调用;运行结束后的调用会被忽略。 + + + Every word `DesignToCode` shows or announces, in English by default. Replace the whole object to localize or reword; sentences with values are format functions so the word order can change. + `DesignToCode` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;带数值的句子是格式化函数,因此可以调整语序。 + + + A layout region found on the design: an id that code chunks refer to, a short label for its tag, and its box in percent of the design's width and height (0 to 100, from the top left). + 在设计上找到的一个布局区域:代码块引用的 id、标签上的简短文字,以及以设计宽度和高度百分比表示的方框(0 到 100,从左上角起算)。 + + + A design and the code a model could write for it, which a `DesignToCode` plays while the app wires no `Generator`: the design (an image or a `Sketch`), the layout regions the scan finds, the code part by part with the region each part implements, and optionally the model credited, the file name and a preview. With `AutoPlay` the card loops it while it is on screen (generate, hold on the code, show the preview, start again) until someone presses or types inside it. Nothing is uploaded or sent. Set the data before handing the simulation to the card; to play another design, give the card a new simulation. + 一个设计以及模型可能为它写出的代码,在应用没有接入 `Generator` 时由 `DesignToCode` 播放:设计(图像或 `Sketch`)、扫描找到的布局区域、逐段的代码及每段实现的区域,以及可选的署名模型、文件名和预览。开启 `AutoPlay` 时,卡片在屏幕上会循环播放它(生成、停留在代码上、显示预览、重新开始),直到有人在卡片内按下或输入。不会上传或发送任何内容。请在把模拟交给卡片之前设置好数据;要播放另一个设计,就给卡片一个新的模拟。 + + + One part of the code of a `DesignToCodeSimulation`: its text and the region it implements. + `DesignToCodeSimulation` 代码中的一段:它的文字以及它实现的区域。 + + + The theme colors, font and corner radius a `DesignToCodeSketch` draws with. + `DesignToCodeSketch` 绘制时使用的主题颜色、字体和圆角。 + + + The design a `DesignToCode` card turns into code: a name, where it came from and its image, or a `Sketch` that draws it in code in the card's colors. + `DesignToCode` 卡片转换为代码的设计:名称、来源以及图像,或者一个用卡片的颜色以代码绘制它的 `Sketch`。 + A focused surface for decisions that need attention: confirmations, short forms, step flows whose title crossfades. Declare it anywhere in XAML; it renders nothing in place and shows its panel above a blurred scrim while `IsOpen` is true: The overlay fades in while the panel rises 8px from 0.96 on the smooth spring; closing is shorter and starts from wherever the entrance is. Escape and a press on the scrim close it (cancellable through `Closing`); Tab cycles inside; focus returns to the opener. Code can await a result with `ShowAsync` or `ConfirmAsync`. 用于需要关注的决策的聚焦表面:确认、简短表单、标题淡入切换的分步流程。可在 XAML 中任意位置声明;它不在原位渲染任何内容,当 `IsOpen` 为 true 时,会在模糊遮罩之上显示其面板。覆盖层淡入,面板以平滑弹簧从 0.96 缩放并上升 8px;关闭比进入更短,并从进入动画当前所在的位置开始。Escape 和在遮罩上按下都会关闭(可通过 `Closing` 取消);Tab 在内部循环;焦点返回到打开它的元素。代码可通过 `ShowAsync` 或 `ConfirmAsync` 等待结果。 @@ -6392,6 +7888,118 @@ Panel width: Small 360, Medium 440 (Arc), Large 560 — always within the viewport minus 32px. 面板宽度:Small 360、Medium 440(Arc)、Large 560——始终不超过视口宽度减去 32px。 + + A chat composer with voice input (Arc Dictation button, design reference): the field, a model label, the dictation mic and send. Pressing the mic springs it into a recording pill with a live waveform, a timer, cancel and Insert; words stream into the field as the person speaks, the recognizer's guesses muted until they settle. Insert appends the text and puts the caret at its end; Enter sends. Speech comes from `Recognizer` (an `IDictationRecognizer` you write for your platform or service). For demos, a `Simulation` speaks the scripts you give it without touching the microphone and, unless `AutoPlay` says otherwise, loops a scripted dictation until the person first touches the card. With neither, the composer is a plain field whose mic says dictation is not available. Handle `SendRequested` to send; without a handler sending is simulated. + 带语音输入的聊天输入框(Arc Dictation button,设计参考):输入框、模型标签、听写麦克风和发送按钮。按下麦克风,它弹性展开为带实时波形、计时器、取消和插入的录音胶囊;用户说话时文字流入输入框,识别器的猜测以浅色显示,确定后转为正文颜色。插入会追加文字并把光标放在末尾;按 Enter 发送。语音来自 `Recognizer`(你为所在平台或服务编写的 `IDictationRecognizer`)。用于演示时,`Simulation` 会在不访问麦克风的情况下朗读你提供的脚本,并且除非 `AutoPlay` 另有设置,会循环播放一段脚本化听写,直到用户第一次操作卡片。两者都没有时,输入框只是一个普通字段,按下麦克风会提示听写不可用。处理 `SendRequested` 来发送;没有处理程序时发送是模拟的。 + + + Loops a scripted dictation (listen, insert, send) until the first pointer, key or focus inside the card; it pauses off screen. Null plays it only with a `Simulation` and no `Recognizer`. For docs and demos, not production. + 循环播放一段脚本化听写(聆听、插入、发送),直到卡片内第一次出现指针、按键或焦点;不在屏幕上时暂停。为 null 时只在有 `Simulation` 且没有 `Recognizer` 时播放。用于文档和演示,不用于生产环境。 + + + Raised with the final text of a dictation, after it has been appended to the field. Bubbles. + 携带听写的最终文字,在它被追加到输入框之后引发。冒泡。 + + + True while a send is pending (the send button shows a spinner). + 发送进行中时为 true(发送按钮显示转圈)。 + + + Every word the composer shows or says. + 输入框显示或朗读的所有文字。 + + + Seconds before listening stops and inserts on its own; 0 turns the limit off. + 聆听自动停止并插入之前的秒数;0 表示不限制。 + + + The model label at the start of the toolbar (the model the composer sends to); null (the default) hides it. + 工具栏开头的模型标签(输入框发送到的模型);为 null(默认)时隐藏。 + + + The field's placeholder, such as "Message" and the assistant's name; null (the default) shows none. + 输入框的占位文字,例如 "Message" 加上助手的名字;为 null(默认)时不显示。 + + + The speech source. Null uses `Simulation`; with neither, the mic says dictation is not available. + 语音来源。为 null 时使用 `Simulation`;两者都没有时,麦克风会提示听写不可用。 + + + Raised to send the message (Enter in the field or the send button). Set `Completion` to your sending task: the button spins until it completes and the field clears when it succeeds. Without a handler the send is simulated. Bubbles. + 发送消息时引发(输入框中按 Enter 或点击发送按钮)。把 `Completion` 设为你的发送任务:按钮会转圈直到任务完成,成功后输入框清空。没有处理程序时发送是模拟的。冒泡。 + + + A simulated voice to dictate with while `Recognizer` is null: it never touches the microphone and speaks its scripts, and with `AutoPlay` left null the composer loops it on its own. Its clock stands still while the card is off screen. Null (the default) plays nothing. Give each composer its own simulation. + 在 `Recognizer` 为 null 时用于听写的模拟声音:它从不访问麦克风,只朗读自己的脚本;`AutoPlay` 保持 null 时输入框会自行循环播放它。卡片不在屏幕上时它的时钟会停止。为 null(默认)时不播放任何内容。请给每个输入框各自的模拟实例。 + + + Where the dictation is. + 听写所处的状态。 + + + Raised on every status change, with the error code for denied and error. Bubbles. + 每次状态变化时引发;被拒绝和出错时带有错误代码。冒泡。 + + + The field's text (two-way): typing, an inserted dictation and the clear after sending change it. + 输入框的文字(双向):输入、插入的听写内容和发送后的清空都会改变它。 + + + Raised whenever the live transcript of the current dictation changes. Bubbles. + 当前听写的实时转写内容每次变化时引发。冒泡。 + + + Every word `DictationButton` and `DictationControl` show or say, in English by default. Replace the whole object to localize or reword; counts, times and errors are format functions. + `DictationButton` 和 `DictationControl` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;计数、时间和错误是格式化函数。 + + + Just the mic and its recording pill, for a composer you already have (Arc Dictation button, design reference). Pressing the mic springs it leftward into a 216px pill: cancel, a live waveform, the elapsed time and Insert, which sits where the mic was. Words arrive through `TranscriptChanged` (render them in your field, the interim part muted) and the final text through `Committed`. A blocked microphone or a failure turns the slot into Try again. Speech comes from `Recognizer`; for demos, a `Simulation` speaks the scripts you give it and records nothing. With neither, pressing the mic says dictation is not available. Same state machine as `DictationButton`, no field. + 只有麦克风和它的录音胶囊,用于你已有的输入框(Arc Dictation button,设计参考)。按下麦克风,它向左弹性展开为 216px 的胶囊:取消、实时波形、已录时长和插入,插入位于麦克风原来的位置。文字通过 `TranscriptChanged` 送达(在你的输入框中渲染,临时部分用浅色),最终文字通过 `Committed` 送达。麦克风被阻止或出错时,该位置变为 Try again。语音来自 `Recognizer`;用于演示时,`Simulation` 朗读你提供的脚本,不录制任何内容。两者都没有时,按下麦克风会提示听写不可用。与 `DictationButton` 使用同一个状态机,但没有输入框。 + + + Raised when the person cancels; drop any live text you rendered. Bubbles. + 用户取消时引发;请丢弃你渲染的实时文字。冒泡。 + + + Raised with the final text of a dictation, to append to your field. Bubbles. + 携带听写的最终文字引发,供追加到你的输入框。冒泡。 + + + Every word the control shows or says. + 控件显示或朗读的所有文字。 + + + Seconds before listening stops and inserts on its own; 0 turns the limit off. + 聆听自动停止并插入之前的秒数;0 表示不限制。 + + + The speech source. Null uses `Simulation`; with neither, starting fails as unavailable. + 语音来源。为 null 时使用 `Simulation`;两者都没有时,启动会以不可用失败。 + + + A simulated voice to dictate with while `Recognizer` is null: it never touches the microphone and speaks its scripts, and the control says nothing is recorded. Null (the default) leaves the mic unavailable. Give each control its own simulation. + 在 `Recognizer` 为 null 时用于听写的模拟声音:它从不访问麦克风,只朗读自己的脚本,控件会说明没有录制任何内容。为 null(默认)时麦克风不可用。请给每个控件各自的模拟实例。 + + + Raised when the recognizer detects the end of an utterance. Do nothing, or confirm hands-free. Bubbles. + 识别器检测到一句话结束时引发。可以什么都不做,或免手动确认。冒泡。 + + + Where the dictation is. + 听写所处的状态。 + + + Raised on every status change, with the error code for denied and error. Bubbles. + 每次状态变化时引发;被拒绝和出错时带有错误代码。冒泡。 + + + The live transcript of the current dictation. + 当前听写的实时转写内容。 + + + Raised whenever the live transcript changes (render it in your field, the interim part muted). Bubbles. + 实时转写内容每次变化时引发(在你的输入框中渲染,临时部分用浅色)。冒泡。 + Turns a document into typed data and shows where each value came from. A quiet viewer of the document sits on the left and an editable form on the right; while a model reads, the fields fill in one by one and the text each value came from is tinted. Pointing at a field or at its text lights both and draws a curved link between them; values the model is unsure of carry its reason and a "Looks right" button; "Accept all" hands the typed result to the app. 把文档转为带类型的数据,并显示每个值的来源。左侧是文档的只读查看器,右侧是可编辑的表单;模型读取时,字段逐个填入,每个值的来源文本会被着色。指向字段或其文本会同时点亮两者,并绘制一条曲线连线;模型不确定的值会附带理由和一个“看起来正确”按钮;“全部接受”把带类型的结果交给应用。 @@ -6516,6 +8124,122 @@ The zoom steps of the buttons and the keys. 按钮和按键的缩放步进。 + + The certificate that was issued once every record was valid. + 证书签发后的信息:所有记录有效之后签发的证书。 + + + Who hosts the domain's DNS, shown as "DNS hosted at {name}" with a small mark. + 托管域名 DNS 的服务商,显示为带一个小标记的“DNS hosted at {name}”。 + + + A DNS record the customer has to create. + 客户需要创建的一条 DNS 记录。 + + + What one check found for one record. + 一次检查对一条记录的发现。 + + + A custom domain setup card. The customer adds a domain and gets the DNS records to create, each with a copy button; a live checker then polls the resolvers, marks every record valid, not found or misconfigured (with what was found and a one-line fix) and finishes with the domain verified and its SSL certificate issued. The three server steps are async delegates: `GetRecordsAction` lists the records, `VerifyAction` is one check and `IssueCertificateAction` runs once every record is valid. The card does the polling, stops on "all valid" or any conflicting record and cancels everything when it is removed or leaves the window. With none of them set it plays a simulated flow (add, check, a conflicting record, fix, verified) until someone clicks or types in it. + 自定义域名设置卡片。客户添加域名并获得需要创建的 DNS 记录(每条都带复制按钮);实时检查器随后轮询解析器,把每条记录标为有效、未找到或配置错误(附带找到的内容和一行修复建议),最后完成域名验证并签发 SSL 证书。三个服务器步骤是异步委托:`GetRecordsAction` 列出记录,`VerifyAction` 做一次检查,`IssueCertificateAction` 在所有记录有效后运行。卡片负责轮询,在“全部有效”或出现冲突记录时停止,并在被移除或离开窗口时取消一切。三个委托都没设置时,它会播放一段模拟流程(添加、检查、冲突记录、修复、验证通过),直到有人在其中点击或输入。 + + + Plays the simulated flow once the card is on screen: add, check, fix, verified. Any click or key press inside the card hands control to the person. Null plays only while no action is set. + 卡片出现在屏幕上后播放模拟流程:添加、检查、修复、验证通过。卡片内的任何点击或按键都会把控制权交给用户。为 null 时仅在没有设置任何动作时播放。 + + + The certificate once it was issued; null before. + 证书签发后的证书;签发之前为 null。 + + + The padding of a record row in the compact layout; the theme sets it from the token. + 紧凑布局下一条记录行的内边距;由主题根据 Token 设置。 + + + Below this width of the card the records stack and the column header leaves (544). + 卡片宽度低于此值时,记录改为堆叠,列标题消失(544)。 + + + The domain the field starts with. A protocol, path, port and trailing dot are stripped before the domain is checked. Empty starts with an empty field. + 输入框的初始域名。检查之前会先去掉协议、路径、端口和末尾的点。为空则输入框初始为空。 + + + The line under the heading (at most 520 wide); null leaves it out. + 标题下方的一行文字(最宽 520);为 null 则不显示。 + + + The domain that was added (host name only); null before one was added and after it was removed. + 已添加的域名(仅主机名);添加之前和移除之后为 null。 + + + Raised when one of the actions threw; the card shows the message and can be tried again. + 某个动作抛出异常时触发;卡片显示消息并可再次尝试。 + + + Lists the records for a domain: awaited when the domain is added, with a token that removing the domain or the card leaving the window cancels. An exception returns to the field with the message under it and raises `Failed`. Null simulates a server (a CNAME, or an A record on an apex domain, an ownership TXT and a CAA). + 列出域名的记录:添加域名时等待它,令牌会在移除域名或卡片离开窗口时取消。异常会回到输入框并在其下显示消息,同时触发 `Failed`。为 null 时模拟服务器(一条 CNAME,顶级域名则为 A 记录,加一条所有权 TXT 和一条 CAA)。 + + + Draws a bordered card with rounded corners (true); false lays the block flat on the page. + 绘制带圆角的边框卡片(true);false 则让区块平铺在页面上。 + + + Issues the SSL certificate once every record is valid; the domain row says "Issuing SSL certificate" until it returns. Null simulates an issuer. + 所有记录有效后签发 SSL 证书;返回之前,域名行显示“Issuing SSL certificate”。为 null 时模拟签发。 + + + Every word the card shows or announces. + 卡片显示或播报的每一个词。 + + + Where the card is in its flow. + 卡片所处的流程阶段。 + + + Raised on every phase change. + 每次阶段变化时触发。 + + + Time between two checks; null uses 800ms while the check is simulated and 5s with a `VerifyAction`. + 两次检查之间的间隔;为 null 时,模拟检查用 800 毫秒,设置了 `VerifyAction` 则用 5 秒。 + + + Who hosts the domain's DNS, shown as "DNS hosted at {name}" with a small mark; null hides the line. + 托管域名 DNS 的服务商,显示为带一个小标记的“DNS hosted at {name}”;为 null 则隐藏这一行。 + + + Raised when the domain is removed (the close button or "Add another"). + 域名被移除时触发(关闭按钮或“Add another”)。 + + + Run with the `DomainRemovedEventArgs` when the domain is removed. + 域名被移除时,以 `DomainRemovedEventArgs` 为参数运行。 + + + The padding of a record row; the theme sets it from the token. + 一条记录行的内边距;由主题根据 Token 设置。 + + + The card's heading; null leaves it out. + 卡片的标题;为 null 则不显示。 + + + Raised once every record is valid and the certificate was issued. + 所有记录有效且证书签发后触发。 + + + Run with the `DomainVerifiedEventArgs` once the certificate is issued. + 证书签发后,以 `DomainVerifiedEventArgs` 为参数运行。 + + + One check of the records: awaited on every poll with the domain, the records and which attempt of which run this is. Return a verdict per record. Return `Missing` while a record propagates and `Invalid` only for a conflicting value. The card polls while any record is missing and stops when all are valid or any is invalid. An exception stops the run, shows the message and raises `Failed`. Null plays a scripted check. + 对记录做一次检查:每次轮询都会等待它,参数是域名、记录,以及这是哪一轮的第几次。为每条记录返回结论。记录仍在生效时返回 `Missing`,只有值冲突时才返回 `Invalid`。只要有记录缺失,卡片就继续轮询,全部有效或任一无效时停止。异常会中止这一轮、显示消息并触发 `Failed`。为 null 时播放脚本化的检查。 + + + The result of one check: a verdict per record and the propagation count. + 一次检查的结果:每条记录的结论和生效统计。 + Arc's donut chart: a few parts of one whole. The legend and the ring show and hide segments together, the center reads the active segment (or the total at rest) and rolls like a drum between readouts, small parts fold into "Other", and new data moves every arc from where it is on screen, keeping its velocity. The first time the chart is seen all segments sweep out of the top together, each trailing edge a beat behind. Arc interpolates start and end angles rather than paths; every angle is a `MotionValue` written into one self-drawn ring, which ControlTheme transitions cannot express. Arc 环形图:表示一个整体的若干部分。图例与圆环同步显示和隐藏分段;中心显示当前激活分段(静止时显示总计),并在读数之间像滚筒一样滚动;过小的部分合并为“Other”;新数据会让每段圆弧从其在屏幕上的当前位置出发,并保留其速度。首次看到该图表时,所有分段会一同从顶部扫出,每段的后缘比前缘晚一拍。Arc 对起始角与结束角进行插值,而非路径;每个角度都是写入一个自绘圆环的 `MotionValue`,这是 ControlTheme 过渡无法表达的。 @@ -6816,6 +8540,122 @@ The title in the Idle phase. Null uses Arc's copy for the scene. Idle 阶段的标题。为 null 时使用 Arc 针对该场景的文案。 + + A table card with an AI column (Arc Enrich column, design reference). One prompt runs down every row; each cell shimmers while it works, then settles into a value with its source, or a failure with a retry. The top bar holds the table's name, the row count and the column button (Run column, Stop, Re-run); the AI column's header shows its name over its prompt with an edit control that opens an editor; the footer names the model and counts the results. Give it `Rows` and an `Enrich` delegate that fills one cell: it gets the row and an `EnrichColumnContext` (the prompt, a cancellation token, the attempt and `SetActivity` for the shimmering label) and returns the value and its source, or throws to fail the cell with the exception's message. At most `Concurrency` cells run at once and the workers start 140ms apart so the column fills top to bottom. Persist values from `CellChanged` and feed them back through `InitialCells`. With no rows of its own the column plays its `Simulation`, a table you supply, and nothing is sent; with neither it shows the empty frame (no rows, Run column unavailable). The rows are a virtualizing list: give the control a height (or a `MaxHeight`) for long tables and only the rows on screen are built; without one every row shows. Below 560px wide the domain, the source text and the model detail fold away; below 440px the gutters tighten, values wrap to two lines and failure reasons hide behind "Failed". + 带一列 AI 列的表格卡片(Arc Enrich column,设计参考)。同一个提示词逐行运行;每个单元格在处理时显示微光,随后落定为带来源的值,或显示可重试的失败。顶栏包含表格名称、行数和列按钮(Run column、Stop、Re-run);AI 列的表头在列名下方显示提示词,并带有打开编辑器的编辑控件;页脚显示模型并统计结果。提供 `Rows` 和一个填充单元格的 `Enrich` 委托:它收到行和一个 `EnrichColumnContext`(提示词、取消令牌、尝试次数,以及设置微光标签的 `SetActivity`),返回值及其来源,或抛出异常,以异常消息把单元格标为失败。同时最多运行 `Concurrency` 个单元格,各工作者相隔 140ms 启动,使这一列从上到下依次填充。在 `CellChanged` 中保存值,并通过 `InitialCells` 传回。这一列自己没有行时,会播放它的 `Simulation`(由你提供的表格),不发送任何内容;两者都没有时显示空框架(没有行,Run column 不可用)。行是虚拟化列表:长表格请给控件设置高度(或 `MaxHeight`),只会创建屏幕上的行;不设置时显示全部行。宽度小于 560px 时,域名、来源文字和模型说明收起;小于 440px 时,左右留白收紧,值换成两行,失败原因隐藏在“Failed”后面。 + + + Runs the column once when it first scrolls into view, if no cell has a value yet. Null (the default) means on while the `Simulation` plays and off with your own `Enrich`. + 在这一列首次滚入视口时运行一次,前提是还没有任何单元格有值。为 null(默认)时,播放 `Simulation` 期间开启,使用你自己的 `Enrich` 则关闭。 + + + Raised whenever a cell changes state (queued, running, done, failed). Use it to persist values. Bubbles. + 每当单元格状态变化(排队、运行、完成、失败)时触发。用它保存值。冒泡。 + + + The AI column's name and prompt (two-way). The person can edit both from the column header; saving re-runs every row. Null shows the simulation's column while it plays, and an unnamed column otherwise. + AI 列的名称和提示词(双向)。用户可以在列标题中编辑两者;保存后会重新运行每一行。为 null 时,播放模拟期间显示模拟的列,否则显示一个未命名的列。 + + + Raised when the person saves an edited column (the column then re-runs). Bubbles. + 用户保存编辑后的列时触发(随后这一列会重新运行)。冒泡。 + + + How many cells run at once. Workers start 140ms apart so the column fills top to bottom. + 同时运行的单元格数。各工作者相隔 140ms 启动,使这一列从上到下依次填充。 + + + Fills one cell: it gets the row and its context (prompt, column, cancellation token, attempt, `SetActivity`) and returns the value with its source. Throw to mark the cell failed with the exception's message. Null plays the `Simulation`'s answers while its rows are shown; with rows of your own and no delegate nothing runs. + 填充一个单元格:它收到行及其上下文(提示词、列定义、取消令牌、尝试次数、`SetActivity`),返回值及其来源。抛出异常即以异常消息把单元格标为失败。为 null 时,显示 `Simulation` 的行期间播放它的答案;使用你自己的行而没有委托时不运行任何内容。 + + + Values you already have, keyed by row id, such as cells saved from an earlier run. + 已有的值,按行 id 索引,例如上一次运行保存下来的单元格。 + + + Shows the edit control on the AI column's header. + 在 AI 列的表头上显示编辑控件。 + + + Whether any cell is queued or running. + 是否有单元格正在排队或运行。 + + + Every word the column shows or says. + 这一列显示或朗读的所有文字。 + + + The model shown in the footer. Null shows the simulation's model while it plays, and hides it otherwise. + 页脚中显示的模型。为 null 时,播放模拟期间显示模拟的模型,否则隐藏。 + + + A short note after the row count. Null shows "sample data" while the simulation plays and nothing otherwise. + 行数后面的简短备注。为 null 时,播放模拟期间显示“sample data”,否则不显示。 + + + The rows to enrich. Null shows the `Simulation`'s rows, or no rows without one. + 要补全的行。为 null 时显示 `Simulation` 的行,没有模拟时不显示任何行。 + + + Raised when a run settles (finished or stopped), with every cell. Bubbles. + 一次运行落定(完成或停止)时触发,并带上所有单元格。冒泡。 + + + The table the column plays on its own while `Rows` is null: your name, column, model, rows and the answer each row settles into, run exactly like your own delegate would (queued, shimmering, settled, a failure to retry). Null shows the empty frame instead. + `Rows` 为 null 时这一列自行播放的表格:你的名称、列、模型、行以及每一行最终得到的答案,运行方式与你自己的委托完全相同(排队、微光、落定、可重试的失败)。为 null 时改为显示空框架。 + + + Raised when the person opens a value's source. Set Handled to open it yourself; otherwise the system opens the address. Bubbles. + 用户打开某个值的来源时触发。设置 Handled 即可自行打开;否则由系统打开该地址。冒泡。 + + + The table's name in the top bar. Null shows the simulation's name while it plays. + 顶栏中的表格名称。为 null 时,播放模拟期间显示模拟的名称。 + + + One cell of the AI column: its status and, once settled, its value and source or the reason it failed. Immutable; the column hands out a new cell on every change. + AI 列的一个单元格:它的状态,以及落定后的值和来源或失败原因。不可变;每次变化时这一列都会给出一个新的单元格。 + + + What the enrich delegate gets with each row: the column and its prompt, a cancellation token that fires when the person stops the run or the column is re-run, the attempt number and `SetActivity` for the shimmering label. + 补全委托随每一行收到的内容:列定义及其提示词、在用户停止运行或重新运行整列时触发的取消令牌、尝试次数,以及设置微光标签的 `SetActivity`。 + + + The AI column: its name in the header and the prompt that runs down every row. + AI 列:表头中的名称,以及逐行运行的提示词。 + + + Every word `EnrichColumn` shows or says, in English by default. Replace the whole object to localize or reword; counts and names are format functions. + `EnrichColumn` 显示或朗读的每个词语,默认为英文。替换整个对象即可本地化或改写;数量和名称是格式化函数。 + + + The model shown in the footer of an `EnrichColumn`. + `EnrichColumn` 页脚中显示的模型。 + + + What the enrich delegate returns for one row: the value and, optionally, its source. + 补全委托为一行返回的结果:值,以及可选的来源。 + + + One row of an `EnrichColumn`: the record the prompt runs on (a company, a lead, a ticket). + `EnrichColumn` 的一行:提示词所作用的记录(一家公司、一条线索、一张工单)。 + + + A table an `EnrichColumn` plays on its own while the app sets no `Rows`: a name, the AI column, the model in the footer, the rows and, for each row, the answer a simulated run settles into. When the column runs (on its own the first time it comes into view, or from Run column), each cell waits through its answer's `Steps`, showing their activities, then shows the value and its source. An answer with an `Error` fails on its first attempt, so the failed state and Retry show. Nothing is sent, and the top bar says the data is a sample. Set the data before handing the simulation to the column; to show another table, give the column a new simulation. + 应用没有设置 `Rows` 时 `EnrichColumn` 自行播放的表格:名称、AI 列、页脚中的模型、行,以及每一行模拟运行后得到的答案。这一列运行时(首次进入视口时自动运行,或通过 Run column),每个单元格依次经过其答案的 `Steps` 并显示它们的活动,然后显示值及其来源。带有 `Error` 的答案在第一次尝试时失败,从而显示失败状态和重试。不会发送任何内容,顶栏会说明这是示例数据。请在把模拟交给这一列之前设置好数据;要显示另一张表格,就给这一列一个新的模拟。 + + + What one row of an `EnrichColumnSimulation` settles into, and how it gets there. + `EnrichColumnSimulation` 中一行最终得到的结果,以及得到它的过程。 + + + One stage of a simulated cell of an `EnrichColumn`: the shimmering activity and how long it lasts. + `EnrichColumn` 模拟单元格的一个阶段:显示微光的活动以及它持续的时间。 + + + Where a value came from: a short label and the address it opens. + 值的出处:一个简短的标签和它打开的地址。 + An environment of `EnvVariables`: a column of the matrix and a choice in the pills and selects. `EnvVariables` 的一个环境:矩阵中的一列,也是胶囊和下拉选择中的一个选项。 @@ -7568,13 +9408,97 @@ The review status. 复核状态。 - - The text. - 文本。 + + The text. + 文本。 + + + The parsed value (date, number, text); null when invalid. + 解析后的值(日期、数字、文本);无效时为 null。 + + + The feedback control of an AI reply (Arc Response feedback, design reference), for your own message rows: thumbs up and down after any `Leading` actions and `Trailing` content pinned to the end of the row. Thumbs down opens an inline panel with reason chips and an optional note; thumbs up fills and asks what was good; a submit shows a short thank-you before the panel folds back into the row. Pressing the active thumb clears the rating. Every thumb press sends a `Rating` submission and the panel sends a `Details` one, through `FeedbackHandler` (a task that throws shows the inline error and Try again; its cancellation token fires when the panel closes or the rating changes) and `FeedbackSubmitted`. Without a handler the sends are simulated and the control says so. + AI 回复的反馈控件(Arc Response feedback,设计参考),用于你自己的消息行:在任意 `Leading` 操作之后是点赞和点踩,`Trailing` 内容固定在行尾。点踩会在行内展开一个带原因标签和可选备注的面板;点赞会填充图标并询问哪里做得好;提交后显示简短的感谢,随后面板收回到操作行中。再次按下当前激活的按钮会清除评分。每次按下点赞或点踩都会发送一个 `Rating` 提交,面板会发送一个 `Details` 提交,经由 `FeedbackHandler`(抛出异常的任务会显示行内错误和 Try again;面板关闭或评分改变时其取消令牌触发)和 `FeedbackSubmitted`。没有处理程序时发送是模拟的,控件会如实说明。 + + + Shows the optional note field after a thumbs down. + 点踩后显示可选的备注框。 + + + The note's maximum length in characters. + 备注的最大长度(字符数)。 + + + The note field's placeholder. + 备注框的占位文字。 + + + Persists feedback: called with a `Rating` submission on every thumb press and a `Details` one when reasons or a note are submitted. Throw (or fault the task) to show the inline error and Try again; the token is cancelled when the panel closes or the rating changes. Null simulates sends. + 持久化反馈:每次按下点赞或点踩时以 `Rating` 提交调用,提交原因或备注时以 `Details` 提交调用。抛出异常(或让任务失败)即可显示行内错误和 Try again;面板关闭或评分改变时令牌会被取消。为 null 时模拟发送。 + + + Raised for every submission, before `FeedbackHandler` runs (and when the send is simulated). Bubbles. + 每次提交时引发,在 `FeedbackHandler` 运行之前(模拟发送时也会引发)。冒泡。 + + + Whether the thumbs can be pressed; turn it off while the reply is still streaming. + 是否可以按下点赞和点踩;回复仍在流式输出时将其关闭。 + + + Every word the control shows or announces. + 控件显示或播报的每一个词。 + + + Actions rendered before the thumbs, such as copy or regenerate. Use 16px icons in small (36px) buttons to match. + 渲染在点赞和点踩之前的操作,例如复制或重新生成。在小号(36px)按钮中使用 16px 图标以保持一致。 + + + Passed back on every submission so feedback can be stored against a message. + 在每次提交时传回,以便把反馈与某条消息关联存储。 + + + Reason chips offered after a thumbs down. A chip labelled Other moves focus to the note and changes its placeholder. + 点踩后提供的原因标签。标为 Other 的标签会把焦点移到备注并更换其占位文字。 + + + Chips offered after a thumbs up. An empty list skips the follow-up row. + 点赞后提供的标签。空列表会跳过后续行。 + + + A quiet line next to Submit about what gets shared. Hidden below 400px of width; null hides it. + Submit 旁边一行低调的文字,说明会分享什么。宽度低于 400px 时隐藏;为 null 时不显示。 + + + The rating (two-way). Set it to show a rating loaded from your database; bind it to own the state. A thumb press sets it and pressing the active thumb sets `None`. + 评分(双向)。设置它可显示从数据库读取的评分;绑定它可自己掌管状态。按下点赞或点踩会设置它,再次按下当前激活的按钮会设为 `None`。 + + + Raised when a thumb press changes the rating, including clearing it. Bubbles. + 按下点赞或点踩改变评分时引发,包括清除评分。冒泡。 + + + How long the thank-you state holds before the panel folds away. + 感谢状态停留多久后面板收起。 + + + The second line of the thank-you state. Null reads "It helps improve future replies." with a `FeedbackHandler`, and says nothing was sent without one. + 感谢状态的第二行。为 null 时,有 `FeedbackHandler` 则显示 "It helps improve future replies.",没有则说明没有发送任何内容。 + + + Content pinned to the end of the row, such as a timestamp or a token count. + 固定在行尾的内容,例如时间戳或令牌数。 - - The parsed value (date, number, text); null when invalid. - 解析后的值(日期、数字、文本);无效时为 null。 + + Arguments of `RatingChanged`. + `RatingChanged` 的参数。 + + + One piece of feedback to store: sent on every thumb press (`Rating`) and when reasons or a note are submitted (`Details`). Store the rating on the first stage and attach the reasons and the note on the second, both against `MessageId`. + 一条要存储的反馈:每次按下点赞或点踩时发送(`Rating`),提交原因或备注时发送(`Details`)。在第一个阶段存储评分,在第二个阶段附加原因和备注,两者都与 `MessageId` 关联。 + + + Arguments of `FeedbackSubmitted`. + `FeedbackSubmitted` 的参数。 Helper copy under the frame (rewrites changed words in place; counts roll). @@ -7980,6 +9904,290 @@ A band that starts at `From` (in value units). The highest band the value reaches sets the ring's tone and names the state under the number, so the state never rests on color alone. 从 `From`(以数值单位计)开始的一个区间。数值所达到的最高区间决定环形的色调,并在数字下方标明状态,因此状态不仅依靠颜色来表达。 + + The model that answered: its name as people know it, your key for its provider, its API id and, optionally, its mark. + 回答问题的模型:人们熟知的名称、你为其服务商设定的键、API id,以及可选的标记。 + + + Prices in US dollars per million tokens. Reasoning tokens bill at the output rate. + 每百万 token 的美元价格。推理 token 按输出单价计费。 + + + A live stats strip for a streaming LLM response (Arc Generation stats, design reference). The head shows the model's mark, its name and a status with an icon and a word (Waiting, Streaming, Done in 4.18s, Stopped at 1.90s, Failed). Below it four cells split by hairlines: time to first token, speed in tokens per second with a small sparkline, output tokens and cost. While the response streams every number counts up in tabular digits and the sparkline draws; when it ends the numbers settle on exact values. Details opens a breakdown with a latency bar (first token, generation, total) and a token table (input, cached input, output, reasoning, total) with rates and cost. A failed response gets a row with the reason and Retry. Feed it from your streaming handler: set `StartedAt` when the request leaves, `Status` Waiting until the first chunk, then Streaming while you set a new `Usage` with the growing output tokens, then Done with the duration. Speed, latency and cost follow. Leave `Status` unset and set `Simulation` to play simulated runs of your own; with neither the strip is idle and every value reads a dash. + 流式 LLM 回复的实时统计条(Arc Generation stats,设计参考)。头部显示模型的标记、名称,以及由图标和文字组成的状态(Waiting、Streaming、Done in 4.18s、Stopped at 1.90s、Failed)。下方是以细线分隔的四个单元格:首个 token 时间、带迷你折线的每秒 token 速度、输出 token 和费用。回复流式输出时,每个数字以等宽数字逐步累加,迷你折线逐步绘出;结束后数字定格在精确值上。Details 会展开明细,其中有延迟条(首个 token、生成、总计)和 token 表(输入、缓存输入、输出、推理、总计)及其单价与费用。失败的回复会多出一行,显示原因和 Retry。用你的流式处理程序提供数据:请求发出时设置 `StartedAt`,在第一个数据块之前 `Status` 为 Waiting,之后为 Streaming,同时每次传入带有不断增长的输出 token 的新 `Usage`,最后以 Done 并附上时长结束。速度、延迟和费用随之得出。不设置 `Status` 并设置 `Simulation`,即可播放你自己的模拟运行;两者都不设置时,统计条处于空闲状态,每个值都显示短横线。 + + + Shows Retry in the error row; it raises `RetryRequested`. + 在错误行中显示 Retry;按下时引发 `RetryRequested`。 + + + Raised when the person presses Details (not when `IsExpanded` is set in code). Bubbles. + 用户按下 Details 时引发(在代码中设置 `IsExpanded` 时不会引发)。冒泡。 + + + The reason shown in the error row when `Status` is Error. + 当 `Status` 为 Error 时,错误行中显示的原因。 + + + Whether the breakdown is open (two-way). Set it in XAML to open it on first render. + 明细是否展开(双向绑定)。在 XAML 中设置可在首次渲染时展开。 + + + Every word the strip shows or says. + 统计条显示或朗读的所有文字。 + + + The model that answered: its name, your provider key, its API id and, optionally, its mark. + 回答问题的模型:名称、你的服务商键、API id,以及可选的标记。 + + + A footnote under the breakdown, for example where the rates come from (text or any content). A playing `Simulation` says its runs are simulated when it is not set. + 明细下方的脚注,例如单价的来源(文字或任意内容)。未设置时,正在播放的 `Simulation` 会说明其运行是模拟的。 + + + Prices in US dollars per million tokens. Enables the Rate and Cost columns and the live cost. Leave it out when the usage carries the provider's cost; the Rate column then hides. + 每百万 token 的美元价格。启用 Rate 和 Cost 列以及实时费用。用量中已带有服务商的费用时可省略;此时 Rate 列隐藏。 + + + Raised when the person presses Retry on a failed response. Bubbles. Send the request again. + 用户在失败的回复上按下 Retry 时引发。冒泡。请重新发送请求。 + + + Simulated runs to play while `Status` is null: they loop while the strip is on screen, hold while the breakdown is open, settle at once under reduced motion and retry from the error row. Null (the default) shows the idle strip. Give each strip its own simulation. + `Status` 为 null 时播放的模拟运行:统计条位于屏幕上时循环播放,明细展开时停留,减弱动效下立即定格,并可从错误行重试。为 null(默认)时显示空闲的统计条。每个统计条使用自己的模拟。 + + + Speed readings in tokens per second, oldest first, for the sparkline. Null (the default) reads the speed every 160ms while the output grows, smoothed, keeping the whole run's shape in at most 48 points. + 供迷你折线使用的速度读数(每秒 token),从旧到新。为 null(默认)时,输出增长期间每 160ms 读取一次速度并平滑,最多用 48 个点保留整个运行的形状。 + + + When the request was sent. While waiting, time to first token counts live from here; while streaming, total latency does. + 请求发出的时间。等待期间,首个 token 时间从这里开始实时计时;流式输出期间,总延迟从这里开始计时。 + + + Where the response is. Null (the default) plays `Simulation`, or shows the idle strip without one. + 回复所处的阶段。为 null(默认)时播放 `Simulation`;没有模拟时显示空闲的统计条。 + + + The token usage and timing. Set a new one as chunks arrive (output tokens), on the first token (`Ttft`) and on the last (`Duration`). Speed and cost are derived unless it carries them. + token 用量与耗时。每个数据块到达时(输出 token)、首个 token 时(`Ttft`)和最后一个 token 时(`Duration`)各设置一个新的值。除非其中已包含,速度和费用会自动推算。 + + + A bordered card with four cells, or one quiet wrapping line for under a chat message. + 带四个单元格的有边框卡片,或用于聊天消息下方的一行安静、可换行的文字。 + + + Every word `GenerationStats` shows or says, in English by default. Replace the whole object to localize or reword; the parts that hold a number or a name are format functions that receive it already formatted. + `GenerationStats` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;包含数字或名称的部分是格式化函数,接收已格式化好的值。 + + + One response a `GenerationStatsSimulation` plays: the model and its prices, the token counts it ends on, how long the first token takes and the peak speed. Give `FailAtTokens` to make it fail once at that output count and succeed when retried. + `GenerationStatsSimulation` 播放的一次回复:模型及其价格、最终的 token 数、首个 token 所需的时间和峰值速度。设置 `FailAtTokens` 可让它在该输出数量处失败一次,重试后成功。 + + + A simulated stream for a `GenerationStats` that has no `Status`: it plays `Runs` in order on its own virtual clock, which the strip advances in 40ms steps while it is on screen (full motion only), so leaving the screen pauses the run. A run waits for its first token, then streams with a ramp up and a few percent of waver, in chunks of four or more tokens, and settles; a run with `FailAtTokens` fails there once and succeeds when retried (by Retry, or on its own after a short wait). A settled run stays on screen while the breakdown is open. Nothing is sent anywhere. Create one per strip: it keeps the playback state. + 供没有 `Status` 的 `GenerationStats` 使用的模拟流:它按顺序在自己的虚拟时钟上播放 `Runs`,统计条位于屏幕上时以 40ms 为步长推进该时钟(仅完整动效),因此离开屏幕会暂停运行。一次运行先等待首个 token,然后以爬升和几个百分点的波动、每块四个或更多 token 的方式流式输出,最后定格;带有 `FailAtTokens` 的运行会在该处失败一次,重试后成功(通过 Retry,或在短暂等待后自动重试)。明细展开时,已定格的运行会停留在屏幕上。不会发送任何内容。每个统计条创建一个:它保存播放状态。 + + + Raised whenever the frame (status, usage, error) changes. + 每当画面(状态、用量、错误)变化时引发。 + + + The token usage and timing of one response, all optional. Make a new one (or a copy with `with`) whenever a value changes: update `OutputTokens` as chunks arrive, set `Ttft` on the first token and `Duration` on the last. Speed and cost are derived unless you pass `TokensPerSecond` or `CostUsd`. + 一次回复的 token 用量与耗时,均为可选。任何值变化时都创建一个新的(或用 `with` 复制):数据块到达时更新 `OutputTokens`,首个 token 时设置 `Ttft`,最后一个时设置 `Duration`。除非你传入 `TokensPerSecond` 或 `CostUsd`,速度和费用会自动推算。 + + + A floating ghost mascot that shows what an assistant is doing (Arc ghost mascot): a soft sheet whose hem ripples while it hovers, a quiet form shade and two simple eyes, drawn in one control. Idle floats, blinks and glances around; listening comes forward with wide eyes and a small head tilt; thinking drifts up, turns translucent and looks up at a swirl that curls in and out; writing leans toward a line it writes with a tiny pen, eyes on the tip; done lifts with happy eyes, a smile and a sparkle; error wobbles once, sinks and droops its eyes. Every status change blends from the pose on screen over 400ms, never a hard cut. Leave `Status` unset and the ghost plays through every state on a loop (for showcases); `StatusChanged` reports each step. + 一只表示助手正在做什么的漂浮幽灵吉祥物(Arc 幽灵吉祥物):悬停时下摆起伏的柔软布单、低调的形体阴影和两只简单的眼睛,绘制在一个控件中。空闲时漂浮、眨眼、四处张望;倾听时向前靠近,睁大眼睛,头微微歪向一侧;思考时向上飘起,变得半透明,抬头看着一个画入又擦去的旋涡;书写时倾向它用小笔写出的一行,眼睛盯着笔尖;完成时升起,眼睛变成开心的弧线,带着微笑和闪光;出错时摇晃一次,下沉,眼睛耷拉。每次状态切换都从屏幕上的姿态在 400ms 内过渡,从不生硬切换。不设置 `Status` 时,幽灵会循环演示每一种状态(用于展示);`StatusChanged` 报告每一步。 + + + Overrides the whole accessible label (null builds it from `AssistantName` and the status). + 替换整个无障碍标签(为 null 时由 `AssistantName` 和状态组成)。 + + + Announces status changes as a polite live region. Turn it on for the one ghost that carries status, not for decorative copies. + 以礼貌的实时区域朗读状态变化。只为承载状态的那一只幽灵打开,装饰用的副本不要打开。 + + + The assistant's name used in the accessible label, for example "Assistant, thinking" (default "Assistant"). + 用于无障碍标签的助手名字,例如“Assistant, thinking”(默认“Assistant”)。 + + + The status the ghost shows: `Status`, or the current self-play step while it is unset. + 幽灵显示的状态:`Status`,未设置时为当前自动演示的步骤。 + + + Any color for the eyes, the happy eyes and the smile. + 眼睛、开心的眼睛和微笑的任意颜色。 + + + Any color for the floating ground shadow (null uses a translucent foreground). + 漂浮地面阴影的任意颜色(为 null 时使用半透明的前景色)。 + + + Freezes the ghost on the current frame; resuming continues from the same frame. The ghost also pauses by itself while it is off screen or hidden. + 把幽灵冻结在当前帧;恢复后从同一帧继续。幽灵在屏幕外或隐藏时也会自行暂停。 + + + Every word of the accessible label. + 无障碍标签中的所有文字。 + + + Any color for the marks: the thinking swirl, the pen and its line, and the sparkle. + 标记的任意颜色:思考的旋涡、笔和它写出的线,以及闪光。 + + + Any color for the form shade clipped to the sheet (null uses a darker, translucent tone of the sheet). + 裁剪在布单内的形体阴影的任意颜色(为 null 时使用布单更深、半透明的色调)。 + + + Any color for the sheet (a solid brush; a gradient uses its first stop). Overrides `Tone`; the eyes become a dark mix of it unless `EyeBrush` is set. + 布单的任意颜色(纯色画刷;渐变取其第一个色标)。覆盖 `Tone`;除非设置了 `EyeBrush`,眼睛会变成它的深色混合。 + + + The soft floating shadow under the ghost, which shrinks and fades as the ghost rises. Drawn only at 56px and up. + 幽灵下方柔和的漂浮阴影,幽灵升高时会缩小变淡。只在 56px 及以上绘制。 + + + Width and height in px (default 96), from 24 inline next to a message to 160 in an empty state. Below 56 a compact drawing takes over: tighter framing, bigger eyes, heavier swirl, pen and line, and no shadow. + 宽度和高度(px,默认 96),从消息旁内联的 24 到空状态中的 160。小于 56 时改用紧凑画法:取景更紧,眼睛更大,旋涡、笔和线更粗,并且没有阴影。 + + + What the assistant is doing. Leave it unset (null) to play through every state on a loop, which is meant for showcases; `StatusChanged` then reports each step. + 助手正在做什么。不设置(null)时循环演示每一种状态,用于展示;此时 `StatusChanged` 报告每一步。 + + + Raised when the self-playing ghost (`Status` unset) moves to the next state. Use it to sync a caption or a control. + 自动演示的幽灵(未设置 `Status`)进入下一个状态时引发。可用它同步说明文字或控件。 + + + The sheet color from the theme: the accent with accent-foreground eyes (default), a pale raised surface with a hairline and foreground eyes (the marks keep the accent), or the foreground ink with background-colored eyes. + 来自主题的布单颜色:强调色配强调前景色的眼睛(默认);浅色的凸起表面加细线和前景色的眼睛(标记保持强调色);或前景色配背景色的眼睛。 + + + Every word a `GhostMascot` speaks, in English by default: the status words of its accessible label and the pattern that joins the assistant's name to them ("Assistant, thinking"). Replace the instance (or set its properties) to translate or reword. + `GhostMascot` 朗读的所有文字,默认为英文:无障碍标签中的状态词,以及把助手名字与状态词连接起来的格式(“Assistant, thinking”)。替换实例(或设置其属性)即可翻译或改写。 + + + Arguments of `StatusChanged`: the status the self-playing ghost moved to. + `StatusChanged` 的参数:自动演示的幽灵进入的状态。 + + + Inline AI autocomplete for a text field (Arc Ghost text, design reference). When the person pauses, muted ghost text continues their sentence right at the caret, streamed from your `Suggest` function. Tab accepts the whole suggestion and it settles from ghost ink to text ink in place; Cmd+Right (Ctrl+Right, Option or Alt+Right too) accepts one word at a time; Escape fades it away; typing the same characters as the ghost keeps it in place instead of asking again. A small chip at the end shows the model's mark and name, a shimmering Thinking while it waits, a Tab key cap and "to accept" when a suggestion is ready (a tap target on touch screens), Accepted with a check after Tab, and No suggestion with Retry when your function throws. The field is a real Avalonia `TextBox` (caret, selection, IME, undo, clipboard and spellcheck stay native); the ghost is the muted tail of the same text layout, so text and ghost wrap identically at any width. A multi-line field grows with its text from `Rows` to `MaxRows` lines on a 180ms tween and has a label row and a footer row; a single-line field scrolls sideways and keeps the chip inline at its end. With no `Suggest` and no text it is a plain field, or plays a `Simulation` you pass in that types, pauses, accepts and loops; clicking it takes over. + 文本框的行内 AI 自动补全(Arc Ghost text,设计参考)。用户停顿时,浅色的幽灵文字会恰好在光标处接着句子写下去,内容由你的 `Suggest` 函数流式提供。Tab 接受整条建议,并让它原地从幽灵墨色过渡到正文墨色;Cmd+Right(Ctrl+Right,Option 或 Alt+Right 亦可)一次接受一个词;Escape 让它淡出;输入与幽灵文字相同的字符会保留它,而不会重新请求。末尾的小标签显示模型的标记与名称,等待时显示闪烁的 Thinking,建议就绪时显示 Tab 键帽与 "to accept"(在触摸屏上可点按),按 Tab 后显示带对勾的 Accepted,函数抛出异常时显示 No suggestion 与 Retry。字段是真正的 Avalonia `TextBox`(光标、选择、输入法、撤销、剪贴板和拼写检查都保持原生);幽灵文字是同一文本布局中浅色的尾部,因此文本与幽灵文字在任何宽度下都以相同方式换行。多行字段以 180ms 补间从 `Rows` 行增高到 `MaxRows` 行,并有标签行和底栏;单行字段横向滚动,标签保持在行尾。没有 `Suggest` 也没有文本时,它是一个普通字段,或播放你传入的 `Simulation`,自动打字、停顿、接受并循环;点击即可接手。 + + + Raised with the text that was accepted: the whole suggestion or the next word. Bubbles. + 以被接受的文字引发:整条建议或下一个词。冒泡。 + + + The pause before `Suggest` is called (450ms). Every keystroke restarts it. + 调用 `Suggest` 前的停顿(450ms)。每次按键都会重新计时。 + + + Raised with the suggestion the person dismissed with Escape. Bubbles. + 以用户用 Escape 关闭的建议引发。冒泡。 + + + Content at the start of the footer row, such as a character count, a note or a send button. Multi-line only. + 底栏开头的内容,例如字数、备注或发送按钮。仅限多行。 + + + Shows the chip, the word and Escape key hints next to it on wide fields, and the keys description for screen readers. + 显示标签、宽字段上标签旁的单词与 Escape 按键提示,以及供屏幕阅读器使用的按键说明。 + + + A growing multi-line field with a label row and a footer row (true), or a single-line field with the chip inline at its end. Line breaks typed or pasted into a single-line field become spaces. + 带标签行和底栏、会增高的多行字段(true),或标签位于行尾的单行字段。输入或粘贴到单行字段的换行会变成空格。 + + + True while a streamed suggestion is still arriving. + 流式建议仍在传入时为 true。 + + + A quiet row above the text, such as a field name with an icon; a string also names the field. Multi-line only. + 文本上方安静的一行,例如带图标的字段名称;字符串还会作为字段的名称。仅限多行。 + + + Every word the field shows or says. + 字段显示或朗读的每一个词。 + + + Lines before a multi-line field stops growing and scrolls, keeping the caret in view (10). + 多行字段停止增高、开始滚动(并保持光标可见)前的行数(10)。 + + + Trimmed characters needed before suggestions start (3). + 开始提供建议所需的去除首尾空白后的字符数(3)。 + + + The model behind the suggestions: its name shows in the idle chip and its mark at 12px in every chip state. + 建议背后的模型:名称显示在空闲状态的标签中,12px 的标记显示在每种标签状态中。 + + + A one-off message for screen readers, said through the field's own polite announcements. + 给屏幕阅读器的一次性消息,通过字段自己的礼貌播报朗读。 + + + Hint shown while the field is empty. + 字段为空时显示的提示。 + + + Minimum visible lines of a multi-line field (3). + 多行字段的最少可见行数(3)。 + + + A simulated session to play while the field has no `Suggest` and no text (demos and prototypes): it types its script, lets the suggestions stream and accepts them, looping; clicking the field takes it over. Null leaves the field to the person. + 字段没有 `Suggest` 也没有文本时要播放的模拟会话(演示与原型):它按剧本输入,让建议流式出现并接受,循环播放;点击字段即可接手。为 null 时字段完全交给用户。 + + + Where the suggestion is: idle, loading, ready or failed. + 建议所处的状态:空闲、加载中、就绪或失败。 + + + Raised when `Status` changes. Bubbles. + `Status` 变化时引发。冒泡。 + + + Returns a continuation for the text before the caret: a string, a task of one, or a stream of chunks (`GhostTextResponse`); null skips without showing Thinking. Cancel your work when the token fires. + 为光标前的文本返回续写:字符串、返回字符串的任务,或分块流(`GhostTextResponse`);返回 null 表示跳过,且不显示 Thinking。令牌触发时请取消你的工作。 + + + The suggestion at the caret (what Tab would accept), or empty. + 光标处的建议(Tab 会接受的内容),或为空。 + + + The text (two-way). Programmatic changes run through the same logic as typing, so a parent can drive the field. + 文本(双向)。以代码修改与打字走同样的逻辑,因此父级可以驱动这个字段。 + + + A bordered field, or plain text that sits flush inside a card you already have. + 带边框的字段,或平贴在你已有卡片中的纯文字。 + + + Every word a `GhostText` shows or says, in English by default: the chip in each state, the key hints, the keys description read with the field and the polite announcements. Replace the whole object to localize or reword; sentences that carry text are functions so word order can change. + `GhostText` 显示或朗读的每一个词,默认为英文:各状态下的标签、按键提示、随字段朗读的按键说明以及礼貌播报。替换整个对象即可本地化或改写;包含文本的句子是函数,以便调整词序。 + + + The answer of a `GhostTextSuggest` function: a whole suggestion now, a task that completes with one, or a stream of chunks that appear one after another (each fades in on its own). A string or a `Task<string?>` converts implicitly; wrap an async iterator with `FromStream`. A null or empty result shows nothing. + `GhostTextSuggest` 函数的回答:现在就给出的整条建议、完成时给出建议的任务,或依次出现的分块流(每块各自淡入)。字符串或 `Task<string?>` 可隐式转换;异步迭代器请用 `FromStream` 包装。结果为 null 或空时不显示任何内容。 + + + A simulated session for a `GhostText`, for demos, prototypes and tests: it types each line of the `Script`, pauses, lets the scripted suggestion stream in 3 to 6 word chunks after a short think, and accepts it; lines alternate between Tab and a word at a time with Cmd+Right, then it rests and starts over. Every step goes through the field's own handling (typing, asking, Tab, the word key), so it shows exactly what a person would see. Set it as the field's `Simulation`: it plays while the field has no `Suggest` and no text, at every motion level, pausing off screen; with reduced motion (or none) it plays once and stops. Clicking or focusing the field hands it to the person, who then gets the scripted suggestions and, for anything else, `Suggest`. Label, placeholder, footer and model stay on the field. Use one simulation per field. + 为 `GhostText` 提供的模拟会话,用于演示、原型和测试:它逐行输入 `Script`,停顿,让剧本中的建议在短暂思考后以 3 到 6 个词的分块流式出现并接受;各行在 Tab 与 Cmd+Right 逐词接受之间交替,然后休息并从头开始。每一步都经过字段自己的处理(输入、请求、Tab、逐词键),所以显示的正是用户会看到的样子。把它设为字段的 `Simulation`:字段没有 `Suggest` 也没有文本时播放它,在所有动效等级下运行,离开屏幕时暂停;减弱动效(或关闭动效)时只播放一次就停止。点击或聚焦字段会把它交给用户,用户随后得到剧本中的建议,其他内容则由 `Suggest` 回答。标签、占位文本、页脚和模型仍设置在字段上。每个字段使用一个模拟。 + + + The lines to type and the suggestions that continue them, in order. + 要输入的各行以及接续它们的建议,按顺序排列。 + + + Suggestions for text the script does not cover, such as what the person types after taking the field over; null suggests nothing there. + 为剧本未覆盖的文本提供建议,例如用户接手字段后输入的内容;为 null 时这些内容不会得到建议。 + + + One line of a `GhostTextSimulation`: the words it types, and the suggestion that continues them. + `GhostTextSimulation` 中的一行:它输入的词,以及接续这些词的建议。 + A grid of square tiles that bends toward the pointer like spacetime around a mass (Arc Gravity well, design reference). Tiles near the well lean in, shrink and dim on their own springs; a click, a tap, Space or Enter sends a shockwave rolling outward through the grid. Each item becomes one tile: photos, avatars, icons or any small content. 一格格正方形图块组成的网格,像时空围绕质量一样朝指针弯曲(Arc 引力井,设计参考)。引力井附近的图块沿各自的弹簧向内倾斜、缩小并变暗;点击、轻触、Space 或 Enter 会发出一道向外滚过网格的冲击波。每个项目成为一个图块:照片、头像、图标或任何小型内容。 @@ -8308,6 +10516,10 @@ A number shown in a `HoverCardProfile` (value over its label). 在 `HoverCardProfile` 中显示的数字(数值位于标签之上)。 + + The playback of fetched speech, supplied by the app (an audio element, a platform player, or an adapter over `IAudioPlayer`). The `SpeechPlayer` drives it and reads `Position` on every frame while it plays, so its position is the player's clock. All calls come from the UI thread. + 由应用提供的已获取语音的播放(音频元素、平台播放器,或基于 `IAudioPlayer` 的适配器)。`SpeechPlayer` 驱动它,并在播放时每一帧读取 `Position`,所以它的位置就是播放器的时钟。所有调用都来自 UI 线程。 + Icons that morph between two states instead of swapping: lines rotate and slide into place, outlines interpolate (P16 `PathMorph`), and a few carry a moment of their own (the bell rings, the heart bursts). Every morph rides an interruptible spring, so toggling quickly reverses from wherever the icon is. Drawn in one `Render` on the 24-unit grid. The icon is decorative: put it in a button that carries the name and the pressed or expanded state. 在两种状态之间变形而非直接切换的图标:线条旋转并滑入就位,轮廓进行插值(P16 `PathMorph`),部分图标还带有自身的动效(铃铛会响,爱心会迸发)。每次变形都沿可被打断的弹簧运行,因此快速切换时会从图标当前位置反向。图标在 24 单位网格上通过一次 `Render` 绘制。该图标仅作装饰:请将其放入带有名称以及按下或展开状态的按钮中。 @@ -8432,6 +10644,258 @@ Shows the captions; they fade as the divider reaches them (Arc `labels={false}` hides them). 显示标注;随分隔线靠近而淡出(Arc `labels={false}` 可将其隐藏)。 + + An image generation card: a prompt, model and aspect chips with Generate, a 2 × 2 grid where each image resolves as a pixel mosaic (coarse colour blocks to full detail under a soft light band) with its own progress ring, and a footer with the status and Upscale, Vary and Download. Pick an image to continue with it: Upscale grows it to fill the grid and sweeps a band of crisp pixels across it, Vary makes four new images from it and Download hands it to the app. Give it a `Generator` that makes one image (it is called once per tile, in parallel, for Generate, Vary and Retry, and reports progress through its `ImageGenerationContext`) and optionally an `Upscaler`. With no generator it plays its `Simulation`, pictures painted by your `Painter`, nothing downloaded, and generates once when it first scrolls into view; with neither, Generate stays unavailable. + 一张图像生成卡片:提示词、模型与画幅标签和 Generate,一个 2 × 2 网格,每张图像以像素马赛克的方式成形(在柔和的光带下从粗糙色块到完整细节),并带有自己的进度环,以及一个显示状态和 Upscale、Vary、Download 的底栏。选中一张图像继续操作:Upscale 把它放大到铺满网格并让一道清晰像素带扫过,Vary 以它为基础生成四张新图,Download 把它交给应用。为它提供一个生成单张图像的 `Generator`(每个格子调用一次,并行执行,用于 Generate、Vary 和 Retry,并通过其 `ImageGenerationContext` 报告进度),以及可选的 `Upscaler`。没有生成器时,它播放它的 `Simulation`:由你的 `Painter` 绘制图片,不下载任何内容,并在第一次滚动进入视口时自动生成一次;两者都没有时,Generate 不可用。 + + + The shapes offered as chips. Null or empty offers 1:1, 4:5 and 16:9. + 作为标签提供的画幅。为 null 或空时提供 1:1、4:5 和 16:9。 + + + Generates once when the card first scrolls into view, then picks image 1 if nobody has touched the card. Null (the default) does so with the simulation only, not with your `Generator`. + 在卡片第一次滚动进入视口时自动生成一次,如果没有人操作过卡片,再选中第 1 张图像。为 null(默认)时只在使用模拟时这样做,使用你的 `Generator` 时不会。 + + + Raised when the person presses Download, with the image and a file name built from the prompt, its number and its size. Save it from here (for example through the window's storage provider). Bubbles. + 用户按下 Download 时触发,附带图像和一个由提示词、编号与尺寸组成的文件名。请在这里保存它(例如通过窗口的存储提供程序)。冒泡。 + + + Makes one image. Called once per tile, in parallel, for Generate, Vary (the request's VariationOf is set) and Retry. Report progress from 0 to 1 through the context, optionally with a preview the mosaic resolves from; throw to mark the tile failed. The context's token is cancelled on Stop. Null uses the `Simulation`'s, if any. + 生成一张图像。每个格子调用一次,并行执行,用于 Generate、Vary(请求的 VariationOf 已设置)和 Retry。通过上下文以 0 到 1 报告进度,可附带供马赛克成形的预览;抛出异常即把该格子标记为失败。按下 Stop 时上下文的令牌会被取消。为 null 时使用 `Simulation` 的生成器(如果有)。 + + + True while the picked image is expanded over the grid. + 选中的图像展开覆盖网格时为 true。 + + + True while a batch runs (Generate reads Stop). + 批次运行时为 true(Generate 显示为 Stop)。 + + + Every word the card shows or announces. + 卡片显示或播报的每一个词。 + + + The models offered as chips. Null or empty offers the simulation's models, or one unnamed model. + 作为标签提供的模型。为 null 或空时提供模拟中的模型,没有模拟时提供一个未命名的模型。 + + + The prompt field's placeholder; null uses `Placeholder`. + 提示词输入框的占位文字;为 null 时使用 `Placeholder`。 + + + The prompt (two-way); the field and the suggestions write it. Null starts with the simulation's prompt, if any. + 提示词(双向);输入框和建议会写入它。为 null 时以模拟的提示词开始(如果有)。 + + + The chosen shape's id (two-way); null or unknown chooses the first. It applies to the next generation; the grid keeps the shape of its batch. + 所选画幅的 id(双向);为 null 或未知时选择第一个。它对下一次生成生效;网格保持本批次的画幅。 + + + The picked image (the upscaled one after an upscale), or null. + 选中的图像(放大后为放大后的图像),或 null。 + + + The chosen model's id (two-way); null or unknown chooses the first model. + 所选模型的 id(双向);为 null 或未知时选择第一个模型。 + + + Raised when the person picks or clears an image, and with the upscaled image once an upscale finishes. Bubbles. + 用户选中或取消选中图像时触发,放大完成后也会以放大后的图像触发。冒泡。 + + + The generator the card plays while `Generator` is null: its prompt, models, suggestions and painter, run exactly like your own generator (four tiles resolving from coarse blocks, Upscale, Vary). Null leaves Generate unavailable instead. + `Generator` 为 null 时卡片播放的生成器:它的提示词、模型、建议和绘制器,运行方式与你自己的生成器完全相同(四个格子从粗糙色块成形、Upscale、Vary)。为 null 时 Generate 不可用。 + + + Prompts offered in the empty grid before the first generation; picking one fills the prompt and generates. Null offers the simulation's, or none. + 第一次生成之前在空网格中提供的提示词;选择其中一个会填入提示词并开始生成。为 null 时提供模拟中的建议,没有模拟时不提供。 + + + Upscales the picked image while it is expanded; return the new image with its width and height. The token is cancelled when the expanded image closes. Null uses the `Simulation`'s, or keeps the image as it is. + 在选中的图像展开时将其放大;返回带有宽度和高度的新图像。展开的图像关闭时令牌会被取消。为 null 时使用 `Simulation` 的放大器,没有模拟时保持图像原样。 + + + An output shape offered as a chip on an `ImageGeneration` card. + 在 `ImageGeneration` 卡片上作为标签提供的输出画幅。 + + + Handed to a generator or upscaler: the cancellation of the run and a way to report progress. + 交给生成器或放大器的对象:本次运行的取消,以及报告进度的方法。 + + + Arguments of `DownloadRequested`. + `DownloadRequested` 的参数。 + + + One generated image. + 一张生成的图像。 + + + Every word `ImageGeneration` shows or says, in English by default. Replace the whole object to localize or reword; text with numbers in it comes from format functions. Image numbers start at 1. + `ImageGeneration` 显示或播报的每一个词,默认为英文。替换整个对象即可本地化或改写;带数字的文字来自格式化函数。图像编号从 1 开始。 + + + A model offered as a chip on an `ImageGeneration` card. + 在 `ImageGeneration` 卡片上作为标签提供的模型。 + + + Paints a picture for an `ImageGenerationSimulation`: `width` × `height` opaque BGRA bytes, row by row (width × height × 4). The same prompt and seed should paint the same picture. Called off the UI thread. + 为 `ImageGenerationSimulation` 绘制一张图片:`width` × `height` 的不透明 BGRA 字节,逐行排列(width × height × 4)。相同的提示词和种子应绘制相同的图片。在 UI 线程之外调用。 + + + What `Generator` is asked to make: one image of a batch. + 要求 `Generator` 生成的内容:一个批次中的一张图像。 + + + Arguments of `SelectionChanged`. + `SelectionChanged` 的参数。 + + + A generator and an upscaler an `ImageGeneration` plays while the app wires no `Generator`, fed by the caller: the prompt it starts with, the models and suggestions on offer, and a `Painter` that paints a picture for a prompt (the same prompt and seed always paint the same picture). Each tile reports progress frame by frame over two to three seconds and resolves from the painted preview; Upscale paints it again at twice the detail over about two seconds. Nothing is downloaded or sent anywhere. Set the data before handing the simulation to the card; to offer other pictures, give the card a new simulation. + 在应用没有接入 `Generator` 时由 `ImageGeneration` 播放的生成器和放大器,数据由调用方提供:起始的提示词、可选的模型和建议,以及一个为提示词绘制图片的 `Painter`(相同的提示词和种子总是绘制相同的图片)。每个格子在两到三秒内逐帧报告进度,并从绘制的预览中逐步成形;Upscale 在约两秒内以两倍细节重新绘制。不会下载或发送任何内容。请在把模拟交给卡片之前设置好数据;要提供其他图片,就给卡片一个新的模拟。 + + + An image generation canvas (Arc Image reveal, design reference): the way current image models show a picture forming. A soft, heavily blurred preview made from the image's own colors, with fine film grain, sharpens top to bottom behind a soft-edged band while the rest gently comes into focus; a translucent pill shows a progress ring and the step count; queued and failed jobs get a calm surface with one soft shimmer, or an alert, the message and Retry. Drive it with `Status`, `Progress` and `Source` (coarse progress updates are smoothed into one continuous reveal; without progress it estimates from `Duration`), or hand it a `Generator` that reports progress and returns the image. A `Source` alone is shown finished. For demos, a `Simulation` plays a looping, simulated generation over the pictures it is given; with nothing at all the canvas rests on its plain surface. The canvas fills its container up to 560px wide and keeps `AspectRatio`. + 图像生成画布(Arc Image reveal,设计参考):以当下图像模型的方式展示图片逐渐成形。一张由图像自身颜色生成、带细腻胶片颗粒的高度模糊柔和预览,在一条边缘柔和的光带后方自上而下变清晰,其余部分慢慢对焦;半透明的胶囊显示进度环与步数;排队中和失败的任务显示一块安静的表面与一道柔和的微光,或警示图标、错误信息与 Retry。用 `Status`、`Progress` 与 `Source` 驱动它(粗粒度的进度更新会被平滑成一段连续的显现;没有进度时按 `Duration` 估算),或交给它一个报告进度并返回图片的 `Generator`。只设置 `Source` 时,图片直接以完成状态显示。用于演示时,`Simulation` 会在传入的图片上播放循环的模拟生成;什么都没有设置时,画布停在它的素色表面上。画布填满容器,最宽 560px,并保持 `AspectRatio`。 + + + The status on screen, in every mode (the simulation's and the generator's too). + 屏幕上显示的状态,适用于所有模式(包括模拟模式与生成器模式)。 + + + Shows a Retry button in the error state; it raises `RetryRequested` (and reruns the `Generator`). + 在错误状态下显示 Retry 按钮;它会引发 `RetryRequested`(并重新运行 `Generator`)。 + + + The image's name once it is done (the canvas's accessible name); "Generated image" by default. + 图片完成后的名称(画布的无障碍名称);默认为 "Generated image"。 + + + The canvas's width over its height (1.5, or 0.8 for 4 / 5); the image is cover-cropped to it. + 画布的宽度除以高度(1.5,或 4 / 5 对应的 0.8);图片按覆盖方式裁剪到该比例。 + + + Simulation and generator: starts a run as soon as the control is shown. Otherwise it waits in the queued look for `Restart`. + 模拟模式与生成器模式:控件一显示就开始运行。否则它以排队外观等待 `Restart`。 + + + Raised once per run when the final image is fully resolved and crisp. Bubbles. + 每次运行中最终图片完全显现并变清晰时引发一次。冒泡。 + + + How long one generation takes when progress is not given (the simulation's length, or the estimate's). + 未提供进度时一次生成所需的时间(模拟的时长,或估算的时长)。 + + + The message of the error state; "Generation failed" by default. + 错误状态的信息;默认为 "Generation failed"。 + + + Runs one image job: it reports progress (and the image as soon as it has one) and returns the final image. Used when `Status` is null. + 运行一次图像任务:报告进度(以及一旦拿到就附上的图片),并返回最终图片。在 `Status` 为 null 时使用。 + + + Simulation only: how long the finished image holds before a loop restarts. + 仅限模拟:完成的图片在循环重新开始前停留的时间。 + + + Simulation only: after `HoldDuration` the image blurs back out and generates again. + 仅限模拟:经过 `HoldDuration` 后,图片重新模糊淡出并再次生成。 + + + Every word the control shows or says. + 控件显示或朗读的每一个词。 + + + Controlled progress from 0 to 1 while generating; coarse updates (one per model step) are smoothed into continuous motion. Null estimates it from `Duration`. + 生成期间受控的进度,取值 0 到 1;粗粒度的更新(每个模型步骤一次)会被平滑成连续的运动。为 null 时按 `Duration` 估算。 + + + Simulation only: how long each run waits in the queue. + 仅限模拟:每次运行在队列中等待的时间。 + + + The chip while queued, such as "Queued, 2 ahead"; "Queued" by default. + 排队时的标签,例如 "Queued, 2 ahead";默认为 "Queued"。 + + + Raised when the person presses Retry in the error state. Bubbles. Restart your job, then set `Status`. + 在错误状态下按下 Retry 时引发。冒泡。请重新启动你的任务,然后设置 `Status`。 + + + The progress ring's color; the accent by default. + 进度环的颜色;默认为强调色。 + + + Simulation and generator: change it to blur back out and run again (a Generate again button can bind its count here). + 模拟模式与生成器模式:修改它会让图片重新模糊淡出并再次运行("Generate again" 按钮可以把计数绑定到这里)。 + + + Seeds the film grain and the drift of the soft preview (and which of the simulation's pictures comes first), so tiles in a grid differ. + 为胶片颗粒与柔和预览的漂移(以及模拟中最先出现的是哪张图片)提供种子,让网格中的图块各不相同。 + + + Shows the translucent progress pill. It fades out shortly after the image resolves. + 显示半透明的进度胶囊。图片显现完成后不久它会淡出。 + + + Plays one soft light sheen across the image when it resolves. Skipped with reduced motion. + 图片显现完成时让一道柔和的光泽横扫而过。减弱动效时跳过。 + + + A simulated job to play while `Status`, `Generator` and `Source` are null: it reveals its pictures in turn on the timing of `Duration`, `QueueDuration` and `HoldDuration`. Null (the default) leaves the canvas on its plain surface. Give each control its own simulation. + 在 `Status`、`Generator` 与 `Source` 都为 null 时播放的模拟任务:按 `Duration`、`QueueDuration` 与 `HoldDuration` 的节奏轮流揭示它的图片。为 null(默认)时画布停在素色表面上。请给每个控件各自的模拟实例。 + + + The overlay's scale and the corner radius: Small (grids, thumbnails) shows only the ring and the count; Medium and Large add the Generating label. + 叠加层的比例与圆角:Small(网格、缩略图)只显示进度环与步数;Medium 和 Large 另加 Generating 标签。 + + + The final image (any `IImage`, usually a decoded `Bitmap`); it is cover-cropped to the canvas. With no `Status` and no `Generator` it is simply shown, finished (a simulation is set aside). + 最终图片(任何 `IImage`,通常是已解码的 `Bitmap`);按覆盖方式裁剪到画布。没有 `Status` 也没有 `Generator` 时,它直接以完成状态显示(模拟会被搁置)。 + + + Simulation only: extra queue time before each run, for staggered grids. + 仅限模拟:每次运行前额外的排队时间,用于错开网格中的图块。 + + + The job's status when the app controls it. Null runs the generator, shows `Source`, or plays the `Simulation` (queued, generating over `Duration`, done). Every change transitions from the current pose. + 由应用控制时任务的状态。为 null 时运行生成器、显示 `Source`,或播放 `Simulation`(排队、在 `Duration` 内生成、完成)。每次变化都从当前姿态过渡。 + + + Raised when the status on screen changes, including the simulation's transitions. Bubbles. + 屏幕上显示的状态变化时引发,包括模拟中的状态切换。冒泡。 + + + The total steps the pill counts, as in 18 / 30 (tabular numerals, reserved width). + 胶囊计数的总步数,如 18 / 30(等宽数字,宽度预留)。 + + + Every word `ImageReveal` shows or says, in English by default. Replace the whole object to localize or reword; the counts are format functions. `QueueLabel` and `ErrorMessage` override the queued chip and the error message for one job. + `ImageReveal` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;计数是格式化函数。`QueueLabel` 与 `ErrorMessage` 可为单个任务覆盖排队标签与错误信息。 + + + One progress report from an `ImageRevealGenerator`: how far the job is (0 to 1) and, when the service has one, the image so far (the soft preview is sampled from it, so pass it as soon as it exists). + 来自 `ImageRevealGenerator` 的一次进度报告:任务进行到哪里(0 到 1),以及服务已有图片时的当前图片(柔和预览从它取样,所以一有图片就传入)。 + + + A simulated image job for an `ImageReveal` that has no `Status`, `Generator` or `Source`, for demos and prototypes. Each run waits in the queue for `QueueDuration` (plus `StartDelay`), generates over `Duration` and reveals one of `Pictures`; with `IsLooping` it holds for `HoldDuration`, blurs back out and runs again with the next picture. The first run shows the picture `Seed` picks (seed 1 the first, seed 2 the second, wrapping), so tiles in a grid differ. Announcements say "simulated". Nothing is generated or downloaded. Create one per control: it keeps its place in the pictures. + 用于演示和原型的 `ImageReveal` 模拟图像任务,适用于没有 `Status`、`Generator` 或 `Source` 的控件。每次运行先在队列中等待 `QueueDuration`(加上 `StartDelay`),再在 `Duration` 内生成并揭示 `Pictures` 中的一张;开启 `IsLooping` 时停留 `HoldDuration`,然后重新模糊淡出,并以下一张图片再次运行。第一次运行显示 `Seed` 选中的图片(种子 1 为第一张,种子 2 为第二张,循环取用),让网格中的图块各不相同。播报会注明 "simulated"。不会生成或下载任何内容。请为每个控件创建一个实例:它会记住自己播放到哪张图片。 + + + One picture of an `ImageRevealSimulation`: the image a simulated run reveals and its name. + `ImageRevealSimulation` 中的一张图片:模拟运行所揭示的图片及其名称。 + + + Where an `ImageReveal` job is. + `ImageReveal` 任务所处的阶段。 + + + Arguments of `StatusChanged`. + `StatusChanged` 的参数。 + A section title that reveals itself when 45% of it scrolls into view (Arc InViewTitle, P18 + P20): opacity leads, the blur clears next and the movement settles last, so the title is readable before it fully arrives. Titles already scrolled past show at once. With `IsReplayEnabled` it hides again (a quick fade with no stagger) when it leaves and replays on every entry. Reduced motion shows the title without movement, blur or masks. Drawn as one control: no per-word controls. 当其 45% 滚动进入视野时自行显现的章节标题(Arc InViewTitle,P18 + P20):不透明度先行,模糊随后消退,位移最后收尾,因此标题在完全到位之前即可阅读。已滚过的标题会立即显示。设置 `IsReplayEnabled` 后,标题离开时会再次隐藏(快速淡出,无错开),并在每次进入时重新播放。减少动效时标题直接显示,不带位移、模糊或遮罩。它作为一个控件绘制:没有逐词控件。 @@ -8628,6 +11092,146 @@ Arc input: a single-line field with a visible label, helper and error rows that open on a spring and rewrite only the words that change, and the Arc frame states (hover and focus darken the border, an error turns it danger, disabled fades). Everything from Avalonia's TextBox is kept: caret, selection, IME, undo, clipboard and a two-way `Text`. Arc 输入框:带可见标签的单行字段,辅助文字行与错误行以弹簧展开,只改写变化的词;外框状态与 Arc 一致(悬停与聚焦加深边框,错误时变为危险色,禁用时变淡)。Avalonia TextBox 的能力全部保留:光标、选择、输入法、撤销、剪贴板与双向绑定的 `Text`。 + + A "get started in 30 seconds" card for SDK onboarding (Arc Install snippet, design reference): framework tabs, three numbered steps (Install, Add your key, Ship) and highlighted snippets that take a developer from install to the first event. Step 1 offers npm, pnpm, yarn and bun when the framework installs packages; every snippet has a copy button; Generate key wipes a fresh key into the placeholder in place. Snippets, file names and step lines crossfade in space that is reserved for the longest of them, so switching frameworks never moves anything. Give it your `Frameworks` (write `{{key}}` where the key goes) and a `KeyGenerator` that creates a real key, or pass `ApiKey` for a reader who already has one. With no frameworks it shows the SDK of its `Simulation`, if any, and otherwise the three steps with empty snippets; with no generator it invents keys (nothing is sent, and step 2 says so). + 用于 SDK 引导的“30 秒上手”卡片(Arc Install snippet,设计参考):框架标签、三个编号步骤(安装、添加密钥、上线)和高亮代码片段,带开发者从安装走到第一个事件。当框架通过包安装时,步骤 1 提供 npm、pnpm、yarn 和 bun;每段片段都有复制按钮;“生成密钥”会把新密钥就地擦入占位符。片段、文件名和步骤说明在为其中最长者预留的空间里交叉淡化,因此切换框架不会移动任何东西。传入你的 `Frameworks`(在密钥所在位置写 `{{key}}`)和能创建真实密钥的 `KeyGenerator`,或为已有密钥的读者传入 `ApiKey`。没有框架时显示其 `Simulation` 的 SDK(如果有),否则显示三个步骤和空的代码片段;没有生成器时会编造密钥(不发送任何内容,步骤 2 会如实说明)。 + + + The reader's own key. When set, the snippets show it and the Generate key button is hidden, for signed-in readers who already have a key. + 读者自己的密钥。设置后,各片段显示该密钥,“生成密钥”按钮隐藏,适用于已登录且已有密钥的读者。 + + + Below this width the framework tabs drop their marks and the version label hides. + 宽度低于此值时,框架标签去掉标记,版本标签隐藏。 + + + Raised after a copy with the step and the exact clipboard text (onboarding analytics). Bubbles. + 复制完成后引发,带有步骤和放入剪贴板的确切文本(用于引导流程分析)。会冒泡。 + + + The key in the snippets: `ApiKey`, else the last generated key, else null (the placeholder shows). + 片段中的密钥:`ApiKey`,否则为最近生成的密钥,否则为 null(显示占位符)。 + + + One line under the title. + 标题下方的一行说明。 + + + Raised when the reader picks another framework tab. Bubbles. + 读者选择另一个框架标签时引发。会冒泡。 + + + Raised with each new key once it is in the snippets. Bubbles. + 每个新密钥进入片段后引发,带有该密钥。会冒泡。 + + + Creates a real key. Return the key and it wipes into the snippets; throw to show the error with Try again. The token is cancelled when the card leaves the window or a new key is requested. Null invents a demo key (nothing is sent). + 创建真实密钥。返回密钥后它会擦入各片段;抛出异常则显示错误和“重试”。卡片离开窗口或再次请求密钥时,令牌会被取消。为 null 时编造演示密钥(不发送任何内容)。 + + + Shown, tinted, wherever `{{key}}` appears until a key exists; copies as written. "YOUR_API_KEY" by default. + 在密钥出现之前,显示在每个 `{{key}}` 处(带底色);按原样复制。默认为 "YOUR_API_KEY"。 + + + Where the Generate key button is: idle, generating, ready or failed. + “生成密钥”按钮所处的状态:空闲、生成中、就绪或失败。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + Below this width the padding tightens and the package manager tabs show marks only. + 宽度低于此值时,内边距收紧,包管理器标签只显示标记。 + + + The package manager of step 1 (two-way). Persist it to remember the reader's choice across pages. + 步骤 1 使用的包管理器(双向)。持久保存它,即可在不同页面间记住读者的选择。 + + + Raised when the reader picks another package manager. Bubbles. + 读者选择另一个包管理器时引发。会冒泡。 + + + The selected framework's id (two-way); null selects the first. + 所选框架的 id(双向);为 null 时选中第一个。 + + + A simulated SDK to show while `Frameworks` is empty: its framework tabs and the prefix of the keys the simulated Generate key invents. Null (the default) shows the three steps with empty snippets. Give each card its own simulation. + 在 `Frameworks` 为空时显示的模拟 SDK:它的框架标签,以及模拟的“生成密钥”所编造密钥的前缀。为 null(默认)时显示三个步骤和空的代码片段。请给每张卡片各自的模拟实例。 + + + The card's title. + 卡片的标题。 + + + A muted label at the top right, such as the SDK version; null (the default) or empty hides it. + 右上角柔和的标签,例如 SDK 版本;为 null(默认)或空时隐藏。 + + + One snippet of an `InstallSnippetFramework`: the file it goes in, its code and one line saying what to do with it. Write `{{key}}` where the API key goes. + `InstallSnippetFramework` 中的一段片段:它所属的文件、代码,以及一行说明该怎么用。在 API 密钥所在位置写 `{{key}}`。 + + + The code; `{{key}}` marks where the API key goes. Keep it under ten lines. + 代码;`{{key}}` 标出 API 密钥所在位置。保持在十行以内。 + + + The line under the step's title; null uses the step's default from the labels. + 步骤标题下方的一行说明;为 null 时使用标签文案中该步骤的默认说明。 + + + The file name in the code block's header, such as `.env` or `app.py`; "Terminal" for a command. + 代码块标题栏中的文件名,例如 `.env` 或 `app.py`;命令使用 “Terminal”。 + + + The highlighting language (`bash`, `ts`, `python`, `go`, `html`, ...); null infers it from the file name. + 高亮所用的语言(`bash`、`ts`、`python`、`go`、`html` 等);为 null 时根据文件名推断。 + + + One framework tab of an `InstallSnippet`: its id and label, an optional mark, and the three snippets. Step 1 is a package install when `Packages` is set (the reader picks npm, pnpm, yarn or bun) and `Install` otherwise (a command such as `pip install`, or a script tag). + `InstallSnippet` 的一个框架标签:它的 id 和名称、可选的标记,以及三段片段。设置了 `Packages` 时,步骤 1 是包安装(读者可选 npm、pnpm、yarn 或 bun),否则使用 `Install`(例如 `pip install` 之类的命令,或脚本标签)。 + + + Step 3: the code that sends the first event. + 步骤 3:发送第一个事件的代码。 + + + A stable id, such as `node`. The ids `node`, `python`, `go` and `html` get a mark of their own. + 稳定的 id,例如 `node`。id 为 `node`、`python`、`go` 和 `html` 时有各自的标记。 + + + Step 1 when there are no `Packages`: a command or a script tag. + 没有 `Packages` 时的步骤 1:一条命令或一个脚本标签。 + + + Step 2: where the key goes, with `{{key}}` in the code. + 步骤 2:密钥放在哪里,代码中写 `{{key}}`。 + + + The tab's name. + 标签的名称。 + + + The tab's mark, a 24 × 24 stroked glyph; null uses the built-in mark of a known id, or none. + 标签的标记,24 × 24 的描边图形;为 null 时使用已知 id 的内置标记,否则不显示。 + + + Package names for step 1, separated by spaces (`@acme/sdk`); the command follows the package manager. + 步骤 1 的包名,以空格分隔(`@acme/sdk`);命令随包管理器变化。 + + + The description of step 1 for a package install; null uses the label's default. + 包安装时步骤 1 的说明;为 null 时使用标签文案的默认值。 + + + Every word `InstallSnippet` shows or says, in English by default (the card's title, description and version are properties of the card). Replace the whole object to localize or reword; announcements that name something are format functions. + `InstallSnippet` 显示或朗读的每一个词,默认为英文(卡片的标题、说明和版本是卡片自身的属性)。替换整个对象即可本地化或改写措辞;需要代入名称的播报是格式化函数。 + + + A simulated SDK for an `InstallSnippet` that has no `Frameworks`, for demos and prototypes: the framework tabs it shows and the prefix of the keys its simulated Generate key invents (nothing is created or sent, and step 2 says so). The app's own frameworks set it aside. Create one per card. + 用于演示和原型、供没有 `Frameworks` 的 `InstallSnippet` 使用的模拟 SDK:它显示的框架标签,以及模拟的“生成密钥”所编造密钥的前缀(不会创建或发送任何内容,步骤 2 会如实说明)。应用自己的框架会让它让位。请为每张卡片创建一个实例。 + A filter of `Integrations`: the items whose `Category` is its key. `Integrations` 的筛选项:`Category` 等于其键的那些项。 @@ -10368,53 +12972,229 @@ Writes a value wherever one is shown or spoken; without one, values are whole numbers like "2,316". 在显示或朗读数值的地方输出文字;不提供时,数值为整数,形如 "2,316"。 - - The name of the measured quantity ("Active"), above the current value and in the slider's spoken value. - 被测量的量的名称("Active"),显示在当前值上方,也用于滑块朗读的数值。 + + The name of the measured quantity ("Active"), above the current value and in the slider's spoken value. + 被测量的量的名称("Active"),显示在当前值上方,也用于滑块朗读的数值。 + + + The readout's ratio label; {0} is `BaselineLabel` with a lower-case first letter. + 读数中比值的标签;{0} 为首字母小写的 `BaselineLabel`。 + + + An event to point out in a `LineReplay`, timed on the same clock as the data. It gets a row in the list of moments, a tick on the slider, and a dot and a card on the chart once the replay gets there. + 在 `LineReplay` 中要指出的事件,与数据使用同一个时钟。它在时刻列表中占一行,在滑块上有一个刻度,回放到达时在图表上显示一个圆点和一张卡片。 + + + One optional line of detail under the title. + 标题下方可选的一行说明。 + + + A stable id, reported back by `MomentReached`. + 稳定的标识,会由 `MomentReached` 报告回来。 + + + The sort of event, which decides the icon (problems also use the warning color). + 事件的类型,决定图标(问题类事件还会使用警告色)。 + + + The clock time of the event in minutes; it may fall between two samples. + 事件的时钟时间,以分钟计;可以落在两个采样之间。 + + + A short name for the event ("Release notes published"). + 事件的简短名称("Release notes published")。 + + + The sort of event a `LineReplayMoment` marks, which decides its icon. + `LineReplayMoment` 所标记的事件类型,决定其图标。 + + + A sample for a `LineReplay`: when it was taken (`Time`, minutes on the block's clock), what it measured (`Value`), and optionally what a normal day measured then (`Baseline`), which the chart shows as a dashed line. + `LineReplay` 的一个采样:采样时间(`Time`,区块时钟上的分钟数)、测得的值(`Value`),以及可选的、平常日子在该时刻的值(`Baseline`),图表会把它画成虚线。 + + + How a `LineReplay` looks before anyone has played it. + `LineReplay` 在还没有人播放之前的样子。 + + + Shows the cell as an accent-colored link with an underline that strengthens on hover. A click on the text raises `CellInvoked` with `Part`; the cell is not selected by that click. + 把单元格显示为强调色链接,下划线在悬停时加深。点击文字会以 `Part` 触发 `CellInvoked`;该次点击不会选中单元格。 + + + A realtime visitors card (Arc Live visitors, design reference): a pulsing live status, the number of people on the site rolling digit by digit, a dotted world map where new arrivals ping in and fade, the top pages right now with bars that reorder as counts change, and a ticker of signups and upgrades with the country and how long ago. A pause button holds the last snapshot. Feed it from your realtime source: set `Count`, `Pages`, `Visitors` and `Events` to new snapshots as they arrive (the card works out what moved from the ids), or push single arrivals and conversions with `PushVisitor` and `PushEvent`. Reflect the socket's state in `Connection`. Without `Count` the card plays its `Simulation`, a `LiveVisitorsSimulation` of a site you describe; with neither, it holds placeholders and says there is no data. + 实时访客卡片(Arc Live visitors,设计参考):脉动的实时状态、逐位滚动的站内人数、新访客闪现后淡去的点阵世界地图、带条形且随人数重新排序的当前热门页面,以及显示国家和多久之前的注册与升级动态。暂停按钮保留最后一份快照。从你的实时数据源获取数据:在新快照到达时设置 `Count`、`Pages`、`Visitors` 和 `Events`(卡片根据 id 判断哪些发生了变化),或用 `PushVisitor` 和 `PushEvent` 逐条推送访客与转化。用 `Connection` 反映连接状态。没有 `Count` 时,卡片播放它的 `Simulation`,即一个描述你所指定网站的 `LiveVisitorsSimulation`;两者都没有时,卡片保持占位状态并说明没有数据。 + + + The stream's state: Connecting shows placeholders in the same space, Reconnecting keeps the last data with a warning dot. + 数据流的状态:Connecting 在同样的位置显示占位,Reconnecting 保留最后的数据并显示警告色圆点。 + + + People on the site right now. Leave it null to play the `Simulation`; once set, the card shows only the data you pass. + 此刻站内的人数。保持为 null 时播放 `Simulation`;一旦设置,卡片只显示你传入的数据。 + + + Recent conversions. The card lists the newest few; with a position the newest is labelled on the map for 2s. + 最近的转化。卡片列出最新的几次;带有位置时,最新的一次会在地图上标注 2 秒。 + + + Shows the pause button. + 显示暂停按钮。 + + + Whether updates are paused (two-way). While paused the card holds its last snapshot, whatever you keep passing. + 是否暂停更新(双向)。暂停期间卡片保留最后一份快照,不管你继续传入什么。 + + + Every word the card shows or says. + 卡片显示或朗读的每个词。 + + + How many conversions to list. + 列出多少次转化。 + + + How many pages to list. + 列出多少个页面。 + + + Pages with people on them. The card sorts them and lists the busiest few with bars scaled to the busiest page; rows slide when the order changes. + 有人正在浏览的页面。卡片对它们排序,列出最热门的几个,条形按最热门页面缩放;顺序变化时各行滑动。 + + + Raised when the person presses the pause button. Bubbles. Pause or close your own subscription to match. + 用户按下暂停按钮时引发。冒泡。相应地暂停或关闭你自己的订阅。 + + + A simulated stream to play while `Count` is null: its count, pages, arrivals and conversions, advanced by the card's clock while it is on screen, live and not paused. Null (the default) leaves the card empty: placeholders and a "No data" status. Give each card its own simulation. + 在 `Count` 为 null 时播放的模拟数据流:它的人数、页面、访客和转化,在卡片位于屏幕上、处于 Live 且未暂停时由卡片的时钟推进。为 null(默认)时卡片为空:显示占位内容和 "No data" 状态。请给每张卡片各自的模拟实例。 + + + Recent arrivals as map dots. New ids ping in, ids no longer passed fade out; keep it to the last few seconds. + 最近到达的访客,显示为地图上的圆点。新的 id 闪现,不再传入的 id 淡出;只保留最近几秒的访客。 + + + A recent conversion in the `LiveVisitors` ticker: what happened (past tense, "Signed up"), where and when. Never put personal data such as an email address in it. + `LiveVisitors` 动态中的一次最近转化:发生了什么(过去时,"Signed up")、在哪里、何时。绝不要在其中放入个人数据,例如电子邮件地址。 + + + Every word `LiveVisitors` shows or says, in English by default. Replace the whole object to localize or reword; counts, times and sentences are format functions. + `LiveVisitors` 显示或朗读的每个词,默认是英文。替换整个对象即可本地化或改写;人数、时间和句子是格式化函数。 + + + A page with people on it, listed by `LiveVisitors` with a bar scaled to the busiest page. + 有人正在浏览的页面,由 `LiveVisitors` 列出,条形按最热门页面缩放。 + + + A recent arrival drawn on the `LiveVisitors` map. A new `Id` pings in; an id that is no longer passed fades out. Send arrivals as a rolling window of the last few seconds. + 绘制在 `LiveVisitors` 地图上的一位最近访客。新的 `Id` 闪现;不再传入的 id 淡出。以最近几秒的滚动窗口发送访客。 + + + A simulated visitors stream for a `LiveVisitors` card that has no `Count`, for demos, prototypes and tests. It plays the site you describe: about `TypicalCount` people spread over `SitePages`, a new arrival from one of `Cities` every 600ms, page counts that drift, and one of `Conversions` every 2.4s; it starts with a few conversions from the last minutes. Nothing is fetched or tracked. It has no timer of its own: call `Advance` with the time that passed (the card does this from its clock while it is on screen and not paused). Setting any data property starts the stream over. Create one per card. + 用于演示、原型和测试,供没有 `Count` 的 `LiveVisitors` 卡片使用的模拟访客数据流。它播放你描述的网站:约 `TypicalCount` 人分布在 `SitePages` 上,每 600ms 有一位来自 `Cities` 之一的新访客,页面人数会漂移,每 2.4s 发生一次 `Conversions` 之一;开始时已有几条最近几分钟的转化。不会获取或跟踪任何内容。它没有自己的计时器:用经过的时间调用 `Advance`(卡片在屏幕上且未暂停时由自己的时钟调用)。设置任何数据属性都会让数据流从头开始。请为每张卡片创建一个实例。 + + + Raised after `Advance` changed the data. + `Advance` 改变数据后引发。 + + + A city a `LiveVisitorsSimulation` sends people from. + `LiveVisitorsSimulation` 中访客来自的一座城市。 + + + A kind of conversion the simulated site records, with how often it happens. + 模拟网站记录的一种转化,以及它发生的频率。 + + + A page of the simulated site, with how much of the traffic it gets. + 模拟网站的一个页面,以及它获得的流量份额。 + + + A pricing calculator for LLM workloads in one panel (Arc LLM cost calculator, design reference). The header shows the selected model's monthly total in a large rolling figure. Below it come your workload presets, three sliders for input tokens per request, output tokens per request and requests per day, and a Cached input switch that bills part of the input at the cache read rate. Every model sits in a ranked list, lowest cost first, with its mark, a bar relative to the most expensive model and a rolling monthly cost; rows glide to their new rank as values change. Picking a row selects that model for the total, the footer breakdown (input, cached, output, per request) and its price line. Prices are inputs: pass your own `Models` with list prices in US dollars per 1M tokens and say where they came from in `Description`. With no models the panel shows its empty state. `ComputeCost` is the arithmetic the panel uses, for server code and invoices. + 一个面板中的 LLM 工作负载价格计算器(Arc LLM cost calculator,设计参考)。头部以大号滚动数字显示所选模型的月度总额。其下是你的工作负载预设、三个滑块(每次请求的输入 token 数、每次请求的输出 token 数和每天的请求数),以及一个 Cached input 开关,开启后部分输入按缓存读取价格计费。所有模型按费用从低到高排在一个列表中,每行有其标志、一根相对于最贵模型的条形和滚动的月度费用;数值变化时各行滑动到新的名次。选中一行,总额、底部明细(输入、缓存、输出、每次请求)和价格行就切换为该模型。价格是输入:传入你自己的 `Models` 及其标价(每 1M token 的美元价格),并在 `Description` 中说明来源。没有模型时,面板显示空状态。`ComputeCost` 是面板使用的算法,可用于服务器代码和发票。 + + + Adds a Try again button to the error state; it raises `RetryRequested`. + 在错误状态中加入 Try again 按钮;它触发 `RetryRequested`。 + + + The share of input tokens billed at the cached rate while Cached input is on, from 0 to 1 (default 0.6). + 开启 Cached input 时按缓存价格计费的输入 token 占比,取值 0 到 1(默认 0.6)。 + + + Days in a billing month (default 30). + 一个计费月的天数(默认 30)。 + + + The line under the heading: say where your prices come from and when they were checked. Null uses the label. + 标题下方的一行:说明价格的来源和核对日期。为 null 时使用标签中的文字。 + + + Shows "Prices did not load" with this reason in place of the list. Null or empty shows the list. + 在列表的位置显示 "Prices did not load" 和此原因。为 null 或空时显示列表。 + + + Shows placeholder rows with the list's exact geometry and disables the controls while prices load. + 价格加载期间显示与列表几何完全一致的占位行,并禁用各个控件。 + + + Every word the panel shows or says. + 面板显示或朗读的每一个词。 + + + The models to compare, each with prices in US dollars per 1M tokens. Null or an empty list shows the empty state. + 要比较的模型,每个都带有每 1M token 的美元价格。为 null 或空列表时显示空状态。 - - The readout's ratio label; {0} is `BaselineLabel` with a lower-case first letter. - 读数中比值的标签;{0} 为首字母小写的 `BaselineLabel`。 + + The fine print at the bottom of the panel. Null uses the label. + 面板底部的小字。为 null 时使用标签中的文字。 - - An event to point out in a `LineReplay`, timed on the same clock as the data. It gets a row in the list of moments, a tick on the slider, and a dot and a card on the chart once the replay gets there. - 在 `LineReplay` 中要指出的事件,与数据使用同一个时钟。它在时刻列表中占一行,在滑块上有一个刻度,回放到达时在图表上显示一个圆点和一张卡片。 + + Workload shortcuts shown as a segmented control; a Custom segment lights up when the sliders match none. Null or an empty list hides the row. + 以分段控件显示的工作负载快捷方式;滑块与任何预设都不匹配时,Custom 分段会亮起。为 null 或空列表时隐藏这一行。 - - One optional line of detail under the title. - 标题下方可选的一行说明。 + + Raised when the person presses Try again in the error state. Bubbles. + 在错误状态中按下 Try again 时触发。冒泡。 - - A stable id, reported back by `MomentReached`. - 稳定的标识,会由 `MomentReached` 报告回来。 + + What the selected model costs for the workload. + 所选模型在当前工作负载下的费用。 - - The sort of event, which decides the icon (problems also use the warning color). - 事件的类型,决定图标(问题类事件还会使用警告色)。 + + The id of the selected model (two-way). Null selects the second model; an id that is not in the models falls back to the cheapest model. + 所选模型的 id(双向)。为 null 时选择第二个模型;不在模型中的 id 回退到最便宜的模型。 - - The clock time of the event in minutes; it may fall between two samples. - 事件的时钟时间,以分钟计;可以落在两个采样之间。 + + The model the total and the breakdown are for, or null when no model is shown. + 总额和明细所对应的模型;没有显示模型时为 null。 - - A short name for the event ("Release notes published"). - 事件的简短名称("Release notes published")。 + + Raised when someone picks a row by pointer or with the arrow keys, with the model and its cost. Bubbles. + 有人通过指针或方向键选中一行时触发,带有该模型及其费用。冒泡。 - - The sort of event a `LineReplayMoment` marks, which decides its icon. - `LineReplayMoment` 所标记的事件类型,决定其图标。 + + The heading of the panel. Null uses the label ("Estimated monthly cost"). + 面板的标题。为 null 时使用标签中的文字("Estimated monthly cost")。 - - A sample for a `LineReplay`: when it was taken (`Time`, minutes on the block's clock), what it measured (`Value`), and optionally what a normal day measured then (`Baseline`), which the chart shows as a dashed line. - `LineReplay` 的一个采样:采样时间(`Time`,区块时钟上的分钟数)、测得的值(`Value`),以及可选的、平常日子在该时刻的值(`Baseline`),图表会把它画成虚线。 + + The workload (two-way): input and output tokens per request, requests a day and whether caching is on. Values between slider stops still compute exactly; the sliders show the nearest stop. Defaults to 16,000 tokens in, 500 out, 5,000 requests a day, cached. + 工作负载(双向):每次请求的输入与输出 token 数、每天的请求数,以及是否开启缓存。介于滑块刻度之间的值仍按精确值计算;滑块显示最近的刻度。默认为每次输入 16,000 个 token、输出 500 个,每天 5,000 次请求,开启缓存。 - - How a `LineReplay` looks before anyone has played it. - `LineReplay` 在还没有人播放之前的样子。 + + Raised on every slider step, preset and cache toggle with the whole workload. Bubbles. + 滑块的每一步、每个预设和每次切换缓存时触发,带有完整的工作负载。冒泡。 - - Shows the cell as an accent-colored link with an underline that strengthens on hover. A click on the text raises `CellInvoked` with `Part`; the cell is not selected by that click. - 把单元格显示为强调色链接,下划线在悬停时加深。点击文字会以 `Part` 触发 `CellInvoked`;该次点击不会选中单元格。 + + Every word `LlmCostCalculator` shows or says, in English by default. Replace the whole object to localize or reword; counts, prices and sentences are format functions. Money reaches them already formatted ("$1,234.56"). + `LlmCostCalculator` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;数量、价格和句子是格式化函数。金额在传入时已经格式化("$1,234.56")。 + + + One model an `LlmCostCalculator` compares: its id, name and provider, an optional 16px mark, and its list prices in US dollars per 1M tokens. + `LlmCostCalculator` 比较的一个模型:其 id、名称和服务商、可选的 16px 标志,以及每 1M token 的美元标价。 + + + A workload shortcut of an `LlmCostCalculator`: an id, a short label and the workload it sets. + `LlmCostCalculator` 的一个工作负载快捷方式:id、简短的标签和它设定的工作负载。 The Login block as one control: it shows one of the five sign in and sign up layouts, chosen by `Variant`. Each layout is also a control of its own (`SignIn`, `LoginCentered`, `LoginSplit`, `LoginImmersive`, `SignupForm`) with its own properties and events; `Form` is the one being shown, so a host that switches layouts at run time configures it after each switch. @@ -11140,6 +13920,86 @@ The track's name: the player's heading while it is current, and its row in the playlist. 曲目名称:作为当前曲目时是播放器的标题,也是它在播放列表中的那一行。 + + One task from the call: an owner, a due date and whether it is done. + 通话中产生的一项任务:一位负责人、一个截止日期,以及是否已完成。 + + + AI notes for one call (Arc Meeting summary, design reference): a header with the platform's mark, the title, the platform, date and length and the attendee stack, then Summary, Decisions and Action items that stream in after a short summarizing pass. Hovering a line, or focusing its timestamp, shows the transcript quote it came from. Action items can be checked off and sent to an issue tracker or a docs app (`SendRequested`); the footer credits the model and offers copy and regenerate. Give it the notes in `Meeting` and drive `Status` yourself, or leave `Status` unset and the card plays its own summarizing pass when it scrolls into view. With `Summarizer` the card calls your async streaming delegate instead and streams each snapshot it yields. With neither it plays a `Simulation` (a call's notes you pass); with no data at all it shows the no transcript state. A send with no handler is simulated. + 一次通话的 AI 纪要(Arc Meeting summary,设计参考):头部包含平台标志、标题、平台、日期与时长以及参会者头像组,随后是经过一段简短的总结过程后流式出现的摘要、决定和待办事项。悬停在一行上或聚焦它的时间戳,会显示它出自的转录原话。待办事项可以勾选完成,也可以发送到问题跟踪工具或文档应用(`SendRequested`);页脚署名模型,并提供复制和重新生成。把纪要放进 `Meeting` 并自己驱动 `Status`,或者不设置 `Status`,让卡片在滚动进入视图时自己播放总结过程。设置 `Summarizer` 后,卡片改为调用你的异步流式委托,并流式显示它产出的每一个快照。两者都没有时,它播放 `Simulation`(你传入的一次通话的纪要);完全没有数据时,显示“无转写记录”状态。没有处理程序的发送是模拟的。 + + + Raised when the person checks or unchecks an action item. The card keeps the checked state itself. Bubbles. + 用户勾选或取消勾选一个待办事项时触发。勾选状态由卡片自己保存。冒泡。 + + + Raised when a send succeeded (or a simulated send finished). Bubbles. + 发送成功(或模拟发送完成)时触发。冒泡。 + + + When the card drives its own status: start summarizing and play the pass. False starts on the finished notes. + 卡片自己驱动状态时:开始总结并播放总结过程。为 false 时一开始就显示完成的纪要。 + + + The message under the error title when the status is Error. + 状态为 Error 时显示在错误标题下方的消息。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个文字。 + + + The notes of the call. Null plays `Simulation`, or shows the no transcript state without one. + 这次通话的纪要。为 null 时播放 `Simulation`,没有模拟时显示“无转写记录”状态。 + + + The model credited in the footer. Null credits the `Simulation`'s model while one plays, or hides the credit. + 页脚中署名的模型。为 null 时,播放 `Simulation` 期间署名其模型,否则隐藏署名。 + + + Raised by Regenerate in the footer and Try again in the error state. Bubbles. A card that drives its own status also replays its pass (or runs `Summarizer` again). + 由页脚的重新生成和错误状态中的 Try again 触发。冒泡。自己驱动状态的卡片还会重新播放它的总结过程(或再次运行 `Summarizer`)。 + + + Raised when the person sends an action item. Bubbles. Set `Completion` to your request before the handler returns; leave it null and the card simulates the send. + 用户发送一个待办事项时触发。冒泡。在处理程序返回之前把 `Completion` 设为你的请求;保持为 null 时,卡片会模拟这次发送。 + + + Where action items can be sent (Issues and Docs by default). An empty list hides the send buttons. + 待办事项可以发送到的位置(默认是 Issues 和 Docs)。空列表会隐藏发送按钮。 + + + A call to play while the card has no `Meeting` and no `Summarizer`: its notes and model, with the summarizing pass, the notes streaming in and a tick and a send played once (sends are simulated). Your own data wins over it; null stops it and shows the no transcript state. + 卡片既没有 `Meeting` 也没有 `Summarizer` 时要播放的通话:它的纪要和模型,包括总结过程、流式显示纪要,以及只播放一次的勾选和发送(发送是模拟的)。你自己的数据优先于它;为 null 时停止播放并显示“无转写记录”状态。 + + + The status, when you drive it. Null (the default) lets the card play its own summarizing pass when it scrolls into view, or run `Summarizer`. Ready with empty lists shows the no transcript state. + 由你驱动时的状态。为 null(默认)时,卡片在滚动进入视图后播放自己的总结过程,或运行 `Summarizer`。Ready 且列表为空时显示没有转录的状态。 + + + Stream the notes in, a few words at a time, when the status moves from Summarizing to Ready. + 状态从 Summarizing 变为 Ready 时,以每次几个词的方式流式显示纪要。 + + + An async streaming delegate that produces the notes: the card calls it when it drives its own status (on first view and on regenerate), shows the summarizing pass until the first snapshot arrives, then streams every snapshot it yields (each one the notes so far). A fault shows the error state with its message. Takes the place of `Meeting` while it runs. + 生成纪要的异步流式委托:卡片自己驱动状态时(首次进入视图和重新生成时)调用它,在第一个快照到达之前显示总结过程,之后流式显示它产出的每一个快照(每一个都是截至目前的纪要)。出错时显示带有错误消息的错误状态。运行期间取代 `Meeting`。 + + + The notes of one call: what it was, who was on it, and the Summary, Decisions and Action items lists. Map your model's output to it; keep `At` as the time from the start of the recording. + 一次通话的纪要:通话内容、参会者,以及摘要、决定和待办事项三个列表。把模型的输出映射到它;`At` 保持为从录音开始算起的时间。 + + + Every word `MeetingSummary` shows or says, in English by default. Replace the whole object to localize or reword; counts, dates and names are format functions. + `MeetingSummary` 显示或朗读的每一个文字,默认为英文。若要本地化或改写,请替换整个对象;数量、日期和姓名是格式化函数。 + + + Somewhere an action item can be sent: an issue tracker, a notes app. A name and a 24 × 24 stroked mark; the defaults are `Issues` and `Docs`. Pass your own mark for a real product. + 待办事项可以发送到的地方:问题跟踪工具、笔记应用。一个名称和一个 24 × 24 的描边标志;默认值是 `Issues` 和 `Docs`。对于真实的产品,请传入你自己的标志。 + + + A call a `MeetingSummary` plays without a summarizer, from data you give it: the notes, the model credited and how simulated sends are numbered. Set it as `Simulation` while the card has no `Meeting` and no `Summarizer`: when the card scrolls into view it plays its summarizing pass, streams the notes in and, unless someone uses the card first, ticks off the second action item and sends the first one. Sends are simulated and announce that nothing was sent. + `MeetingSummary` 在没有总结器时按你提供的数据播放的通话:纪要、署名的模型,以及模拟发送的编号方式。在卡片既没有 `Meeting` 也没有 `Summarizer` 时把它设为 `Simulation`:卡片滚动进入视图后会播放总结过程、流式显示纪要,并且只要没有人先操作卡片,就会勾选第二个待办事项并发送第一个。发送是模拟的,并会播报什么都没有发送。 + A filing group of memories (About you, Preferences, Projects, People by default). 记忆的一个归档分组(默认为“关于你”“偏好”“项目”“人物”)。 @@ -12416,6 +15276,142 @@ Run when a vote is cast, with the `ArenaVote` as the parameter. 投票时执行,参数为 `ArenaVote`。 + + A model details card for a model picker, docs or a marketplace that keeps one size in every state (Arc Model card, design reference). The header shows the model's mark, name and provider with a Compare switch. The details view has a two line summary, all seven modality chips (unsupported ones dashed and muted), the context window with a gauge that reads as pages, max output and knowledge cutoff, input, output and cached prices per million tokens, a monthly volume slider that rolls the estimated cost live, and benchmark bars against two rivals where hovering or focusing a row shows the exact score, its rank and the gap to the leader. The footer shows the API model id with Copy model id. Compare crossfades to a side by side table against any rival, with a check and the margin on the better value and a tally of who leads. Details, compare, loading and error are stacked in one grid cell, so switching models, views or benchmarks never moves the card: every price, limit and score rolls in place in tabular digits and every text value crossfades. Without a `Model` the card keeps its frame, says no model is selected and reads every value as not listed. + 用于模型选择器、文档或市场的模型详情卡片,在任何状态下都保持同一尺寸(Arc Model card,设计参考)。头部显示模型的标志、名称和提供方,以及比较开关。详情视图包含两行简介、全部七个模态标签(不支持的为虚线并变淡)、带有以页数表示的容量条的上下文窗口、最大输出和知识截止时间,每百万 token 的输入、输出和缓存价格,实时滚动估算费用的每月用量滑块,以及与两个对手对比的基准测试条:悬停或聚焦某一行会显示确切分数、排名以及与领先者的差距。底部显示 API 模型 id 和 Copy model id。比较以交叉淡化切换到与任一对手并排的表格,较优的值带对勾和领先幅度,并统计谁领先。 详情、比较、加载和错误叠放在同一个网格单元格中,因此切换模型、视图或基准测试都不会移动卡片:每个价格、上限和分数都以等宽数字在原位滚动,每个文本值都交叉淡化。没有 `Model` 时,卡片保留其框架,说明未选择模型,并把每个值显示为未列出。 + + + The id of the benchmark the bars show (two-way). Null or one the model has no score for uses the first scored benchmark. + 基准测试条显示的基准测试 id(双向)。为 null 或模型在该基准上没有分数时,使用第一个有分数的基准测试。 + + + Raised when the person picks a benchmark in the switcher. Bubbles. + 用户在切换器中选择基准测试时触发。冒泡。 + + + Benchmarks in order. Only those the model has a score for are offered in the switcher; with none the benchmark section hides. Null shows no benchmarks. + 按顺序排列的基准测试。切换器只提供模型有分数的基准测试;一个都没有时隐藏基准测试区域。为 null 时不显示基准测试。 + + + Adds a Retry button to the error state; it raises `RetryRequested`. + 在错误状态中添加 Retry 按钮;它会触发 `RetryRequested`。 + + + Adds a primary Use model button to the footer, for model pickers; it raises `SelectRequested`. + 在底部添加一个主要的 Use model 按钮,用于模型选择器;它会触发 `SelectRequested`。 + + + The id of the rival in the compare view (two-way). Null or an unknown id uses the first rival. + 比较视图中对手的 id(双向)。为 null 或未知 id 时使用第一个对手。 + + + Raised when the person picks a rival from the compare menu. Bubbles. + 用户从比较菜单中选择对手时触发。冒泡。 + + + The ISO 4217 currency of the prices ("USD" writes "$"). + 价格使用的 ISO 4217 货币("USD" 显示为 "$")。 + + + Replaces the body with this error message (announced as an alert). The header still shows the model. + 用此错误消息替换卡片主体(作为警报播报)。头部仍显示模型。 + + + Raised after Copy model id has written the id to the clipboard. Bubbles. + Copy model id 把 id 写入剪贴板后触发。冒泡。 + + + Share of the monthly volume billed at the input price; the rest is billed at the output price. + 每月用量中按输入价格计费的比例;其余按输出价格计费。 + + + Shows a skeleton of the card and disables Compare; the status region says the details are loading. + 显示卡片的骨架屏并禁用比较;状态区域说明详情正在加载。 + + + Labels prices and scores as samples in the benchmark caption and the compare footnote. Turn it on while your figures are placeholders. + 在基准测试说明和比较脚注中把价格和分数标注为示例。数据仍是占位时打开它。 + + + Shows the select button as Selected, with a check. + 把选择按钮显示为 Selected,并带对勾。 + + + Every word the card shows or says. + 卡片显示或朗读的每个词。 + + + The model the card describes. Null shows the empty card: no model selected, every value not listed. + 卡片描述的模型。为 null 时显示空卡片:未选择模型,每个值都未列出。 + + + The monthly token volume of the cost estimate (two-way). It snaps to the nearest stop: 1M, 5M, 10M, 25M, 50M, 100M, 250M, 500M or 1B. + 费用估算的每月 token 用量(双向)。会吸附到最近的档位:1M、5M、10M、25M、50M、100M、250M、500M 或 1B。 + + + Raised with the new volume as the person moves the volume slider. Bubbles. + 用户移动用量滑块时以新用量触发。冒泡。 + + + The footnote under the compare table: say where the prices and scores come from and when they were checked. Null shows the sample footnote while `IsSample` is on, and nothing otherwise. + 比较表下方的脚注:说明价格和分数的来源以及核对日期。为 null 时,`IsSample` 打开则显示示例脚注,否则不显示。 + + + Raised when the person presses Retry in the error state. Bubbles. + 用户在错误状态中按下 Retry 时触发。冒泡。 + + + Other models. The first two appear under the model in the benchmark rows, ranks count every rival with a score, and every rival can be picked in the compare view. The model itself is filtered out, so one shared pool works for every model. Null or an empty list hides the Compare switch and the rival rows. + 其他模型。前两个显示在基准测试行中模型的下方,排名计入每个有分数的对手,比较视图中可以选择任一对手。模型本身会被排除,因此所有模型可共用同一组对手。为 null 或空列表时隐藏比较开关和对手行。 + + + The select button's label; null uses `UseModel`. + 选择按钮的文字;为 null 时使用 `UseModel`。 + + + Raised when the person presses Use model. Bubbles. + 用户按下 Use model 时触发。冒泡。 + + + The view on screen (two-way): the details or the compare table. + 屏幕上的视图(双向):详情或比较表格。 + + + Raised when the person flips the Compare switch. Bubbles. + 用户拨动比较开关时触发。冒泡。 + + + A benchmark the card can show bars for. + 卡片可以为其显示条形的基准测试。 + + + Arguments of the card's events that carry an id: the rival picked, the benchmark chosen or the id copied. + 卡片中携带 id 的事件的参数:所选的对手、所选的基准测试或复制的 id。 + + + Every word `ModelCard` shows or says, in English by default. Replace the whole object to localize or reword; sentences that carry values are format functions. + `ModelCard` 显示或朗读的每个词,默认为英文。替换整个对象即可本地化或改写;带数值的句子是格式化函数。 + + + One model a `ModelCard` describes or compares against. Every value but `Id` and `Name` is optional; a missing one reads as "Not listed". + `ModelCard` 描述或比较的一个模型。除 `Id` 和 `Name` 外每个值都是可选的;缺少的值显示为 "Not listed"。 + + + Arguments of `SelectRequested`. + `SelectRequested` 的参数。 + + + Arguments of `MonthlyTokensChanged`. + `MonthlyTokensChanged` 的参数。 + + + Per token prices of a model, per million tokens, in the card's `Currency`. + 模型的按 token 计费价格,单位为每百万 token,以卡片的 `Currency` 计。 + + + Arguments of `ViewChanged`. + `ViewChanged` 的参数。 + Chooses a model and its reasoning effort from one compact pill. The pill shows the current choice; its menu lists a few featured models with a one-line note each, an effort setting, and every other model grouped by provider. Under the pill a summary shows what the model can hold and costs, and confirms each change for a moment. A composition of `ModelPill`, `ModelMenu` and `ModelSummary`: the catalog, the selection and the effort are properties, so bind them or read them back from the events. The pill opens on a pointer press or on Enter, Space and ArrowDown; the menu has a featured list, an Effort row and a More models row that open submenus (or pages in a narrow window); choosing closes the menu and returns focus to the pill; the effort moves to the nearest level a new model offers. Set `IsSummaryVisible` to false for the pill alone (a toolbar, a header). Screen readers hear a polite announcement after each change ("Now using Aurora Prime 3, medium effort"); the visible confirmation under the summary stays for `NoticeDuration`. 从一个紧凑的胶囊中选择模型及其推理强度。胶囊显示当前选择;其菜单列出几个精选模型(每个附一行说明)、一项推理强度设置,以及按提供方分组的其他所有模型。胶囊下方的概览显示该模型可容纳的内容与费用,并在每次更改后短暂给出确认。它由 `ModelPill`、`ModelMenu` 和 `ModelSummary` 组合而成:目录、选择和推理强度都是属性,因此可以绑定它们,或从事件中读回。胶囊在指针按下,或按 Enter、Space 与 ArrowDown 时打开菜单;菜单包含精选列表、Effort 行和 More models 行,它们会打开子菜单(窗口较窄时改为页面);选择后菜单关闭,焦点回到胶囊;推理强度会移到新模型所提供的最接近的一档。把 `IsSummaryVisible` 设为 false,就只剩胶囊(用于工具栏或标题栏)。每次更改后,屏幕阅读器会听到一条礼貌式播报("Now using Aurora Prime 3, medium effort");概览下方的确认提示会保留 `NoticeDuration` 的时长。 @@ -13672,6 +16668,90 @@ Arguments of `StepChanged`. `StepChanged` 的参数。 + + + The category the logo belongs to. The category chips are built from these. + 标志所属的类别。类别胶囊由这些类别生成。 + + + The color a path-data mark is painted in. Leave it out for black-and-white brands so the mark follows the text color. Image and control marks keep their own colors. + 路径数据标志所用的绘制颜色。黑白品牌可以不设置,让标志跟随文本颜色。图片和控件标志保留自己的颜色。 + + + The line of text in the card. + 卡片中的一行文字。 + + + Address of the documentation. When set, the card has a Docs link. + 文档地址。设置后卡片中会出现 Docs 链接。 + + + The mark: SVG path data (24 by 24), an image address, an image source or a control. + 标志:SVG 路径数据(24 × 24)、图片地址、图片源或控件。 + + + Stable identifier. When empty, `Name` identifies the logo. + 稳定的标识符。为空时由 `Name` 标识该标志。 + + + Whether the integration is connected. It starts the status and is set when a connection succeeds. + 集成是否已连接。它决定初始状态,连接成功后会被设置。 + + + The brand or product name, used in the chip's name tag, the card and the accessible name. + 品牌或产品名称,用于标志的名称标签、卡片和无障碍名称。 + + + An integrations orbit (Arc Orbit logos, design reference): your product mark at the centre with brand logos on two flat rings that turn slowly and hold still whenever the pointer or keyboard focus is inside. Category chips turn the matching logos toward the detail side and dim the rest. Choosing a logo springs its ring so the logo glides to a fixed spot, draws an accent connector from the centre and morphs a detail card out of the chip, with a description, the status, a Connect action, a Docs link and previous and next. + 集成轨道(Arc Orbit logos,设计参考):你的产品标志位于中央,品牌标志排在两条平面环上,环缓慢转动,指针或键盘焦点在内时保持静止。类别胶囊会把匹配的标志转向详情一侧并让其余标志变暗。选中一个标志时,它所在的环以弹簧转动,使标志滑到固定位置,从中央画出强调色连线,并从标志中长出详情卡片,包含说明、状态、Connect 操作、Docs 链接以及上一个和下一个。 + + + Label of the first category chip, the one that shows every logo. + 第一个类别胶囊的文字,该胶囊显示所有标志。 + + + Content of the centre disc, such as your product mark. Defaults to a simple mark in the accent color. + 中央圆盘的内容,例如你的产品标志。默认是强调色的简单图形。 + + + Raised when the Connect button is pressed. A handler can set `Completion`. + 按下 Connect 按钮时引发。处理程序可以设置 `Completion`。 + + + Color of the connector from the centre to the chosen logo. The theme uses the accent. + 从中央到所选标志的连线颜色。主题使用强调色。 + + + Id (or name) of the logo open on first display, when `SelectedId` is not set. + 首次显示时打开的标志的 id(或名称),在未设置 `SelectedId` 时使用。 + + + Holds the orbit still, for a page-level motion control. + 让轨道保持静止,用于页面级的动效控制。 + + + Accessible name of the logo list, and the title of the idle panel. + 标志列表的无障碍名称,也是空闲面板的标题。 + + + Color of the two ring lines. The theme uses the subtle border. + 两条环线的颜色。主题使用弱边框色。 + + + Id (or name) of the logo whose card is open. Null closes the card. Two-way. + 卡片已打开的标志的 id(或名称)。为 null 时关闭卡片。双向。 + + + Raised when a logo is opened, when previous or next steps to another, and with no logo and index -1 when the card closes. + 打开标志、用上一个或下一个切换到另一个标志时引发;卡片关闭时没有标志且索引为 -1。 + + + Multiplies the rotation speed. At 1 the inner ring turns once every 64 seconds and the outer ring, the other way, every 88 seconds. 0 holds the orbit still. + 转速倍数。为 1 时内环每 64 秒转一圈,外环反向每 88 秒转一圈。0 让轨道保持静止。 + A row of single-character slots for one-time codes (Arc's OTP input block): a visible label, one slot per character, helper and error copy under the row. Typing fills a slot and moves on; a paste fills the slots from the focused one; arrow keys move between slots and Backspace on an empty slot clears the one before it. Characters rise into their slot, a paste lands as a short left to right wave, one focus ring glides between the slots, and a new error nudges the row side to side once. `Value` is two-way; `Completed` fires when an edit fills the last slot, which is where a host checks the code. 用于一次性验证码的单字符格子行(Arc 的 OTP 输入块):可见的标签、每个字符一个格子,以及行下方的辅助文字和错误文字。输入时填入格子并移到下一格;粘贴时从当前聚焦的格子开始依次填入;方向键可在格子之间移动,在空格子上按 Backspace 会清除前一个格子。字符会向上升入格子,粘贴时呈现一道从左到右的短促波浪动画,一个焦点环在格子之间滑动,新的错误会让整行左右轻晃一次。`Value` 为双向绑定;编辑填满最后一个格子时触发 `Completed`,宿主应在此处校验验证码。 @@ -14080,6 +17160,182 @@ Defines the `ValueChanged` event. 定义 `ValueChanged` 事件。 + + Arguments of `Redacted`: the safe prompt and every entity it replaced. + `Redacted` 的参数:安全提示词以及它替换掉的每个实体。 + + + A guardrail card that shows personal data being redacted before a prompt reaches the model (Arc PII redaction, design reference). Scan marks each name, email, phone number and card number in the message with its type, then swaps each one for a numbered placeholder such as `[EMAIL_1]` in a single crossfade. Send carries the safe prompt along a small flow (you, the PII guard, the model), the reply streams back in quick chunks with its placeholders, and the guard puts the original values back for the person. Show original flips the message between the safe prompt and what was typed, and the message can be edited before a scan. Detection comes from `Detector` (your DLP or NER service) or the built-in `PiiRedactor`; the reply from `Sender` or a simulation, which the card says. Without a `Message` it plays a `Simulation` (a conversation you pass) on a loop while at least a quarter of it is on screen, until someone interacts; with neither it shows an empty message field. + 一张防护卡片,展示个人数据在提示词到达模型之前被脱敏的过程(Arc PII redaction,设计参考)。扫描会在消息中标出每个姓名、邮箱、电话号码和卡号及其类型,然后在一次交叉淡化中把它们各自换成带编号的占位符,例如 `[EMAIL_1]`。发送会让安全提示词沿一条小流程(你、PII 防护、模型)传递,回复带着占位符分小块快速流式返回,防护再为用户放回原始值。显示原文在安全提示词与输入的内容之间切换,扫描前消息可以编辑。检测来自 `Detector`(你的 DLP 或 NER 服务)或内置的 `PiiRedactor`;回复来自 `Sender` 或模拟,卡片会注明模拟。没有 `Message` 时,只要卡片至少四分之一在屏幕内,它就循环播放 `Simulation`(你传入的一段对话),直到有人操作;两者都没有时显示空的消息框。 + + + Plays scan, send and restore on a loop while at least a quarter of the card is on screen. Stops for good as soon as someone edits, toggles or presses a button. Off with reduced motion. + 只要卡片至少四分之一在屏幕内,就循环播放扫描、发送和还原。一旦有人编辑、切换或按下按钮就永久停止。减弱动效时关闭。 + + + The color of card numbers (theme: Series4). + 卡号的颜色(主题:Series4)。 + + + Replaces the built-in detector, for example with your DLP or NER service. Return spans with an exclusive end; the token is cancelled when the person presses Stop or Reset. A thrown exception shows its message with Retry. + 替换内置检测器,例如换成你的 DLP 或 NER 服务。返回终点不含在内的区间;用户按下 Stop 或 Reset 时令牌会被取消。抛出的异常会显示其消息并提供 Retry。 + + + The color of emails (theme: Series2). + 邮箱的颜色(主题:Series2)。 + + + Whether people can edit the message before a scan. Cmd or Ctrl plus Enter scans from the field. + 扫描前用户能否编辑消息。在输入框中按 Cmd 或 Ctrl 加 Enter 即开始扫描。 + + + Every word the card shows or announces. + 卡片显示或播报的每个词语。 + + + The message to check (two-way). Null shows the `Simulation`'s message, or a blank field without one. + 要检查的消息(双向)。为 null 时显示 `Simulation` 的消息,没有模拟时显示空白输入框。 + + + The model's mark, a 24 × 24 stroked glyph drawn at 16px; the sparkle by default. + 模型的标记,一个 24 × 24 的描边图形,以 16px 绘制;默认是火花。 + + + The mark's color; the accent by default. + 标记的颜色;默认是强调色。 + + + The model the safe prompt goes to, on the flow and above the reply; null names the `Simulation`'s model, or none. + 安全提示词发往的模型,显示在流程条上和回复上方;为 null 时显示 `Simulation` 的模型,没有则不显示。 + + + The color of names: their marks, placeholders and cell (theme: Series1). + 姓名的颜色:用于其标记、占位符和类型格(主题:Series1)。 + + + Names the built-in detector always flags, on top of introductions like "I'm", "this is" and "my name is". Pass the signed-in person's name and their contacts. Null uses the `Simulation`'s names, or none. + 内置检测器始终标出的姓名,此外还会识别 "I'm"、"this is" 和 "my name is" 之类的自我介绍。传入已登录用户的姓名及其联系人。为 null 时使用 `Simulation` 的姓名,没有则不标。 + + + Where the card is in its flow (read only). + 卡片处于流程中的哪个阶段(只读)。 + + + Raised on every phase change. Bubbles. + 每次阶段变化时触发。冒泡。 + + + The color of phone numbers (theme: Series3). + 电话号码的颜色(主题:Series3)。 + + + Raised once the message is redacted, with the safe prompt and every entity. Bubbles. + 消息脱敏完成时触发,携带安全提示词和每个实体。冒泡。 + + + The restored reply once it is back, otherwise null (read only). + 回复返回后为还原后的回复,否则为 null(只读)。 + + + Raised when the reply is restored, with the model's raw reply and the text the person sees. Bubbles. + 回复还原后触发,携带模型的原始回复和用户看到的文本。冒泡。 + + + The safe prompt once the message is redacted, otherwise null (read only). + 消息脱敏后为安全提示词,否则为 null(只读)。 + + + Sends the safe prompt to your model and returns its reply, placeholders included; the card restores them. Without it the reply is simulated (the `Simulation`'s reply, or a short stand-in) and the card says so. A thrown exception shows its message with Retry. + 把安全提示词发送给你的模型并返回其回复(含占位符);卡片会还原它们。没有它时回复是模拟的(`Simulation` 的回复,或一条简短的替身回复),卡片会注明。抛出的异常会显示其消息并提供 Retry。 + + + Shows the original message instead of the safe prompt once it is redacted (two-way). + 消息脱敏后显示原始消息而不是安全提示词(双向)。 + + + A conversation to play: its message (while the card has no `Message`), the names to flag, the model and its written reply. Your own values win over the simulation's; null stops it and shows the empty field. + 要播放的对话:它的消息(卡片没有 `Message` 时)、要标记的姓名、模型以及写好的回复。你自己的值优先于模拟的值;为 null 时停止播放并显示空的消息框。 + + + Every word a `PiiRedaction` shows or announces, in English by default. Replace the whole object to translate or reword; sentences with a number or a name are functions so the word order can change. + `PiiRedaction` 显示或播报的每个词语,默认为英文。替换整个对象即可翻译或改写;含数字或名称的句子是函数,以便调整语序。 + + + Arguments of `PhaseChanged`: the phase the card left and the one it entered. + `PhaseChanged` 的参数:卡片离开的阶段和进入的阶段。 + + + A conversation a `PiiRedaction` plays without a model, from data you give it: the message, the names the built-in detector should always flag, the model the safe prompt goes to and the model's reply with its placeholders. Set it as `Simulation`: while the card has no `Message` it shows this message and, unless `AutoPlay` is false, plays scan, send and restore on a loop. Without a `Sender` the reply is simulated and the card says so; nothing is sent. + `PiiRedaction` 在没有模型时按你提供的数据播放的对话:消息、内置检测器应始终标出的姓名、安全提示词发往的模型,以及带占位符的模型回复。把它设为 `Simulation`:卡片没有 `Message` 时会显示这条消息,并且除非 `AutoPlay` 为 false,会循环播放扫描、发送和还原。没有 `Sender` 时回复是模拟的,卡片会注明;不会发送任何内容。 + + + Arguments of `ReplyRestored`: the model's reply and the text the person sees. + `ReplyRestored` 的参数:模型的回复以及用户看到的文本。 + + + A pixel-art robot on a 24 × 24 grid that shows what an assistant is doing (Arc Pixel mascot, design reference). It idles, thinks, types, celebrates and errors in frame-stepped sprite animations on a fixed 40ms clock, and status changes morph from the current pose one pixel at a time. Every sprite pixel is `Scale` device-independent pixels, snapped to whole device pixels, so it stays crisp at every size from 24px to 192px. Set `Status` from your request lifecycle, or leave it null to play the built-in loop through every state. The palette follows the theme neutrals and `AccentBrush` (the theme's accent by default). + 一个画在 24 × 24 网格上的像素机器人,展示助手正在做什么(Arc Pixel mascot,设计参考)。它以固定 40ms 时钟上的逐帧精灵动画待机、思考、打字、庆祝和报错,状态切换时从当前姿势一次一个像素地变形过去。每个精灵像素占 `Scale` 个设备无关像素,并对齐到整数设备像素,因此在 24px 到 192px 的每个尺寸下都保持清晰。根据请求生命周期设置 `Status`,或留空以循环播放内置的全部状态。调色板跟随主题中性色和 `AccentBrush`(默认为主题强调色)。 + + + The accent of the antenna, chest light, typed bar, sparkles and the eyes' glow; the theme's accent by default. + 天线、胸灯、打字进度条、闪光和眼睛光晕使用的强调色;默认为主题强调色。 + + + Announces status changes politely to assistive technology. Null (the default) announces when `Status` is set and stays quiet for the built-in loop. Turn it off when nearby text already announces progress. + 以礼貌方式向辅助技术播报状态变化。为 null(默认)时,设置了 `Status` 就播报,内置循环则保持安静。附近文字已播报进度时请关闭。 + + + Who the mascot stands for, used in the accessible label ("Assistant is thinking"). + 吉祥物代表的助手名称,用于无障碍标签("Assistant is thinking")。 + + + Freezes the animation on the current frame. The mascot also pauses by itself while off screen. + 把动画冻结在当前帧。离开屏幕时吉祥物也会自动暂停。 + + + Replaces the generated accessible label. + 替换自动生成的无障碍标签。 + + + Every word the mascot says. + 吉祥物说出的所有文字。 + + + Pixel scale from 1 to 8: each sprite pixel is this many device-independent pixels, so 1 renders 24px, 2 renders 48px, 4 renders 96px and 8 renders 192px. Other values clamp. + 像素缩放,1 到 8:每个精灵像素占这么多个设备无关像素,所以 1 渲染为 24px,2 为 48px,4 为 96px,8 为 192px。其他值会被限制在范围内。 + + + The status the mascot shows: `Status`, or the built-in loop's current step when it is null. + 吉祥物正在显示的状态:`Status`,为 null 时则是内置循环的当前一步。 + + + Raised when `ShownStatus` changes, with the old and new status. Bubbles. + `ShownStatus` 变化时触发,带有旧状态和新状态。会冒泡。 + + + What the assistant is doing. Leave it null to play the built-in loop through every state. + 助手正在做什么。留空则循环播放内置的全部状态。 + + + Every word `PixelMascot` says, in English by default: the accessible label for each status, where `{0}` is the assistant's name. Replace the whole object to localize or reword. + `PixelMascot` 说出的所有文字,默认为英文:每种状态的无障碍标签,其中 `{0}` 是助手名称。替换整个对象即可本地化或改写措辞。 + + + The built-in status loop of `PixelMascot` (idle, thinking, writing, done, idle, thinking, error) as a shared clock. A mascot without a `Status` plays this loop on its own; put one `PixelMascotLoop` in resources and bind several mascots' `Status` to its `Status` to keep them in step. It switches only at step boundaries (one timer tick every few seconds), never per frame. + `PixelMascot` 的内置状态循环(待机、思考、书写、完成、待机、思考、出错),作为共享时钟。没有 `Status` 的吉祥物会自己播放这个循环;在资源中放一个 `PixelMascotLoop`,把多个吉祥物的 `Status` 绑定到它的 `Status`,即可让它们步调一致。它只在步与步的边界切换(每隔几秒一次计时器触发),从不逐帧运行。 + + + Whether the loop moves on. False holds the current status. + 循环是否继续推进。为 false 时停在当前状态。 + + + The status the loop is on. + 循环当前所处的状态。 + + + Arguments of `ShownStatusChanged`. + `ShownStatusChanged` 的参数。 + A small surface anchored to a trigger and opened by a click, for interactive content that should not block the page: share settings, a small filter form, a picker built from your own controls. Two ways to use it: Code can open any content anchored to any control with `ShowAt`. The panel starts 5px toward its trigger at scale 0.97, fades in fast and settles on a spring from the side it actually landed on; it leaves in about 160ms at half the travel, and an open and a close reverse each other mid-flight. Escape and a press outside close it and return focus to the trigger; focus moves into the panel on open. The trigger answers presses with color only (no press scale), since the panel is measured against it. 一个锚定在触发器上、由点击打开的小型浮层,适用于不应阻塞页面的交互内容:分享设置、小型筛选表单、由您自己的控件组成的选择器。有两种用法:代码可通过 `ShowAt` 将任意内容锚定到任意控件上打开。面板从触发器方向偏移 5px 开始,缩放比例为 0.97,快速淡入并以弹簧从实际落位的一侧稳定下来;关闭时约 160ms 内完成,移动距离为一半,且打开与关闭在过程中可相互反转。按下 Escape 或在外部按下会关闭面板,并将焦点返回触发器;打开时焦点会移入面板。触发器仅以颜色响应按下(无按压缩放),因为面板是相对于它测量的。 @@ -14944,6 +18200,178 @@ The user message of this version. 该版本的用户消息。 + + One provider's stored key in `ProviderKeys`. Pass `Value` to enable reveal and copy, or only `Last4` when your server never returns full keys (the row then shows the masked key and disables both). + `ProviderKeys` 中某个提供商已保存的密钥。传入 `Value` 以启用显示和复制;如果服务器从不返回完整密钥,则只传 `Last4`(该行随后显示掩码密钥,并禁用这两项操作)。 + + + A provider row's identity in `ProviderKeys`: its id, name, mark and the shape of its keys (a prefix and a minimum length), which the add and rotate form checks as the person types. + `ProviderKeys` 中一个提供商行的身份:其 id、名称、标志以及密钥的形态(前缀和最小长度),添加和轮换表单会在用户输入时据此检查。 + + + A settings card for bring-your-own-key AI products (Arc Provider keys, design reference). One row per provider with its mark, the masked key (`sk-••••4f2a`), a status as icon plus label (Connected, Invalid, Not set), when the key was last used, and four actions: reveal for five seconds with a countdown ring, copy, rotate, and remove with undo. Pressing a status tests the stored key again. Add key and Rotate open an inline form under the row that checks the key's format as you type, then shows Testing connection and Connected before the row takes the new key. Bind `Keys` (two-way) and handle `KeySaved` and `KeyRemoved` to persist; set `TestKey` to test keys on your server. Providers without a key read Not set. For a demo, set `Simulation` instead of `TestKey` to simulate every test; with neither, a test fails with `NoKeyTest`, so no key reads Connected without a check. + 面向自带密钥 AI 产品的设置卡片(Arc Provider keys,设计参考)。每个提供商一行,显示其标志、掩码密钥(`sk-••••4f2a`)、由图标加文字组成的状态(Connected、Invalid、Not set)、密钥最近一次使用的时间,以及四个操作:带倒计时圆环的五秒显示、复制、轮换和可撤销的删除。按下状态会重新测试已保存的密钥。Add key 和 Rotate 会在该行下方打开内联表单,在输入时检查密钥格式,然后显示 Testing connection 和 Connected,再让该行采用新密钥。绑定 `Keys`(双向)并处理 `KeySaved` 和 `KeyRemoved` 以持久化;设置 `TestKey` 以在你的服务器上测试密钥。没有密钥的提供商显示为 Not set。用于演示时,设置 `Simulation` 而不是 `TestKey` 来模拟每次测试;两者都没有时,测试会以 `NoKeyTest` 失败,因此任何密钥都不会未经检查就显示为 Connected。 + + + A line under the title; null or empty hides it. + 标题下方的一行文字;为 null 或空时隐藏。 + + + A quiet line under the list, an encryption note by default; null or empty hides it. + 列表下方一行低调的文字,默认是加密说明;为 null 或空时隐藏。 + + + Shows placeholder rows with the real row geometry while keys load. + 在密钥加载期间显示占位行,几何尺寸与真实的行一致。 + + + Raised when a key was copied to the clipboard, for an audit log. Bubbles. + 密钥被复制到剪贴板时引发,可用于审计日志。会冒泡。 + + + Raised when the undo window of a removal closes, so a removal only reaches your server once it sticks. Bubbles. + 删除的撤销窗口关闭时引发,所以删除只有在确定后才会到达你的服务器。会冒泡。 + + + Raised when a new key passed its test; persist it here (the row already shows it as connected). Bubbles. + 新密钥通过测试时引发;在这里持久化(该行已显示为已连接)。会冒泡。 + + + The keys, one per provider (two-way): the card writes the new list on every add, rotation, removal, undo and retest. Providers without an entry read Not set; null or an empty list is the empty state, every row Not set. + 密钥,每个提供商一个(双向):每次添加、轮换、删除、撤销和重新测试时,卡片都会写回新的列表。没有条目的提供商显示为 Not set;为 null 或空列表时即为空状态,每一行都是 Not set。 + + + Raised on every change to the keys: added, rotated, removed, restored with undo, or retested. Bubbles. + 密钥每次变化时引发:添加、轮换、删除、撤销恢复或重新测试。会冒泡。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + The card width under which each row stacks (status beside the name, the key on its own line) and the form stacks. + 卡片宽度低于此值时,每一行改为堆叠排列(状态在名称旁边,密钥单独占一行),表单也会堆叠。 + + + Which providers get a row, in order. Null or an empty list shows no rows. + 哪些提供商有一行,以及它们的顺序。为 null 或空列表时不显示任何行。 + + + How long a revealed key stays visible before it masks itself again. + 显示的密钥在重新遮盖之前保持可见的时长。 + + + Simulated key tests while `TestKey` is null, for demos: stored invalid keys fail again, everything else connects, and announcements say nothing was sent. Null (the default) runs no simulated test. + `TestKey` 为 null 时使用的模拟密钥测试,用于演示:已保存的无效密钥会再次失败,其余密钥都连接成功,播报会说明没有发送任何内容。为 null(默认)时不运行模拟测试。 + + + Tests a key and returns whether the provider accepted it. The request carries the pasted key when adding or rotating, and no key when retesting a stored one (test it on your server by provider); its token is cancelled when the form closes. Null uses `Simulation`; with neither, every test fails with `NoKeyTest`. + 测试密钥并返回提供商是否接受它。添加或轮换时,请求带有粘贴的密钥;重新测试已保存的密钥时不带密钥(请在服务器上按提供商测试);其令牌会在表单关闭时取消。为 null 时使用 `Simulation`;两者都没有时,每次测试都以 `NoKeyTest` 失败。 + + + The card's title, also the name of the section. + 卡片的标题,也是该区域的名称。 + + + How long Undo stays after a removal; `KeyRemoved` is raised when it closes. + 删除后 Undo 保留的时长;它关闭时会引发 `KeyRemoved`。 + + + Every word `ProviderKeys` shows or says, in English by default. Replace the whole object to localize or reword; words that name a provider, a prefix, a time or an error are format functions. + `ProviderKeys` 显示或朗读的每一个词,默认为英文。替换整个对象即可本地化或改写;涉及提供商、前缀、时间或错误的词是格式化函数。 + + + Simulated key tests for a `ProviderKeys` without `TestKey`, for demos and prototypes: after `Delay`, retesting a stored key whose status is Invalid fails again with its own error (or `RejectedError`), and every other test connects. Nothing is sent anywhere, and the card adds "Simulated, nothing was sent." to what it announces. + 供没有 `TestKey` 的 `ProviderKeys` 使用的模拟密钥测试,用于演示和原型:等待 `Delay` 之后,重新测试状态为 Invalid 的已保存密钥会以其自身的错误(或 `RejectedError`)再次失败,其余测试都连接成功。不会发送任何内容,卡片会在播报中附加 "Simulated, nothing was sent."。 + + + A status card for the AI providers a product depends on (Arc Provider status, design reference). Each row shows the provider's mark, its health as an icon and label (Operational, Degraded, Outage), the median latency in tabular numerals and a 30 day uptime bar of small ticks with a tooltip per day. A row with an incident is a button that opens its updates (Investigating, Identified, Monitoring, Resolved), newest first; new updates grow the panel smoothly. The header sums up health across providers and checks again on a visible countdown ring, with a refresh button, a Checking state and an error state that keeps the last results. Cards narrower than 560px take two lines per row. Give it `Providers` and a `RefreshHandler` that fetches fresh status (from your own aggregation endpoint). With neither, a `Simulation` plays its story on simulated checks; without one the card says there are no providers. + 一张展示产品所依赖的 AI 服务商状态的卡片(Arc Provider status,设计参考)。每一行显示服务商的标志、以图标和标签表示的健康状况(Operational、Degraded、Outage)、以等宽数字显示的中位延迟,以及由小刻度组成、每天都带提示的 30 天可用率条。带有事件的行是一个按钮,按从新到旧展开其更新(Investigating、Identified、Monitoring、Resolved);新的更新会让面板平滑变高。标题栏汇总所有服务商的健康状况,并按可见的倒计时环再次检查,带有刷新按钮、Checking 状态,以及保留上次结果的错误状态。宽度小于 560px 的卡片每行占两行。传入 `Providers` 和一个获取最新状态的 `RefreshHandler`(来自你自己的聚合端点)。两者都没有时,由 `Simulation` 在模拟检查中播放其故事;没有模拟时,卡片说明没有服务商。 + + + An error to show in the header, such as a failed first load. Errors thrown by the refresh handler show on their own. + 在标题栏中显示的错误,例如首次加载失败。刷新处理程序抛出的错误会自行显示。 + + + Raised when an incident row is opened or closed by the person. Bubbles. + 用户展开或收起事件行时触发。冒泡。 + + + The provider whose incident is open (two-way), or null. + 当前展开事件的服务商(双向),或 null。 + + + A quiet line under the list, such as how latency and uptime are measured. A playing simulation shows a sample data note. + 列表下方一行低调的说明,例如延迟和可用率是如何测量的。正在播放的模拟会显示一条示例数据说明。 + + + Raised by the Status page link of an open incident. Bubbles. When nothing handles it, the link opens the incident's `Url`; with neither a handler nor a URL the link is hidden. + 由展开事件中的 Status page 链接触发。冒泡。没有任何处理程序处理它时,链接会打开事件的 `Url`;既没有处理程序也没有 URL 时,链接隐藏。 + + + True while a check runs. + 检查进行期间为 true。 + + + Shows skeleton rows with the same row height, for the first load. + 首次加载时显示行高相同的骨架行。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + A one-off message for screen readers, such as the result of an action; it is read each time it changes. + 给屏幕阅读器的一次性消息,例如某个操作的结果;每次变化时都会朗读。 + + + The providers to show. Leave it null (with no `RefreshHandler`) to play `Simulation`. A check that returns providers writes them here. + 要显示的服务商。保持为 null(且没有 `RefreshHandler`)即播放 `Simulation`。返回服务商的检查会把结果写回这里。 + + + Fetches fresh status on the countdown and on the refresh button. Return the new providers, or update `Providers` yourself and return null. Throw to show the error state: the last results stay and the countdown turns into a retry. A newer check cancels the older one through the token. Without it the refresh controls are hidden. + 在倒计时结束和点击刷新按钮时获取最新状态。返回新的服务商,或自行更新 `Providers` 并返回 null。抛出异常即显示错误状态:上次的结果保留,倒计时变为重试。新的检查会通过取消令牌取消较早的检查。没有它时,刷新控件隐藏。 + + + Time between automatic checks (30 seconds by default). The countdown pauses while the card is off screen or hidden; retries after an error wait at most 10 seconds. Null turns automatic checks off and keeps the refresh button. + 两次自动检查之间的时间(默认 30 秒)。卡片不在屏幕上或被隐藏时倒计时暂停;出错后的重试最多等待 10 秒。为 null 时关闭自动检查,但保留刷新按钮。 + + + A simulated status feed to play while `Providers` and `RefreshHandler` are null: its first step shows at once, and each simulated check on the countdown moves its story on. Null (the default) shows no providers. Give each card its own simulation. + `Providers` 和 `RefreshHandler` 为 null 时播放的模拟状态源:其第一步立即显示,倒计时上的每次模拟检查都会推进故事。为 null(默认)时不显示服务商。每张卡片使用自己的模拟。 + + + The card's title. + 卡片的标题。 + + + One day in a provider's history. + 服务商历史中的一天。 + + + An incident of a provider: a title and its updates. + 服务商的一个事件:标题及其更新。 + + + One AI provider in a `ProviderStatus` card: its mark, health, latency, daily history and incident. + `ProviderStatus` 卡片中的一个 AI 服务商:它的标志、健康状况、延迟、每日历史和事件。 + + + Every word `ProviderStatus` shows or says, in English by default. Replace the whole object to localize or reword; numbers, dates and sentences that carry values are format functions. + `ProviderStatus` 显示或朗读的每一个词,默认是英文。替换整个对象即可本地化或改写措辞;数字、日期以及带数值的句子是格式化函数。 + + + A simulated status feed for a `ProviderStatus` that has no `Providers` and no `RefreshHandler`, for demos and prototypes. The card shows the first step's providers, then runs a simulated check every `Interval` on its countdown (or when the refresh button is pressed): each check takes `CheckDelay` and moves to the next step, looping, so incidents can open, post updates and resolve. Nothing is fetched. Create one per card: it keeps its place in the story. + 供没有 `Providers` 和 `RefreshHandler` 的 `ProviderStatus` 使用的模拟状态源,用于演示和原型。卡片先显示第一步的服务商,然后按倒计时每隔 `Interval`(或按下刷新按钮时)进行一次模拟检查:每次检查耗时 `CheckDelay`,并进入下一步、循环播放,因此事件可以开启、发布更新并解决。不会获取任何数据。每张卡片创建一个:它会记住故事进行到哪里。 + + + One simulated check of a `ProviderStatusSimulation`: what it returns, or that it fails. + `ProviderStatusSimulation` 的一次模拟检查:它返回的内容,或者它失败。 + + + One update of an incident. + 事件的一条更新。 + One option of `RadioCards` (Arc `RadioCardOption`): a label with optional description, a meta value (price, estimate), a decorative icon, and a reason shown in place of the description when the option is disabled. `RadioCards` 的一个选项(Arc 的 `RadioCardOption`):包含标签(可选描述)、元数据值(价格、预估时长)、装饰性图标,以及选项被禁用时代替描述显示的原因。 @@ -15028,6 +18456,94 @@ Raised when the viewer adds or removes a reaction (Arc's onValueChange), after `Value` changed. 观看者添加或移除回应时触发(Arc 的 onValueChange),在 `Value` 变化之后。 + + The collapsible thinking block of an AI reply (Arc Reasoning block, design reference). While the model thinks, a shimmering label counts the seconds and the latest reasoning streams under it in muted text, two lines tall. When it finishes it folds to "Thought for 7s" with a chevron; opening it shows the full reasoning as steps with headings on a quiet rail, then a footer with the outcome and a copy button. Give it the reasoning as one growing string in `Reasoning` (blank lines split paragraphs and a line written as `**Heading**` or `## Heading` starts a step) or as `ReasoningStep`s in `Steps`, and set `Status`. The seconds are counted here unless you set `Elapsed`. With no data it reads "Thought process" and has nothing to open; a `Simulation` plays a reasoning you pass in instead. + AI 回复中可折叠的思考块(Arc Reasoning block,设计参考)。模型思考时,一个闪烁的标签逐秒计时,最新的推理以淡色文字在其下方流式出现,最多两行。完成后它折叠为带箭头的 "Thought for 7s";展开后以轨道上的步骤呈现完整推理,底部是结果和复制按钮。把推理作为一个不断增长的字符串传给 `Reasoning`(空行分隔段落,写成 `**Heading**` 或 `## Heading` 的行开始一个新步骤),或放入 `Steps`,并设置 `Status`。除非你自行设置 `Elapsed`,秒数由该块自己计时。没有数据时,它显示 "Thought process",也没有可展开的内容;`Simulation` 则会播放你传入的推理。 + + + Shows a Stop button while the model thinks; it raises `StopRequested`. + 在模型思考时显示停止按钮;它会引发 `StopRequested`。 + + + The time the model has thought. Null counts it here while thinking and keeps the count after. + 模型已思考的时间。为 null 时,在思考期间由该块自行计时,结束后保留计数。 + + + The reason shown at the end of the reasoning when `Status` is `Error`. + `Status` 为 `Error` 时,显示在推理末尾的原因。 + + + Whether the steps are open (two-way). + 步骤是否展开(双向)。 + + + Every word the block shows or says. + 该块显示或朗读的每一个文字。 + + + The 18px mark at the left of the head (a 24 × 24 stroked glyph); the sparkle by default. + 头部左侧 18px 的标记(24 × 24 的描边图标);默认为星芒。 + + + The mark's color; the accent by default. + 标记的颜色;默认为强调色。 + + + The reasoning so far as one string. Set it as it grows; it takes the place of `Steps`. + 到目前为止的推理,作为一个字符串。随其增长不断设置它;它取代 `Steps`。 + + + Raised when the person presses Retry on a failed block. Bubbles. + 当人在失败的块上按下重试时引发。会冒泡。 + + + A simulated model to play while the block has no reasoning, steps, status or time of its own (demos and prototypes). The block starts it over each time it takes it up; null shows the block's own state. + 要播放的模拟模型,在该块没有自己的推理、步骤、状态或时间时使用(演示与原型)。该块每次接手它时都会从头开始;为 null 时显示该块自己的状态。 + + + Where the block is: thinking, done, stopped or failed. + 该块所处的状态:思考中、已完成、已停止或失败。 + + + Raised when the person presses Stop. Bubbles. Abort your request, then set `Status` to Stopped. + 当人按下停止时引发。会冒泡。终止你的请求,然后把 `Status` 设为 Stopped。 + + + A card or a plain block. + 卡片或精简样式的块。 + + + Every word `ReasoningBlock` shows or says, in English by default. Replace the whole object to localize or reword; the times are format functions. + `ReasoningBlock` 显示或朗读的每一个文字,默认为英文。若要本地化或改写,请替换整个对象;时间是格式化函数。 + + + Plays a reasoning into a `ReasoningBlock` the way a model streams it, for demos, prototypes and tests: the block thinks, the seconds count, and `Reasoning` arrives three words every 45ms; then the block finishes with the time it took. Nothing is sent to a model. Set it as the block's `Simulation`. The block plays it while it has no reasoning, steps, status or time of its own, starts it over each time it takes it up, and advances it only while it is on screen; Stop on the block stops it. Use one simulation per block. It has no timer of its own: without a block, call `Advance` with the time that passed. + 像模型流式输出那样把一段推理播放到 `ReasoningBlock` 中,用于演示、原型和测试:该块进入思考、逐秒计时,`Reasoning` 以每 45ms 三个词的速度到达;然后该块以所用的时间完成。不会向任何模型发送内容。把它设为该块的 `Simulation`。该块在没有自己的推理、步骤、状态或时间时播放它,每次接手时从头开始,只在显示于屏幕上时推进;该块上的停止按钮会停止它。每个块使用一个模拟。它没有自己的计时器:没有块时,调用 `Advance` 并传入经过的时间。 + + + Raised when the text, the status or the shown second changes. + 文字、状态或显示的秒数变化时引发。 + + + Raised once the last word is in and the simulated model has stopped thinking. + 最后一个词到达、模拟模型停止思考时引发一次。 + + + The whole reasoning to stream, written the way `Reasoning` reads it (blank lines split paragraphs, `**Heading**` starts a step). A change takes effect the next time the simulation starts. + 要流式输出的完整推理,写法与 `Reasoning` 的读取方式相同(空行分隔段落,`**Heading**` 开始一个步骤)。修改会在模拟下次开始时生效。 + + + One step of a model's reasoning: an optional heading and the text under it. Change `Text` in place as the model streams; the `ReasoningBlock` follows every change. + 模型推理的一个步骤:一个可选的标题和其下的文字。模型流式输出时就地修改 `Text`;`ReasoningBlock` 会跟随每次修改。 + + + The step's heading (a few words); empty for a step that is only text. + 步骤的标题(几个词);仅有文字的步骤为空。 + + + The step's text. + 步骤的文字。 + One thought of a `ReasoningTree`: a short label for the node, the full sentence for the tag and screen readers, an illustrative score and the thoughts it leads to. The tree reads three levels: the thoughts given to `Thoughts` are depth 1, their children depth 2 and their grandchildren depth 3; deeper thoughts are ignored. Add a thought to one parent only. `ReasoningTree` 中的一个思路:节点的简短标签、用于标签与屏幕阅读器的完整句子、一个示意分数,以及它所通向的思路。树会呈现三个层级:给予 `Thoughts` 的思路为深度 1,它们的子节点为深度 2,孙节点为深度 3;更深的思路将被忽略。请只把一个思路添加到一个父节点下。 @@ -15092,9 +18608,129 @@ The arguments of `Answered`: the answer the search reached and how it got there. `Answered` 的参数:搜索到达的答案,以及它是如何得出的。 - - Every word a `ReasoningTree` shows or announces, in English by default: column headers, the header status, the beam width label, the tag and accessible names of the thoughts and the polite announcements. Replace the instance to translate or reword; sentences with numbers are functions so that word order can change. The playback buttons are named by `PlaybackLabels`. - `ReasoningTree` 显示或宣读的每一个文字,默认为英文:列标题、标题状态、束宽标签、思路的标签与无障碍名称,以及实时区域的播报。替换整个实例即可翻译或改写;包含数字的句子为函数,以便调整词序。播放按钮的名称由 `PlaybackLabels` 指定。 + + Every word a `ReasoningTree` shows or announces, in English by default: column headers, the header status, the beam width label, the tag and accessible names of the thoughts and the polite announcements. Replace the instance to translate or reword; sentences with numbers are functions so that word order can change. The playback buttons are named by `PlaybackLabels`. + `ReasoningTree` 显示或宣读的每一个文字,默认为英文:列标题、标题状态、束宽标签、思路的标签与无障碍名称,以及实时区域的播报。替换整个实例即可翻译或改写;包含数字的句子为函数,以便调整词序。播放按钮的名称由 `PlaybackLabels` 指定。 + + + One person invited through a `ReferralCard`. + 通过 `ReferralCard` 邀请的一个人。 + + + A give-and-get referral card (Arc Referral card, design reference). The ask on top ("Give $20, get $20"), the invite link in a quiet field with a Copy button and three share buttons (a post, a network and email, drawn with generic glyphs), and an invite-by-email field that turns each address into a chip. Below, the funnel as three rolling numbers (invited, signed up, earned), a segmented milestone bar with the reward it unlocks, and the people invited with their avatar and stage as a glyph plus a word. Everything is derived from `Referrals`: pass the list from your backend and replace it when sign-ups and payments come in. Stage changes crossfade in place, the earned total counts up, and reaching the goal turns the bar and its line into the unlocked state. Sending invites calls `InviteHandler`; without one the send is simulated. With no referrals the card plays its `Simulation` (your program) once when it is 40% on screen; with neither it is empty: zero counts and "No invites yet". + 双向奖励的推荐卡片(Arc Referral card,设计参考)。顶部是邀约("Give $20, get $20"),邀请链接放在一个安静的字段里,带复制按钮和三个分享按钮(动态、人脉和邮件,使用通用图标),另有一个按邮箱邀请的字段,把每个地址变成一个标签。下方是三个滚动数字组成的漏斗(已邀请、已注册、已赚取)、显示所解锁奖励的分段里程碑进度条,以及被邀请的人:头像加上以图标和文字表示的阶段。一切都由 `Referrals` 推导:传入后端的列表,并在有人注册或付款时替换它。阶段变化在原位交叉淡化,已赚取总额累加,达到目标时进度条及其文字变为已解锁状态。发送邀请会调用 `InviteHandler`;没有它时发送是模拟的。没有推荐数据时,卡片在有 40% 进入屏幕时播放一次它的 `Simulation`(你的计划);两者都没有时卡片为空:计数为零,并显示 "No invites yet"。 + + + The ISO 4217 code used to format the reward and the total ("USD" writes "$20"). + 用于格式化奖励和总额的 ISO 4217 货币代码("USD" 写作 "$20")。 + + + One or two lines under the heading. Null says what both sides get. + 标题下方的一两行文字。为 null 时说明双方各得什么。 + + + The subject of the share email, or null for none. + 分享邮件的主题,为 null 时没有主题。 + + + Sends the invites. Send stays busy until the task ends; a failed task keeps the chips and shows the exception's message; on success the addresses join the top of the list as Invited. The token is cancelled when the card leaves the screen. Null simulates the send. + 发送邀请。任务结束前发送按钮保持忙碌状态;任务失败时保留标签并显示异常消息;成功后这些地址以 Invited 状态加入列表顶部。卡片离开屏幕时取消令牌。为 null 时模拟发送。 + + + Raised after a send succeeded (or was simulated), with the addresses. Bubbles. + 发送成功(或模拟发送)后引发,带上这些地址。会冒泡。 + + + Every word the card shows or says. + 卡片显示或朗读的每一个词。 + + + The person's invite link. The domain shows muted and the code in full color; Copy copies the whole URL. + 此人的邀请链接。域名以弱化色显示,邀请码以正文色显示;复制按钮复制完整 URL。 + + + Raised after Copy has written the link to the clipboard. Bubbles. + 复制按钮把链接写入剪贴板后引发。会冒泡。 + + + The most addresses one send can hold. Extra addresses are refused with a note. + 一次发送最多可包含的地址数。多出的地址会被拒绝并给出说明。 + + + The bonus after a number of paid referrals: the goal (up to 12 segments are drawn), the reward in a few words and an optional line shown once unlocked. Null (the default) hides the milestone. + 达到一定数量的付费推荐后获得的额外奖励:目标(最多绘制 12 段)、用几个词说明的奖励,以及解锁后显示的可选说明。为 null(默认)时隐藏里程碑。 + + + Raised once when paid referrals reach the milestone's goal. Bubbles. + 付费推荐达到里程碑目标时引发一次。会冒泡。 + + + The share URL of the network button, with `{url}` and `{text}` placeholders. Null passes the link itself. + 人脉按钮的分享 URL,带 `{url}` 和 `{text}` 占位符。为 null 时直接传递链接本身。 + + + The share URL of the post button, with `{url}` and `{text}` placeholders (filled escaped), for example your social network's share intent. Null passes the link itself. + 动态按钮的分享 URL,带 `{url}` 和 `{text}` 占位符(以转义后的值填入),例如你的社交网络的分享地址。为 null 时直接传递链接本身。 + + + Everyone invited, newest first. The counts, the milestone and the list are derived from it, and the card follows it when you replace it. Null shows the `Simulation`'s program, or the empty card. + 所有被邀请的人,按从新到旧排列。计数、里程碑和列表都由它推导,替换它时卡片随之更新。为 null 时显示 `Simulation` 的计划,否则显示空卡片。 + + + The credit each side gets, in whole currency units. Drives the heading, the Earned total and the rows' Earned label; 0 (the default) leaves the amount out of the words. + 双方各得的奖励,以整数货币单位计。决定标题、已赚取总额以及各行的 Earned 标签;为 0(默认)时文字中不写金额。 + + + Text that goes with the link in a post, a network share and the email body. + 在动态、人脉分享和邮件正文中与链接一起出现的文字。 + + + Raised when a share button is pressed, with the channel and a ready share URL. Open the URL and set `Handled`; when nobody does, the share is simulated and the card says so. Bubbles. + 按下分享按钮时引发,带上渠道和现成的分享 URL。打开该 URL 并设置 `Handled`;没有人这样做时,分享是模拟的,卡片会说明这一点。会冒泡。 + + + Plays the `Simulation`'s beats once on screen and moves the first address of each simulated send through the stages. Null means true while a `Simulation` shows (no `Referrals`) and false otherwise. + 在屏幕上播放一次 `Simulation` 的节拍,并让每次模拟发送的第一个地址依次经历各个阶段。为 null 时,显示 `Simulation`(没有 `Referrals`)则为 true,否则为 false。 + + + A program to play while `Referrals` is null: the card shows its list and plays its beats once when it is 40% on screen. Null (the default) leaves a card without referrals empty. Give each card its own. + 在 `Referrals` 为 null 时播放的计划:卡片显示它的列表,并在卡片有 40% 进入屏幕时播放一次它的节拍。为 null(默认)时,没有推荐数据的卡片为空。每张卡片各用一个。 + + + The heading. Null builds it from the reward: "Give $20, get $20" (or "Invite friends" without one). + 标题。为 null 时由奖励生成:"Give $20, get $20"(没有奖励时为 "Invite friends")。 + + + Every word `ReferralCard` shows or says, in English by default. Replace the whole object to localize or reword; counts, amounts and dates are format functions. Amounts arrive already formatted ("$20"). + `ReferralCard` 显示或朗读的每一个词,默认是英文。替换整个对象即可本地化或改写措辞;数量、金额和日期是格式化函数。金额传入时已经格式化("$20")。 + + + A referral program for a `ReferralCard` to play, written by your app (nothing ships with the package): the list it starts from and the beats that move friends through the stages. Give it to `Simulation`; while the card has no referrals of its own it shows this list and plays `Beats` once when it is 40% on screen (twice as fast under reduced motion), and moves the first address of each simulated send through the stages. Data only; the card keeps the playback state. + 供 `ReferralCard` 播放的推荐计划,由你的应用编写(包里不附带任何计划):它起始的列表,以及让好友依次经历各个阶段的节拍。把它交给 `Simulation`;卡片没有自己的推荐数据时,会显示这个列表,并在卡片有 40% 进入屏幕时播放一次 `Beats`(减少动态效果时以两倍速度播放),还会让每次模拟发送的第一个地址依次经历各个阶段。仅为数据;播放状态由卡片保存。 + + + One beat of a `ReferralCardSimulation`: after `Delay`, the referral `Id` moves to `Stage` (taking `Name` when it has none yet, as a friend who signs up does). + `ReferralCardSimulation` 的一个节拍:经过 `Delay` 后,推荐 `Id` 进入 `Stage`(尚无名字时采用 `Name`,就像一位注册的好友)。 + + + Arguments of `InvitesSent`. + `InvitesSent` 的参数。 + + + Arguments of `LinkCopied`. + `LinkCopied` 的参数。 + + + The bonus a `ReferralCard` unlocks after a number of paid referrals. + `ReferralCard` 在达到一定数量的付费推荐后解锁的额外奖励。 + + + Arguments of `MilestoneReached`. + `MilestoneReached` 的参数。 + + + Arguments of `ShareRequested`. Open `Url` and set `Handled`; when no handler does, the card treats the share as simulated and says so. + `ShareRequested` 的参数。打开 `Url` 并设置 `Handled`;没有处理程序这样做时,卡片把这次分享视为模拟并加以说明。 One check of a `ReleaseReadiness` gate: a title, an optional line under it and whether it is done. Every required check must be done before the release can start; optional checks (`IsRequired` false) are shown and can be ticked, but do not hold the release back. @@ -15340,6 +18976,162 @@ A row of a `ReorderableList` as Arc models it: an id, a title and an optional detail line. Arc 建模的 `ReorderableList` 中的一行:包含一个 id、一个标题和一个可选的详细信息行。 + + An AI email reply drafter (Arc Reply draft, design reference): a received email with an AI reply streaming under it. Quick reply chips steer a fresh draft, which streams in chunks of 3 to 8 words that each fade in over 140ms while the draft box springs to its height. Tone (Friendly, Concise, Formal) and length (Short, Medium, Long) rewrite a finished draft with one crossfade: the old text dims, fades out in 100ms and the new one fades in over 160ms rising 2px. The finished draft is plain editable text; Regenerate writes a new one, and Insert (or Cmd/Ctrl+Enter anywhere in the card) hands the text, with the reader's edits, to `InsertHandler` and `InsertRequested`. Give it a `Message`, `Suggestions` and a `Generator` that streams your model's reply. Without a message it plays a `Simulation` (an email and the replies you wrote for it, nothing is sent) and, unless the reader touches it, tours tones, length, a second quick reply and Insert on a loop while it is on screen. With neither it shows an empty email and a draft box that cannot draft. + AI 邮件回复起草器(Arc Reply draft,设计参考):一封收到的邮件,下方流式写出 AI 回复。快捷回复标签引导一份新的草稿,草稿以每块 3 到 8 个词的片段流式出现,每块在 140ms 内淡入,同时草稿框以弹簧过渡到新高度。语气(Friendly、Concise、Formal)与长度(Short、Medium、Long)以一次交叉淡化改写已完成的草稿:旧文字变暗,在 100ms 内淡出,新文字在 160ms 内淡入并上升 2px。完成的草稿是可编辑的纯文本;Regenerate 重新写一份,Insert(或在卡片内任意位置按 Cmd/Ctrl+Enter)把包含读者修改的文字交给 `InsertHandler` 与 `InsertRequested`。为它提供 `Message`、`Suggestions` 以及流式输出模型回复的 `Generator`。没有邮件时,它播放 `Simulation`(一封邮件和你为它写好的回复,不发送任何内容),并且只要读者没有操作它,就会在可见时循环演示语气、长度、第二个快捷回复与 Insert。两者都没有时,显示一封空白邮件和一个无法起草的草稿框。 + + + Tours tones, length, a second quick reply and Insert on a loop. It stops for good on the first pointer or key press inside the card and pauses off screen. Null turns it on only while a `Simulation` plays; leave it off in products. + 循环演示语气、长度、第二个快捷回复与 Insert。在卡片内第一次按下指针或按键后永久停止,离开屏幕时暂停。为 null 时仅在播放 `Simulation` 时开启;在产品中请关闭。 + + + Starts the first draft when the card scrolls into view. When false the draft box shows an empty state with a Draft reply button. + 卡片滚动进入视图时开始写第一份草稿。为 false 时草稿框显示带 Draft reply 按钮的空状态。 + + + The draft on screen, including the reader's edits; empty before the first draft. + 屏幕上的草稿,包含读者的修改;第一份草稿之前为空。 + + + Raised each time a draft or rewrite finishes. Cancelled and failed requests do not raise it. Bubbles. + 每当一份草稿或改写完成时引发。被取消或失败的请求不会引发。冒泡。 + + + Writes the draft as a stream of text deltas. Every new action cancels the previous request's token. Without it a message of yours cannot be drafted; a playing `Simulation` does not use it. + 以文字增量流的形式写出草稿。每个新操作都会取消上一个请求的令牌。没有它时,你自己的邮件无法起草;正在播放的 `Simulation` 不会使用它。 + + + Puts the draft into your composer. While its task runs, Insert shows a spinner. With neither this nor a handler that marks `InsertRequested` handled, Insert is simulated and says that nothing was sent. + 把草稿放入你的编辑器。其任务运行期间,Insert 显示加载圈。既没有它,也没有把 `InsertRequested` 标记为已处理的处理程序时,插入是模拟的,并会说明没有发送任何内容。 + + + Raised when Insert is pressed (or Cmd/Ctrl+Enter), with the final draft and the reader's edits, before `InsertHandler` runs. Mark it handled when you insert the text here, so the card does not say it was simulated. Bubbles. + 按下 Insert(或 Cmd/Ctrl+Enter)时,在 `InsertHandler` 运行之前引发,带有最终草稿与读者的修改。如果你在这里插入文字,请将它标记为已处理,这样卡片就不会说是模拟的。冒泡。 + + + Adds "Simulated, nothing was sent" to announcements and errors. Null turns it on while a `Simulation` plays or there is no `Generator`. + 在播报与错误中加上 "Simulated, nothing was sent"。为 null 时,在播放 `Simulation` 或没有 `Generator` 时开启。 + + + Every word the card shows or says. + 卡片显示或播报的每个词。 + + + The length (two-way). Works like `Tone`. + 长度(双向)。行为与 `Tone` 相同。 + + + Raised when the reader picks a length (not when the app or the tour sets it). Bubbles. + 读者选择长度时引发(应用或演示设置时不引发)。冒泡。 + + + The received email; null plays `Simulation`, or shows an empty email without one. + 收到的邮件;为 null 时播放 `Simulation`,没有模拟时显示一封空白邮件。 + + + The model credited on the reply, with its 16px mark; null credits the `Simulation`'s model while one plays, or none. + 回复上署名的模型及其 16px 标记;为 null 时,播放 `Simulation` 期间署名其模型,否则不署名。 + + + The id of the quick reply steering the draft (two-way). Null, or an id that is not in the list, means the first suggestion. Setting it after the first draft writes a fresh one. + 正在引导草稿的快捷回复的 id(双向)。为 null 或不在列表中的 id 时表示第一个快捷回复。在第一份草稿之后设置它会写出一份新的草稿。 + + + An email to play while the card has no `Message`: the message, quick replies, model and the replies written for them, streamed as a simulation (nothing is sent). Your own `Message` wins over it; null stops it and shows the empty card. + 卡片没有 `Message` 时要播放的邮件:邮件、快捷回复、模型以及为它们写好的回复,以模拟方式流式输出(不发送任何内容)。你自己的 `Message` 优先于它;为 null 时停止播放并显示空卡片。 + + + The 24 × 24 stroked mark before the subject; null draws a generic mail glyph. + 主题前的 24 × 24 描边标记;为 null 时绘制通用的邮件图标。 + + + Where the card is: idle, drafting, rewriting, ready, inserting, inserted or failed. + 卡片所处的状态:空闲、起草中、改写中、就绪、插入中、已插入或失败。 + + + Raised when the reader chooses a quick reply chip, before its draft starts. Bubbles. + 读者选择快捷回复标签时、在其草稿开始之前引发。冒泡。 + + + The quick replies shown as chips above the draft; choosing one streams a fresh draft steered by it. Null shows the `Simulation`'s while one plays; null or an empty list hides the row. + 以标签显示在草稿上方的快捷回复;选择其中一个会流式写出由它引导的新草稿。为 null 时,播放 `Simulation` 期间显示其快捷回复;为 null 或空列表时隐藏这一行。 + + + The tone (two-way). Changing it rewrites a finished draft with one crossfade, or restarts a draft that is still streaming. + 语气(双向)。改变它会以一次交叉淡化改写已完成的草稿,或重新开始仍在流式输出的草稿。 + + + Raised when the reader picks a tone (not when the app or the tour sets it). Bubbles. + 读者选择语气时引发(应用或演示设置时不引发)。冒泡。 + + + Arguments of `DraftCompleted` and `InsertRequested`: the text and how it was written. + `DraftCompleted` 与 `InsertRequested` 的参数:文字及其写法。 + + + Writes a `ReplyDraft` reply as a stream of text deltas. Yield chunks from your model's stream, or wrap a whole text with `FromText` to have it streamed locally in chunks. Throw to show the error state; honour `CancellationToken`. + 以文字增量流的形式写出 `ReplyDraft` 的回复。从模型的流中逐块产出,或用 `FromText` 包装整段文字,让它在本地分块流式输出。抛出异常即显示错误状态;请遵守 `CancellationToken`。 + + + Puts a finished `ReplyDraft` reply into your composer. While the returned task runs, Insert shows a spinner; a fault shows the error in the status line and keeps the draft. + 把完成的 `ReplyDraft` 回复放入你的编辑器。返回的任务运行期间,Insert 显示加载圈;出错时在状态行显示错误并保留草稿。 + + + Every word `ReplyDraft` shows or says, in English by default. Replace the whole object to localize or reword; the words that carry a name or a count are format functions. + `ReplyDraft` 显示或播报的每个词,默认为英文。替换整个对象即可本地化或改写;带有名称或数量的词是格式化函数。 + + + How long a `ReplyDraft` reply is. + `ReplyDraft` 回复的长度。 + + + Arguments of `LengthChanged`. + `LengthChanged` 的参数。 + + + The received email a `ReplyDraft` answers: who sent it, the subject, the body (blank lines split paragraphs) and when it arrived. Data only. + `ReplyDraft` 回复的那封收到的邮件:发件人、主题、正文(空行分隔段落)以及到达时间。仅为数据。 + + + What a `ReplyDraftRequest` asks the generator for. + `ReplyDraftRequest` 要求生成器写什么。 + + + What a `ReplyDraftGenerator` is asked to write. + 要求 `ReplyDraftGenerator` 写出的内容。 + + + An email a `ReplyDraft` plays without a generator, from data you give it: the received message, the quick replies, the model credited and the replies you wrote for each quick reply, tone and length. Set it as `Simulation` while the card has no `Message`: the card shows the email, streams the matching reply after a short read (3 to 8 words every 34ms), says it is simulated (nothing is sent) and, unless `AutoPlay` is false, tours tones, length, a second quick reply and Insert on a loop. + `ReplyDraft` 在没有生成器时按你提供的数据播放的邮件:收到的邮件、快捷回复、署名的模型,以及你为每个快捷回复、语气和长度写好的回复。在卡片没有 `Message` 时把它设为 `Simulation`:卡片显示邮件,在短暂的阅读之后流式输出匹配的回复(每 34ms 3 到 8 个词),说明这是模拟(不发送任何内容),并且除非 `AutoPlay` 为 false,会循环演示语气、长度、第二个快捷回复与 Insert。 + + + Where a `ReplyDraft` is. + `ReplyDraft` 所处的状态。 + + + Streams for `ReplyDraft`: a wrapper that streams a whole text locally and the chunker it and `ReplyDraftSimulation` use (3 to 8 words every 34ms). Nothing is sent. + `ReplyDraft` 的流:在本地流式输出整段文字的包装器,以及它和 `ReplyDraftSimulation` 共用的分块器(每 34ms 3 到 8 个词)。不发送任何内容。 + + + A quick reply chip of a `ReplyDraft`: a stable id the prompt understands and the label readers see. + `ReplyDraft` 的一个快捷回复标签:提示词能理解的稳定 id,以及读者看到的标签文字。 + + + Arguments of `SuggestionSelected`. + `SuggestionSelected` 的参数。 + + + The voice of a `ReplyDraft` reply. + `ReplyDraft` 回复的语气。 + + + Arguments of `ToneChanged`. + `ToneChanged` 的参数。 + + + One reply of a `ReplyDraftSimulation`: the quick reply, tone and length it answers, and its text. + `ReplyDraftSimulation` 的一条回复:它回应的快捷回复、语气和长度,以及它的文字。 + Fails an empty value (null, empty text). 空值(null、空文字)不通过。 @@ -15392,6 +19184,106 @@ Each pane's share in percent. Updated after every resize settles; set it (for example from saved settings) to restore a layout. Two-way. 每个窗格的占比(百分比)。每次调整大小稳定后更新;设置它(例如从保存的设置中读取)即可恢复布局。双向绑定。 + + An AI reply card with its feedback (Arc Response feedback, design reference): the person's message in a bubble, the model's mark and name, the reply and an action row with copy, an optional regenerate and the thumbs of `FeedbackActions`. Thumbs down opens an inline panel with reason chips and an optional note, thumbs up fills and asks what was good, and a submit shows a short thank-you before the panel folds back into the row. With no data it shows the frame alone: the model's mark, an empty reply and the action row, whose feedback is simulated until you set a `FeedbackHandler`. Every property of `FeedbackActions` except `Leading` is available here. + 带反馈的 AI 回复卡片(Arc Response feedback,设计参考):气泡中的用户消息、模型的标志和名称、回复,以及包含复制、可选的重新生成和 `FeedbackActions` 点赞点踩的操作行。点踩会在行内展开一个带原因标签和可选备注的面板,点赞会填充图标并询问哪里做得好,提交后显示简短的感谢,随后面板收回到操作行中。没有数据时只显示框架:模型标识、空的回复和操作行;在你设置 `FeedbackHandler` 之前,反馈为模拟发送。除 `Leading` 外,`FeedbackActions` 的每个属性在这里都可用。 + + + Shows the optional note field after a thumbs down. + 点踩后显示可选的备注框。 + + + Shows a regenerate action in the row; it raises `RegenerateRequested`. + 在操作行中显示重新生成操作;它会引发 `RegenerateRequested`。 + + + The note's maximum length in characters. + 备注的最大长度(字符数)。 + + + The note field's placeholder. + 备注框的占位文字。 + + + Plain text for the copy action; null (the default) hides copy. + 复制操作使用的纯文本;为 null(默认)时隐藏复制。 + + + Persists feedback: called with a `Rating` submission on every thumb press and a `Details` one when reasons or a note are submitted. Throw (or fault the task) to show the inline error and Try again; the token is cancelled when the panel closes or the rating changes. Null simulates sends. + 持久化反馈:每次按下点赞或点踩时以 `Rating` 提交调用,提交原因或备注时以 `Details` 提交调用。抛出异常(或让任务失败)即可显示行内错误和 Try again;面板关闭或评分改变时令牌会被取消。为 null 时模拟发送。 + + + Raised for every submission (the event of the inner `FeedbackActions`, which bubbles here). + 每次提交时引发(内部 `FeedbackActions` 的事件,冒泡到这里)。 + + + Whether the thumbs can be pressed; turn it off while the reply is still streaming. + 是否可以按下点赞和点踩;回复仍在流式输出时将其关闭。 + + + Every word the control shows or announces. + 控件显示或播报的每一个词。 + + + Passed back on every submission so feedback can be stored against a message. + 在每次提交时传回,以便把反馈与某条消息关联存储。 + + + The model's mark, a 24 × 24 stroked glyph drawn at 16px; a neutral sparkle by default. + 模型的标志,一个以 16px 绘制的 24 × 24 描边图形;默认是一个中性的闪光图标。 + + + The name of the model that wrote the reply; null (the default) shows the mark alone. + 写出这条回复的模型名称;为 null(默认)时只显示标识。 + + + Reason chips offered after a thumbs down. A chip labelled Other moves focus to the note and changes its placeholder. + 点踩后提供的原因标签。标为 Other 的标签会把焦点移到备注并更换其占位文字。 + + + Chips offered after a thumbs up. An empty list skips the follow-up row. + 点赞后提供的标签。空列表会跳过后续行。 + + + A quiet line next to Submit about what gets shared. Hidden below 400px of width; null hides it. + Submit 旁边一行低调的文字,说明会分享什么。宽度低于 400px 时隐藏;为 null 时不显示。 + + + The person's message shown above the reply; null (the default) hides it. + 显示在回复上方的用户消息;为 null(默认)时隐藏。 + + + The rating (two-way). Set it to show a rating loaded from your database; bind it to own the state. A thumb press sets it and pressing the active thumb sets `None`. + 评分(双向)。设置它可显示从数据库读取的评分;绑定它可自己掌管状态。按下点赞或点踩会设置它,再次按下当前激活的按钮会设为 `None`。 + + + Raised when a thumb press changes the rating (the event of the inner `FeedbackActions`, which bubbles here). + 按下点赞或点踩改变评分时引发(内部 `FeedbackActions` 的事件,冒泡到这里)。 + + + Raised when the person presses regenerate. Bubbles. + 用户按下重新生成时引发。冒泡。 + + + The reply body: text or any control. Null leaves the reply empty. + 回复正文:文本或任意控件。为 null 时回复为空。 + + + How long the thank-you state holds before the panel folds away. + 感谢状态停留多久后面板收起。 + + + The second line of the thank-you state. Null reads "It helps improve future replies." with a `FeedbackHandler`, and says nothing was sent without one. + 感谢状态的第二行。为 null 时,有 `FeedbackHandler` 则显示 "It helps improve future replies.",没有则说明没有发送任何内容。 + + + Content pinned to the end of the row, such as a timestamp or a token count. + 固定在行尾的内容,例如时间戳或令牌数。 + + + Every word `FeedbackActions` and `ResponseFeedback` show or announce, in English by default. Replace the whole object to localize or reword. The reasons, the note's placeholder, the privacy note and the second line of the thank-you state are properties of the control. + `FeedbackActions` 和 `ResponseFeedback` 显示或播报的每一个词,默认为英文。替换整个对象即可本地化或改写。原因、备注的占位文字、隐私说明和感谢状态的第二行是控件的属性。 + One cohort: people (or accounts, workspaces) who started in the same period, and how many of them were still active in each month after that. The number of values in `Retained` is the number of months measured so far, so newer cohorts have shorter rows and the heatmap becomes a triangle. 一个队列:在同一周期开始的人(或账户、工作区),以及其中有多少人在之后每个月仍然活跃。`Retained` 中值的个数就是目前已测量的月数,因此较新的队列行更短,热力图呈三角形。 @@ -16244,6 +20136,10 @@ Where the save of a `RolesPermissions` matrix stands. `RolesPermissions` 矩阵的保存当前所处的状态。 + + A ticket that landed in a lane of a `TicketRouter`. + 落入 `TicketRouter` 某条泳道的工单。 + One axis of a `ConnectedScatter`. The range and the step are used exactly as given (no rounding to nice numbers, not forced to start at zero); leave them unset to derive them from the data of every series. `ConnectedScatter` 的一条坐标轴。范围和步长按给定值原样使用(不会取整为好看的数字,也不会强制从零开始);不设置则由所有系列的数据推算。 @@ -18228,6 +22124,10 @@ Arguments of `ProviderSelectedEvent` (Arc's `onProvider`). `ProviderSelectedEvent`(Arc 的 `onProvider`)的参数。 + + A recognizer that never touches the microphone: it speaks the `Scripts` you give it as scripted speech with interim guesses, corrections and speech-like levels. Set it as the `Simulation` of a `DictationControl` or a `DictationButton` (where the composer can loop it) for demos and design reviews, or use it as a recognizer in tests. Each start plays the next script; with no scripts the voice stays silent. + 从不访问麦克风的识别器:把你提供的 `Scripts` 以带临时猜测、纠正和类似语音电平的脚本化语音朗读出来。可把它设为 `DictationControl` 或 `DictationButton` 的 `Simulation`(输入框可以循环播放它),用于演示和设计评审,也可以在测试中当作识别器使用。每次启动播放下一段脚本;没有脚本时声音保持静默。 + A website footer in three layouts: link columns with a newsletter signup (`Columns`), a single quiet row (`Minimal`), and a closing layout where the Arc arch rises in, oversized and cropped by the bottom edge, fading into the page (`Logo`). The newsletter validates in place, keeps its width while the button morphs, and confirms without a toast. Every text, link, column and the status line can be replaced; with nothing set the footer shows Arc's sample footer. 网站页脚,提供三种布局:带电子报订阅的链接列(`Columns`)、单行简洁布局(`Minimal`),以及收尾布局(`Logo`),在此布局中 Arc 的拱形图案从底部升起,尺寸放大并被底边裁切,渐隐于页面之中。电子报订阅会就地验证,按钮变形时保持宽度,并在不弹出提示的情况下确认。所有文字、链接、列和状态行都可以替换;未设置时显示 Arc 的示例页脚。 @@ -18596,6 +22496,86 @@ Placeholder text lines (Arc clamps to 1–6). Defaults to 3. 占位文本行数(Arc 会限制在 1–6 之间)。默认为 3。 + + An image generation visual that paints the way an artist does (Arc Sketch to image, design reference). Pencil lines sketch the scene stroke by stroke with a pen tip that lifts and travels between strokes, a round brush sweeps flat color blocks in region by region from back to front, two soft detail passes, film grain and a warm light refine it, and it resolves into the finished picture. Under the canvas a stage label with an icon crossfades above the prompt, beside a thin progress line, a percent and a Replay button. Give it a `Scene` (what gets drawn) and, optionally, the finished `Image`. With no `Status` it loops on its own over them; set `Status` (or `Progress`) to follow a real generation. Nothing is generated: it is a simulated preview over your scene. Without a scene or an image it rests on blank paper, waiting. One clock drives a single 0 → 1 timeline; a frame redraws the ink layer and sets layer opacities, and allocates nothing. + 像画家一样作画的图像生成视觉效果(Arc Sketch to image,设计参考)。铅笔线条一笔一笔勾出场景,笔尖在笔画之间抬起并移动;圆头画笔从后到前逐个区域扫过,铺上平涂色块;两层柔和的细节、胶片颗粒与暖光加以润色,最后呈现完成的画面。画布下方,带图标的阶段标签在提示词上方交叉淡入淡出,旁边是一条细进度线、百分比和“重播”按钮。给它一个 `Scene`(要绘制的内容),也可以再给完成的 `Image`。不设置 `Status` 时它在这些内容上自行循环;设置 `Status`(或 `Progress`)即可跟随真实的生成。这里并不生成任何东西:它是在你的场景上播放的模拟预览。没有场景也没有图片时,它停在空白画纸上等待。一个时钟驱动唯一一条 0 → 1 的时间线;每一帧重绘墨迹层并设置各层的不透明度,不分配任何内存。 + + + Raised each time the picture is fully revealed. Bubbles. + 每次画面完全呈现时引发。冒泡。 + + + One full pass from blank paper to the picture. Sketching takes about 38%, blocking 24%, refining 26% and the final crossfade 12%. + 从空白画纸到完成画面的完整一遍所需时间。勾线约占 38%,铺色 24%,润色 26%,最后的交叉淡入 12%。 + + + The finished picture (your real result), stretched to fill the canvas; it overrides the scene's own. + 完成的画面(你的真实结果),拉伸以填满画布;它会取代场景自带的画面。 + + + When it plays on its own, holds the picture for about two seconds, fades back to blank paper and paints again. + 自行播放时,在画面上停留约两秒,淡回空白画纸后再画一遍。 + + + Every word the card shows or says. + 卡片显示或朗读的所有文字。 + + + Where the pass should be, from 0 (blank paper) to 1 (the picture), in place of `Status`'s stage ends (sketching ends at 0.38, blocking at 0.62, refining at 0.88). The painting still travels there at its own pace. + 这一遍应到达的位置,从 0(空白画纸)到 1(画面),用来代替 `Status` 的各阶段终点(勾线在 0.38 结束,铺色在 0.62,润色在 0.88)。绘画仍按自己的节奏前往那里。 + + + The prompt under the stage label, on one line with the full text in a tooltip. Empty or null (the default) hides it. + 阶段标签下方的提示词,单行显示,完整文字在提示框中。为空或 null(默认)时隐藏。 + + + Raised when Replay is pressed, for example to rerun a real request. Bubbles. + 按下“重播”时引发,例如用于重新发起真实的请求。冒泡。 + + + What gets drawn and the picture it resolves into. Null (with no `Image`) leaves blank paper: the card waits and does not loop. + 要绘制的内容及其最终呈现的画面。为 null(且没有 `Image`)时保持空白画纸:卡片等待,不会循环。 + + + Shows the Replay button, which fades to blank paper and paints again up to the current stage. + 显示“重播”按钮,它会淡回空白画纸,再重新画到当前阶段。 + + + The card's maximum width (340, 520 or 720px) and pencil weight. Small shows only Replay's icon. + 卡片的最大宽度(340、520 或 720px)与铅笔粗细。Small 只显示“重播”的图标。 + + + The stage on screen (it can trail `Status` while the painting travels there). + 屏幕上显示的阶段(绘画前往目标期间可能落后于 `Status`)。 + + + Raised once each time the stage on screen changes, never per frame. Bubbles. + 屏幕上的阶段每变化一次引发一次,从不逐帧引发。冒泡。 + + + The generation stage. Null (default) plays the whole pass on its own and loops (a card with no scene and no image waits on blank paper instead). A stage plays to its end and holds there with a shimmering label until the next one arrives; moving back (for example to idle for a new request) fades the canvas to blank paper first. Showing the control with done shows the picture straight away. + 生成阶段。null(默认)时自行播放完整一遍并循环(没有场景也没有图片的卡片则停在空白画纸上等待)。每个阶段播放到结尾并停在那里,标签保持闪烁,直到下一个阶段到来;往回走(例如为新请求回到 idle)时会先把画布淡回空白画纸。以 done 显示控件时直接展示画面。 + + + The color of the pencil lines, pen tip, stage icon and progress line. + 铅笔线条、笔尖、阶段图标与进度线的颜色。 + + + Every word `SketchToImage` shows or says, in English by default. Replace the whole object to localize or reword; the percent, the canvas name and the stage announcement are format functions. + `SketchToImage` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;百分比、画布名称与阶段播报是格式化函数。 + + + One region of a `SketchToImageScene`: shapes the brush sweeps in together through one rectangle. + `SketchToImageScene` 中的一个区域:画笔在同一个矩形内一起扫入的若干形状。 + + + What a `SketchToImage` draws and the picture it resolves into, on a 600 × 400 canvas: the pencil `Lines` drawn in order, the `Regions` a brush paints back to front, the point the warm light comes from, and the finished picture. Trace the lines and regions from your picture so the sketch matches it. + `SketchToImage` 在 600 × 400 画布上绘制的内容及其最终呈现的画面:按顺序绘制的铅笔 `Lines`、画笔从后到前上色的 `Regions`、暖光的来源位置,以及完成的画面。按你的画面描出线条与区域,让草图与之吻合。 + + + One shape of a `SketchToImageRegion`. + `SketchToImageRegion` 中的一个形状。 + Arc slider: a value or a range on a track. The thumb follows the pointer 1:1, rubber-bands up to 9px past its limits and settles on the step grid with the velocity of the release (on a coarse grid the release may carry it one step further); pressing the track springs the nearest thumb there. A bubble with rolling digits rides the thumb while it is dragged or keyboard focused and lingers 700ms after a release. Arrows, Shift+arrows, PageUp / PageDown and Home / End work as in a native range input; a key at a limit makes the thumb strain toward the press and spring home. Unlike `Slider`, `SmallChange` is the step the value snaps to. Set `IsRange` for two thumbs (`LowerValue` / `UpperValue`). Arc 滑块:在轨道上选择一个值或一个范围。滑块跟随指针 1:1 移动,超出限制时会有 9px 的回弹,并以松开时的速度落到步进网格上(在粗网格上,松开时可能再多移动一步);按下轨道会使最近的滑块弹到该处。拖动或键盘聚焦时,一个带滚动数字的气泡会跟随滑块,松开后再保留 700ms。方向键、Shift+方向键、PageUp / PageDown 以及 Home / End 的行为与原生范围输入一致;在极限处按键时,滑块会向按压方向拉伸,然后弹回原位。与 `Slider` 不同,`SmallChange` 是数值吸附的步长。设置 `IsRange` 可使用两个滑块(`LowerValue` / `UpperValue`)。 @@ -18932,6 +22912,402 @@ The order of the panels of a `SmallMultiples`. `SmallMultiples` 中面板的顺序。 + + A paste-to-fill form card (Arc Smart form fill, design reference). People paste an email signature, a forwarded message or messy notes into the well; AI reads it and fills the contact form field by field. Each value fades in with a confidence dot, the text it came from lights up in the pasted text, fields below `ReviewThreshold` ask for review (and hold Save until they are checked or edited), and Undo puts the values from before the fill back. Give it your model through `Extract`: it receives the text and a cancellation token (Stop, Escape and a new paste cancel) and returns a `SmartFormFillResult` with a value, a confidence, the exact source substring and an optional note per field id. With no extractor the card uses a simulated reader (a sample's known result, a small rule-based parser for anything else) and plays a `Simulation` if you give it one: its samples behind Try sample and, when motion is full, a loop until someone touches it. `Submit` saves the form. + 粘贴即填的表单卡片(Arc Smart form fill,设计参考)。用户把邮件签名、转发的邮件或零散笔记粘贴到粘贴区;AI 读取后逐个字段填写联系人表单。每个值淡入时带一个置信度圆点,它所来自的文字在粘贴文本中高亮;置信度低于 `ReviewThreshold` 的字段需要复核(在核对或修改之前无法保存),撤销会恢复填写之前的值。通过 `Extract` 接入你的模型:它接收文本和一个取消令牌(Stop、Escape 和新的粘贴会取消),并返回一个 `SmartFormFillResult`,按字段 id 给出值、置信度、原文子串和可选的备注。没有提取器时,卡片使用模拟读取器(示例的已知结果,其他文本由基于规则的小型解析器处理),并在你提供 `Simulation` 时播放它:Try sample 背后的示例,以及动效为完整时的自动循环演示,直到有人操作它。`Submit` 保存表单。 + + + Loops paste, fill, review and undo while the card is on screen, and stops for good on the first pointer or key press inside it. Null (default) plays while a `Simulation` plays. Never plays with reduced motion. + 卡片在屏幕上时循环演示粘贴、填写、复核和撤销,卡片内第一次指针按下或按键后永久停止。为 null(默认)时,在播放 `Simulation` 时播放。减弱动效时从不播放。 + + + Whether Undo can restore the values from before the last fill. + 撤销是否能恢复上一次填写之前的值。 + + + Starting values keyed by field id, applied when the card is built and whenever this changes. + 按字段 id 指定的初始值,在卡片构建时以及该属性变化时应用。 + + + One line under the title; an empty string hides it. + 标题下方的一行说明;空字符串会隐藏它。 + + + Reads the pasted text: receives it and a token that is cancelled by Stop, Escape, a new paste or the card leaving the tree, and returns a value per field id. Null uses the simulated reader (a sample's known result, a local parser for anything else) and plays `Simulation` when one is set. + 读取粘贴的文本:接收文本和一个令牌(Stop、Escape、新的粘贴或卡片离开可视树时取消),并按字段 id 返回值。为 null 时使用模拟读取器(示例的已知结果,其他文本由本地解析器处理),并在设置了 `Simulation` 时播放它。 + + + Raised once every field from a read has been filled. Bubbles. + 一次读取的所有字段都填写完成后触发一次。冒泡。 + + + Every other word the card shows or says. + 卡片显示或朗读的其他所有文字。 + + + The model's mark beside its name (a 24 × 24 stroked glyph); the sparkle by default. + 模型名称旁的标志(24 × 24 描边图标);默认为闪光图标。 + + + The model named beside the paste area; null names the `Simulation`'s model while one plays, or none. + 粘贴区旁显示的模型名称;为 null 时,播放 `Simulation` 期间显示其模型,否则不显示。 + + + Where the card is: idle, reading, filling, filled, saving, saved or failed. + 卡片当前所处的阶段:空闲、读取中、填写中、已填写、保存中、已保存或失败。 + + + + How many filled fields still ask for review. + 仍需复核的已填写字段数量。 + + + Fields whose confidence is below this ask for review and hold Save until checked or edited. Default 0.8. + 置信度低于此值的字段需要复核,在核对或修改之前无法保存。默认 0.8。 + + + The texts behind Try sample, cycled in order. Null uses the `Simulation`'s while one plays; null or an empty list hides the button and the autoplay. + Try sample 背后的文本,按顺序循环。为 null 时,播放 `Simulation` 期间使用其示例;为 null 或空列表时隐藏该按钮并关闭自动演示。 + + + Texts to play while the card has no `Extract`: the samples behind Try sample with the results a read returns, and the model to name (nothing is sent). Your own `Extract` wins over it; null stops it and leaves the empty form. + 卡片没有 `Extract` 时要播放的文本:Try sample 背后的示例及读取时返回的结果,以及要显示的模型(不会发送任何内容)。你自己的 `Extract` 优先于它;为 null 时停止播放并留下空表单。 + + + Saves the form with its values. The button shows a loading state while the task runs; a fault shows an error status. Null simulates a save. + 用表单的值保存表单。任务运行期间按钮显示加载状态;任务出错时显示错误状态。为 null 时模拟保存。 + + + The primary action's label. + 主要操作按钮的文字。 + + + Raised after the form was saved; carries the saved values. Bubbles. + 表单保存后触发;携带保存的值。冒泡。 + + + The pasted text (two-way). Setting it while the paste area shows does not start a read; call `ReadAsync`. + 粘贴的文本(双向)。在显示粘贴框时设置它不会开始读取;请调用 `ReadAsync`。 + + + The card's title. + 卡片的标题。 + + + Raised after Undo restored the values from before the last fill; carries the restored values. Bubbles. + 撤销恢复上一次填写之前的值后触发;携带恢复的值。冒泡。 + + + The form's values by field id (a new snapshot on every change). + 按字段 id 给出的表单值(每次变化都是新的快照)。 + + + Raised on every change of the values, typed or filled. Bubbles. + 每次值变化时触发,无论是输入还是填写。冒泡。 + + + One field of a `SmartFormFill`: its id (the key of values and of extraction results), its label, what it holds, a placeholder and how much of the row it takes. + `SmartFormFill` 的一个字段:它的 id(值和提取结果中的键)、标签、所存内容的类型、占位提示,以及在一行中所占的宽度。 + + + The key of the field in values and extraction results ("email"). + 字段在值和提取结果中的键("email")。 + + + What the field holds: text, an email, a phone number, a web address or a company (which adds a logo slot). + 字段所存的内容:文本、邮箱、电话号码、网址或公司(公司会增加一个标志位)。 + + + The visible label; also the input's accessible name. + 可见的标签;也是输入框的无障碍名称。 + + + The hint shown while the field is empty. + 字段为空时显示的提示。 + + + Half a row (default) or the full row. + 半行(默认)或整行。 + + + Every word `SmartFormFill` shows or says, in English by default (the title, the description and the submit button are properties of the control). Replace the whole object to localize or reword; counts and names are format functions. + `SmartFormFill` 显示或朗读的每一个文字,默认为英文(标题、说明和提交按钮是控件的属性)。替换整个对象即可本地化或改写;计数和名称是格式化函数。 + + + A text behind the Try sample button: a short label and the text it pastes. + Try sample 按钮背后的文本:一个简短的标签和它粘贴的文本。 + + + Texts a `SmartFormFill` plays without an extractor, from data you give it: the samples behind Try sample, each with the result a read returns, and the model named beside the paste area. Set it as `Simulation` while the card has no `Extract`: Try sample pastes the samples in turn, a read returns the sample's result after a short wait (other text is read by a few plain rules), and unless `AutoPlay` is false the card loops paste, fill, review and undo until someone touches it. Nothing is sent. + `SmartFormFill` 在没有提取器时按你提供的数据播放的文本:Try sample 背后的示例(每个都带有读取时返回的结果),以及粘贴区旁显示的模型。在卡片没有 `Extract` 时把它设为 `Simulation`:Try sample 依次粘贴这些示例,读取在短暂等待后返回示例的结果(其他文本由几条简单规则读取),并且除非 `AutoPlay` 为 false,卡片会循环演示粘贴、填写、复核和撤销,直到有人操作它。不会发送任何内容。 + + + One value read from the pasted text: the value, how sure the reader is, the exact text it came from and why it is unsure. + 从粘贴文本中读取的一个值:值本身、读取器的把握程度、它来自的确切文字,以及不确定的原因。 + + + One entry in a `SocialProofToast` feed: a person or a company, what they did, and when. Feed it real, recent events from your billing or analytics backend. + `SocialProofToast` 动态中的一条记录:一个人或一家公司、他们做了什么,以及何时发生。请使用来自计费或分析后端的真实、近期事件。 + + + What they did, read after the name: "started a Pro trial". + 他们做了什么,紧接在名称之后读出:"started a Pro trial"。 + + + A person (round avatar) or a company (rounded square mark). Person by default. + 个人(圆形头像)或公司(圆角方块标识)。默认为个人。 + + + Stable id, used to match a toast to its event in callbacks. + 稳定的 id,用于在回调中把通知对应到它的事件。 + + + Shows a check badge with `VerifiedLabel`, or "Verified". + 显示一个对勾徽章,文字为 `VerifiedLabel` 或 "Verified"。 + + + + Short context before the time, such as a city or a seat count. + 显示在时间之前的简短上下文,例如城市或席位数。 + + + A first name or a company, shown in medium weight. + 名字或公司名称,以中等字重显示。 + + + A portrait for a person. Initials show while it loads or when it fails. + 个人的头像照片。加载期间或加载失败时显示姓名首字母。 + + + When it happened. Shown as relative time: Just now, 4 min ago, 2 h ago. + 事件发生的时间。显示为相对时间:Just now、4 min ago、2 h ago。 + + + A time written out, shown as is (it takes the place of `Time`). + 写好的时间文字,原样显示(取代 `Time`)。 + + + The verified badge's label, such as "Verified purchase". Setting it also shows the badge. + 已验证徽章的文字,例如 "Verified purchase"。设置它也会显示徽章。 + + + A stack of social proof notifications for landing and pricing pages (Arc Social proof toast, design reference): recent trials, upgrades and verified purchases from people and companies arrive every few seconds. A new toast rises in at the front while the others step back and shrink; hovering or keyboard focus pauses the cycle and fans the stack out, and visitors can dismiss any toast. Give it the feed in `Events` (oldest first) and it shows one every `Interval`, or call `Push` to show an event the moment it happens. With no events it plays its `Simulation`, if any, and otherwise shows nothing until you push one. Inline it fills its container's width; a corner `Position` pins the stack to the window instead. + 用于落地页和定价页的社交证明通知堆叠(Arc Social proof toast,设计参考):来自个人和公司的最新试用、升级与已验证购买每隔几秒出现一条。新通知从前方升起,其他通知后退并缩小;悬停或键盘焦点会暂停轮播并展开堆叠,访客可以关闭任意一条通知。在 `Events` 中传入动态(从旧到新),它会每隔 `Interval` 显示一条;或调用 `Push` 在事件发生时立即显示。没有事件时播放它的 `Simulation`(如果有),否则在你推送之前不显示任何内容。内联时它填满容器宽度;设置角落 `Position` 则把堆叠固定在窗口上。 + + + Raised when a visitor dismisses a toast. Bubbles. + 访客关闭一条通知时引发。会冒泡。 + + + Fans the stack out while it is hovered or holds keyboard focus. + 堆叠被悬停或持有键盘焦点时将其展开。 + + + Time before the first toast. + 第一条通知出现前的时间。 + + + Time between toasts. The countdown keeps its remaining time across hover and focus pauses. + 两条通知之间的时间。悬停和聚焦暂停期间,倒计时会保留剩余时间。 + + + Pauses the cycle from outside, for example while a modal or cookie banner is open. + 从外部暂停轮播,例如在模态框或 Cookie 横幅打开期间。 + + + Makes each toast a button that raises `Selected`, for example to open pricing or a customer story. + 让每条通知成为一个引发 `Selected` 的按钮,例如用来打开定价页或客户案例。 + + + Every word the stack shows or says. + 堆叠显示或朗读的所有文字。 + + + Stops after this many toasts in a session, so the page stays calm. Null shows toasts for as long as the feed lasts. + 一次会话显示这么多条通知后停止,让页面保持安静。为 null 时只要动态还有内容就继续显示。 + + + Starts the feed over when it ends. False stops after the last event. + 动态播完后从头开始。为 false 时在最后一个事件后停止。 + + + How many toasts stack at once. Older ones fade out from the back. + 同时堆叠的通知数量。较早的通知从后方淡出。 + + + Inline in the layout (default), or pinned to a window corner 24px from the edges (16px and full width on narrow windows). + 内联在布局中(默认),或固定在窗口角落、距离边缘 24px(窄窗口上为 16px 并占满宽度)。 + + + How many toasts already show when the stack appears, without an entrance. Useful for previews and screenshots. + 堆叠出现时已经显示的通知数量,不播放入场动画。适合预览和截图。 + + + Raised when a visitor presses a toast while `IsSelectable` is set. Bubbles. + 设置 `IsSelectable` 时,访客按下一条通知时引发。会冒泡。 + + + Raised each time a toast appears, for impression tracking. Bubbles. + 每当一条通知出现时引发,用于曝光统计。会冒泡。 + + + A simulated feed to play while `Events` is empty, with times measured back from each pass. Null (the default) leaves the stack empty until an event is pushed. Give each stack its own simulation. + 在 `Events` 为空时播放的模拟动态,时间从每一轮开始时往回计算。为 null(默认)时,堆叠在推送事件之前保持为空。请给每个堆叠各自的模拟实例。 + + + Arguments of `Shown`, `Dismissed` and `Selected`. + `Shown`、`Dismissed` 与 `Selected` 的参数。 + + + Every word `SocialProofToast` shows or says, in English by default. Replace the whole object to localize or reword; the relative times and the names that include the toast's sentence are format functions. + `SocialProofToast` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;相对时间以及包含通知句子的名称都是格式化函数。 + + + A simulated feed for a `SocialProofToast` that has no `Events`, for demos and prototypes: the events it plays, oldest first, with their times measured back from the start of each pass. The stack plays it like a feed of its own (prefill, interval, limit and loop included). Create one per stack. + 用于演示和原型、供没有 `Events` 的 `SocialProofToast` 使用的模拟动态:它播放的事件按从旧到新排列,时间从每一轮开始时往回计算。堆叠会像播放自己的动态一样播放它(包括预填、间隔、上限和循环)。请为每个堆叠创建一个实例。 + + + One entry of a `SocialProofToastSimulation`: an event and how long before each pass it happened. + `SocialProofToastSimulation` 中的一条:一个事件,以及它发生在每一轮开始之前多久。 + + + A row of cited sources under an AI answer (Arc Source cards, design reference). Each card shows the site's mark, its domain, the citation number and a two-line title. While searching, cards stream into reserved slots under "Searching 3 of 7"; past `Max` the rest collapse into a stack of overlapping marks ("+4 more"). Hovering or focusing a card lifts a preview with the passage, and a `SourceCitation` in the answer highlights its card. Give it the sources in citation order in `Sources` (append as results arrive), set `Status` and, while searching, `Total`. Put the answer in `Content`: it sits between the status line and the cards, and its height springs as it streams so the cards glide down. With `Sources` null the cards show the state you set (by default "No sources found"), or play a `Simulation` you pass in. + AI 回答下方的一排引用来源(Arc Source cards,设计参考)。每张卡片显示网站标记、域名、引用编号和两行标题。搜索时,卡片在 "Searching 3 of 7" 下方流入预留的位置;超过 `Max` 的部分收拢为一叠相互重叠的标记("+4 more")。悬停或聚焦卡片会浮出带原文片段的预览,回答中的 `SourceCitation` 会高亮它的卡片。按引用顺序在 `Sources` 中提供来源(结果到达时追加),设置 `Status`,搜索时还要设置 `Total`。把回答放进 `Content`:它位于状态行和卡片之间,流式输出时高度以弹簧变化,卡片随之平滑下移。`Sources` 为 null 时,卡片显示你设置的状态(默认为 "No sources found"),或播放你传入的 `Simulation`。 + + + Shows Retry after a failed search and Search again when nothing was found; both raise `RetryRequested`. + 搜索失败后显示 Retry,没有找到结果时显示 Search again;两者都会引发 `RetryRequested`。 + + + The search time, shown after the count when done ("7 sources · 2.1s"). + 搜索用时,完成时显示在数量之后("7 sources · 2.1s")。 + + + The detail shown after "Search failed", such as "The search provider timed out after 8s." + 显示在 "Search failed" 之后的详情,例如 "The search provider timed out after 8s." + + + Raised when the stack, the Show all toggle or a citation opens or closes the stack. Bubbles. + 叠放、Show all 开关或引用展开或收起叠放时引发。冒泡。 + + + Raised when a citation highlights its card, and when that highlight ends. Bubbles. + 引用高亮它的卡片时,以及该高亮结束时引发。冒泡。 + + + The highlighted source number (1-based), or null (two-way). A citation press sets it and clears it again after 2.6s unless it has changed meanwhile; set it yourself to hold a highlight. + 高亮的来源编号(从 1 开始),或 null(双向绑定)。按下引用会设置它,并在 2.6 秒后清除(除非期间已改变);自己设置它可以保持高亮。 + + + Whether the stack is open and every card shows (two-way). The stack, the Show all toggle and a citation that points into the stack change it and raise `ExpandedChanged`. + 叠放是否展开、所有卡片是否都显示(双向绑定)。叠放、Show all 开关以及指向叠放内部的引用会改变它并引发 `ExpandedChanged`。 + + + Whether the status line shows; turn it off when your UI already shows the search progress (the Show all toggle and Retry stay). + 是否显示状态行;界面已显示搜索进度时可将其关闭(Show all 开关和 Retry 仍保留)。 + + + Every word the cards and their citations show or say. + 卡片及其引用显示或朗读的每一个词。 + + + Cards shown before the rest collapse into the stack. The grid has Max + 1 columns on wide containers and two below 540px. + 收拢进叠放之前显示的卡片数。宽容器中网格有 Max + 1 列,窄于 540px 时为两列。 + + + Whether pressing a card opens its address in the browser when no `SourceInvoked` handler marks the event handled. Cards say so in their name. + 当没有 `SourceInvoked` 处理程序把事件标记为已处理时,按下卡片是否在浏览器中打开其地址。卡片的名称会说明这一点。 + + + The search query, shown muted after the status while searching or when nothing was found. + 搜索查询,搜索时或没有找到结果时以弱化样式显示在状态之后。 + + + Raised when the person presses Retry or Search again. Bubbles. + 用户按下 Retry 或 Search again 时引发。冒泡。 + + + A simulated search to play while `Sources` is null (demos and prototypes): its sources stream in and its answer streams with citations. Simulated cards open no address; null shows the cards' own state. + 没有 `Sources` 时要播放的模拟搜索(演示与原型):它的来源依次流入,回答带着引用流式出现。模拟的卡片不会打开任何地址;为 null 时显示卡片自己的状态。 + + + Raised when a card is pressed, before its address opens. Bubbles. Mark it handled to open the source in your own reader instead. + 按下卡片时、在其地址打开之前引发。冒泡。将其标记为已处理即可改在自己的阅读器中打开来源。 + + + The sources in citation order (source 1 is cited as [1]). Append as results arrive; each new card fades into its slot. An observable list is followed as it changes. While null, a `Simulation` plays. + 按引用顺序排列的来源(第 1 个来源以 [1] 引用)。结果到达时追加;每张新卡片淡入自己的位置。可观察列表的变化会被跟随。为 null 时播放 `Simulation`。 + + + Searching shows a spinner, a shimmering "Searching", a rolling count and reserved slots; Done shows "7 sources" and the time ("No sources found" with none); Error keeps what arrived and shows "Search failed". + Searching 显示加载圈、闪烁的 "Searching"、滚动的数量和预留的位置;Done 显示 "7 sources" 和用时(没有来源时为 "No sources found");Error 保留已到达的结果并显示 "Search failed"。 + + + How many sources the search will return. Drives "Searching 3 of 7" and reserves that many slots, so the row never shifts while it fills. + 搜索将返回的来源数量。它驱动 "Searching 3 of 7" 并预留同样多的位置,因此填充过程中这一排不会跳动。 + + + Every word `SourceCards` and `SourceCitation` show or say, in English by default. Replace the whole object to localize or reword; counts and names are format functions. + `SourceCards` 和 `SourceCitation` 显示或朗读的每一个词,默认是英文。替换整个对象即可本地化或改写;数量与名称是格式化函数。 + + + A simulated search for a `SourceCards`, for demos, prototypes and tests. In the `Live` scenario the `Sources` you pass stream into their slots (the first after 0.45s, then one every 0.33s), the search finishes, the `Answer` streams a word every 70ms with its [n] citations, the cards tour two citations, close the stack and start over, looping until someone interacts. In `Error` the search fails after two sources with `ErrorText`; in `Empty` it finds nothing for `EmptyQuery`. Nothing is searched, and simulated cards open no address. Set it as the cards' `Simulation`: it plays while `Sources` is null and only while the cards are on screen, and starts over when its `Scenario` changes or the person presses Retry or Search again. The other values take effect at the next start. Use one simulation per `SourceCards`. + 为 `SourceCards` 提供的模拟搜索,用于演示、原型和测试。在 `Live` 场景中,你传入的 `Sources` 依次流入各自的位置(第一个在 0.45s 后,之后每 0.33s 一个),搜索完成,`Answer` 每 70ms 流出一个词并带有 [n] 引用,卡片依次高亮两个引用,收起叠放后从头开始,一直循环到有人操作为止。在 `Error` 中,搜索在两个来源之后以 `ErrorText` 失败;在 `Empty` 中,它为 `EmptyQuery` 什么也找不到。不会进行任何搜索,模拟的卡片也不会打开任何地址。把它设为卡片的 `Simulation`:它在 `Sources` 为 null 且卡片显示在屏幕上时播放,在 `Scenario` 改变或用户按下重试、重新搜索时从头开始。其他值在下次开始时生效。每个 `SourceCards` 使用一个模拟。 + + + The answer the live scenario streams, words separated by spaces; a word written as `[n]` (optionally followed by punctuation, as in `[3].`) becomes a `SourceCitation` of source n. + 实时场景流式输出的回答,词与词之间用空格分隔;写成 `[n]` 的词(后面可以带标点,如 `[3].`)会变成来源 n 的 `SourceCitation`。 + + + Raised when the simulated search changes: a source arrives, the status changes or a word of the answer streams. + 模拟搜索变化时引发:一个来源到达、状态改变,或回答流出一个词。 + + + The search time the live scenario reports when it finishes ("6 sources · 2.1s"). + 实时场景完成时报告的搜索用时("6 sources · 2.1s")。 + + + The query of the empty scenario, one that finds nothing; `Query` when not set. + 空结果场景的查询,一个什么也找不到的查询;未设置时使用 `Query`。 + + + The detail the error scenario shows after "Search failed". + 错误场景在 "Search failed" 之后显示的详情。 + + + The query shown while searching. + 搜索时显示的查询。 + + + Which story to tell: a live search with a cited answer, a failed search or an empty one. + 要讲述的情景:带引用回答的实时搜索、失败的搜索或空结果的搜索。 + + + The sources the search finds, in citation order (source 1 is cited as [1]). + 搜索找到的来源,按引用顺序排列(第 1 个来源以 [1] 引用)。 + + + One source of a `SourceCards`: a page the answer cites. Sources are numbered by their place in `Sources`, so the first is cited as [1]. + `SourceCards` 的一个来源:回答引用的一个网页。来源按其在 `Sources` 中的位置编号,因此第一个以 [1] 引用。 + + + An inline [n] chip for answer text, styled like the number on each card of a `SourceCards`. Inside the cards' content it names its source for screen readers ("Source 2: Request: text() method, webref.example"), turns accent while its card is highlighted, and pressing it highlights the card (opening the stack when the card is in it). Its hit area reaches a few pixels past the chip without pushing the text around it. Put it in a text block with `InlineUIContainer`; the regular `Click` event runs first, for example to scroll the cards into view. + 回答文本中的行内 [n] 标签,样式与 `SourceCards` 每张卡片上的编号相同。在卡片的内容中,它为屏幕阅读器说出自己的来源("Source 2: Request: text() method, webref.example"),在其卡片高亮时变为强调色,按下它会高亮该卡片(卡片在叠放中时会先展开)。它的点击区域比标签多出几个像素,但不会推开周围的文字。用 `InlineUIContainer` 把它放进文本块;常规的 `Click` 事件先运行,例如用来把卡片滚动到视野内。 + + + The 1-based number of the source it cites. + 它所引用的来源编号(从 1 开始)。 + + + The number as the chip shows it. + 标签上显示的编号。 + Arc's sparkline: a compact trend beside a value, for cards, table rows and KPI blocks. Scrub it with the pointer or the arrow keys: the cursor springs between points, the stretch after it dims as if it has not happened yet, and the headline and aside roll to the scrubbed point. The line draws once like a pen the first time it is half visible; the fill follows the tip, then the end dot lands. New data morphs the line from the shape on screen. Motion is code-driven because the pen tip, cursor and morph are coordinates inside the self-drawn plot. Arc 迷你趋势图:紧挨数值旁的紧凑趋势图,适用于卡片、表格行和 KPI 区块。可用指针或方向键拖动浏览:光标在各数据点之间弹动,其后的区段会变暗,仿佛尚未发生;标题与附注会滚动到所浏览的数据点。首次半数可见时,折线会像画笔一样绘制一次;填充区随笔尖跟进,然后末端圆点落定。新数据会从屏幕上的形状变形为新折线。由于笔尖、光标和变形都是自绘绘图区内的坐标,因此动效由代码驱动。 @@ -19060,6 +23436,162 @@ Every word `SpeculativeDecoding` shows or announces, apart from the title, the lane names (`DraftLabel` and the others) and the playback buttons (`PlaybackLabels`). English by default; replace the instance to translate or reword. Texts with numbers are functions so that word order can change. `SpeculativeDecoding` 显示或朗读的每个文字,标题、泳道名称(`DraftLabel` 等)和播放按钮(`PlaybackLabels`)除外。默认为英文;替换该实例即可翻译或改写。含数字的文字使用函数,以便调整词序。 + + What your text to speech service returned: the playback, the word timings and the length. Each part is optional: without `Audio` the player keeps time itself (simulated, no sound); without `Timings` they are estimated from the text and fitted to the length. + 文本转语音服务返回的内容:播放、逐词时间和总时长。每一部分都是可选的:没有 `Audio` 时播放器自行计时(模拟,无声音);没有 `Timings` 时根据文本估算,并按总时长拉伸对齐。 + + + Arguments of the `SpeechPlayer` playback events: where playback is, with which voice and speed. + `SpeechPlayer` 播放事件的参数:播放到哪里、用哪个音色和语速。 + + + Read aloud for an AI reply (Arc Speech player, design reference). The reply sits above an action row with Read aloud, Copy, Good response and Bad response. Read aloud crossfades the row into a compact player of the same height: play and pause, the elapsed time, a thin scrubbable progress bar, the total time, a speed button (1×, 1.5×, 2×), a voice picker and Stop reading. As it plays, one rounded highlight glides from word to word along a line and cuts to the next line, while the words still to come stay dimmed; a click on a word jumps there. Give it the reply in `Text` and your text to speech service in `Synthesize`: it receives a `SpeechRequest` (abort when its token is cancelled) and returns a `SpeechAudio` with the playback (`ISpeechAudio`, whose position becomes the player's clock), the word timings and the length. Without `Synthesize` the player is a timed simulation that plays no sound. With no text, a `Simulation` is read once the player is in view and loops through two voices until someone interacts; with neither, the action row waits with Read aloud disabled. + 为 AI 回复朗读(Arc Speech player,设计参考)。回复下方是带有 Read aloud、Copy、Good response 和 Bad response 的操作行。Read aloud 把这一行交叉淡化为同样高度的紧凑播放器:播放与暂停、已播放时间、可拖动的细进度条、总时长、语速按钮(1×、1.5×、2×)、音色选择和 Stop reading。播放时,一个圆角高亮在一行内逐词滑动,到下一行时直接切换,尚未朗读的词保持变淡;点击一个词即跳到那里。把回复放在 `Text` 中,把文本转语音服务放在 `Synthesize` 中:它接收一个 `SpeechRequest`(令牌取消时中止),返回一个 `SpeechAudio`,包含播放(`ISpeechAudio`,其位置成为播放器的时钟)、逐词时间和总时长。没有 `Synthesize` 时,播放器是一个不发声的定时模拟。没有文本时,会在播放器进入视口后朗读 `Simulation`,并在两个音色之间循环,直到有人操作;两者都没有时,操作行等待,Read aloud 保持禁用。 + + + Buttons after Read aloud in the action row, in place of the default Copy, Good response and Bad response. + 操作行中 Read aloud 之后的按钮,替代默认的 Copy、Good response 和 Bad response。 + + + The last sentence said through the polite status region. + 通过礼貌模式状态区域播报的最后一句话。 + + + The author's mark: a 24 × 24 stroked glyph drawn at 16px (the sparkle by default). + 作者标志:一个 24 × 24 的描边图形,以 16px 绘制(默认为闪光图标)。 + + + The author mark's color; the accent by default. + 作者标志的颜色;默认为强调色。 + + + Who wrote the reply, shown above it with a 16px mark. Null shows no author line (a playing simulation shows its own). + 回复的作者,带 16px 标志显示在回复上方。为 null 时不显示作者行(正在播放的模拟会显示它自己的作者)。 + + + Starts reading once the player is in view. Null: on while a `Simulation` plays, off when `Text` is set. + 播放器进入视口后开始朗读。为 null 时:播放 `Simulation` 时开启,设置了 `Text` 时关闭。 + + + Stop reading was pressed and the action row returned. Fetched speech is kept, so Read aloud starts again at once. Bubbles. + 按下了 Stop reading,操作行已恢复。已获取的语音会保留,因此 Read aloud 会立即重新开始。冒泡。 + + + The index of the word being spoken (−1 before the first). + 正在朗读的词的索引(第一个词之前为 −1)。 + + + The last word finished. Bubbles. + 最后一个词已读完。冒泡。 + + + Whether the player shows in place of the action row. + 播放器是否显示在操作行的位置上。 + + + Notes in the voice menu and the announcements that no audio plays. Null: on when `Synthesize` is not set. + 在音色菜单和播报中注明不会播放声音。为 null 时:未设置 `Synthesize` 时开启。 + + + Every word the player shows or says. + 播放器显示或播报的每一个词。 + + + Playback paused. Bubbles. + 播放已暂停。冒泡。 + + + Playback started or resumed. Bubbles. + 播放已开始或继续。冒泡。 + + + A scrub, an arrow key or a click on a word moved playback. Raised once when a drag ends, not on every move. Bubbles. + 拖动、方向键或点击某个词移动了播放位置。拖动结束时触发一次,而不是每次移动都触发。冒泡。 + + + The selected voice's id (two-way). Null or an unknown id uses the first voice. A new voice while reading resumes from the word being spoken. + 所选音色的 id(双向)。为 null 或未知 id 时使用第一个音色。朗读中换用新音色时,从正在朗读的词继续。 + + + Shows Copy, Good response and Bad response after Read aloud when `Actions` is null (feedback stays local and is announced as simulated). False shows Read aloud alone. + 当 `Actions` 为 null 时,在 Read aloud 之后显示 Copy、Good response 和 Bad response(反馈只在本地,并以模拟方式播报)。为 False 时只显示 Read aloud。 + + + A self-playing reading to show while `Text` is null: its reply, author and voices, read once the player is in view and looped through two voices until someone interacts. Null (the default) leaves the player empty. Give each player its own simulation. + 在 `Text` 为 null 时显示的自动朗读:它的回复、作者和音色,在播放器进入视口后朗读,并在两个音色之间循环,直到有人操作。为 null(默认)时播放器为空。请给每个播放器各自的模拟实例。 + + + The playback speed (two-way). The speed button cycles 1×, 1.5×, 2×. + 播放语速(双向)。语速按钮在 1×、1.5×、2× 之间循环。 + + + The speed changed. Bubbles. + 语速已改变。冒泡。 + + + Where the player is: the action row, preparing, playing, paused, finished or failed. + 播放器所处的状态:操作行、准备中、播放中、已暂停、已结束或失败。 + + + Fetches speech from your text to speech service. Return the playback, the word timings (one start and end per word, from the provider's alignment data) and the length; missing timings are estimated and fitted to the length. Throw with a short, human message to show it next to "Couldn't generate audio". Null: a simulation with no audio. + 从你的文本转语音服务获取语音。返回播放、逐词时间(每个词一个开始和结束时间,来自服务商的对齐数据)和总时长;缺少的逐词时间会被估算并按总时长拉伸对齐。抛出带有简短、易懂信息的异常,即可显示在 "Couldn't generate audio" 旁边。为 null 时:不发声的模拟。 + + + The reply to read. Blank lines start new paragraphs; each whitespace separated word is one highlight step. Null reads the `Simulation`, if any. + 要朗读的回复。空行开始新段落;每个以空白分隔的词是一个高亮步骤。为 null 时朗读 `Simulation`(如果有)。 + + + A bordered card, or plain to sit inside a chat column. + 带边框的卡片,或融入聊天栏的 plain 样式。 + + + A voice was picked. Bubbles. + 选择了一个音色。冒泡。 + + + The voices in the picker. Null or empty offers the playing simulation's voices, else one default voice with an empty id (your service's default). + 音色选择中的音色。为 null 或空时提供正在播放的模拟的音色,否则提供一个 id 为空的默认音色(你的服务的默认音色)。 + + + The spoken word changed; use it to sync a caption or analytics. Bubbles. + 正在朗读的词已改变;可用于同步字幕或统计。冒泡。 + + + Every word `SpeechPlayer` shows or says, in English by default. Replace the whole object to localize or reword; sentences that carry a value are format functions. + `SpeechPlayer` 显示或播报的每一个词,默认为英文。替换整个对象即可本地化或改写;带有数值的句子是格式化函数。 + + + What a `SpeechPlayer` asks your text to speech service for. + `SpeechPlayer` 向你的文本转语音服务请求的内容。 + + + A self-playing reading for the `Simulation` of a `SpeechPlayer`, for demos and prototypes: the reply to read, its author and the voices to offer. A player that has no `Text` reads it once it is in view and, with full motion, reads it again with the other of the first two voices after a short pause, until someone interacts. It is also the stand-in text to speech behind every player without `Synthesize`: `SynthesizeAsync` waits like a network round trip, then answers with estimated word timings and no audio. Nothing is sent anywhere. `EstimateTimings` and `FitTimings` are what the player uses when a real service returns audio without alignment data. + 用于演示和原型的 `SpeechPlayer` 的 `Simulation` 自动朗读:要朗读的回复、它的作者和提供的音色。没有 `Text` 的播放器在进入视口后朗读它,在完整动效下,短暂停顿后会换用前两个音色中的另一个再读一遍,直到有人操作。它也是所有未设置 `Synthesize` 的播放器背后的替身文本转语音:`SynthesizeAsync` 像一次网络往返那样等待,然后返回估算的逐词时间,不带音频。不会向任何地方发送内容。真实服务返回的音频没有对齐数据时,播放器使用的就是 `EstimateTimings` 和 `FitTimings`。 + + + Options of `SynthesizeAsync`. + `SynthesizeAsync` 的选项。 + + + Arguments of `SpeedChanged`. + `SpeedChanged` 的参数。 + + + One voice in the `SpeechPlayer` voice picker. + `SpeechPlayer` 音色选择中的一个音色。 + + + Arguments of `VoiceChanged`. + `VoiceChanged` 的参数。 + + + Arguments of `WordChanged`: the playback event plus the word now spoken. + `WordChanged` 的参数:播放事件加上当前朗读的词。 + + + When one word of the text is spoken, on the 1× timeline. + 文本中一个词在 1× 时间线上的朗读时间。 + One default action joined to a chevron menu of nearby alternatives (Arc split button): Merge pull request with Squash and merge or Rebase and merge, an export with its usual format, Copy page that answers Copied in place. The whole pill presses in for the main action; the chevron half anchors the menu, so it answers in color only and turns while the menu is open. The main label morphs letter by letter (shared runs glide, changed letters rise out of a soft blur), a new `Icon` grows in while the old one shrinks away, and the main half springs to the new width. Menu rows are `MenuEntry` items declared inline or supplied with `ItemsSource`; the menu opens 4px below the chevron, aligned to the end. Keyboard: Enter or Space runs the main action on the main half and opens the menu on the first row from the chevron (ArrowDown too); arrow keys move through the rows and loop, Escape closes the menu and returns focus to the chevron. Label changes are announced politely. 将一个默认操作与一个相近替代项的下拉菜单(Arc 分段按钮)合并:例如“合并拉取请求”带有“Squash 并合并”或“Rebase 并合并”,导出操作带有其常用格式,“复制页面”操作则会就地显示“已复制”。主操作时整个药丸形按钮会按下;下拉箭头部分用于锚定菜单,因此仅以颜色作出反应,并在菜单打开时旋转。主标签逐字母变形(相同的部分平滑滑动,变化的字母从柔和的模糊中升起),新的 `Icon` 会渐显而旧图标缩小消失,主区域则以弹簧过渡到新宽度。菜单行为内联声明的 `MenuEntry` 项,或由 `ItemsSource` 提供;菜单在箭头下方 4px 处打开,并靠末端对齐。键盘:Enter 或 Space 在主区域执行主操作,从箭头处打开菜单并定位到第一行(ArrowDown 同样可以);方向键在各行之间移动并循环,Escape 关闭菜单并将焦点返回箭头。标签变化时以礼貌方式朗读。 @@ -19104,9 +23636,97 @@ Accessible name of the chevron half; null uses "{Label} more actions". 箭头部分的无障碍名称;为 null 时使用“{Label} 更多操作”。 - - Visual weight: Primary (default, a filled button) or Secondary (a compact outlined pill). - 视觉权重:Primary(默认,实心按钮)或 Secondary(紧凑的描边胶囊)。 + + Visual weight: Primary (default, a filled button) or Secondary (a compact outlined pill). + 视觉权重:Primary(默认,实心按钮)或 Secondary(紧凑的描边胶囊)。 + + + One starter card of `StarterPrompts`: a short verb-first `Title`, an optional quiet `Description`, and the full `Prompt` that is placed in the composer when the card is picked. + `StarterPrompts` 的一张起始卡片:简短、以动词开头的 `Title`,可选的安静说明 `Description`,以及选择卡片时填入输入框的完整 `Prompt`。 + + + A connected app a `StarterPrompt` uses, such as a mail, code or calendar integration. Its mark shows in a tinted square on the card and its name reads beside it and in the card's accessible name. The presets are neutral Lucide glyphs with plain names; pass your own `Mark` (a 24 × 24 stroked geometry) and `Name` for a real integration. + `StarterPrompt` 使用的已连接应用,例如邮件、代码或日历集成。它的标志显示在卡片上一个浅色底的方块里,名称显示在旁边,也会读入卡片的无障碍名称。预设是名称朴素的中性 Lucide 图标;真实的集成请传入你自己的 `Mark`(24 × 24 描边几何图形)和 `Name`。 + + + A tab of `StarterPrompts`: an id the starters refer to, a label and an optional 24 × 24 stroked icon. + `StarterPrompts` 的一个标签页:起始提示引用的 id、一个标签和可选的 24 × 24 描边图标。 + + + Arguments of `PromptSelected`: the starter that was picked. + `PromptSelected` 的参数:被选择的起始提示。 + + + The empty state of an AI app (Arc Starter prompts, design reference): a greeting, starter cards filtered by category tabs, a Shuffle that swaps the cards with a short staggered crossfade, and a composer the cards fill. Picking a card places its full prompt in the composer, focuses it and raises `PromptSelected`; Enter sends and raises `SubmitRequested`. Cards that use a connected app show its mark and name. With no `Prompts` it shows the greeting, the All tab and the composer with no cards, and with no handler that supplies `Sending` it simulates the send. Set `ShowsComposer` to false to pair the cards with a composer of your own: picks still set `Text` and raise the event. + AI 应用的空状态(Arc Starter prompts,设计参考):问候语、按分类标签页筛选的起始卡片、以短暂错开的交叉淡化切换卡片的“换一批”,以及由卡片填入的输入框。选择卡片会把它完整的提示词填入输入框、聚焦输入框并引发 `PromptSelected`;按 Enter 发送并引发 `SubmitRequested`。使用已连接应用的卡片会显示该应用的标志与名称。没有 `Prompts` 时显示问候语、“全部”标签页和输入框,但没有卡片;没有处理程序提供 `Sending` 时模拟发送。将 `ShowsComposer` 设为 false,可以把卡片与你自己的输入框搭配:选择卡片仍会设置 `Text` 并引发事件。 + + + Cards on screen at once. The grid keeps this many slots so nothing jumps. + 同时显示在屏幕上的卡片数。网格始终保留这么多个位置,内容不会跳动。 + + + One quiet line under the heading; null or empty hides it. + 标题下方的一行安静说明;为 null 或空时隐藏。 + + + The heading. + 标题。 + + + Shows placeholder cards with the same size and rhythm while the starters load. + 起始提示加载期间显示尺寸与节奏相同的占位卡片。 + + + Every word the control shows or says, beyond the greeting, description and placeholder. + 控件显示或朗读的所有文字(问候语、说明和占位文字除外)。 + + + The model's mark beside its name (a 24 × 24 stroked glyph); the sparkle by default, null hides it. + 模型名称旁的标志(24 × 24 描边图标);默认为闪光图标,为 null 时隐藏。 + + + The model named in the composer's footer; null (the default) shows the mark alone. + 输入框底栏中显示的模型名称;为 null(默认)时只显示标识。 + + + The composer's placeholder; null (the default) shows none. + 输入框的占位文字;为 null(默认)时不显示。 + + + Raised when a card is picked, after its prompt fills the composer. Bubbles. + 选择卡片、其提示词填入输入框之后引发。冒泡。 + + + The id of the selected tab (two-way): `"all"` or a `Id`. + 所选标签页的 id(双向):`"all"` 或某个 `Id`。 + + + The message of the last failed send, or null. + 最近一次发送失败的消息,或 null。 + + + Where the composer is with its message: idle, sending, sent, stopped or failed. + 输入框中消息的状态:空闲、发送中、已发送、已停止或失败。 + + + Shows the built-in composer. Turn it off to pair the cards with a composer of your own. + 显示内置的输入框。关闭后可以把卡片与你自己的输入框搭配。 + + + Raised when the composer sends (Enter or the send button). Bubbles. Put your request's task in `Sending` and honor its cancellation token; without it the send is simulated. + 输入框发送时引发(Enter 或发送按钮)。冒泡。把请求的任务放进 `Sending` 并遵守其取消令牌;不设置时为模拟发送。 + + + The composer's text (two-way); changes from typing and from a picked card. + 输入框的文本(双向);随输入和所选卡片变化。 + + + Every word `StarterPrompts` shows or says, in English by default (the greeting, description and placeholder are properties of the control). Replace the whole object to localize or reword; sentences that carry a value are format functions. + `StarterPrompts` 显示或朗读的所有文字,默认为英文(问候语、说明和占位文字是控件的属性)。替换整个对象即可本地化或改写;带有数值的句子是格式化函数。 + + + Arguments of `SubmitRequested`: the message to send. Start your request and put its task in `Sending`; the composer shows Sending until it completes, the error state with Retry if it throws, and Stop cancels `CancellationToken`. Leave `Sending` null and the send is simulated. + `SubmitRequested` 的参数:要发送的消息。发起请求并把它的任务放进 `Sending`;任务完成前输入框显示发送中,抛出异常时显示带“重试”的错误状态,停止会取消 `CancellationToken`。`Sending` 留空则为模拟发送。 Shows where someone is in a strict multi-step flow and which steps are done (Arc Stepper). Drive it with `CurrentIndex` (the step count marks the whole flow complete). When progress jumps across several steps the connectors fill one after another (80ms apart) from the previous current step, then the halo grows around the new current step; numbers turn into drawn checks or alerts. With `IsNavigable` completed steps can be chosen by click or keyboard (arrows between reachable steps, Home / End, Enter / Space). Horizontal steppers narrower than 30rem fold into markers with the current step named underneath. Steps come from `ItemsSource` or the inline `Steps`. @@ -19172,6 +23792,22 @@ Step name, shown beside its marker. 步骤名称,显示在其标记旁。 + + A surface of stickers (Arc Sticker peel, design reference). Hovering lifts the nearest corner a few pixels, pulling folds the sticker back along a real fold with its paper backing showing, pulling far enough peels it off to carry around, and letting go sticks it down where it was dropped. It works with the keyboard and with touch, and `PositionsChanged` gives the layout to save. + 一面贴纸表面(Arc 贴纸揭起,设计参考)。悬停会把最近的一角翘起几个像素,拉动会沿真实的折线把贴纸折回并露出纸质背面,拉得足够远就会整张揭下并可以拿着走,松手后贴纸贴在放下的位置。支持键盘和触摸,`PositionsChanged` 给出可保存的布局。 + + + Accessible name of the surface. + 表面的无障碍名称。 + + + Controlled placements by sticker id. Without it (or for ids it leaves out) the stickers start where their items say. + 按贴纸 id 受控的位置。没有它(或它未包含的 id)时,贴纸从各自的项所设的位置开始。 + + + Raised once a sticker has settled somewhere new, with every placement. + 贴纸在新位置落定后触发一次,带上每张贴纸的位置。 + Arc's streamgraph: layers stacked on a wiggle baseline, flowing over time. Hovering a layer isolates it and reads every layer at that moment; switching ranges blends every layer from the shape on screen on a shared 161-point grid, a hidden layer thins to nothing while the others close over it, and the first time the chart is seen the whole stream swells out of a hairline along its center. Motion is code-driven (a per-sample spring array and `MotionValue`) because every layer edge is recomputed from the stack each frame inside one self-drawn plot. Arc 的流图:各图层堆叠在摆动基线上,随时间流动。悬停某一图层会将其单独突出,并读取该时刻每个图层的数值;切换范围时,所有图层会从屏幕上的形状出发,在共享的 161 点网格上过渡融合,隐藏的图层会收缩至消失,其余图层随之闭合;图表首次出现时,整个流从中线处的细线舒展开。动画由代码驱动(每个采样点的弹簧数组加 `MotionValue`),因为每个图层边缘都要在单个自绘绘图区内逐帧根据堆叠结果重新计算。 @@ -19848,6 +24484,146 @@ What `SendInvitesAction` is asked to send. `SendInvitesAction` 被要求发送的内容。 + + A workspace switcher for multi-tenant apps (Arc Team switcher, design reference). The trigger shows the current workspace's tile, its name and its plan; the menu under it has a search field, your role in each workspace and a Cmd/Ctrl+1 to 9 shortcut for each of the first nine, and lets you create or join a workspace inline. One highlight glides between rows, and switching crossfades the trigger to the new workspace through a short loading state. Give it your workspaces in `Workspaces` and the active id in `Value` (two-way). Loading a workspace, creating one and joining one are your async delegates (`SwitchHandler`, `CreateHandler`, `JoinHandler`); each one left out is simulated. With no workspaces, a `Simulation` supplies demo workspaces and the invites its simulated join accepts; with neither, the trigger says there is no workspace. The root is up to 360px wide and fills narrower containers. + 面向多租户应用的工作区切换器(Arc Team switcher,设计参考)。触发器显示当前工作区的图块、名称和套餐;其下的菜单有搜索框、你在每个工作区中的角色,并为前九个工作区各提供一个 Cmd/Ctrl+1 到 9 的快捷键,还能就地创建或加入工作区。一个高亮在行之间滑动,切换时触发器会经过短暂的加载状态交叉淡化到新的工作区。把你的工作区放进 `Workspaces`,把当前工作区的 id 放进 `Value`(双向)。加载、创建和加入工作区都由你的异步委托完成(`SwitchHandler`、`CreateHandler`、`JoinHandler`);未提供的委托会被模拟。没有工作区时,`Simulation` 提供演示用的工作区以及模拟加入所接受的邀请;两者都没有时,触发器显示没有工作区。根元素最宽 360px,在更窄的容器中填满宽度。 + + + Creates a workspace from its name. Return it to add it and switch to it (or null to only close the menu); throw to show the exception's message under the field. Null simulates it. + 根据名称创建工作区。返回它即可添加并切换过去(返回 null 则只关闭菜单);抛出异常会在输入框下方显示异常消息。为 null 时进行模拟。 + + + Whether the menu is open (two-way). Setting it opens the menu without moving focus. + 菜单是否打开(双向)。设置它会打开菜单,但不移动焦点。 + + + Joins a workspace from an invite link or code. Return the workspace to add it and switch to it; throw to show the exception's message. Null simulates it: it accepts the invites of `Simulation`, if any. + 通过邀请链接或邀请码加入工作区。返回该工作区即可添加并切换过去;抛出异常会显示异常消息。为 null 时进行模拟:接受 `Simulation` 中的邀请(如果有)。 + + + Every word the switcher shows or says. + 切换器显示或朗读的所有文字。 + + + Raised when the menu opens or closes. Bubbles. + 菜单打开或关闭时触发。冒泡。 + + + How long the simulated switch, create and join take when no delegate is set. + 未设置委托时,模拟的切换、创建和加入所需的时间。 + + + Demo workspaces to show while `Workspaces` is empty, with the invites the simulated join accepts and what the simulated create makes. Null (the default) shows no workspace. Give each switcher its own simulation. + 在 `Workspaces` 为空时显示的演示工作区,以及模拟加入所接受的邀请和模拟创建的内容。为 null(默认)时不显示工作区。请给每个切换器各自的模拟实例。 + + + Raised when `SwitchHandler` threw: the trigger went back to the previous workspace. Bubbles. + `SwitchHandler` 抛出异常时触发:触发器已回到之前的工作区。冒泡。 + + + Loads a workspace. The trigger crossfades to it with a switching state until the task finishes; an exception reverts and shows its message. A newer switch cancels the token of the older one. Null simulates a load of `SimulatedDelay`. + 加载工作区。在任务完成之前,触发器以切换状态交叉淡化到该工作区;出现异常时恢复原状并显示异常消息。较新的切换会取消较早切换的令牌。为 null 时模拟一次耗时 `SimulatedDelay` 的加载。 + + + Listens for Cmd/Ctrl+1 to 9 anywhere in the window. Off, the shortcuts work while focus is inside the switcher. + 在窗口的任何位置监听 Cmd/Ctrl+1 到 9。关闭时,只有焦点在切换器内部时快捷键才有效。 + + + The id of the active workspace (two-way). Null starts on the first workspace. + 当前工作区的 id(双向)。为 null 时从第一个工作区开始。 + + + Raised once a switch has finished loading (and `Value` has changed). Bubbles. + 切换加载完成(且 `Value` 已改变)后触发。冒泡。 + + + Raised when someone created or joined a workspace from the menu. Add it to your list too; the switcher already shows it (de-duplicated by id) and switches to it. Bubbles. + 有人从菜单创建或加入工作区时触发。也请把它加入你的列表;切换器已经显示它(按 id 去重)并切换过去。冒泡。 + + + How a workspace joined the list of a `TeamSwitcher`. + 工作区是如何加入 `TeamSwitcher` 列表的。 + + + Every word `TeamSwitcher` shows or says, in English by default. Replace the whole object to localize or reword; texts with values in them are format functions. + `TeamSwitcher` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;包含数值的文字是格式化函数。 + + + Arguments of `OpenChanged`. + `OpenChanged` 的参数。 + + + Simulated workspaces for a `TeamSwitcher` that has no `Workspaces`, for demos and prototypes: the list it shows, the invites its simulated join accepts (the join view notes them) and what its simulated create makes. The app's own workspaces set it aside entirely. Switching, creating and joining without the app's delegates take `SimulatedDelay` and are announced as simulated; nothing is sent. Create one per switcher: workspaces created or joined are added to that switcher. + 用于演示和原型、供没有 `Workspaces` 的 `TeamSwitcher` 使用的模拟工作区:它显示的列表、模拟加入所接受的邀请(加入视图会注明)以及模拟创建的内容。应用自己的工作区会让它完全让位。没有应用委托时,切换、创建和加入都耗时 `SimulatedDelay`,并播报为模拟;不会发送任何内容。请为每个切换器创建一个实例:创建或加入的工作区会添加到那个切换器。 + + + An invite the simulated join of a `TeamSwitcherSimulation` accepts, and the workspace it joins. + `TeamSwitcherSimulation` 的模拟加入所接受的一个邀请,以及它加入的工作区。 + + + Arguments of `SwitchFailed`: the workspace that could not load and why. + `SwitchFailed` 的参数:无法加载的工作区及原因。 + + + A workspace's avatar on its own: the app's logo image (`LogoImage`), the brand mark (`Logo`) on its brand color (`TileBackground`), or the first letter of the name in a square tinted from the theme's accent and series colors (the tint is picked from the name, so a workspace keeps its color). The corner radius and the mark scale with `TileSize`. Drawn in one pass; nothing loads. + 单独的工作区头像:应用提供的标志图片(`LogoImage`)、品牌色(`TileBackground`)上的品牌标志(`Logo`),或者在一个方块中显示名称的首字母,方块的色调取自主题的强调色和系列色(色调由名称决定,所以同一个工作区始终保持同一种颜色)。圆角和标志随 `TileSize` 缩放。一次绘制完成,不加载任何资源。 + + + The tile's width and height in pixels; the corner radius, letter and mark scale with it. + 图块的宽度和高度(像素);圆角、字母和标志随之缩放。 + + + The workspace to draw. + 要绘制的工作区。 + + + Arguments of `ValueChanged`: the workspace that is now active. + `ValueChanged` 的参数:当前生效的工作区。 + + + One workspace (organization, team, project or store) in a `TeamSwitcher`: its id and name, and optionally its plan, member count, your role in it and a logo (an image, or a mark on a brand color). Without a logo its tile shows the first letter of the name in a tinted square. + `TeamSwitcher` 中的一个工作区(组织、团队、项目或店铺):它的 id 和名称,以及可选的套餐、成员数、你在其中的角色和标志(一张图片,或品牌色上的品牌标志)。没有标志时,它的图块在一个着色方块中显示名称的首字母。 + + + The workspace's id; the switcher's `Value` and its de-duplication use it. + 工作区的 id;切换器的 `Value` 和去重都使用它。 + + + + An app-supplied logo image (a bitmap or a vector `DrawingImage`) drawn in the tile instead of `Logo` or the letter: it fills the tile, cropped to a square from its center and clipped to the tile's corners. Null falls back to `Logo`, then to the letter. + 应用提供的标志图片(位图或矢量 `DrawingImage`),在图块中代替 `Logo` 或字母绘制:它铺满图块,从中心裁成正方形,并按图块的圆角裁剪。为 null 时依次回退到 `Logo` 和字母。 + + + How many people belong to it, shown after the plan; null hides the count. + 工作区中的人数,显示在套餐之后;为 null 时不显示人数。 + + + The name people know it by. + 大家熟悉的名称。 + + + The plan it is on ("Pro", "Enterprise"), shown under the name. + 所用的套餐(“Pro”“Enterprise”),显示在名称下方。 + + + Your role in it ("Owner", "Admin", "Member"), shown at the end of its row. + 你在其中的角色(“Owner”“Admin”“Member”),显示在所在行的末尾。 + + + The tile's brand color. Null tints the tile from the theme's accent and series colors, picked by the name. + 图块的品牌色。为 null 时,图块的色调取自主题的强调色和系列色,由名称决定。 + + + The color of the letter or mark on `TileBackground`. Null picks white or near-black, whichever reads better on that color, in both themes. + `TileBackground` 上字母或标志的颜色。为 null 时在白色和近黑色之间选择在该颜色上更易读的一种,两种主题下都是如此。 + + + Arguments of `WorkspaceAdded`: a workspace someone created or joined from the menu. Add it to your own list as well; the switcher de-duplicates by id. + `WorkspaceAdded` 的参数:有人从菜单创建或加入的工作区。也请把它加入你自己的列表;切换器按 id 去重。 + Morphs one short label into the next in place (P4): letters both strings share glide to their new positions, new letters sharpen in with a short stagger, removed letters blur away, and the width follows on a spring. Single line; assistive technology reads the plain text. Rendered directly: no control per letter. 将一个短标签原地变形为下一个标签(P4):两个字符串共有的字母滑动到新位置,新字母以短暂错开的节奏清晰浮现,被移除的字母模糊消失,宽度随弹簧调整。单行;辅助技术读取纯文本。直接渲染:不为每个字母创建控件。 @@ -19968,6 +24744,10 @@ Height chosen with the grip, in px; NaN follows the minimum height. 通过手柄选定的高度(单位 px);NaN 表示遵循最小高度。 + + Smallest height of the field's frame, border and padding included, in px. The theme sets Arc's 110px; lower it for a short note or prompt box (two lines of text at the default size need 66px). The grip never drags below it. + 字段外框的最小高度(含边框和内边距,单位 px)。主题设为 Arc 的 110px;简短的备注框或提示词框可将其调低(默认尺寸下两行文字需要 66px)。手柄不会拖到它以下。 + Shows the corner grip that drags the field taller (Arc: `resize: vertical`). Default true. 显示可拖动字段加高的右下角手柄(Arc:`resize: vertical`)。默认为 true。 @@ -20004,6 +24784,122 @@ A comment and its replies (Arc's `ThreadComment`). Immutable: every change made in a `CommentThread` produces a new tree (unchanged comments are shared), so it can be stored as is. 一条评论及其回复(Arc 的 `ThreadComment`)。不可变:在 `CommentThread` 中做出的每次更改都会生成新的树(未更改的评论会被共享),因此可以原样存储。 + + What the classifier read in a `TicketMessage`: the intent, the sentiment and how sure it is. + 分类器从 `TicketMessage` 中读出的内容:意图、情绪以及有多确定。 + + + Arguments of `ClassificationFailed`: the classifier threw for a message. + `ClassificationFailed` 的参数:分类器在处理某条消息时抛出了异常。 + + + A team lane of a `TicketRouter`, or the review lane that collects low confidence tickets. + `TicketRouter` 的团队泳道,或收集低置信度工单的复核泳道。 + + + One incoming support message for a `TicketRouter`. Data only, safe to read from any thread (your classifier may run off the UI thread). + `TicketRouter` 的一条传入支持消息。仅为数据,可在任意线程读取(你的分类器可以在 UI 线程之外运行)。 + + + Arguments of `ReviewConfirmed`: a review ticket sent to its suggested lane. + `ReviewConfirmed` 的参数:一张复核工单被送往其建议的泳道。 + + + Arguments of `TicketRouted`: the ticket that landed in a lane. + `TicketRouted` 的参数:落入某条泳道的工单。 + + + A support ticket router card (Arc Ticket router, design reference). Messages from several channels (email, chat, messages, help desk) arrive one at a time, an AI classifier labels each with an intent, a sentiment and a confidence, and the ticket travels into a team lane where it gets the next assignee in turn. Tickets below `ReviewThreshold` wait in the review lane with the classifier's pick as a suggestion; one click routes them there. Give it `Tickets` and a `Classifier` (your model call); handle `TicketRouted` to assign in your help desk. With no tickets of its own it plays its `Simulation`, an inbox you supply, with a simulated classifier and loops; with neither it rests on "No tickets yet" in empty lanes. About two seconds per ticket at `Speed` 1; the flow pauses with `IsPaused`, off screen and when unloaded, and each pause cancels a running classifier call. + 支持工单路由卡片(Arc Ticket router,设计参考)。来自多个渠道(邮件、聊天、消息、帮助台)的消息逐条到达,AI 分类器为每条消息标注意图、情绪和置信度,工单随后移入团队泳道,由轮到的下一位负责人接手。低于 `ReviewThreshold` 的工单在复核泳道中等待,以分类器的选择作为建议;点一下即可送往该处。传入 `Tickets` 和一个 `Classifier`(你的模型调用);处理 `TicketRouted` 以在你的帮助台中分派。自己没有工单时,它以模拟分类器播放它的 `Simulation`(由你提供的收件箱)并循环;两者都没有时,它停在空泳道中的 “No tickets yet”。`Speed` 为 1 时每张工单约两秒;流程会在 `IsPaused`、离开屏幕和卸载时暂停,每次暂停都会取消正在进行的分类器调用。 + + + Raised when the classifier throws. Bubbles. + 分类器抛出异常时触发。冒泡。 + + + Your classifier: return the intent, sentiment and confidence, or throw to show the error state with Retry and Send to review. The token cancels on pause, off screen and unload. Null reads each message's `Sample`. + 你的分类器:返回意图、情绪和置信度,或抛出异常以显示带有 Retry 和 Send to review 的错误状态。令牌在暂停、离开屏幕和卸载时取消。为 null 时读取每条消息的 `Sample`。 + + + Tickets already in the lanes on first show, newest first. Null uses the simulation's while it plays. + 首次显示时已在泳道中的工单,最新的在前。为 null 时,播放模拟期间使用模拟中的工单。 + + + Whether the flow is paused (two-way); the Pause and Resume button sets it. + 流程是否已暂停(双向);暂停与继续按钮会设置它。 + + + Every word the card shows or says. + 卡片显示或播报的每一个词。 + + + The lanes left to right. Null uses the simulation's lanes while it plays, else the default lanes: Billing, Engineering, Success and Needs review (the ids `DefaultRoute` picks), with no assignees. + 从左到右的泳道。为 null 时,播放模拟期间使用模拟中的泳道,否则使用默认泳道:Billing、Engineering、Success 和 Needs review(`DefaultRoute` 挑选的 id),没有负责人。 + + + Starts over when the queue runs out. Null loops a simulation and stops after your own tickets ("All caught up"). + 队列用完时从头开始。为 null 时循环播放模拟,而你自己的工单处理完后停止("All caught up")。 + + + Rows shown per lane. Narrow cards show at most 2. + 每条泳道显示的行数。窄卡片最多显示 2 行。 + + + The classifier model's name, in the header and the classifying label. Null shows the simulation's while it plays, and no name otherwise. + 分类器模型的名称,显示在头部和分类中的标签里。为 null 时,播放模拟期间显示模拟中的名称,否则不显示名称。 + + + The mark before the title (a 24 × 24 stroked glyph); the sparkle by default. + 标题前的标记(24 × 24 描边图形);默认为闪光图标。 + + + Raised when `IsPaused` changes, by the Pause and Resume button or by code. Bubbles. + `IsPaused` 变化时触发,无论由暂停与继续按钮还是代码引起。冒泡。 + + + Raised when someone confirms a review ticket's suggested lane. Bubbles. + 有人确认复核工单的建议泳道时触发。冒泡。 + + + Confidence below this sends a ticket to the review lane, with the picked lane as its suggestion. + 置信度低于此值的工单进入复核泳道,并以挑选出的泳道作为建议。 + + + Picks the lane id for a classification; null uses `DefaultRoute`. An unknown id falls back to the first team lane. + 为一次分类挑选泳道 id;为 null 时使用 `DefaultRoute`。未知的 id 会回退到第一条团队泳道。 + + + The inbox the router plays on its own while `Tickets` is null: the model's name, the lanes, the messages with their simulated answers and the tickets already routed, played exactly like your own stream (one ticket about every two seconds, pausing off screen, looping). Null rests on the empty state instead. + `Tickets` 为 null 时路由器自行播放的收件箱:模型名称、泳道、带有模拟答案的消息以及已路由的工单,播放方式与你自己的消息流完全相同(约每两秒一张工单、离开屏幕时暂停、循环)。为 null 时改为停在空状态。 + + + Playback speed multiplier of the flow (1 is about two seconds per ticket). + 流程的播放速度倍数(1 约为每张工单两秒)。 + + + Raised when a ticket lands in a lane, with its classification, lane, assignee and suggested lane. Bubbles. + 工单落入某条泳道时触发,附带其分类、泳道、负责人和建议泳道。冒泡。 + + + The incoming messages, processed in order. Null plays the `Simulation`'s inbox, or rests on "No tickets yet" without one; append to keep the flow going. + 传入的消息,按顺序处理。为 null 时播放 `Simulation` 的收件箱,没有模拟时停在 “No tickets yet”;追加消息可让流程继续。 + + + The card's title. + 卡片的标题。 + + + Every word `TicketRouter` shows or says, in English by default. Replace the whole object to localize or reword; words built from data are format functions. + `TicketRouter` 显示或播报的每一个词,默认为英文。替换整个对象即可本地化或改写措辞;由数据组成的词是格式化函数。 + + + Arguments of `PausedChanged`. + `PausedChanged` 的参数。 + + + An inbox a `TicketRouter` plays on its own while the app sets no `Tickets`: the classifier model's name, the lanes, the stream of messages with the answer the simulated classifier gives for each (`Sample`) and the tickets already in the lanes. The router reads, labels and routes one message at a time, sends the ones below the review threshold to the review lane, and loops when the stream runs out; it pauses off screen and with `IsPaused`. Nothing is sent. Set the data before handing the simulation to the router; to play another inbox, give the router a new simulation. + 应用没有设置 `Tickets` 时 `TicketRouter` 自行播放的收件箱:分类器模型的名称、泳道、消息流(每条附带模拟分类器给出的答案 `Sample`)以及已在泳道中的工单。路由器一次读取、标注并路由一条消息,把低于复核阈值的消息送入复核泳道,消息流用完时循环;离开屏幕时以及 `IsPaused` 时暂停。不会发送任何内容。请在把模拟交给路由器之前设置好数据;要播放另一个收件箱,就给路由器一个新的模拟。 + Arc time field: a trigger over a scrolling list of times generated every `MinuteStep` minutes. The list opens centred on the chosen time, arrow keys move the highlight (the list glides to keep it in view, wrapping at both ends), Enter or a click chooses and closes, Escape closes. The shown time rolls like a clock face: a later time rises from below, an earlier one drops from above. 12- or 24-hour display follows the culture unless `ClockFormat` says otherwise; the value is always a 24-hour `TimeOnly`. Arc 时间字段:触发器下方是一个可滚动的时间列表,每隔 `MinuteStep` 分钟生成一项。列表打开时以所选时间为中心,方向键移动高亮(列表会滑动以保持其可见,两端循环),Enter 或点击选择并关闭,Escape 关闭。显示的时间如同钟面一样滚动:较晚的时间从下方升起,较早的时间从上方落下。除非 `ClockFormat` 另有指定,否则 12 小时制或 24 小时制显示跟随文化设置;值始终为 24 小时制的 `TimeOnly`。 @@ -20104,6 +25000,110 @@ One update in a `Timeline` (Arc's `TimelineEvent`): who did what and when, with optional context and a detail revealed in place. Events are read when they are added; replace an event to change it. `Timeline` 中的一条更新(Arc 的 `TimelineEvent`):谁在何时做了什么,可附带上下文,并可在原位展开详细信息。事件在添加时读取;如需修改,请替换该事件。 + + The article a `TldrToggle` shows and summarizes: its title, its paragraphs (with stable ids) and the optional source, author, date and reading time of the header. Data only; give the card a new article to change it. + `TldrToggle` 显示并摘要的文章:标题、段落(带稳定的 id),以及页眉中可选的来源、作者、日期和阅读时间。仅为数据;要更换文章,请给卡片一篇新文章。 + + + Arguments of `Completed`: a finished result. + `Completed` 的参数:一个完成的结果。 + + + Arguments of `Jumping`. + `Jumping` 的参数。 + + + A key point: its text and the id of the paragraph it came from. + 一条要点:它的文字和它所出自段落的 id。 + + + Arguments of `LengthChanged`. + `LengthChanged` 的参数。 + + + Arguments of `ModeChanged`. + `ModeChanged` 的参数。 + + + One paragraph of a `TldrArticle`. Its `Id` is what key points link back to. + `TldrArticle` 中的一个段落。要点通过它的 `Id` 链接回来。 + + + What a `TldrToggle` asks its summarizer for. + `TldrToggle` 向其摘要器请求的内容。 + + + One event of a summary stream: a piece of text, or the start of a key point with its source paragraph. + 摘要流中的一个事件:一段文字,或一条要点的开始及其出处段落。 + + + Streams for a `TldrToggle`: the stand-in summarizer behind an article of yours with no summarizer, and pacing for a provider that returns whole results. Both send chunks of 3 to 8 words every 30 to 40ms and stop when the request's token is cancelled. Nothing is sent anywhere. + `TldrToggle` 的流:没有摘要器时你的文章背后的替身摘要器,以及为一次返回完整结果的服务提供的节奏控制。两者都每 30 到 40ms 发送一块 3 到 8 个词的内容,并在请求的令牌被取消时停止。不会向任何地方发送内容。 + + + An article card with a Full, Summary and Key points switch and a length slider (Arc TL;DR toggle, design reference). Switching streams an AI summary in place of the article in chunks of 3 to 8 words while the body's height follows on a bounce-free spring. Key points arrive as a checklist: each point ticks when it is written, and once the list is done its ¶ jump link switches back to the article, scrolls the source paragraph into view, focuses it and highlights it. Results are kept per mode and length, so switching back is instant; Regenerate asks again, Stop or Escape ends a stream and keeps what arrived, and a failure shows the message with Try again. Give it an `Article` with stable paragraph ids and a `Summarizer` that streams `TldrStreamEvent`s; without a summarizer the summaries are simulated from the article's lead sentences. Without an article it plays a `Simulation` (an article and its written summaries you pass, opening the summary by itself once it has been half in view for a second); with neither it shows the quiet empty state. + 一张文章卡片,带有全文、摘要和要点切换以及长度滑块(Arc TL;DR toggle,设计参考)。切换后,AI 摘要以每块 3 到 8 个词的节奏在原文位置流式出现,正文高度以无回弹的弹簧跟随。要点以清单形式出现:每条要点写完时打勾,整张清单完成后,它的 ¶ 跳转链接会切回文章,把出处段落滚动到视野中、聚焦并高亮。结果按模式和长度保留,所以切回去是即时的;重新生成会再次请求,停止或 Escape 会结束流式输出并保留已收到的内容,失败时显示消息和“Try again”。为它提供段落 id 稳定的 `Article`,以及流式返回 `TldrStreamEvent` 的 `Summarizer`;没有摘要器时,摘要由文章的首句模拟。没有文章时它播放 `Simulation`(你传入的文章及其写好的摘要,在至少一半进入视野满一秒后自动打开摘要);两者都没有时显示安静的空状态。 + + + The article to show and summarize; null plays `Simulation`, or shows the quiet empty state without one. + 要显示并摘要的文章;为 null 时播放 `Simulation`,没有模拟时显示安静的空状态。 + + + Opens the summary by itself once the card has been half in view for a second, unless the reader has already touched it. Null turns it on only while a `Simulation` plays. + 卡片至少一半进入视野满一秒后自动打开摘要,除非读者已经操作过卡片。为 null 时,仅在播放 `Simulation` 时开启。 + + + Raised when a result finishes, for analytics or a server-side cache. Stopped and failed runs do not raise it. Bubbles. + 一个结果完成时引发,可用于统计或服务端缓存。被停止或失败的运行不会引发它。会冒泡。 + + + Adds "Simulated, nothing was sent" to announcements and error text. Null turns it on while a `Simulation` plays or there is no `Summarizer`; set it when you wrap `Simulate` or `Summarize` yourself. + 在播报和错误文字中加上 "Simulated, nothing was sent"。为 null 时,在播放 `Simulation` 或没有 `Summarizer` 时开启;自行包装 `Simulate` 或 `Summarize` 时请设置它。 + + + Raised when a key point's jump link is used, before the card scrolls to the paragraph. Bubbles. + 某条要点的跳转链接被使用时引发,在卡片滚动到段落之前。会冒泡。 + + + Every word the card shows or says. + 卡片显示或播报的所有文字。 + + + The length from 1 to 5 (two-way): summaries aim for about 35, 60, 85, 100 and 130 words; key points return 3 to 7 items. + 长度,从 1 到 5(双向):摘要分别以约 35、60、85、100 和 130 个词为目标;要点返回 3 到 7 条。 + + + Raised when a slider drag is released or a key moves it (not on every step of a drag), so each change makes one request. Bubbles. + 滑块拖动松开或按键调整时引发(不会在拖动的每一步引发),所以每次更改只发一个请求。会冒泡。 + + + What the body shows (two-way): the article, a summary or key points. + 正文显示的内容(双向):文章、摘要或要点。 + + + Raised when the switch, a jump link or autoplay changes `Mode` (not when the app sets it). Bubbles. + 切换控件、跳转链接或自动播放更改 `Mode` 时引发(应用自行设置时不引发)。会冒泡。 + + + The model credited in the footer, with its 14px mark; null credits the `Simulation`'s model while one plays, or none. + 页脚中署名的模型,附带 14px 标志;为 null 时,播放 `Simulation` 期间署名为其模型,否则不署名。 + + + An article to play while the card has no `Article`: the article, the model and the summaries and key points written for it, streamed as a simulation (nothing is sent). Your own `Article` wins over it; null stops it and shows the quiet empty state. + 卡片没有 `Article` 时要播放的文章:文章、模型以及为它写好的摘要和要点,以模拟方式流式输出(不会发送任何内容)。你自己的 `Article` 优先于它;为 null 时停止播放并显示安静的空状态。 + + + Produces summaries and key points as a stream. Null simulates them from the article's lead sentences (`Simulate`), which sends nothing. The request's token is cancelled on Stop, Escape, a mode or length change, a new article, or when the card leaves the window. A playing `Simulation` does not use it. + 以流的形式生成摘要和要点。为 null 时根据文章的首句模拟(`Simulate`),不会发送任何内容。在停止、Escape、切换模式或长度、换了文章,或卡片离开窗口时,请求的令牌会被取消。正在播放的 `Simulation` 不会使用它。 + + + Every word `TldrToggle` shows or says, in English by default. Replace the whole object to localize or reword; counts and names go through format functions. + `TldrToggle` 显示或播报的所有文字,默认为英文。替换整个对象即可本地化或改写;计数和名称通过格式化函数生成。 + + + An article a `TldrToggle` plays without a summarizer, from data you give it: the article, the model credited, a summary written for each length and key points ranked by weight. Set it as `Simulation` while the card has no `Article`: the card shows the article, streams the written summary or points after `ThinkTime` in chunks of 3 to 8 words, says it is simulated (nothing is sent) and, unless `AutoPlay` is false, opens the summary by itself once it has been half in view for a second. A length without a written summary gets the article's lead sentences, and without `KeyPoints` each paragraph's lead sentence is a point (the same stand-in as `Simulate`; not a model). + `TldrToggle` 在没有摘要器时按你提供的数据播放的文章:文章、署名的模型、为每个长度写好的摘要,以及按重要性排序的要点。在卡片没有 `Article` 时把它设为 `Simulation`:卡片显示文章,在 `ThinkTime` 之后以每块 3 到 8 个词流式输出写好的摘要或要点,说明这是模拟(不会发送任何内容),并且除非 `AutoPlay` 为 false,会在至少一半进入视野满一秒后自动打开摘要。没有写好摘要的长度会使用文章的首句;没有 `KeyPoints` 时,每段的首句就是一条要点(与 `Simulate` 相同的替身,不是模型)。 + Arc toast: a brief confirmation for a completed background action (one locally controlled success message such as "Changes saved"). It rises from its edge on the morph spring while its check mark draws itself, closes after `Duration` (the timer does not pause, so keep the copy short), and can be swiped away in either direction with velocity. Text changes rise in place while the height springs to fit. For app-wide notifications (types, actions, updates, stacking) call `Show` from anywhere: it goes to the window's `ToastStack` (see `ToastHost`). Arc 通知提示(Toast):用于已完成后台操作的简短确认(一条本地控制的成功消息,如“Changes saved”)。它沿边缘升起,依托形变弹簧运动,同时其对勾逐步绘出;经过 `Duration` 后关闭(计时器不会暂停,因此请保持文案简短),并可按速度向任一方向滑动移除。文本变化时原位上浮,高度以弹簧方式调整至合适大小。若需应用级通知(类型、操作、更新、堆叠),请在任意位置调用 `Show`:它会进入窗口的 `ToastStack`(参见 `ToastHost`)。 @@ -20336,9 +25336,113 @@ Defines the `Symbol` property. 定义 `Symbol` 属性。 + + A voice chip of a `ToneTuner`: pressing it sets `Voice` and moves both sliders to `Formality` and `Detail`; pressing it again returns to the neutral voice. + `ToneTuner` 的一个语气标签:按下时设置 `Voice`,并把两个滑块移到 `Formality` 和 `Detail`;再按一次则回到中性语气。 + + + What a `ToneTuner` asks its `Rewrite` function for. + `ToneTuner` 向其 `Rewrite` 函数请求的内容。 + + + The tone of a `ToneTuner`: formality and detail on three levels each (0, 1, 2) and a voice. Values outside 0–2 are clamped where they are used. + `ToneTuner` 的语气:正式程度与详略各分三档(0、1、2),外加一种语气。超出 0–2 的值在使用时会被限制到范围内。 + + + A brand voice card (Arc Tone tuner, design reference): a customer reply with Friendly, Confident and Empathetic voice chips and two sliders, formality and detail, that rewrite it. Each change waits `Debounce` and then asks `Rewrite` for a new version; the sentences that changed crossfade in place while the rest stay still, words that are new against the original can be highlighted (the Changes switch), and Copy takes the version on screen. The status says where the text comes from: Original draft, Rewriting with the model (shimmering), Rewritten, or Couldn't rewrite with Retry. Give it the draft in `Text` and your model call in `Rewrite`; without `Rewrite` the rewrites are simulated by a few plain rules. Without a draft it plays a `Simulation` (a draft and its versions you pass, toured by itself until someone touches it); with neither, it shows an empty card whose dials are off. + 一张品牌语气卡片(Arc Tone tuner,设计参考):一封客户回复,配有 Friendly、Confident 和 Empathetic 语气标签以及正式程度与详略两个滑块,用来改写它。每次调整等待 `Debounce` 后向 `Rewrite` 请求新版本;改动过的句子原地交叉淡化,其余保持不动,相对原稿新增的词可以高亮显示(Changes 开关),复制按钮取走屏幕上的版本。状态说明文字的来源:Original draft、Rewriting(带模型名称,闪烁)、Rewritten,或带重试按钮的 Couldn't rewrite。把原稿传给 `Text`,把模型调用传给 `Rewrite`;没有 `Rewrite` 时,改写由几条简单规则模拟。没有原稿时它播放 `Simulation`(你传入的原稿及其各个版本,在有人操作之前自动轮流展示);两者都没有时,显示一张空卡片,其中的调节控件处于关闭状态。 + + + Tours the voices while the card is on screen, until the first pointer press or focus inside it. Null (default) tours only while a `Simulation` plays. Reduced motion never tours. + 卡片在屏幕上时自动轮流展示各种语气,直到卡片内第一次指针按下或获得焦点。为 null(默认)时只在播放 `Simulation` 时轮播。减弱动效时从不轮播。 + + + How many words of the version on screen are new against the original draft. + 屏幕上的版本中,相对原稿新增的词数。 + + + Raised after Copy put the version on screen on the clipboard. Bubbles. + 复制按钮把屏幕上的版本放入剪贴板后引发。会冒泡。 + + + Quiet time after the last change before a rewrite starts (default 220ms). + 最后一次调整后、开始改写前的静默时间(默认 220ms)。 + + + The version on screen as plain text (what Copy takes). + 屏幕上的版本,纯文本形式(即复制按钮取走的内容)。 + + + Every word the card shows or says. + 卡片显示或朗读的所有文字。 + + + The model named in the status while a rewrite runs; null names the `Simulation`'s model while one plays, or none. + 改写进行时状态中显示的模型名称;为 null 时,播放 `Simulation` 期间显示其模型,否则不显示。 + + + The voice chips; null shows Friendly, Confident and Empathetic (`Defaults`). + 语气标签;为 null 时显示 Friendly、Confident 和 Empathetic(`Defaults`)。 + + + The recipient's picture; without it the avatar shows their initials. + 收件人的头像图片;没有时头像显示其姓名首字母。 + + + Who the message is for, shown in the header with an avatar. Null while a `Simulation` plays shows its recipient. + 消息的收件人,与头像一起显示在页眉。播放 `Simulation` 时为 null 会显示其收件人。 + + + The subject under the recipient's name. + 收件人姓名下方的主题。 + + + Calls your model and returns the rewritten plain text. The request carries the draft, the tone and a ready made instruction; the token is cancelled when a newer change supersedes the request. Null simulates the rewrites of your draft with a few plain rules (nothing is sent). + 调用你的模型并返回改写后的纯文本。请求包含原稿、语气和一条现成的指令;当更新的调整取代这次请求时,令牌会被取消。为 null 时,用几条简单规则模拟你原稿的改写(不会发送任何内容)。 + + + Raised when a rewrite lands, with the new plain text and its tone. Bubbles. + 改写落定时引发,带有新的纯文本及其语气。会冒泡。 + + + Highlights the words that differ from the original draft (two-way; the Changes switch). + 高亮与原稿不同的词(双向;即 Changes 开关)。 + + + A tuning session to play while the card has no `Text`: its draft, recipient, model and versions, with simulated rewrites (nothing is sent). Your own `Text` wins over it; null stops it and shows the empty card. + 卡片没有 `Text` 时要播放的调整过程:它的原稿、收件人、模型和各个版本,改写是模拟的(不会发送任何内容)。你自己的 `Text` 优先于它;为 null 时停止播放并显示空卡片。 + + + Where the text on screen comes from: the original, a rewrite on its way, a landed rewrite, or a failure. + 屏幕上文字的来源:原稿、正在进行的改写、已落定的改写,或一次失败。 + + + The original draft, the source of every rewrite. Blank lines split paragraphs; single line breaks keep lines (a sign off). Null plays `Simulation`, or shows the empty card without one. + 原稿,每次改写的来源。空行分隔段落;单个换行保留为行(例如落款)。为 null 时播放 `Simulation`,没有模拟时显示空卡片。 + + + The tone (two-way): formality and detail (0–2) and the voice. The original draft shows at `Default`. + 语气(双向):正式程度与详略(0–2)以及语气。原稿在 `Default` 下显示。 + + + Raised on every slider step or chip press (and when code sets `Value`), before the debounce. Bubbles. + 每次滑块步进或标签按下时(以及代码设置 `Value` 时)引发,早于防抖。会冒泡。 + + + Every word `ToneTuner` shows or says, in English by default. Replace the whole object to localize or reword; counts and announcements are format functions. + `ToneTuner` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写措辞;计数与播报是格式化函数。 + + + A tuning session a `ToneTuner` plays without a model, from data you give it: a draft, who it is for, the model the status names and the rewritten versions per tone. Set it as `Simulation` while the card has no `Text`: the card then tunes `Text`, answers each tone with its version after `Latency`, says the rewrites are simulated (nothing is sent) and, unless `AutoPlay` is false, tours the voices by itself until someone touches it. A tone with no version in `Versions` is written by a few plain rules (contractions and greetings for formality, fewer sentences or a closing offer for detail, an opening line for the voice). That is a stand-in so the dials do something in a demo, not a language model. + `ToneTuner` 在没有模型时按你提供的数据播放的调整过程:一份原稿、收件人、状态中显示的模型,以及每种语气改写后的版本。在卡片没有 `Text` 时把它设为 `Simulation`:卡片会调整 `Text`,在 `Latency` 之后用对应版本回应每种语气,说明改写是模拟的(不会发送任何内容),并且除非 `AutoPlay` 为 false,会在有人操作之前自动轮流展示各种语气。`Versions` 中没有版本的语气会用几条简单规则改写(正式程度对应缩写和问候语,详略对应减少句子或加一句结尾提议,语气对应一句开场白)。这只是让调节控件在演示中有所反应的替身,不是语言模型。 + + + One rewritten version of a `ToneTunerSimulation`'s draft: the tone it is written in and its text. + `ToneTunerSimulation` 原稿的一个改写版本:它所用的语气及其文字。 + - One tool call of an agent transcript (Arc Tool call, design reference): the tool's mark, the call as a verb phrase, quiet meta ("4 results · 1.2s") and a status icon. The whole row is a button: it springs open into Input (the arguments as a mono key / value list) and Output (the result as a list, a table, tinted JSON or text, folding long ones behind "Show N more lines"), each with a copy button. A running call shimmers its title next to a spinner; a failed one shows its reason and Retry; one that waits for approval sits on an accent outline with Deny and Allow; a denied one is muted. Bind `Item` and change the item in place as the call moves on. Allow, Deny and Retry raise `ApproveRequested`, `DenyRequested` and `RetryRequested` (they bubble, so a `ToolCallGroup` or any parent can handle them): update the call's status yourself and set `Handled`. When nothing handles them, Deny marks the call denied here; Allow and Retry only simulate a run on the built-in sample data (`ToolCallSamples`) and change nothing otherwise. With no item the row shows a sample web search. On its own the row sits in a 1px bordered Surface frame (radius 16, 4px in); inside a group it is flush. - 智能体转录中的一次工具调用(Arc Tool call,设计参考):工具的标记、以动词短语表示的调用、低调的元信息("4 results · 1.2s")以及状态图标。整行是一个按钮:点击后以弹性动效展开为输入(参数,等宽的键/值列表)和输出(结果,以列表、表格、着色的 JSON 或文本呈现,过长内容折叠在"Show N more lines"之后),两者都带复制按钮。运行中的调用会在旋转指示器旁让标题闪烁;失败的调用会显示原因和重试;等待批准的调用带强调色轮廓,并显示拒绝和允许;被拒绝的调用会变为弱化显示。绑定 `Item`,调用推进时就地修改该项。允许、拒绝和重试会引发 `ApproveRequested`、`DenyRequested` 和 `RetryRequested`(它们会冒泡,因此 `ToolCallGroup` 或任何父级都可以处理):由你自行更新调用的状态并设置 `Handled`。若无人处理,拒绝会在此处将调用标记为已拒绝;允许和重试只会在内置示例数据(`ToolCallSamples`)上模拟运行,其他情况下不做任何改变。没有项目时,该行显示一个示例网页搜索。单独使用时,该行位于 1px 带边框的 Surface 外框中(圆角 16,内缩 4px);在分组中则与边缘齐平。 + One tool call of an agent transcript (Arc Tool call, design reference): the tool's mark, the call as a verb phrase, quiet meta ("4 results · 1.2s") and a status icon. The whole row is a button: it springs open into Input (the arguments as a mono key / value list) and Output (the result as a list, a table, tinted JSON or text, folding long ones behind "Show N more lines"), each with a copy button. A running call shimmers its title next to a spinner; a failed one shows its reason and Retry; one that waits for approval sits on an accent outline with Deny and Allow; a denied one is muted. Bind `Item` and change the item in place as the call moves on. Allow, Deny and Retry raise `ApproveRequested`, `DenyRequested` and `RetryRequested` (they bubble, so a `ToolCallGroup` or any parent can handle them): update the call's status yourself and set `Handled`. When nothing handles them, Deny marks the call denied here; Allow and Retry go to the `ToolCallSimulation` that appended the call, and change nothing otherwise. With no item the row is empty: a wrench and "No tool call" (`EmptyTitle`) without a status, and it does not open. On its own the row sits in a 1px bordered Surface frame (radius 16, 4px in); inside a group it is flush. + 智能体转录中的一次工具调用(Arc Tool call,设计参考):工具的标记、以动词短语表示的调用、低调的元信息("4 results · 1.2s")以及状态图标。整行是一个按钮:点击后以弹性动效展开为输入(参数,等宽的键/值列表)和输出(结果,以列表、表格、着色的 JSON 或文本呈现,过长内容折叠在"Show N more lines"之后),两者都带复制按钮。运行中的调用会在旋转指示器旁让标题闪烁;失败的调用会显示原因和重试;等待批准的调用带强调色轮廓,并显示拒绝和允许;被拒绝的调用会变为弱化显示。绑定 `Item`,调用推进时就地修改该项。允许、拒绝和重试会引发 `ApproveRequested`、`DenyRequested` 和 `RetryRequested`(它们会冒泡,因此 `ToolCallGroup` 或任何父级都可以处理):由你自行更新调用的状态并设置 `Handled`。若无人处理,拒绝会在此处将调用标记为已拒绝;允许和重试会交给追加该调用的 `ToolCallSimulation`,其他情况下不做任何改变。没有项目时,该行为空:一个扳手和 "No tool call"(`EmptyTitle`),没有状态,也无法展开。单独使用时,该行位于 1px 带边框的 Surface 外框中(圆角 16,内缩 4px);在分组中则与边缘齐平。 Raised when the person presses Allow on a call waiting for approval. Bubbles. @@ -20349,8 +25453,8 @@ 当人员在等待批准的调用上按下“拒绝”时引发。会冒泡。 - The call on screen: `Item`, or the sample when it is null. - 屏幕上显示的调用:即 `Item`;为 null 时则显示示例。 + The call on screen: `Item`, or an empty call when it is null. + 屏幕上显示的调用:即 `Item`;为 null 时则是一个空调用。 Raised when Input and Output open or close. @@ -20365,8 +25469,8 @@ 去掉该行自身的外框(分组会为其行设置此项)。 - The call shown. Change it in place; the row follows every change. Null shows a sample web search. - 展示的调用。直接修改它,行会随每次修改同步更新。为 null 时展示一个示例网页搜索。 + The call shown. Change it in place; the row follows every change. Null shows the empty row. + 展示的调用。直接修改它,行会随每次修改同步更新。为 null 时显示空行。 Every word the row shows or says. @@ -20393,8 +25497,8 @@ `ExpandedChanged` 与 `ExpandedChanged` 的数据。 - Every tool call of one assistant turn in one quiet card (Arc Tool call group, design reference): a header with the tools' overlapping marks, "Used 4 tools" (or "Using 3 tools" with a shimmer while the agent works, or "Waiting for approval") and, on the right, "1 failed" or the total duration; below it, one `ToolCall` row per call. Append calls to `Items` (or an observable `ItemsSource`) as the agent starts them and change them in place: a new row grows in from zero height, a new tool's mark joins the stack on a spring. Opening the header staggers the rows in. While the agent works the group stays open; 450ms after every call finished it folds itself away (`AutoCollapse`) unless the person opened or closed the group or a row. A call that needs a decision (a failure, an approval) opens a folded group. An empty group takes no space; its first call grows it in. With neither `Items` nor `ItemsSource` it shows four finished sample calls. Allow, Deny and Retry bubble up from the rows as `ApproveRequested`, `DenyRequested` and `RetryRequested`; see `ToolCall` for what happens when nothing handles them. - 将一个助手回合的所有工具调用放在一张安静的卡片中(Arc 工具调用组,设计参考):标题栏包含工具的重叠标记,"Used 4 tools"(代理工作时为带闪烁效果的 "Using 3 tools",或 "Waiting for approval"),右侧为 "1 failed" 或总时长;下方每个调用对应一行 `ToolCall`。代理开始调用时将其追加到 `Items`(或可观察的 `ItemsSource`),并原地修改它们:新行从零高度展开,新工具的标记以弹簧动画加入标记堆叠。展开标题栏时各行依次错开进入。代理工作期间该组保持展开;每次调用完成后 450ms,若用户未展开或收起该组或任一行,它会自动折叠(`AutoCollapse`)。需要决定的调用(失败、需要批准)会展开已折叠的组。空组不占用空间;其第一个调用会使其展开。若既未设置 `Items` 也未设置 `ItemsSource`,则显示四个已完成的示例调用。行中的允许、拒绝和重试会以 `ApproveRequested`、`DenyRequested` 和 `RetryRequested` 冒泡上传;未被处理时的行为见 `ToolCall`。 + Every tool call of one assistant turn in one quiet card (Arc Tool call group, design reference): a header with the tools' overlapping marks, "Used 4 tools" (or "Using 3 tools" with a shimmer while the agent works, or "Waiting for approval") and, on the right, "1 failed" or the total duration; below it, one `ToolCall` row per call. Append calls to `Items` (or an observable `ItemsSource`) as the agent starts them and change them in place: a new row grows in from zero height, a new tool's mark joins the stack on a spring. Opening the header staggers the rows in. While the agent works the group stays open; 450ms after every call finished it folds itself away (`AutoCollapse`) unless the person opened or closed the group or a row. A call that needs a decision (a failure, an approval) opens a folded group. An empty group takes no space; its first call grows it in. With neither `Items` nor `ItemsSource` it plays its `Simulation` when one is set, and otherwise stays empty. Allow, Deny and Retry bubble up from the rows as `ApproveRequested`, `DenyRequested` and `RetryRequested`; see `ToolCall` for what happens when nothing handles them. + 将一个助手回合的所有工具调用放在一张安静的卡片中(Arc 工具调用组,设计参考):标题栏包含工具的重叠标记,"Used 4 tools"(代理工作时为带闪烁效果的 "Using 3 tools",或 "Waiting for approval"),右侧为 "1 failed" 或总时长;下方每个调用对应一行 `ToolCall`。代理开始调用时将其追加到 `Items`(或可观察的 `ItemsSource`),并原地修改它们:新行从零高度展开,新工具的标记以弹簧动画加入标记堆叠。展开标题栏时各行依次错开进入。代理工作期间该组保持展开;每次调用完成后 450ms,若用户未展开或收起该组或任一行,它会自动折叠(`AutoCollapse`)。需要决定的调用(失败、需要批准)会展开已折叠的组。空组不占用空间;其第一个调用会使其展开。若既未设置 `Items` 也未设置 `ItemsSource`,则在设置了 `Simulation` 时播放它,否则保持为空。行中的允许、拒绝和重试会以 `ApproveRequested`、`DenyRequested` 和 `RetryRequested` 冒泡上传;未被处理时的行为见 `ToolCall`。 Raised when a row's Allow is pressed (it bubbles from the row). @@ -20433,8 +25537,8 @@ 列表是否展开(双向绑定)。在设置之前,只要有任一调用在运行、等待或失败,该组就会展开;已完成的记录默认折叠。 - An observable list of calls that wins over `Items`. - 可观察的调用列表,优先于 `Items`。 + An observable list of calls that wins over `Items` and `Simulation`. + 可观察的调用列表,优先于 `Items` 和 `Simulation`。 Every word the group and its rows show or say. @@ -20448,6 +25552,10 @@ Raised when a row's Retry is pressed (it bubbles from the row). 按下某行的重试时引发(从该行冒泡)。 + + A simulated turn to play while the group has no calls of its own (no `ItemsSource` and no `Items`): the group shows its `Items`, says "Using" while it `IsWorking` (unless `IsActive` is set), starts it from the top when it comes on screen and stops it when it leaves or is replaced. Null (the default) leaves a group without calls empty. Give each group its own. + 在分组没有自己的调用时(既无 `ItemsSource` 也无 `Items`)播放的模拟回合:分组显示其 `Items`,在它 `IsWorking` 时显示 "Using"(除非设置了 `IsActive`),出现在屏幕上时从头开始运行,离开或被替换时停止。为 null(默认)时,没有调用的分组保持为空。每个分组各用一个。 + The sum of the successful calls' durations (shown once nothing runs or waits). 成功调用耗时的总和(在没有任何调用运行或等待后显示)。 @@ -20529,8 +25637,8 @@ 工具调用的返回内容,在调用成功后显示在"Output"下方:`ToolCallListOutput`、`ToolCallTableOutput`、`ToolCallJsonOutput` 或 `ToolCallTextOutput` 之一。赋予新的输出即可更改该行显示的内容;行在打开时才读取输出。 - Plays a scripted agent turn into `Items` on a clock: calls are appended one after another, run for their time and settle; a failing call waits for `Retry` and a call that needs approval waits for `Approve` or `Deny`. Bind a `ToolCallGroup`'s `ItemsSource` to `Items` and its `IsActive` to `IsWorking`: Allow, Deny and Retry on the group's rows reach the simulation when nothing else handles them. The default script is `Script`. Time comes from a `TimeProvider`, so tests can step it; callbacks run on the UI thread. - 按时间顺序将脚本化的代理回合播放到 `Items` 中:调用依次追加,运行其耗时后结束;失败的调用等待 `Retry`,需要批准的调用等待 `Approve` 或 `Deny`。将 `ToolCallGroup` 的 `ItemsSource` 绑定到 `Items`,并将其 `IsActive` 绑定到 `IsWorking`:群组各行上的允许、拒绝和重试,若无其他处理方,会传递给模拟。默认脚本为 `Script`。时间来自 `TimeProvider`,因此测试可以逐步推进;回调在 UI 线程上运行。 + Plays a scripted agent turn into `Items` on a clock: calls are appended one after another, run for their time and settle; a failing call waits for `Retry` and a call that needs approval waits for `Approve` or `Deny`. Your app writes the `Script`; nothing ships with the package. Give it to `Simulation` (the group shows `Items`, follows `IsWorking`, starts the run when it is on screen and stops it when it leaves), or bind a group's `ItemsSource` to `Items` and its `IsActive` to `IsWorking` yourself. Allow, Deny and Retry on the rows reach the simulation when nothing else handles them. Time comes from a `TimeProvider`, so tests can step it; callbacks run on the UI thread. Create one per group: it keeps the playback state. + 按时间顺序将脚本化的代理回合播放到 `Items` 中:调用依次追加,运行其耗时后结束;失败的调用等待 `Retry`,需要批准的调用等待 `Approve` 或 `Deny`。`Script` 由你的应用编写,包里不附带任何脚本。把它交给 `Simulation`(分组显示 `Items`、跟随 `IsWorking`,出现在屏幕上时启动运行,离开时停止),或自行将分组的 `ItemsSource` 绑定到 `Items`、将其 `IsActive` 绑定到 `IsWorking`。各行上的允许、拒绝和重试,若无其他处理方,会传递给模拟。时间来自 `TimeProvider`,因此测试可以逐步推进;回调在 UI 线程上运行。每个分组各建一个:它保存播放状态。 Raised on the UI thread at every beat of the run (a call appended, settled, failed, retried, allowed, denied; the start and the end). @@ -20580,6 +25688,154 @@ Tooltip content: a string, or any object (bind it when the text comes from the view model). 提示内容:字符串或任意对象(当文本来自视图模型时进行绑定)。 + + A fine-tuning job card (Arc Training run, design reference): the base model with its mark and the job's name, a live train and validation loss curve drawn across a fixed step axis that doubles as the epoch progress bar, the step, learning rate, elapsed time and ETA, the newest three events (evals, checkpoints, warnings), Pause and Cancel while it runs, and the results with a Deploy button once it completes. Drive it from your job: set `Status`, `TotalSteps` and `Epochs` from the job config so the axis is right from the first sample, then keep `Step`, `LearningRate`, `Elapsed` and `Eta` current and append to `Loss` and `Events` as they stream (no need to throttle: the card eases toward each sample), or hand it updates with `Apply` or `FollowAsync`. The buttons raise events; Deploy calls `DeployHandler`. With no `Status` it plays its `Simulation`, a job you describe, as `Scenario` says, freezing while the card is off screen; with neither it shows a queued job with no data yet. + 微调任务卡片(Arc Training run,设计参考):带标识的基础模型和任务名称,沿着固定步数轴实时绘制的训练与验证损失曲线(这条轴同时也是轮次进度条),步数、学习率、已用时间和预计剩余时间,最新的三条事件(评测、检查点、警告),运行时的暂停和取消,以及完成后带 Deploy 按钮的结果。从你的任务驱动它:根据任务配置设置 `Status`、`TotalSteps` 和 `Epochs`,让坐标轴从第一个样本起就正确;然后持续更新 `Step`、`LearningRate`、`Elapsed` 和 `Eta`,并在数据流入时追加到 `Loss` 和 `Events`(无需节流:卡片会缓动到每个样本),或者用 `Apply` 或 `FollowAsync` 传入更新。按钮会引发事件;Deploy 调用 `DeployHandler`。没有 `Status` 时,它按 `Scenario` 播放它的 `Simulation`(由你描述的任务),卡片离开屏幕时暂停;两者都没有时,显示一个排队中、尚无数据的任务。 + + + Which action buttons may show (all by default; None for a read-only monitor). A button also needs the right status. + 哪些操作按钮可以显示(默认全部;只读监控设为 None)。按钮还需要相应的状态。 + + + A line over the empty chart while queued or starting, such as "Waiting for 8× H200 · 2nd in queue". + 排队或启动时显示在空图表上的一行文字,例如 "Waiting for 8× H200 · 2nd in queue"。 + + + The base model and its 24px mark (a monogram when the model has no mark); null shows the simulation's while it plays. + 基础模型及其 24px 标识(模型没有标识时显示首字母);为 null 时,播放模拟期间显示模拟中的模型。 + + + Raised on the second press, on Confirm cancel, within 3 seconds of the first. Bubbles. + 在第一次按下后 3 秒内第二次按下(即 Confirm cancel)时引发。冒泡。 + + + The training data, shown in the footer with its 14px mark and size. + 训练数据,显示在底部,带 14px 标识和规模。 + + + Deploys the completed model: called when Deploy is pressed, with a token that is cancelled when the card unloads or the run leaves Completed. Return the endpoint (or null). The button loads while it runs; a thrown exception shows its message and Retry deploy. Deploy shows only when this is set (or while simulating). + 部署已完成的模型:按下 Deploy 时调用,传入的令牌会在卡片卸载或运行离开 Completed 时取消。返回端点(或 null)。运行期间按钮显示加载状态;抛出的异常会显示其消息和 Retry deploy。只有设置了它(或处于模拟时)才显示 Deploy。 + + + Short facts after the model name, such as "LoRA r16" and "8× H200". + 模型名称后的简短事实,例如 "LoRA r16" 和 "8× H200"。 + + + Time spent training so far, excluding pauses. + 到目前为止的训练时间,不含暂停。 + + + Number of epochs: the axis splits into one band and one progress segment per epoch. + 轮次数:坐标轴按轮次分成一条背景带和一段进度。 + + + Why the run failed, shown in the footer in place of the dataset. + 运行失败的原因,显示在底部,取代数据集。 + + + Estimated time left. While queued it reads as "About 18m 40s". + 预计剩余时间。排队时显示为 "About 18m 40s"。 + + + The job or output model name; null shows the simulation's name while it plays, else "Fine-tuning job". + 任务或输出模型的名称;为 null 时,播放模拟期间显示模拟中的名称,否则显示 "Fine-tuning job"。 + + + Every word the card shows or says. + 卡片显示或朗读的所有文字。 + + + The current learning rate, shown as 1.84e-4. + 当前学习率,显示为 1.84e-4。 + + + Up to four results that replace the live stats on completion; null shows the final train and validation loss, the steps and the duration. + 完成时取代实时统计的最多四项结果;为 null 时显示最终训练和验证损失、步数和时长。 + + + A one-off message for screen readers, such as the result of an action. + 一次性的屏幕阅读器消息,例如某个操作的结果。 + + + Raised when Pause is pressed. Bubbles. + 按下 Pause 时引发。冒泡。 + + + Raised when Run again is pressed after the run completed, failed or was cancelled. Bubbles. + 运行完成、失败或被取消后按下 Run again 时引发。冒泡。 + + + Raised when Resume is pressed. Bubbles. + 按下 Resume 时引发。冒泡。 + + + Raised when Retry from checkpoint is pressed after a failure. Bubbles. + 失败后按下 Retry from checkpoint 时引发。冒泡。 + + + How the `Simulation` plays while `Status` is null: Healthy, or Oom (fails once at step 940). + `Status` 为 null 时 `Simulation` 的播放方式:Healthy,或 Oom(在第 940 步失败一次)。 + + + The job the card plays on its own while `Status` is null: its name, base model, details, dataset, hardware and endpoint, played as `Scenario` says. Null shows a queued job with no data instead. + `Status` 为 null 时卡片自行播放的任务:它的名称、基础模型、详情、数据集、硬件和端点,按 `Scenario` 播放。为 null 时改为显示一个排队中、尚无数据的任务。 + + + Where the job is. Leave it null to play the `Simulation` (as `Scenario` says), or to show a queued job with no data without one. + 任务所处的阶段。保持 null 则播放 `Simulation`(按 `Scenario`);没有模拟时显示一个排队中、尚无数据的任务。 + + + Optimizer steps done. + 已完成的优化器步数。 + + + Steps in the whole run. It fixes the x axis so the curve never rescales mid-run. + 整个运行的步数。它固定 x 轴,让曲线在运行中途不会重新缩放。 + + + The action buttons a `TrainingRun` may offer. A button also needs the right status. + `TrainingRun` 可以提供的操作按钮。按钮还需要相应的状态。 + + + The training data of a `TrainingRun`, shown in the footer. + `TrainingRun` 的训练数据,显示在底部。 + + + One entry of a `TrainingRun`'s event log. + `TrainingRun` 事件日志中的一条记录。 + + + What a `TrainingRunEvent` reports; it picks the row's icon and color. + `TrainingRunEvent` 报告的内容;它决定该行的图标和颜色。 + + + Every word `TrainingRun` shows or says, in English by default. Replace the whole object to localize or reword; counts, steps and times are format functions. + `TrainingRun` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;计数、步数和时间是格式化函数。 + + + One loss sample of a `TrainingRun`: the optimizer step and the train and validation loss at it. + `TrainingRun` 的一个损失样本:优化器步数以及该步的训练和验证损失。 + + + One result a completed `TrainingRun` shows in place of the live stats. + 已完成的 `TrainingRun` 取代实时统计时显示的一项结果。 + + + One update of a running job for `Apply` and `FollowAsync`: every value that is set replaces the card's, a loss sample and an event are appended, and nulls leave the card as it is. + 供 `Apply` 和 `FollowAsync` 使用的一次运行中任务更新:设置了的值替换卡片上的值,损失样本和事件被追加,null 则保持卡片不变。 + + + How a `TrainingRun` plays its `Simulation`. + `TrainingRun` 如何播放它的 `Simulation`。 + + + The fine-tuning job a `TrainingRun` plays on its own while the app sets no `Status`: its name, base model, details, dataset, the hardware it queues for and trains on, and the endpoint a simulated deploy returns. The card plays it as `Scenario` says: it queues, starts up and trains over 2,400 steps in three epochs with a procedural loss curve, checkpoints, evals and a done event (or an out of memory failure to retry from the last checkpoint), freezing while the card is off screen. Nothing is sent. Set the data before handing the simulation to the card; to play another job, give the card a new simulation. + 应用没有设置 `Status` 时 `TrainingRun` 自行播放的微调任务:它的名称、基础模型、详情、数据集、它排队等待和训练所用的硬件,以及模拟部署返回的端点。卡片按 `Scenario` 播放它:排队、启动,并以程序生成的损失曲线、检查点、评估和完成事件在三个 epoch 中训练 2,400 步(或者出现一次显存不足的失败,可从上一个检查点重试),卡片离开屏幕时暂停。不会发送任何内容。请在把模拟交给卡片之前设置好数据;要播放另一个任务,就给卡片一个新的模拟。 + + + Where a `TrainingRun` job is. + `TrainingRun` 任务所处的阶段。 + A table whose rows nest. Rows expand and collapse on a spring with indent guides, parents carry tri-state checkboxes (`IsSelectable`), sorting reorders siblings without breaking the hierarchy, and children can load on first expand (`ChildrenLoader`). The first column shown holds the tree. Keyboard follows the treegrid pattern: ↑/↓ move, → opens or enters, ← closes or goes to the parent, Home/End, PageUp/PageDown move ten rows, `*` opens all siblings, Space selects, Enter toggles a parent or activates a leaf, Ctrl/Cmd+A selects all. Rows come from `ItemsSource` and `ChildrenSelector`; each row is identified by `KeySelector` (the item itself when not set), which is what `ExpandedKeys` and `SelectedKeys` hold. Like Arc, rows are not virtualized: every visible row exists and animates, so keep an expanded tree to a few hundred rows and load deeper levels on demand. 行可以嵌套的表格。行在弹簧上展开和收起并带有缩进引导线,父行带有三态复选框(`IsSelectable`),排序会重排同级行而不破坏层级,子行可以在第一次展开时加载(`ChildrenLoader`)。显示出来的第一列承载树。键盘遵循树状网格模式:↑/↓ 移动,→ 展开或进入,← 收起或转到父级,Home/End,PageUp/PageDown 移动十行,`*` 展开所有同级行,Space 选择,Enter 切换父行或激活叶子,Ctrl/Cmd+A 全选。行来自 `ItemsSource` 和 `ChildrenSelector`;每一行由 `KeySelector` 标识(未设置时为行项本身),`ExpandedKeys` 和 `SelectedKeys` 保存的就是这些键。与 Arc 一样,行不做虚拟化:每个可见行都存在并带有动画,因此展开的树请保持在几百行以内,更深的层级按需加载。 @@ -20832,6 +26088,226 @@ A node of a `Treemap`: a leaf with a value, or a branch whose value is the sum of its children. `Treemap` 的节点:叶节点带有一个值,或分支节点的值为其子节点之和。 + + A trial status banner (Arc Trial banner, design reference): a ring counts down the days left, the copy names what the workspace loses, and the upgrade path is one click away. It comes as a slim `Banner` row, a sidebar `Card` with the feature list, or a header `Pill` that opens the card in a popover. All three share one state model: active, the warning tone in the last `WarnAt` days (the copy then names the end day), expired (a lock) and upgraded (a check). Dismiss and snooze fold the banner into the trial pill, which opens it again. Give it `DaysLeft` from your billing provider, or `EndsAt` and let it count the local calendar days with `TimeProvider` (it recounts at local midnight). `AddPaymentHandler` opens your checkout: return once a payment method is on file to show the upgraded state, throw to show Try again. Without it the call is simulated (900ms). With neither `DaysLeft` nor `EndsAt` there is no trial to show: the banner reads "No trial in progress" over an empty ring, without actions. The plan, the workspace, the loss and the features are your app's data and start empty. + 试用状态横幅(Arc Trial banner,设计参考):圆环倒数剩余天数,文案说明工作区将失去什么,升级只需点击一次。它可作为细长的 `Banner` 横幅、带功能列表的侧边栏 `Card`,或在弹出层中打开卡片的标题栏 `Pill`。三者共用一套状态:进行中、最后 `WarnAt` 天的警告色调(文案会说出结束的那一天)、已结束(锁形图标)和已升级(对勾)。关闭与稍后提醒会把横幅折叠为试用胶囊,点击胶囊可再次展开。传入来自计费服务的 `DaysLeft`,或传入 `EndsAt`,由它借助 `TimeProvider` 按本地日历日计数(在本地午夜重新计数)。`AddPaymentHandler` 打开你的结账流程:付款方式登记后返回即显示已升级状态,抛出异常则显示 Try again。没有它时该调用为模拟(900ms)。`DaysLeft` 和 `EndsAt` 都没有时没有可显示的试用:横幅在空的圆环上方显示 "No trial in progress",没有操作按钮。方案、工作区、失去的内容和功能都是你的应用的数据,默认为空。 + + + Opens checkout or the billing portal. Return once a payment method is on file to show the upgraded state; throw to show the error state with Try again. The token is cancelled when the banner leaves the window. Null simulates it (900ms). + 打开结账页面或计费门户。付款方式登记后返回即显示已升级状态;抛出异常则显示带 Try again 的错误状态。横幅离开窗口时令牌会被取消。为 null 时以模拟方式运行(900ms)。 + + + After a dismiss or snooze: fold into the trial pill, which opens the banner again, or render nothing. + 关闭或稍后提醒之后:折叠为试用胶囊(点击可再次展开横幅),或什么也不显示。 + + + Raised when the person presses Compare plans. Bubbles. Open your plans page and set Handled; unhandled, the banner shows a short simulated note. + 用户按下“比较方案”时引发。冒泡。打开你的方案页面并设置 Handled;未处理时,横幅显示一条简短的模拟提示。 + + + Whole days left, from your billing provider; 0 or less shows the expired state. Null counts them from `EndsAt` and today; with neither, the sample shows 9. + 剩余的整天数,来自你的计费服务;0 或更小显示已结束状态。为 null 时根据 `EndsAt` 和今天计数;两者都没有时,示例显示 9。 + + + Raised when the person dismisses the banner. Bubbles. Persist it. + 用户关闭横幅时引发。冒泡。请保存此状态。 + + + When the trial ends, for the date copy (and the days left when `DaysLeft` is null). Null uses a sample date: October 27, 2026 plus the days left. Pass your real end date. + 试用结束的时间,用于文案中的日期(`DaysLeft` 为 null 时也用于计算剩余天数)。为 null 时使用示例日期:2026 年 10 月 27 日加上剩余天数。请传入真实的结束日期。 + + + Whether the banner is folded after a dismiss or snooze (two-way). Set it to false to show the banner again. + 横幅在关闭或稍后提醒后是否已折叠(双向)。设为 false 可再次显示横幅。 + + + Shows the dismiss button. + 显示关闭按钮。 + + + Shows the snooze menu. + 显示稍后提醒菜单。 + + + Whether a payment method is on file (two-way). Set by a successful `AddPaymentHandler`. + 是否已登记付款方式(双向)。`AddPaymentHandler` 成功后会设置它。 + + + Every word the banner shows or says. + 横幅显示或朗读的所有文字。 + + + One short phrase for the banner copy ("SAML SSO and 12 seats"). Keep it short so the banner stays on one line. Empty by default. + 横幅文案中的一个简短短语("SAML SSO and 12 seats")。请保持简短,让横幅保持一行。默认为空。 + + + Raised when the person presses Manage billing in the upgraded state. Bubbles. Set Handled; unhandled, the banner shows a short simulated note. + 已升级状态下用户按下“管理账单”时引发。冒泡。请设置 Handled;未处理时,横幅显示一条简短的模拟提示。 + + + The plan being trialed ("Pro"), used in every line of copy. Empty by default. + 正在试用的方案("Pro"),每行文案都会用到。默认为空。 + + + Raised with the chosen snooze and when it ends. Bubbles. Persist `Until` and stop rendering the banner until then. + 附带所选的稍后提醒选项及其结束时间引发。冒泡。请保存 `Until`,并在此之前不再显示横幅。 + + + Where the trial is: active, warning, expired or upgraded. + 试用所处的状态:进行中、警告、已结束或已升级。 + + + Where "today" comes from when the days are counted from `EndsAt`, and the snooze dates. The system clock by default. + 根据 `EndsAt` 计数天数以及计算稍后提醒日期时,"今天"从何而来。默认为系统时钟。 + + + The trial's length in days. The ring shows the days left of it. + 试用期的天数。圆环显示其中剩余的天数。 + + + Banner (a slim row), Card (a sidebar card with the features) or Pill (a header pill that opens the card). + Banner(细长横幅)、Card(带功能列表的侧边栏卡片)或 Pill(打开卡片的标题栏胶囊)。 + + + The last N days switch to the warning tone, with copy that names the end day. + 最后 N 天切换为警告色调,文案会说出结束的那一天。 + + + The workspace's name, used in the expired and upgraded copy. Empty (the default) reads "Your workspace". + 工作区名称,用于已结束和已升级状态的文案。为空(默认)时写作 "Your workspace"。 + + + Every word `TrialBanner` shows or says, in English by default. Replace the whole object to localize or reword; lines that carry values are format functions. Dates are local calendar days. + `TrialBanner` 显示或朗读的所有文字,默认为英文。替换整个对象即可本地化或改写;带有数值的行是格式化函数。日期为本地日历日。 + + + One thing the workspace loses without paying, listed in the card of a `TrialBanner`. + 工作区不付费就会失去的一项内容,列在 `TrialBanner` 的卡片中。 + + + A short muted detail after the name ("12 seats"), or null. + 名称后面一段简短的次要说明("12 seats"),或 null。 + + + The 14px mark before the name (a 24 × 24 stroked glyph); a check when null. + 名称前的 14px 图标(24 × 24 描边图形);为 null 时显示对勾。 + + + A stable id for the row. + 该行的稳定 id。 + + + The feature's name ("SAML single sign-on"). + 功能名称("SAML single sign-on")。 + + + The arguments of `Snoozed`: the chosen option and when the snooze ends. + `Snoozed` 的参数:所选选项及稍后提醒的结束时间。 + + + The upgrade nudge shown the moment someone hits a limit (Arc Upgrade prompt, design reference): a title that says what was used, a usage meter, the next plan's price rolling between monthly and yearly with a billing toggle, the three things the plan unlocks, and Maybe later beside Upgrade. Upgrading runs `UpgradeHandler` and rolls into a success state with the new limits while the meter settles to the new limit; a failure shows its message under the price with Try again; Maybe later shows a calm reminder with See Pro again. Every state shares one cell, so the card never changes size between offer, processing, error, success and reminder and the buttons stay in place. Your app gives the plan and the usage: with no `Plan` the card is empty ("No plan to offer", no meter and no offer, Upgrade and Maybe later off). With no handler it simulates a 1.1s upgrade that charges nothing. + 在用户用完额度的那一刻显示的升级提示(Arc Upgrade prompt,设计参考):说明用掉了什么的标题、用量条、在月付和年付之间滚动并带计费切换的下一档方案价格、方案解锁的三项内容,以及并排的 Maybe later 和 Upgrade。升级会运行 `UpgradeHandler`,并滚入列出新额度的成功状态,同时用量条落到新的上限;失败时在价格下方显示信息并提供 Try again;Maybe later 显示一条安静的提醒和 See Pro again。所有状态共用一个单元格,因此卡片在报价、处理中、错误、成功和提醒之间从不改变尺寸,按钮也待在原位。方案和用量由你的应用提供:没有 `Plan` 时卡片为空("No plan to offer",没有用量条也没有报价,Upgrade 和 Maybe later 不可用)。没有处理程序时,它模拟一次 1.1 秒、不收取任何费用的升级。 + + + Raised by Continue on the success state, usually to close the prompt and retry the blocked action. Set `Handled` to keep the success state; left unhandled, the prompt resets to the offer. Bubbles. + 由成功状态的 Continue 引发,通常用于关闭提示并重试被拦下的操作。设置 `Handled` 可保留成功状态;未处理时,提示重置回报价状态。会冒泡。 + + + Replaces the line under the offer's title. Null writes it from the plan and the reset date. + 替换报价状态标题下方的那行文字。为 null 时根据方案和重置日期生成。 + + + Raised by Maybe later; record the snooze here. The prompt then shows the reminder. Bubbles. + 由 Maybe later 引发;请在这里记录暂缓。提示随后显示提醒。会冒泡。 + + + The message of the last failed upgrade while the prompt is in the error state, otherwise null. + 提示处于错误状态时为上一次升级失败的信息,否则为 null。 + + + The billing interval (two-way). Yearly by default. + 计费周期(双向)。默认为 Yearly。 + + + Raised when the person changes the billing toggle. Bubbles. + 用户更改计费切换时引发。会冒泡。 + + + Every word the prompt shows or announces. + 提示显示或播报的每个词。 + + + The current plan's limit. At or over it the meter reads Limit reached; below it, how many are left. + 当前方案的上限。达到或超过时用量条显示 Limit reached;低于时显示还剩多少。 + + + The plan offered. Null (the default) shows the empty card: nothing to offer. + 提供的方案。为 null(默认)时显示空卡片:没有可提供的内容。 + + + When the current limit resets ("Nov 1"). Shown in the description, the meter and the reminder; empty by default. + 当前额度何时重置("Nov 1")。显示在说明、用量条和提醒中;默认为空。 + + + What the prompt shows: the offer, processing, an error, success or the reminder. + 提示显示的内容:报价、处理中、错误、成功或提醒。 + + + Replaces the limit title of the offer, such as "You've used all 3 seats". Null writes it from the usage. + 替换报价状态的上限标题,例如 "You've used all 3 seats"。为 null 时根据用量生成。 + + + What is counted, plural ("AI credits", "seats", "generations"). Used in the title, the meter and announcements. + 计数的对象,复数形式("AI credits"、"seats"、"generations")。用于标题、用量条和播报。 + + + Raised when the upgrade handler threw, with the exception. Bubbles. + 升级处理程序抛出异常时引发,带有该异常。会冒泡。 + + + Starts the upgrade with the selection (plan, interval, amount charged today, currency). Complete the task when the plan is active; throw to show the exception's message under the price with Try again. The token is cancelled when the prompt leaves the visual tree. Null simulates a 1.1s upgrade that charges nothing. + 以所选内容(方案、计费周期、今天收取的金额、货币)开始升级。方案生效后完成任务;抛出异常则在价格下方显示异常信息并提供 Try again。提示离开可视树时令牌会被取消。为 null 时模拟一次 1.1 秒、不收取任何费用的升级。 + + + Raised once the upgrade succeeded, after the success state shows. Bubbles. + 升级成功、成功状态显示之后引发。会冒泡。 + + + How much of the limit is used. + 已用掉上限中的多少。 + + + Arguments of `UpgradeFailed`. + `UpgradeFailed` 的参数。 + + + One unlock an `UpgradePromptPlan` lists in the offer, such as more credits or a better model. + `UpgradePromptPlan` 在报价中列出的一项解锁内容,例如更多点数或更好的模型。 + + + Arguments of `IntervalChanged`. + `IntervalChanged` 的参数。 + + + Every word an `UpgradePrompt` shows or announces, in English by default. Replace the instance to translate or reword; sentences with values are functions so the word order can change. Counts and amounts arrive already formatted ("2,000", "$192"); `unit` is the prompt's `Unit` and `plan` the plan's name. + `UpgradePrompt` 显示或播报的每个词,默认为英文。替换该实例即可翻译或改写;带值的句子是函数,因此可以调整语序。计数和金额传入时已格式化("2,000"、"$192");`unit` 是提示的 `Unit`,`plan` 是方案名称。 + + + One of the new limits an `UpgradePrompt` lists once the upgrade succeeded. + 升级成功后 `UpgradePrompt` 列出的一项新额度。 + + + The plan an `UpgradePrompt` offers: its name, the monthly price and the per month price when billed yearly (the savings badge and the yearly charge derive from them), the new limit, the unlocks and the new limits. + `UpgradePrompt` 提供的方案:名称、月付价格和按年付费时的每月价格(节省徽标和年付金额由它们算出)、新的上限、解锁内容和新的额度。 + + + What the person chose: the plan, the interval, the amount charged today and its currency. + 用户的选择:方案、计费周期、今天收取的金额及其货币。 + + + Arguments of `Upgraded`. + `Upgraded` 的参数。 + One day of a service's history, derived from the incidents. 一项服务历史中的一天,由事故推算而来。 @@ -21700,6 +27176,114 @@ The header value. 请求头的值。 + + One attempt in the recent list: what was sent, and what came back. + 最近列表中的一次尝试:发送了什么,以及返回了什么。 + + + An event type the tester can send, with the JSON payload it starts from. + 测试器可以发送的事件类型,以及它起始的 JSON 负载。 + + + The signed request `WebhookTester` hands to `SendAction`. Send it from your server, not from the browser: the server can re-sign the same body and check that the signature matches. + `WebhookTester` 交给 `SendAction` 的已签名请求。请从你的服务器发送它,而不是从浏览器:服务器可以对同一份正文重新签名,并核对签名是否一致。 + + + What an endpoint answered. A `Status` of 0 means no answer arrived: set `Error` (and `TimedOut` when it ran out of time). + 端点的应答。`Status` 为 0 表示没有收到应答:请设置 `Error`(超时则同时设置 `TimedOut`)。 + + + A tester for the endpoints of a product that sends webhooks: pick an event, edit its JSON payload and send a signed test event. The request timeline walks from signed to sent to the answer, the response panel shows the body, the headers and the exact request, and a list of recent deliveries keeps the last attempts, failed ones with a Retry button. The block signs in the app (HMAC SHA-256 of `timestamp.body`, sent as `t=…,v1=…` in `SignatureHeader`) but never posts to your customer's endpoint itself: `SendAction` receives the signed `WebhookTestRequest` and passes it to your server, which does the real POST. Without a `SendAction` every delivery is simulated and says so in its announcements. + Webhook 端点测试器:选择事件、编辑 JSON 负载并发送带签名的测试事件。请求时间线从已签名走到已发送再到应答,响应面板显示正文、响应头和确切的请求,最近投递列表保留最近的尝试,失败的带有 Retry 按钮。区块在应用内签名(对 `timestamp.body` 做 HMAC SHA-256,以 `t=…,v1=…` 放入 `SignatureHeader`),但它自己绝不会向客户的端点发请求:`SendAction` 收到已签名的 `WebhookTestRequest` 并交给你的服务器,由服务器真正发出 POST。未设置 `SendAction` 时,每次投递都是模拟的,并在播报中说明。 + + + Sends one sample event about a second after the block scrolls half into view, unless someone used it first. Only while `SendAction` is null. + 区块滚动到一半进入视口约一秒后发送一个示例事件,除非用户已经操作过它。仅在 `SendAction` 为 null 时生效。 + + + The widest the card gets (760); the block fills narrower containers. + 卡片的最大宽度(760);更窄的容器中区块会填满容器。 + + + Raised after a send or a retry finished, with the new delivery (already at the top of the list). + 发送或重试完成后触发,携带新的投递(已位于列表顶部)。 + + + Run with a `WebhookTestDelivery` after a send or a retry finished. + 发送或重试完成后以 `WebhookTestDelivery` 为参数执行。 + + + The recent deliveries, newest first; a send or a retry inserts at the top. + 最近的投递,最新的在前;发送或重试会插入到顶部。 + + + The endpoint test events go to (two-way). https is required, except http for localhost. + 测试事件发往的端点(双向)。必须是 https,localhost 允许 http。 + + + The events to choose from; while it is empty five sample events are offered. Choosing an event loads its payload. + 可供选择的事件;为空时提供五个示例事件。选择事件会载入它的负载。 + + + Draws a bordered card with rounded corners (true); false lays the block flat on the page. + 绘制带圆角边框的卡片(true);false 则让区块平铺在页面上。 + + + Every word the block shows or announces. + 区块显示或播报的每一个词。 + + + The payload as it stands in the editor (two-way), valid JSON or not. + 编辑器中当前的负载(双向),无论是否为合法 JSON。 + + + Where the timeline stands. + 时间线当前所处的阶段。 + + + Sends a failed delivery again on your server: awaited when Retry is pressed, with the delivery's body under a fresh timestamp and signature. Null uses `SendAction`. + 在你的服务器上重新发送失败的投递:按下 Retry 时被等待,使用该投递的正文,并带上新的时间戳和签名。为 null 时使用 `SendAction`。 + + + The signing secret. It is shown masked with its prefix visible; the body is signed with it in the app. + 签名密钥。显示时被遮罩,前缀可见;区块在应用内用它对正文签名。 + + + The delivery whose response the panel shows (two-way); a send selects the new delivery. + 面板显示其响应的投递(双向);发送后会选中新的投递。 + + + The chosen event (two-way); null selects the first one. + 当前选择的事件(双向);为 null 时选中第一个。 + + + Sends the signed request on your server: awaited when the person sends, with a token that leaving the window cancels. Return the endpoint's answer; a status of 0 with `Error` (and `TimedOut`) says no answer arrived, and an exception counts as a failed delivery. Null simulates the endpoint (a 200 after `SimulatedLatency`). + 在你的服务器上发送已签名的请求:用户发送时被等待,并带有离开窗口即取消的令牌。返回端点的应答;状态为 0 并带有 `Error`(以及 `TimedOut`)表示没有收到应答,抛出异常同样算作失败的投递。为 null 时模拟端点(经过 `SimulatedLatency` 后返回 200)。 + + + The header that carries the signature (`x-webhook-signature`). + 携带签名的请求头(`x-webhook-signature`)。 + + + How long a simulated delivery takes to answer (0.65 s). + 模拟投递的应答耗时(0.65 秒)。 + + + The product that sends the events: its name and mark in the header. + 发送事件的产品:页头中显示它的名称和标志。 + + + From this width of the block the request and the response sit side by side (680). + 区块达到这个宽度时,请求与响应并排显示(680)。 + + + Every word `WebhookTester` shows or announces, in English by default. Replace the instance to translate or reword; texts with values are functions so that word order can change. + `WebhookTester` 显示或播报的每一个词,默认为英文。替换该实例即可翻译或改写;带数值的文本是函数,因此语序可以改变。 + + + The product that sends the events: a name for the subtitle and a mark for the header. + 发送事件的产品:副标题使用的名称和页头使用的标志。 + One slice of an endpoint's traffic chart (Arc shows 48 half hours): how many events were sent in it and how many of those still fail. 端点流量图表中的一个时间片(Arc 显示 48 个半小时):该时间片内发送了多少事件,其中多少仍然失败。 @@ -22320,6 +27904,46 @@ The canvas the buttons zoom. 按钮所缩放的画布。 + + A small book or magazine that opens on its cover (Arc Page curl, design reference). Tap the right page to go forward and the left page to go back, or drag a page: it folds along a line that follows the pointer, showing its back, a lit curl and a shadow on the page below. A page count with previous and next buttons sits underneath. + 一本从封面开始翻开的小书或小杂志(Arc Page curl,设计参考)。轻点右页向前翻,轻点左页向后翻,或者拖动书页:它沿着跟随指针的折线翻折,露出背面、一道被照亮的卷边,以及落在下一页上的阴影。下方有页码以及上一页、下一页按钮。 + + + The hint under the lower outer corner, shown until the first interaction. Null hides it. + 位于外侧下角下方的提示,在第一次交互之前显示。设为 Null 则隐藏。 + + + The accessible name of the book. + 书的无障碍名称。 + + + A page's width divided by its height. + 页宽除以页高。 + + + The template that builds a page from an item that is not a control. + 为非控件条目生成页面的模板。 + + + Whether the page count with previous and next buttons shows under the book. + 是否在书下方显示页码以及上一页、下一页按钮。 + + + The spread on show: 0 is the closed cover, spread `n` shows pages `2n` and `2n + 1`. Two-way. + 当前显示的跨页:0 为合上的封面,第 `n` 个跨页显示第 `2n` 与 `2n + 1` 页。双向。 + + + Raised when a page finishes turning, by a drag, a tap, a key or a button. + 一页翻完时触发,无论由拖动、点按、按键还是按钮引起。 + + + The number of spreads: the closed cover plus one per two pages. + 跨页的数量:合上的封面加上每两页一个。 + + + Whether the cover corner curls once when the book first comes into view, and a little on hover until the first turn. + 书第一次进入视野时封面一角是否卷起一次,以及在第一次翻页之前悬停时是否轻轻卷起。 + diff --git a/gallery/TKCode.Arc.Gallery/Docs/Api/zh-TW.xlf b/gallery/TKCode.Arc.Gallery/Docs/Api/zh-TW.xlf index 2b32121f..4ebb89fb 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Api/zh-TW.xlf +++ b/gallery/TKCode.Arc.Gallery/Docs/Api/zh-TW.xlf @@ -420,6 +420,266 @@ This week's 168 hourly values, Monday 00:00 first (day × 24 + hour). 本週 168 個小時數值,從週一 00:00 開始(天序號 × 24 + 小時)。 + + A live document where an AI agent edits like a collaborator (Arc Agent cursor, design reference). The agent's labeled cursor comes out of its avatar, selects a paragraph and streams in a rewrite, adds a heading and fixes a typo, while a teammate's cursor reads along. The changes stay tinted in the agent's color until someone accepts or undoes them from a floating review bar. Give it the document as `Blocks` (title, heading and paragraph blocks with stable ids) and the edits as `Edits`, or fetch them from your agent with `Run` (cancelled when someone presses Stop). Store the document from `Accepted` and pass it back as `Blocks`. With none of them it plays a `Simulation` (a document, its edits, an agent and a teammate you pass), looping; with no data at all it shows an empty document. + 一份即時文件,AI 代理像協作者一樣在其中編輯(Arc Agent cursor,設計參考)。代理帶名字標籤的游標從它的大頭貼中出來,選取一個段落並串流寫入改寫,新增一個標題並修正一個錯字,同時一位同事的游標跟著閱讀。變更以代理的顏色著色,直到有人在浮動的審閱列中接受或復原。把文件以 `Blocks`(帶穩定 id 的標題、小標題和段落區塊)交給它,把編輯以 `Edits` 交給它,或用 `Run` 從你的代理取得(有人按下 Stop 時取消)。從 `Accepted` 儲存文件,再以 `Blocks` 傳回。三者都沒有時,它循環播放 `Simulation`(你傳入的文件、編輯、代理和同事);完全沒有資料時顯示一份空白文件。 + + + Raised with the edited document when someone accepts the changes. Bubbles. + 有人接受變更時引發,參數是編輯後的文件。會向上傳遞。 + + + The agent: its name on the cursor, its 24 × 24 stroked mark in the avatar and, from a solid `MarkBrush`, the color of its cursor, selection and change tint (the accent otherwise). Null is the `Simulation`'s agent while one plays, or an unnamed one. + 代理:游標上的名字、大頭貼中 24 × 24 的描邊標記,以及(來自純色的 `MarkBrush`)其游標、選取範圍和變更著色的顏色(否則為強調色)。為 null 時,播放 `Simulation` 期間使用其代理,否則使用一個未命名的代理。 + + + Starts a run as soon as the card is on screen. Off, the header shows an Ask button instead. + 卡片一出現在畫面上就開始執行。關閉時,頁首顯示 Ask 按鈕。 + + + The document as title, heading and paragraph blocks with stable ids; null shows the `Simulation`'s while one plays, or an empty document. Compared by content, so passing back an equal list does nothing. + 文件,由帶穩定 id 的標題、小標題和段落區塊組成;為 null 時,播放 `Simulation` 期間顯示其文件,否則顯示空白文件。依內容比較,所以傳回相同的清單不會有任何改變。 + + + A person in the document, drawn as a second cursor with their initials and first name. Null is the `Simulation`'s teammate while one plays, or nobody. + 文件中的一個人,繪製為第二個游標,帶有其姓名縮寫和名字。為 null 時,播放 `Simulation` 期間使用其同事,否則沒有同事。 + + + The edits to play in order (rewrite, insert, fix); ignored when `Run` is set. Null plays the `Simulation`'s while one plays, or none. + 依序播放的編輯(改寫、插入、修正);設定了 `Run` 時忽略。為 null 時,播放 `Simulation` 期間播放其編輯,否則沒有編輯。 + + + Whether the collaborator is shown; false hides their cursor and avatar. + 是否顯示協作者;false 會隱藏其游標和大頭貼。 + + + Adds "Simulated, nothing was saved" to every announcement. Null: only while a `Simulation` plays. + 在每次播報後附加 "Simulated, nothing was saved"。為 null 時只在播放 `Simulation` 時附加。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個字詞。 + + + Replays the run: the collaborator accepts after a short pause (never while the pointer is over the card or focus is in it), then the document resets. Null loops only while a `Simulation` plays. Never loops below full motion. + 重播執行:協作者在短暫停頓後接受(指標停在卡片上或焦點在卡片內時從不接受),然後文件重設。為 null 時只在播放 `Simulation` 時循環。低於完整動態效果時從不循環。 + + + A muted line under the title, such as the owner and a target date. + 標題下方的一行淡化文字,例如負責人和目標日期。 + + + How many edits are waiting for review. + 等待審閱的編輯數量。 + + + Where the run is: idle, thinking, editing, review, accepted, reverted, stopped, empty or error. + 執行所處的階段:閒置、思考、編輯、審閱、已接受、已復原、已停止、無變更或錯誤。 + + + Raised when `Phase` changes. Bubbles. + `Phase` 變化時引發。會向上傳遞。 + + + Fetches the edits from your agent. The token is cancelled when someone presses Stop; a failed task shows the error with Retry. + 從你的代理取得編輯。有人按下 Stop 時權杖被取消;失敗的工作會顯示錯誤和 Retry。 + + + A session to play while the card has no `Blocks`, `Edits` or `Run`: its document, edits, agent, teammate and meta line, looping with full motion (nothing is saved). Your own data wins over it; null stops it and shows the empty document. + 卡片沒有 `Blocks`、`Edits` 或 `Run` 時要播放的工作階段:它的文件、編輯、代理、同事和中繼資訊行,在完整動態效果下循環播放(不會儲存任何內容)。你自己的資料優先於它;為 null 時停止播放並顯示空白文件。 + + + Playback speed multiplier for cursor travel, streaming and pauses (above zero). + 游標移動、串流寫入和停頓的播放速度倍數(大於零)。 + + + Raised with the restored document when someone undoes the changes. Bubbles. + 有人復原變更時引發,參數是還原後的文件。會向上傳遞。 + + + One block of the document an `AgentCursor` shows: a title, heading or paragraph with a stable id. + `AgentCursor` 所顯示文件中的一個區塊:帶穩定 id 的標題、小標題或段落。 + + + A person in an `AgentCursor` document, drawn as a second cursor with their initials and first name. + `AgentCursor` 文件中的一個人,繪製為第二個游標,帶有其姓名縮寫和名字。 + + + Arguments of `Accepted` and `Undone`: the document after the change. + `Accepted` 和 `Undone` 的參數:變化後的文件。 + + + One edit the agent makes in an `AgentCursor` document. Edits address blocks by id: a rewrite replaces a block's text, an insert adds a block after one, a fix replaces an exact word inside a block. + 代理在 `AgentCursor` 文件中進行的一次編輯。編輯以 id 指向區塊:改寫取代一個區塊的文字,插入在某個區塊之後新增一個區塊,修正取代區塊中一個確切的字詞。 + + + Every word `AgentCursor` shows or says, in English by default. Replace the whole object to localize or reword; texts that hold a name or a count are format functions. + `AgentCursor` 顯示或朗讀的每一個字詞,預設是英文。取代整個物件即可在地化或改寫用語;包含名字或數量的文字是格式化函式。 + + + Arguments of `PhaseChanged`. + `PhaseChanged` 的參數。 + + + A document session an `AgentCursor` plays without an agent, from data you give it: the document, the edits the agent makes, the agent and a teammate who reads along, and the muted line under the title. Set it as `Simulation` while the card has no `Blocks`, `Edits` or `Run`: the card shows the document, plays the edits, says nothing was saved and, with full motion, loops (the teammate accepts after a short pause, then it starts over). + `AgentCursor` 在沒有代理時依你提供的資料播放的文件工作階段:文件、代理所做的編輯、代理和一位跟著閱讀的同事,以及標題下方的淺色行。在卡片沒有 `Blocks`、`Edits` 或 `Run` 時把它設為 `Simulation`:卡片顯示文件、播放編輯、說明什麼都沒有儲存,並在完整動態效果下循環(同事在短暫停頓後接受,然後重新開始)。 + + + A support conversation where an AI assistant hands off to a person (Arc Agent handoff, design reference). The assistant answers first; when it transfers, a card in the thread shows the summary it passes to the team and the typical wait. The person then joins and takes over, and a thin timeline on the left marks each step: the assistant, the handoff (with the help desk's mark), the person joining and the resolution. Drive it from your support backend: set `Stage`, keep the assistant's messages in `Messages` and everything after the handoff in `AgentMessages`, set `Typing`, `Summary` and `Agent`, and handle the events. Stream assistant text by updating a message's `Text` with `IsStreaming` set; each appended chunk fades in. Leave `Stage` unset for an empty chat, or to play a `Simulation` you pass in (`AgentHandoffSimulation`). The card fills its container up to 560px and has a fixed height (600 by default, set `Height`); the thread scrolls inside and stays pinned to the newest message, so the page never shifts and the width never follows the text. + AI 助理把對話轉交給真人的客服對話(Arc Agent handoff,設計參考)。助理先回答;轉接時,對話中的一張卡片顯示它交給團隊的摘要與一般的等候時間。接著真人加入並接手,左側一條細時間軸標出每一步:助理、轉接(附客服系統的標誌)、真人加入與問題解決。由你的客服後端驅動:設定 `Stage`,助理的訊息放在 `Messages`,轉接之後的一切放在 `AgentMessages`,設定 `Typing`、`Summary` 與 `Agent`,並處理事件。要串流輸出助理的文字,在設定了 `IsStreaming` 的訊息上更新 `Text`;每次附加的片段都會淡入。沒有設定 `Stage` 時顯示一個空對話,或播放你傳入的 `Simulation`(`AgentHandoffSimulation`)。卡片填滿容器,最寬 560px,高度固定(預設 600,可設定 `Height`);對話在內部捲動並固定在最新訊息,所以頁面不會跳動,寬度也不會隨文字改變。 + + + The person who takes over: shown on the join step, in the card and in the header from connected on. + 接手的人:從已接通起顯示在加入步驟、卡片與標題列中。 + + + Shows Cancel on the handoff card while transferring; it raises `CancelTransferRequested`. + 轉接中在轉接卡片上顯示 Cancel;它觸發 `CancelTransferRequested`。 + + + Shows a Talk to a person reply under the assistant's last answer while the stage is Assistant; it raises `HumanRequested`. + 階段為 Assistant 時,在助理最後一則回答下方顯示 Talk to a person 快速回覆;它觸發 `HumanRequested`。 + + + Shows Try again on the handoff card when the stage is Unavailable; it raises `RetryRequested`. + 階段為 Unavailable 時在轉接卡片上顯示 Try again;它觸發 `RetryRequested`。 + + + The assistant's mark: a 24 × 24 stroked glyph drawn at 14px (the sparkle by default). Pass your model's own mark. + 助理的標誌:一個 24 × 24 的描邊圖形,以 14px 繪製(預設為星芒)。傳入你的模型自己的標誌。 + + + The assistant mark's color; the accent by default. + 助理標誌的顏色;預設為強調色。 + + + The model behind the assistant, shown muted after its name; null (the default) hides it. + 助理背後的模型,以淡化顏色顯示在名稱之後;為 null(預設)時隱藏。 + + + The assistant's name on the first step. + 第一步上的助理名稱。 + + + Raised when the customer presses Cancel on the handoff card. Bubbles. Cancel the escalation, then set `Stage` back to Assistant. + 客戶在轉接卡片上按下 Cancel 時觸發。冒泡。取消升級,然後把 `Stage` 設回 Assistant。 + + + Raised when the customer presses Talk to a person. Bubbles. Escalate, then set `Stage` to Transferring. + 客戶按下 Talk to a person 時觸發。冒泡。升級給真人,然後把 `Stage` 設為 Transferring。 + + + Whether the summary in the handoff card is open (two-way). + 轉接卡片中的摘要是否展開(雙向)。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個字詞。 + + + A one-off message for screen readers, such as "Refund issued"; announced once each time it changes. + 給螢幕閱讀器的一次性訊息,例如「Refund issued」;每次變化時朗讀一次。 + + + The composer placeholder; null follows the stage ("Ask a question", "Message Jordan"). + 輸入框的提示文字;為 null 時隨階段改變(「Ask a question」、「Message Jordan」)。 + + + The help desk's mark on the timeline next to the handoff card: a 24 × 24 stroked glyph (an inbox by default). + 時間軸上轉接卡片旁的客服系統標誌:一個 24 × 24 的描邊圖形(預設為收件匣)。 + + + The provider mark's color; the secondary text color by default. + 客服系統標誌的顏色;預設為次要文字顏色。 + + + The name of the help desk the conversation moves to. + 對話轉入的客服系統名稱。 + + + The typical wait shown while transferring, such as "2 min"; null (the default) shows none. + 轉接中顯示的一般等候時間,例如 "2 min";為 null(預設)時不顯示。 + + + Raised when the customer presses Try again on the handoff card. Bubbles. Escalate again, then set `Stage` to Transferring. + 客戶在轉接卡片上按下 Try again 時觸發。冒泡。再次升級,然後把 `Stage` 設為 Transferring。 + + + Raised when the customer sends a message, with the trimmed text. Bubbles. Append it to `Messages` or `AgentMessages` yourself. + 客戶傳送訊息時觸發,附帶去除前後空白的文字。冒泡。由你自己把它加到 `Messages` 或 `AgentMessages`。 + + + Shows the composer; sending raises `SendRequested` with the trimmed text. + 顯示輸入框;傳送會觸發 `SendRequested`,附帶去除前後空白的文字。 + + + A simulated chat to play while `Stage` is unset (demos and prototypes). The card plays it from the top each time it is shown, with the composer and the Talk to a person, Cancel and Try again actions; null shows the card's own state. + 未設定 `Stage` 時要播放的模擬對話(示範與原型)。卡片每次顯示時從頭播放它,輸入框以及 Talk to a person、Cancel 與 Try again 操作都可使用;為 null 時顯示卡片自己的狀態。 + + + Where the conversation is. Leave it unset (null) to play the `Simulation`, if any. + 對話進行到哪一步。不設定(null)時播放 `Simulation`(如果有)。 + + + A muted line under the title. + 標題下方一行淡化的文字。 + + + What the assistant passes to the person; shown in the handoff card from transferring on, collapsible. + 助理交給真人的內容;從轉接中起顯示在轉接卡片裡,可以收合。 + + + The header title, such as the order or ticket number; null shows "Support chat". + 標題列標題,例如訂單編號或工單編號;為 null 時顯示「Support chat」。 + + + Who is typing: the assistant shows a short shimmering Thinking line, a person shows typing dots. + 誰在輸入:助理顯示一行簡短的閃光 Thinking,真人顯示輸入中的圓點。 + + + Every word `AgentHandoff` shows or says, in English by default. Replace the whole object to localize or reword; texts that name the person are format functions of their first name. + `AgentHandoff` 顯示或朗讀的每一個字詞,預設為英文。替換整個物件即可在地化或改寫;提到那個人的文字是以其名字為參數的格式化函式。 + + + One message of an `AgentHandoff` conversation. Change `Text` in place while it streams (with `IsStreaming` set); the conversation fades each appended chunk in. + `AgentHandoff` 對話中的一則訊息。串流輸出時就地修改 `Text`(並設定 `IsStreaming`);對話會讓每次附加的片段淡入。 + + + Who wrote the message. + 訊息由誰撰寫。 + + + Whether the text is still streaming in; each appended chunk fades in while it is. + 文字是否仍在串流輸出;在此期間每次附加的片段都會淡入。 + + + The message text; it wraps. + 訊息文字;會換行。 + + + An optional time shown under the bubble, such as "14:05". + 顯示在對話泡泡下方的選用時間,例如「14:05」。 + + + The words of the support chat an `AgentHandoffSimulation` plays, in the order they come: the customer opens, the assistant answers, the customer asks for a person, the assistant hands off with a `Summary`, the customer adds a note, the person (`Agent`) joins, greets, resolves and closes. The replies at the end answer the customer's own actions (Cancel, a typed message). A line left null is skipped. Share one script between simulations; it is only read. + `AgentHandoffSimulation` 播放的客服對話台詞,依出現順序排列:客戶開口,助理回答,客戶要求找真人,助理附上 `Summary` 轉接,客戶補充一則備註,真人(`Agent`)加入、問候、解決問題並結束。最後幾則回覆用來回應客戶自己的操作(取消、輸入的訊息)。為 null 的台詞會被略過。多個模擬可以共用同一份劇本;它只會被讀取。 + + + A simulated support chat for `AgentHandoff` (Arc's `useHandoffSimulation`), for demos, prototypes and tests: it plays the `Script` you pass and exposes the stage, the messages, who is typing, the summary, the person and a notice, and takes the same actions as the view (talk to a person, cancel, try again, send). Set it as the card's `Simulation`: the card plays it while its own `Stage` is unset, from the top each time it is shown. The customer opens, the assistant streams its answers in chunks of 3 to 8 words about every 34ms, the customer asks for a person and the assistant hands off with a summary. In `Handoff` a person joins, resolves the issue and the chat, and the story starts over a few seconds later; in `Unavailable` nobody picks the transfer up and it waits for `Retry`. Reduced motion: replies appear whole and the story plays once. The card's clock runs at every motion level but only while the card is on screen, so the story pauses off screen. Use one simulation per card. + 為 `AgentHandoff` 提供的模擬客服對話(Arc 的 `useHandoffSimulation`),用於示範、原型和測試:它播放你傳入的 `Script`,公開階段、訊息、誰在輸入、摘要、接手的人與通知,並接受與畫面相同的動作(找真人、取消、重試、傳送)。把它設為卡片的 `Simulation`:卡片在自己的 `Stage` 未設定時播放它,每次顯示時從頭開始。客戶先開口,助理每隔約 34ms 以 3 到 8 個字詞的片段串流輸出回答,客戶要求找真人,助理附上摘要轉接。在 `Handoff` 中,真人加入、解決問題並結束對話,幾秒後故事重新開始;在 `Unavailable` 中,無人接手轉接,故事等待 `Retry`。減少動態效果時,回覆整段出現,故事只播放一次。卡片的時鐘在所有動態效果等級下執行,但只在卡片位於畫面上時執行,所以離開畫面時故事會暫停。每張卡片使用一個模擬。 + + + Raised when the stage, the typing state, the summary, the person or the notice changes. Messages are observable on their own. + 階段、輸入狀態、摘要、接手的人或通知變化時觸發。訊息本身可以單獨觀察。 + + + Which story to play: a handoff a person picks up, or one nobody picks up. A change starts the story over. + 要播放的故事:有人接手的轉接,或無人接手的轉接。修改會讓故事從頭開始。 + + + The words of the chat; null plays an empty chat that still moves through the stages. + 對話的台詞;為 null 時播放一段沒有訊息、但仍會依序經過各階段的對話。 + An animated explainer of the agent loop: a bright head with a gradient trail circles a ring past three stations (Gather context, Take action, Verify) around the model. At Gather a context block joins the stack beside the ring and the token total counts up; at Take action one to three tool calls drop out of the ring on a pin and resolve; at Verify a check passes or sends the loop round again. The run ends done (the ring draws in, a check at the center, the summary as the caption) or stopped when the last check fails. Play, pause, step, restart and a 0.5× / 1× / 2× speed control drive it; under reduced motion it opens on its end. The run is sample data: nothing calls a real service. 代理循環的動畫講解:一個明亮的光點帶著漸層拖尾繞環而行,依序經過三個站點(收集上下文、執行動作、驗證)環繞模型。在收集上下文時,一個上下文區塊加入環旁的堆疊,token 總數逐步增加;在執行動作時,一到三個工具呼叫經由圖釘從環中落下並完成;在驗證時,檢查通過,或讓循環再次運行。運行結束時顯示完成(環線繪製收攏,中心出現勾號,摘要作為說明)或在最後一次檢查失敗時停止。播放、暫停、單步、重新開始以及 0.5×/1×/2× 速度控制項驅動演示;在減少動態效果下直接開啟結束狀態。此運行使用範例資料,不呼叫任何真實服務。 @@ -649,12 +909,12 @@ `PauseChanged` 的引數:暫停(true)或繼續(false)。 - A step with its simulation script: what the sample draft, the suggestions and the inserted step are made of. - 帶有模擬腳本的步驟:範例草稿、建議與插入步驟的內容皆由它組成。 + A step with its simulation script: what a simulation's draft, its suggestions and the steps it inserts are made of. + 帶有模擬腳本的步驟:模擬的草稿、它的建議以及它插入的步驟皆由它組成。 - The built-in simulated agent of an `AgentPlan` that has no `Status`: it answers the plan's buttons (Approve plan, Keep planning, Pause, Resume, Retry, Skip, Start over) and plays a scripted run, one step after the other, with the beats measured on the reference. Scripts are found by step id; a step without one runs `GenericScript`. It is event-driven: exactly one timer waits per state change, nothing runs while the plan is idle, and Pause keeps the remaining time of that timer so Resume continues where the run stopped. - `AgentPlan` 的內建模擬智慧代理,適用於沒有設定 `Status` 的情況:它回應計畫的按鈕(核准計畫、繼續規劃、暫停、繼續、重試、略過、重新開始),並依照參考實作測得的節拍,逐步播放預設的執行過程。腳本透過步驟 id 查找;沒有腳本的步驟執行 `GenericScript`。它由事件驅動:每次狀態變化只等待一個計時器,計畫閒置時不執行任何內容;暫停(Pause)會保留該計時器剩餘的時間,因此繼續(Resume)會從停止處接著執行。 + A simulated agent for an `AgentPlan` that has no `Status`: give it to `Simulation` and it answers the plan's buttons (Approve plan, Keep planning, Pause, Resume, Retry, Skip, Start over) and plays a scripted run, one step after the other, with the beats measured on the reference. Your app writes the content: the draft (`Steps`), what Keep planning suggests (`Suggestions`), the goal, the model and the summary; nothing ships with the package. Scripts are found by step id; a step without one runs `GenericScript`. It is event-driven: exactly one timer waits per state change, nothing runs while the plan is idle, and Pause keeps the remaining time of that timer so Resume continues where the run stopped. The data is read when the plan attaches the simulation; create one per plan, since it keeps the run's state. + 用於未設定 `Status` 的 `AgentPlan` 的模擬智慧代理:把它交給 `Simulation`,它會回應計畫的按鈕(核准計畫、繼續規劃、暫停、繼續、重試、略過、重新開始),並依照參考實作測得的節拍,逐步播放預設的執行過程。內容由你的應用程式撰寫:草稿(`Steps`)、繼續規劃給出的建議(`Suggestions`)、目標、模型與摘要;套件不附帶任何內容。腳本透過步驟 id 查找;沒有腳本的步驟執行 `GenericScript`。它由事件驅動:每次狀態變化只等待一個計時器,計畫閒置時不執行任何內容;暫停(Pause)會保留該計時器剩餘的時間,因此繼續(Resume)會從停止處接著執行。計畫掛接模擬時讀取這些資料;每個計畫各建一個,因為它保存執行狀態。 One step of an `AgentPlan`: an id, an instruction-style title, and while the plan runs its state, the activity lines the agent reports, an optional note (a suggested or agent-inserted step), the reason of a failure and the time a finished step took. Every property is observed, so changing a step updates the plan on screen. @@ -689,12 +949,12 @@ `StepAction` 的引數:對失敗的步驟執行重試(Retry)或略過(Skip)。 - How the built-in simulation runs one step: the activity lines it reports, the time it took, and optionally a failure (after the last line) with the lines and time of the retry, and a step the agent inserts after one of the lines. - 內建模擬如何執行一個步驟:它回報的活動行與耗時;可選地在最後一行之後出現失敗,以及重試所用的行與耗時,還有智慧代理在某一行之後插入的步驟。 + How an `AgentPlanSimulation` runs one step: the activity lines it reports, the time it took, and optionally a failure (after the last line) with the lines and time of the retry, and a step the agent inserts after one of the lines. Your app writes it; nothing ships with the package. + `AgentPlanSimulation` 如何執行一個步驟:它回報的活動行與耗時;可選地在最後一行之後出現失敗,以及重試所用的行與耗時,還有智慧代理在某一行之後插入的步驟。由你的應用程式撰寫,套件不附帶任何腳本。 - A scripted agent run for `AgentRunSimulation`: the agent, its steps with their timing, the outcome and the notices the simulation announces for each action. `AgentStatusSamples` has two invented runs. Data only. - 用於 `AgentRunSimulation` 的腳本化智慧代理執行:代理、其帶時序的步驟、結果,以及模擬為每個操作宣告的通知。`AgentStatusSamples` 提供兩個虛構的執行。僅為資料。 + A scripted agent run for `AgentRunSimulation`: the agent, its steps with their timing, the outcome and the notices the simulation announces for each action. Your app writes the run; nothing ships with the package. Data only. + 用於 `AgentRunSimulation` 的腳本化智慧代理執行:代理、其帶時序的步驟、結果,以及模擬為每個操作宣告的通知。執行由你的應用程式撰寫,套件不附帶任何執行。僅為資料。 One step of an `AgentRunScenario`: its present- and past-tense labels, icon and active duration, an optional count that ticks in place, an optional approval gate before it runs and an optional failure on its first attempt. @@ -709,8 +969,8 @@ 在狀態每次變更之後引發(一個節拍、一個操作或一次重新開始)。 - A compact live status pill for a running agent (Arc Agent status, design reference): the agent's mark inside a thin live ring, the current activity with a matching icon, and an elapsed timer in tabular figures. A new activity rolls in from below while the pill's width follows on a spring, and the label fades at its right edge while it is wider than its room. Pressing the pill opens it in place into a small floating panel with the latest steps (icon, text, duration), a segmented step progress bar and Pause, Stop and Open run; Escape or a press outside closes it and returns focus to the pill. It covers idle (Ready), working, needs input (an accent dot and inline Approve and Deny), paused, done (the ring closes and the mark becomes a drawn check, then it settles back to Ready), failed (with Retry) and stopped. Drive it from your agent's events: set `Status`, keep `Activity` to the latest tool call in the present tense, add `Steps`, give a fixed `StartedAt` (the timer leaves out paused and failed time itself) and handle the action events; `Actions` chooses which buttons are offered, the counterpart of showing a control only when its handler exists. With nothing set, or with a `Simulation`, it plays an invented sample run on its own. - 執行中智慧代理的精簡即時狀態膠囊(Arc 智慧代理狀態,設計參考):代理標記位於細的即時環內,顯示目前活動及與之相符的圖示,並以表格數字顯示已用時間。新活動從下方滾入,膠囊寬度以彈簧方式跟隨變化,標籤寬於可用空間時在右側邊緣淡出。按下膠囊會就地展開為一個小型浮動面板,列出最新步驟(圖示、文字、耗時)、分段式步驟進度條,以及暫停(Pause)、停止(Stop)和開啟執行(Open run)按鈕;按下 Escape 或在外部按下即可關閉面板,並將焦點返回膠囊。它涵蓋閒置(就緒,Ready)、工作中、需要輸入(強調色圓點,並內嵌核准(Approve)與拒絕(Deny))、已暫停、已完成(環閉合,標記變為繪製的勾號,隨後回落到就緒)、失敗(帶重試(Retry))與已停止。請透過智慧代理的事件驅動它:設定 `Status`,將 `Activity` 保持為最新的工具呼叫(現在式),新增 `Steps`,給出固定的 `StartedAt`(計時器自行排除暫停與失敗的時間),並處理操作事件;`Actions` 選擇提供哪些按鈕,這相當於僅在存在處理常式時才顯示控制項。若兩者皆未設定,或設定了 `Simulation`,則它會自行播放一段虛構的範例執行。 + A compact live status pill for a running agent (Arc Agent status, design reference): the agent's mark inside a thin live ring, the current activity with a matching icon, and an elapsed timer in tabular figures. A new activity rolls in from below while the pill's width follows on a spring, and the label fades at its right edge while it is wider than its room. Pressing the pill opens it in place into a small floating panel with the latest steps (icon, text, duration), a segmented step progress bar and Pause, Stop and Open run; Escape or a press outside closes it and returns focus to the pill. It covers idle (Ready), working, needs input (an accent dot and inline Approve and Deny), paused, done (the ring closes and the mark becomes a drawn check, then it settles back to Ready), failed (with Retry) and stopped. Drive it from your agent's events: set `Status`, keep `Activity` to the latest tool call in the present tense, add `Steps`, give a fixed `StartedAt` (the timer leaves out paused and failed time itself) and handle the action events; `Actions` chooses which buttons are offered, the counterpart of showing a control only when its handler exists. Without run data of its own it plays a `Simulation` when one is set (your scripted run), and otherwise rests at Ready. + 執行中智慧代理的精簡即時狀態膠囊(Arc 智慧代理狀態,設計參考):代理標記位於細的即時環內,顯示目前活動及與之相符的圖示,並以表格數字顯示已用時間。新活動從下方滾入,膠囊寬度以彈簧方式跟隨變化,標籤寬於可用空間時在右側邊緣淡出。按下膠囊會就地展開為一個小型浮動面板,列出最新步驟(圖示、文字、耗時)、分段式步驟進度條,以及暫停(Pause)、停止(Stop)和開啟執行(Open run)按鈕;按下 Escape 或在外部按下即可關閉面板,並將焦點返回膠囊。它涵蓋閒置(就緒,Ready)、工作中、需要輸入(強調色圓點,並內嵌核准(Approve)與拒絕(Deny))、已暫停、已完成(環閉合,標記變為繪製的勾號,隨後回落到就緒)、失敗(帶重試(Retry))與已停止。請透過智慧代理的事件驅動它:設定 `Status`,將 `Activity` 保持為最新的工具呼叫(現在式),新增 `Steps`,給出固定的 `StartedAt`(計時器自行排除暫停與失敗的時間),並處理操作事件;`Actions` 選擇提供哪些按鈕,這相當於僅在存在處理常式時才顯示控制項。沒有自己的執行資料時,若設定了 `Simulation`(你撰寫的腳本化執行)便播放它,否則停在就緒狀態。 Which action buttons may show (all by default); a button also needs the right state. @@ -721,8 +981,8 @@ 智慧代理目前正在做什麼,使用現在式。需要輸入時為請求內容("Needs your approval to run a command");完成時為結果,接在 "Done in …" 之後;失敗後為失敗原因。 - The agent and its mark (drawn at 12px inside the ring); null is a plain "Agent" with a default mark. - 智慧代理及其標記(在環內以 12px 繪製);為 null 時顯示一般的 "Agent",並使用預設標記。 + The agent and its mark (drawn at 12px inside the ring); null takes the simulated run's agent, or a plain "Agent" with a default mark. + 智慧代理及其標記(在環內以 12px 繪製);為 null 時使用模擬執行的代理,否則顯示一般的 "Agent",並使用預設標記。 Raised when Approve is pressed. @@ -785,16 +1045,16 @@ 執行完成或停止後回到就緒狀態時觸發。 - A simulated run that drives the pill (its status, activity, steps and times; its buttons are routed back to it). With no data set at all the pill plays the first sample run on its own. - 模擬執行,用來驅動狀態膠囊(其狀態、活動、步驟與時間;其按鈕會轉回給它)。若完全未設定資料,膠囊會自行播放第一個範例執行。 + A simulated run that drives the pill while the app sets no `Status`, `Activity`, `StartedAt` or steps: the pill starts it once attached, shows its status, activity, steps and times, and routes its buttons back to it. Null (the default) leaves a pill without data at Ready. Give each pill its own. + 模擬執行,在應用程式未設定 `Status`、`Activity`、`StartedAt` 或步驟時驅動膠囊:膠囊掛載後啟動它,顯示其狀態、活動、步驟與時間,並將按鈕轉回給它。為 null(預設)時,沒有資料的膠囊停在就緒狀態。每個膠囊各用一個。 When the run started: the timer counts from here and leaves out time spent paused or failed. 執行開始的時間:計時器從此處開始計時,並且不計入暫停或失敗期間所花的時間。 - Where the run is. Ignored while a `Simulation` drives the pill. - 執行所處的狀態。在 `Simulation` 驅動膠囊時會被忽略。 + Where the run is. Setting it (or `Activity`, `StartedAt` or a step) shows your data instead of a `Simulation`. + 執行所處的狀態。設定它(或 `Activity`、`StartedAt`、某個步驟)後,膠囊顯示你的資料,而不是 `Simulation`。 The steps list: every step of the run, pending ones included, so the progress bar knows the total. @@ -824,10 +1084,6 @@ Every word an `AgentStatus` shows or announces, in English by default. Replace the instance (or set its properties) to translate or reword; texts with numbers or names are functions so that the word order can change. The durations they receive are already formatted by `FormatTimer`, `FormatTotal` and the other format functions. `AgentStatus` 顯示或播報的每一個文字,預設為英文。替換此實例(或設定其屬性)即可翻譯或改寫;含數字或名稱的文字為函式,以便調整詞序。它們接收的時長已由 `FormatTimer`、`FormatTotal` 等格式化函式處理完畢。 - - Two invented runs for `AgentRunSimulation` (the agents, file names and texts are made up). They keep the reference's rhythm: the coding run takes six steps of 1.9, 1.5, 2.5, 1.9 and 3.3 seconds (the fifth counting 12 specs in place) and then waits for an approval for as long as it takes; the research run takes five steps, and its third fails once after five of nine pages so Retry can be seen. - 為 `AgentRunSimulation` 準備的兩個虛構執行(智慧代理、檔案名稱與文字均為虛構)。它們保留了參考設計的節奏:編碼執行共六個步驟,耗時分別為 1.9、1.5、2.5、1.9 和 3.3 秒(第五步原地計數 12 個規格),之後等待核准,等待多久都可以;研究執行共五個步驟,其中第三步在處理完九頁中的第五頁後失敗一次,以便看到重試。 - One step of an agent's run in an `AgentStatus`. List every step, pending ones included, so the progress bar knows the total; the panel lists the latest finished and running ones. Write a running step's label in the present tense and a finished one's in the past tense ("Running tests", then "Passed 12 of 12 tests"), so the words carry the state. Changes reach the screen at once (each property is observed). `AgentStatus` 中智慧代理執行的一個步驟。應列出每一個步驟(含尚未開始的步驟),讓進度條得知總數;面板會列出最近完成的和正在進行的步驟。正在進行步驟的標籤使用現在式,已完成步驟的標籤使用過去式("Running tests",隨後為 "Passed 12 of 12 tests"),讓文字本身表達狀態。變更會立即反映到畫面上(每個屬性都會被監聽)。 @@ -1492,6 +1748,258 @@ The name without the slash; the person filters by its start, ignoring case. 不含斜線的名稱;人員依開頭字元篩選,不區分大小寫。 + + A Cmd+K command palette with an AI mode (Arc AI command, design reference). Search pages and actions, or press Tab to ask a question: the answer streams inside the palette with numbered sources, and the next steps it suggests run straight from the list. The palette never changes size: the results area keeps `ResultsHeight` while typing, switching modes and streaming, and its views crossfade in place. Give it pages and actions in `Items` (or `ItemsSource`) and answer questions with `Responder`: stream text with `Append`, land the sources early with `SetSources`, and resolve with the suggested actions. With no data it is an empty palette; a `Simulation` you pass in fills it with a workspace and a simulated model, and plays a looping walkthrough while it is on screen until someone uses it. Focus stays in the input: Up and Down move the highlight, Enter opens, asks, runs or retries, Tab asks the model about the query (or opens Ask AI with suggested questions, and goes back from there), and Escape goes back to search, clears the query, then raises `CloseRequested`. Put it in an `AiCommandPaletteDialog` to open it over the page with Cmd+K or Ctrl+K. + 具備 AI 模式的 Cmd+K 命令面板(Arc AI command,設計參考)。搜尋頁面與動作,或按 Tab 提問:回答在面板內串流呈現並附有編號來源,模型建議的後續步驟可直接在清單中執行。面板的尺寸從不改變:在輸入、切換模式和串流輸出時,結果區域始終維持 `ResultsHeight`,各檢視就地交叉淡化。透過 `Items`(或 `ItemsSource`)提供頁面與動作,並用 `Responder` 回答問題:用 `Append` 串流輸出文字,用 `SetSources` 提早放出來源,最後傳回建議的動作。沒有資料時它是一個空的面板;你傳入的 `Simulation` 會為它補上工作區和模擬模型,並在畫面上可見時循環播放示範,直到有人開始使用。焦點始終留在輸入框中:Up 和 Down 移動反白,Enter 開啟、提問、執行或重試,Tab 針對查詢詢問模型(查詢為空時開啟附有建議問題的 Ask AI,並可從那裡返回),Escape 返回搜尋、清除查詢,然後引發 `CloseRequested`。把它放進 `AiCommandPaletteDialog`,即可用 Cmd+K 或 Ctrl+K 在頁面上方開啟。 + + + Raised after a suggested action finishes, with the label it ended on ("Created ENG-2481"). Bubbles. + 建議的動作完成後引發,附帶它最後顯示的標籤("Created ENG-2481")。會向上傳遞。 + + + Plays a looping walkthrough while on screen. It stops for good on the first real pointer or key press inside, pauses off screen and starts over from a clean state, and never runs with reduced motion. Null plays it only when the palette has a `Simulation` and neither items nor a responder of its own. + 畫面上可見時循環播放示範。在面板內第一次真正的指標或按鍵操作後永久停止,不可見時暫停並從乾淨的狀態重新開始,減少動態效果時從不播放。為 null 時,只在面板有 `Simulation` 且本身既沒有項目也沒有回應器時播放。 + + + Raised by Escape in an empty search: close the surrounding dialog here. Bubbles. + 在空白搜尋中按 Escape 時引發:在這裡關閉外層對話方塊。會向上傳遞。 + + + Adds the floating shadow, for use inside a dialog or a popover. + 加上浮層陰影,用於對話方塊或彈出框中。 + + + Listens for Cmd+K or Ctrl+K in the window and focuses the input. + 在視窗中監聽 Cmd+K 或 Ctrl+K,並聚焦輸入框。 + + + Raised when a page or action is chosen with Enter or a click. Close your dialog or navigate here. Bubbles. + 用 Enter 或點按選取頁面或動作時引發。在這裡關閉對話方塊或進行導覽。會向上傳遞。 + + + The pages and actions to search; when set it takes the place of `Items`. + 要搜尋的頁面與動作;設定後取代 `Items`。 + + + Every word the palette shows or says. + 面板顯示或朗讀的所有文字。 + + + Search or Ask AI (two-way). Switching back to search cancels a streaming answer. + 搜尋或 Ask AI(雙向)。切回搜尋會取消串流中的回答。 + + + Raised when the palette switches between Search and Ask AI. Bubbles. + 面板在搜尋與 Ask AI 之間切換時引發。會向上傳遞。 + + + The model's mark (a 24 × 24 stroked glyph) in the footer, the input and beside each answer; the sparkle by default. + 模型的標記(24 × 24 描邊圖形),顯示在頁尾、輸入框和每則回答旁;預設為閃光圖示。 + + + The model that answers, shown in the footer and beside each answer; null (the default) shows the mark alone. + 回答問題的模型,顯示在頁尾和每則回答旁;為 null(預設)時只顯示標誌。 + + + The text in the input (two-way): the search query, or the question being typed in Ask AI. + 輸入框中的文字(雙向):搜尋查詢,或在 Ask AI 中正在輸入的問題。 + + + Answers a question: stream text with `Append`, show sources early with `SetSources`, and resolve with the actions (and the text, if it was not streamed). Throw to show the failed state with a retry row. Null uses the `Simulation`'s model; with neither, a question gets no answer. + 回答問題:用 `Append` 串流輸出文字,用 `SetSources` 提早顯示來源,最後傳回動作(如果沒有串流輸出,還要傳回文字)。擲回例外狀況會顯示附有重試列的失敗狀態。為 null 時使用 `Simulation` 的模型;兩者都沒有時,提問不會得到回答。 + + + The height of the results area in px; the palette keeps this size while typing, switching modes and streaming. + 結果區域的高度(px);在輸入、切換模式和串流輸出時,面板都維持這個尺寸。 + + + A simulated workspace (demos and prototypes): its items, questions and model fill in what the palette lacks, and with no items and no responder of its own the palette plays its walkthrough. Null leaves the palette to its own data. + 模擬工作區(示範與原型):它的項目、問題和模型會補上面板缺少的部分;面板本身既沒有項目也沒有回應器時,會播放它的示範。為 null 時面板只使用自己的資料。 + + + Raised when a source chip or a citation is chosen. Bubbles. Unless handled, a source with an address opens it with the system launcher. + 選取來源標籤或引用時引發。會向上傳遞。除非已處理,帶有網址的來源會用系統啟動器開啟。 + + + The questions offered while Ask AI is open and empty; null offers the `Simulation`'s, if any. + Ask AI 開啟且為空時提供的問題;為 null 時提供 `Simulation` 的問題(如果有)。 + + + A suggested next step returned with an answer and run in place: a row with a mark, a label and quiet meta text that turns into a spinner while `Run` works, then a check and the done label (or an alert, and Enter retries). + 隨回答一起傳回並就地執行的建議下一步:一列帶有標記、標籤和低調附註文字,在 `Run` 執行時變成載入圈,接著顯示勾號和完成標籤(或顯示警示圖示,按 Enter 重試)。 + + + The label once done when `Run` returns nothing; null uses `Done`. + `Run` 沒有傳回任何內容時完成後的標籤;為 null 時使用 `Done`。 + + + The meta text once done; a simulated action says here that nothing was sent. + 完成後的附註文字;模擬的動作會在這裡說明沒有送出任何內容。 + + + The label after a failure; null uses `ActionFailed`. + 失敗後的標籤;為 null 時使用 `ActionFailed`。 + + + A stable id. + 穩定的 ID。 + + + The row label ("Create issue in Linear"). + 列標籤("Create issue in Linear")。 + + + The 16px mark of the tool it runs in, a 24 × 24 stroked glyph; null shows an arrow. + 執行該動作之工具的 16px 標記,24 × 24 描邊圖形;為 null 時顯示箭頭。 + + + Quiet detail after the label ("ENG · Billing"). + 標籤後面的低調細節("ENG · Billing")。 + + + Does the work. Resolve with a done label ("Created ENG-2481") or null; throw to show the failed state (Enter retries). The token is cancelled when the person asks again, goes back to search or the palette leaves the window. + 執行工作。傳回完成標籤("Created ENG-2481")或 null;擲回例外狀況會顯示失敗狀態(Enter 重試)。當使用者再次提問、返回搜尋或面板離開視窗時,權杖會被取消。 + + + The label while it runs; null uses `Running`. + 執行期間的標籤;為 null 時使用 `Running`。 + + + What a `Responder` resolves with: the whole text (when it did not stream it with `Append`), the sources (when it did not set them early) and the suggested actions. + `Responder` 傳回的內容:完整文字(未用 `Append` 串流輸出時)、來源(未提早設定時)以及建議的動作。 + + + The question a `Responder` answers, with a token that is cancelled when the person asks again or goes back, and the two ways to fill the answer while it works: `Append` streams text, `SetSources` lands the source chips before the text. + `Responder` 要回答的問題,附帶一個在使用者再次提問或返回時取消的權杖,以及工作期間填入回答的兩種方式:`Append` 串流輸出文字,`SetSources` 讓來源標籤先於文字出現。 + + + An `AiCommandPalette` as a dialog over the page: declare it once and Cmd+K (Ctrl+K elsewhere) opens it near the top of the window over a dimmed scrim, or bind `IsOpen`. It renders nothing in place. The palette gets the floating shadow and the focus when it opens. Escape in an empty search, a press on the scrim and choosing a page or action (`CloseOnSelect`) close it, and the focus returns to what had it before. Without a palette of its own it shows the sample one. + 以對話方塊形式顯示在頁面上方的 `AiCommandPalette`:宣告一次後,Cmd+K(其他平台為 Ctrl+K)會在視窗頂端附近、調暗的遮罩上方開啟它,也可以繫結 `IsOpen`。它在原位不轉譯任何內容。面板開啟時會取得浮層陰影和焦點。在空白搜尋中按 Escape、點按遮罩以及選取頁面或動作(`CloseOnSelect`)都會關閉它,焦點會回到先前擁有焦點的元素。沒有自己的面板時會顯示範例面板。 + + + Choosing a page or action closes the dialog (default true). + 選取頁面或動作會關閉對話方塊(預設為 true)。 + + + Shown while true; set false (or press Escape in an empty search) to close it. Two-way by default. + 為 true 時顯示;設為 false(或在空白搜尋中按 Escape)即可關閉。預設為雙向。 + + + Turns the window-level open gesture on (default) or off. + 開啟(預設)或關閉視窗層級的開啟手勢。 + + + The key combination that toggles the dialog from anywhere in the window; null is Cmd+K or Ctrl+K. + 在視窗任何位置切換對話方塊的組合鍵;為 null 時為 Cmd+K 或 Ctrl+K。 + + + Set on a button: clicking it opens that dialog. + 設定在按鈕上:點按按鈕會開啟該對話方塊。 + + + The palette to show; null shows a sample palette. + 要顯示的面板;為 null 時顯示範例面板。 + + + A page or an action an `AiCommandPalette` searches: a label, an optional group, a 16px icon, quiet meta text, a shortcut and extra keywords. Rows marked `IsSuggested` show while the query is empty. + `AiCommandPalette` 搜尋的頁面或動作:一個標籤、選用的分組、16px 圖示、低調的附註文字、快速鍵和額外的關鍵字。標記為 `IsSuggested` 的列會在查詢為空時顯示。 + + + Runs when the row is chosen, with `CommandParameter` (or the item when that is null). + 選取該列時執行,參數為 `CommandParameter`(為 null 時為該項目本身)。 + + + The parameter passed to `Command`. + 傳給 `Command` 的參數。 + + + The section the row is listed under ("Pages", "Actions"); rows without one share an untitled section. + 該列所在的分區("Pages"、"Actions");沒有分組的列共用一個無標題分區。 + + + The 16px icon, a 24 × 24 stroked glyph (`ArcIcons`); null shows a neutral dot. + 16px 圖示,24 × 24 描邊圖形(`ArcIcons`);為 null 時顯示一個中性圓點。 + + + A stable id (a route, a command name). + 穩定的 ID(路由、命令名稱)。 + + + Lists the row while the query is empty. + 查詢為空時列出該列。 + + + More words that find the row, separated by spaces or commas ("invoice payment card"). + 能找到該列的更多字詞,以空格或逗號分隔("invoice payment card")。 + + + The row label, kept short ("Billing"). + 列標籤,保持簡短("Billing")。 + + + Quiet detail after the label ("Settings", "12 unread"); it truncates before the label does. + 標籤後面的低調細節("Settings"、"12 unread");它會比標籤先被截斷。 + + + A shortcut shown as key caps at the end of the row, keys separated by spaces ("G I"). + 在列尾以鍵帽顯示的快速鍵,按鍵以空格分隔("G I")。 + + + Every word `AiCommandPalette` shows or says, in English by default. Replace the whole object to localize or reword; the texts that carry a value are format functions. + `AiCommandPalette` 顯示或朗讀的所有文字,預設為英文。取代整個物件即可在地化或改寫;帶有數值的文字是格式化函式。 + + + A simulated workspace for an `AiCommandPalette`, for demos, prototypes and tests: the pages and actions to search (`Items`), the questions offered in Ask AI (`Questions`), and a simulated model that answers from `Answers`. It "searches" for 700ms, lands the sources, waits 250ms and streams the text in small uneven pieces every 45ms, then offers the actions, which pretend to run and say "Simulated, nothing was sent". Nothing talks to any service. Set it as the palette's `Simulation`. Each part fills in only what the palette lacks: `Items` while the palette has no items, `Questions` while it has no suggested questions, and the answers while it has no responder. While the palette has neither items nor a responder of its own it also plays the walkthrough (unless `AutoPlay` says otherwise): it searches for `SearchQuery`, opens Ask AI, asks the first question, runs its first action and starts over. + 為 `AiCommandPalette` 提供的模擬工作區,用於示範、原型和測試:要搜尋的頁面和動作(`Items`)、Ask AI 中提供的問題(`Questions`),以及依據 `Answers` 作答的模擬模型。它「搜尋」700ms,放出來源,等待 250ms,再每 45ms 以大小不一的小段串流輸出文字,然後提供動作;這些動作只是假裝執行,並顯示「Simulated, nothing was sent」。不會連接任何服務。把它設為面板的 `Simulation`。每一部分只補上面板缺少的內容:面板沒有項目時用 `Items`,沒有建議問題時用 `Questions`,沒有回應器時用其中的回答。面板本身既沒有項目也沒有回應器時,它還會播放示範(除非 `AutoPlay` 另有設定):搜尋 `SearchQuery`,開啟 Ask AI,提出第一個問題,執行其第一個動作,然後從頭開始。 + + + The canned answers the simulated model picks from. + 模擬模型從中挑選的預設回答。 + + + The pages and actions to search. + 要搜尋的頁面和動作。 + + + The questions offered while Ask AI is open and empty; the walkthrough asks the first. + Ask AI 開啟且為空時提供的問題;示範會提出第一個。 + + + What the walkthrough types into the search before it looks through the results; null skips the search. + 示範在瀏覽結果之前輸入到搜尋中的內容;為 null 時略過搜尋。 + + + An action an `AiCommandPaletteSimulationAnswer` offers: pressing it pretends to run for 1.1s, then reports `Result` with "Simulated, nothing was sent". + `AiCommandPaletteSimulationAnswer` 提供的動作:按下後假裝執行 1.1 秒,然後回報 `Result`,並附上「Simulated, nothing was sent」。 + + + A canned answer of an `AiCommandPaletteSimulation`: the question words it answers, its sources, its text with [n] citations and its actions. + `AiCommandPaletteSimulation` 的一則預設回答:它回答的問題字詞、來源、帶 [n] 引用的文字以及動作。 + + + A document an answer cites as [1], [2] in its text, shown as a numbered chip above the answer. + 回答在文字中以 [1]、[2] 引用的文件,以編號標籤顯示在回答上方。 + + + Longer text for the tooltip and the accessible name of the chip and its citations. + 用於工具提示以及標籤和其引用之無障礙名稱的較長文字。 + + + An address opened with the system launcher when the source is chosen (unless the event is handled). + 選取該來源時用系統啟動器開啟的網址(除非事件已被處理)。 + + + A stable id. + 穩定的 ID。 + + + The 14px mark in the chip, a 24 × 24 stroked glyph; null shows a document glyph. + 標籤中的 14px 標記,24 × 24 描邊圖形;為 null 時顯示文件圖形。 + + + The short chip title ("billing-api #1893"). + 簡短的標籤標題("billing-api #1893")。 + The prompt box of an AI app: a text area that grows with its text, "/" commands and "@" mentions that become tokens, attachments that upload in place (picked, pasted or dropped on the box), tool toggles, a model and effort menu, a character counter and a send button that turns into a stop button while a reply is generated. The composer reports a send through `Sent` (or `SendCommand`); your code starts the reply and sets `IsGenerating` while it runs, and the send button becomes a stop button that raises `StopRequested`. While a reply is generated the box stays editable but Enter does not send. AI 應用程式的提示輸入框:一個會隨文字增高的文字區域,"/" 命令與 "@" 提及會變為權杖,附件可就地上傳(選取、貼上或拖放到輸入框上),並提供工具切換、模型與推理強度選單、字元計數器,以及在產生回覆時變為停止按鈕的傳送按鈕。輸入框透過 `Sent`(或 `SendCommand`)回報傳送;你的程式碼負責開始回覆,並在執行期間設定 `IsGenerating`,此時傳送按鈕會變為停止按鈕,並引發 `StopRequested`。產生回覆期間輸入框仍可編輯,但按 Enter 不會傳送。 @@ -1688,6 +2196,86 @@ The name the token shows; the person filters by it (and by `Hint`), ignoring case. 權杖上顯示的名稱;人員依此(以及 `Hint`)篩選,不區分大小寫。 + + A small assistant character that shows what an AI is doing (Arc AI mascot, design reference): a soft pebble with pill eyes, a quiet form shade and a companion dot. Idle breathes, floats, blinks and glances around. Listening leans in with wider eyes while the dot pulses like a voice level. Thinking tilts the head and looks up as the dot orbits it, passing behind and in front. Writing looks down and reads along a line the dot writes, then the pen lifts and returns. Done squints happily as the dot turns into a sparkle. Error shakes once and shows worried brows. A status change blends from the pose on screen over about 420ms, and the eyes follow the pointer on hover. Set `Status` from your assistant's phases; leave it unset to play through every state on a loop (a showcase). The mascot is a fixed square of `Size` px, from 24 (inline) to 160 (empty state); below 56 a compact drawing takes over. It pauses by itself off screen, and reduced motion keeps a still pose for each state. + 一個呈現 AI 正在做什麼的小助理角色(Arc AI mascot,設計參考):一顆柔和的鵝卵石,有膠囊形的眼睛、一道安靜的形體陰影和一個伴隨圓點。閒置時呼吸、浮動、眨眼並四處張望。聆聽時往前靠近、眼睛睜大,圓點像音量一樣跳動。思考時歪頭往上看,圓點繞頭旋轉,從後方和前方經過。書寫時往下看,跟著圓點寫下的線讀,然後提筆返回。完成時開心地瞇眼,圓點變成閃光。出錯時搖晃一次並露出擔憂的眉毛。狀態切換在約 420ms 內從螢幕上的姿勢混合過去,懸停時眼睛跟隨指標。依據助理的階段設定 `Status`;不設定時循環播放所有狀態(用於展示)。吉祥物是邊長為 `Size` px 的固定正方形,從 24(行內)到 160(空白狀態);小於 56 時改用精簡畫法。離開螢幕時自行暫停,減少動態效果時每個狀態保持靜止姿勢。 + + + The accessible label in use, for example "Assistant, thinking". + 目前使用的無障礙標籤,例如「Assistant, thinking」。 + + + Announces status changes in a polite live region. Turn it on for the one mascot that carries status, not for decorative copies. + 在禮貌型即時區域中朗讀狀態變化。只為承載狀態的那一個吉祥物開啟,不要為裝飾性的副本開啟。 + + + The assistant's name in the accessible label ("Assistant, thinking"); `DefaultName` when null. + 無障礙標籤中的助理名稱(「Assistant, thinking」);為 null 時使用 `DefaultName`。 + + + Any color for the body. Overrides `Tone`; the eyes become a dark mix of it unless `EyeColor` is set. + 身體的任意顏色。覆寫 `Tone`;除非設定了 `EyeColor`,眼睛會變成它的深色混合。 + + + The state shown now: `Status`, or the self-playing tour's state while it is null. + 目前顯示的狀態:`Status`,或在它為 null 時自動輪播所處的狀態。 + + + Any color for the companion dot and the sparkle; the body color (the foreground for the soft tone) by default. + 伴隨圓點和閃光的任意顏色;預設為身體顏色(`Soft` 色調為前景色)。 + + + Any color for the eyes, the squint and the brows. + 眼睛、瞇眼和眉毛的任意顏色。 + + + When the eyes follow the pointer: over the mascot, anywhere in the window or never. Touch is ignored; thinking and writing keep their own gaze. + 眼睛何時跟隨指標:在吉祥物上方時、在視窗內任何位置或從不。忽略觸控;思考和書寫保持自己的視線。 + + + Freezes the mascot on the current frame. It also pauses by itself off screen and while hidden, and resumes without a jump. + 讓吉祥物停在目前影格。它在離開螢幕或被隱藏時也會自行暫停,恢復時不會跳動。 + + + Overrides the whole accessible label. + 取代整個無障礙標籤。 + + + Every word the mascot says. + 吉祥物使用的所有文字。 + + + Shows the companion dot that floats, pulses, orbits, writes and becomes the done sparkle. + 顯示伴隨圓點:它會漂浮、跳動、繞頭旋轉、書寫,並變成完成時的閃光。 + + + Shows the soft contact shadow and the writing line under the mascot (drawn only at 56px and up). + 顯示吉祥物下方柔和的接觸陰影和書寫線(只在 56px 以上繪製)。 + + + Width and height in px, from 24 (inline next to a message) to 160 (empty state). Below 56 the compact drawing takes over. + 以 px 為單位的寬和高,從 24(訊息旁的行內)到 160(空白狀態)。小於 56 時改用精簡畫法。 + + + What the assistant is doing. Leave it null to play through every state on a loop (a showcase). + 助理正在做什麼。保持為 null 時循環播放所有狀態(用於展示)。 + + + Raised when the self-playing tour (`Status` null) moves to the next state. Bubbles. Use it to sync a caption or a control. + 自動輪播(`Status` 為 null)進入下一個狀態時引發。會向上傳遞。可用於同步說明文字或控制項。 + + + Where the body color comes from: the accent, the foreground ink or a muted surface with a border. + 身體顏色的來源:強調色、前景墨色,或帶框線的柔和表面色。 + + + Every word an `AiMascot` says, in English by default. Replace the instance (or set its properties) to translate or reword; the accessible label is a function so that the word order can change. + `AiMascot` 使用的所有文字,預設為英文。取代執行個體(或設定其屬性)即可翻譯或改寫;無障礙標籤是一個函式,以便調整語序。 + + + Arguments of `StatusChanged`: the state the self-playing tour moved to and the one it left. + `StatusChanged` 的引數:自動輪播進入的狀態和它離開的狀態。 + A model a person can choose in the AI blocks: the model picker, the model pill and menu in a composer, a chat header or a prompt studio. Data only: an `AvaloniaObject` so the properties bind and a change (a new description, a price update) reaches what is on screen. AI 區塊中人可以選擇的模型:模型選擇器、撰寫區、聊天標題或提示工作室中的模型膠囊與選單。僅為資料:它是一個 `AvaloniaObject`,因此屬性可以繫結,其變化(新的描述、價格更新)會反映到畫面上。 @@ -2008,6 +2596,282 @@ Where the source can be opened; null makes the citation plain. 來源可以開啟的位置;為 null 時引用僅為純文字。 + + An animated spark mark that shows an AI at work (Arc AI spark): four tapered primary rays and four lighter diagonal rays around a soft core. Each status is its own motion: a slow breath at rest, a ripple while thinking, a heartbeat while writing, a gather when done and a small shake on an error, and every change blends from the pose the mark is already in, so states never hard cut. Set `Variant` to Pill for a status label or to Avatar for a round tile. Leave `Status` unset and the spark plays through idle, thinking, writing and done on its own (for demos). + 表示 AI 正在工作的動畫火花標記(Arc AI spark):四道漸細的主光線和四道較淺的斜向光線圍繞柔和的核心。每種狀態都有自己的動態效果:靜止時緩慢呼吸,思考時漣漪流轉,書寫時像心跳一樣搏動,完成時向中心收攏,出錯時輕微搖晃;每次切換都從標記目前所處的姿態過渡,狀態之間從不生硬切換。把 `Variant` 設為 Pill 可加上狀態標籤,設為 Avatar 則放進圓形底塊。不設定 `Status` 時,火花會自行依序播放閒置、思考、書寫和完成(用於示範)。 + + + The accessible name; null (default) uses a phrase for the status ("AI is thinking"), or the label in the pill. + 無障礙名稱;為 null(預設)時使用狀態短語("AI is thinking"),膠囊中則使用標籤文字。 + + + Announces status changes as a polite live region. Turn it off for decorative copies so only one spark speaks; they then present as an image with an accessible name. + 以禮貌型即時區域播報狀態變化。在裝飾性副本上關閉它,只讓一個火花發聲;關閉後它們以帶無障礙名稱的影像呈現。 + + + The status the spark shows: `Status`, or the current self-play step while it is unset. + 火花正在顯示的狀態:`Status`,未設定時為目前自行播放的那一步。 + + + Freezes the animation in place; resuming continues from the same phase. + 讓動畫原地定格;恢復後從同一相位繼續。 + + + The pill's text, overriding the built-in label for the current status ("Reading 12 files"). + 膠囊的文字,覆寫目前狀態的內建標籤("Reading 12 files")。 + + + Every word the spark shows or announces. + 火花顯示或播報的所有文字。 + + + A color for the mark that overrides `Tone` (a solid brush; a gradient uses its first stop). + 覆寫 `Tone` 的標記顏色(純色筆刷;漸層筆刷取其第一個色標)。 + + + The size in px of the mark, of the avatar tile, or of the mark inside the pill; NaN (default) gives 24, 32 for the avatar and 16 in the pill. Set it to the surrounding font size to drop the mark into a line of text. Small marks get heavier rays and under 18px the diagonals become dots. + 標記、頭像底塊或膠囊內標記的尺寸(px);NaN(預設)時為 24,頭像為 32,膠囊內為 16。設為周圍文字的字級即可把標記嵌入一行文字。小標記的光線更粗,小於 18px 時斜線變成圓點。 + + + Multiplies every loop and spin speed (1 is the designed pace). + 讓所有循環與旋轉速度成倍變化(1 為設計速度)。 + + + What the AI is doing. Leave it unset (null) and the spark plays through idle, thinking, generating and done on its own, which is meant for demos and marketing pages. + AI 正在做什麼。不設定(null)時,火花會自行依序播放閒置、思考、書寫和完成,這只用於示範和行銷頁面。 + + + The semantic color of the mark; the diagonals and the core use a lighter tone of it. An error always eases to a muted danger tone. + 標記的語意顏色;斜線和核心使用其較淺色調。出錯時一律過渡到柔和的危險色。 + + + A very slow breathing twinkle while idle. Turn it off for a fully still idle mark. + 閒置時非常緩慢的呼吸閃爍。關閉後閒置標記完全靜止。 + + + The spark alone, with a status label in a capsule, or in a round tinted tile. + 單獨的火花、帶狀態標籤的膠囊,或放在圓形著色底塊中。 + + + Every word an `AiSpark` shows or announces, in English by default: the pill's status labels and the spoken phrases the mark uses as its accessible name. Replace the instance (or set its properties) to translate or reword. + `AiSpark` 顯示或播報的所有文字,預設為英文:膠囊的狀態標籤,以及標記用作無障礙名稱的播報短語。取代執行個體(或設定其屬性)即可翻譯或改寫。 + + + An inline translator for one message or paragraph (Arc AI translate, design reference). A byline with the author and a detected-language chip sits above the text; below it a Translate picker, a Show original toggle and a status line led by the model's mark. Picking a language dims the text on screen and streams the translation in place in fast chunks, each new sentence retiring one sentence of the old text, while the paragraph's height springs to fit. Finished translations are cached per text, so Show original and switching back crossfade at once. A failure puts the previous text back and shows Couldn't translate with Retry. Wire `Translator` to your model: it gets an `AiTranslateRequest` and returns the translation as an async stream of text deltas (yield the whole text once to have the card stream it in chunks itself). Without one the card simulates the stream from `Translations`. Without `Text` it plays a `Simulation` (a message and its translations you pass, toured by itself); with neither it shows an empty card whose picker is off. + 針對一則訊息或一段文字的內嵌翻譯器(Arc AI translate,設計參考)。文字上方是含作者和偵測到的語言標籤的署名列;下方是 Translate 選擇器、Show original 開關,以及以模型標誌開頭的狀態列。選擇一種語言後,畫面上的文字變暗,譯文以快速的小塊在原處串流出現,每開始一個新句子就撤下一句舊文字,同時內文高度以彈簧動畫配合。完成的譯文依文字快取,因此「顯示原文」和切換回來時會立即交叉淡化。失敗時還原先前的文字,並顯示 Couldn't translate 和 Retry。把 `Translator` 接到你的模型:它會收到一個 `AiTranslateRequest`,並以文字增量的非同步串流傳回譯文(一次 yield 整段文字時,卡片會自行分塊串流輸出)。沒有它時,卡片從 `Translations` 模擬串流輸出。沒有 `Text` 時它播放 `Simulation`(你傳入的訊息及其譯文,自動輪播);兩者都沒有時顯示一張空卡片,其中的選擇器處於關閉狀態。 + + + Who wrote the text, shown with an avatar. Null hides the byline (the language chip stays), or shows the `Simulation`'s author while one plays. + 文字的作者,搭配頭像顯示。為 null 時隱藏署名列(語言標籤仍保留),播放 `Simulation` 時則顯示其作者。 + + + Tours the languages (the `Simulation`'s `Tour`, or every other language in the picker), then the original, and loops. It pauses off screen and stops for good on the first pointer or key press inside the card; with reduced motion it translates once and stops. Null tours only while a `Simulation` plays. + 依序切換語言(`Simulation` 的 `Tour`,或選擇器中的其他每種語言),再回到原文,並循環播放。離開畫面時暫停,在卡片內第一次按下指標或按鍵後永久停止;減少動態效果時只翻譯一次就停止。為 null 時只在播放 `Simulation` 時輪播。 + + + The language of the text: `SourceLanguage`, or the one detected from the text. + 文字的語言:`SourceLanguage`,或從文字中偵測到的語言。 + + + Adds "Simulated, nothing was sent" to the announcements. Null: true without a `Translator` or while a `Simulation` plays. + 在朗讀中加上「Simulated, nothing was sent」。為 null 時:沒有 `Translator` 或正在播放 `Simulation` 時為 true。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每個字詞。 + + + The languages offered in the picker, in order; null offers all six. The source language is always listed, as Original. + 選擇器中依序提供的語言;為 null 時提供全部六種。來源語言一定會列出,標為 Original。 + + + The model's mark (a 24 × 24 stroked glyph) that leads every status; a sparkle by default. Pass your provider's mark. + 每個狀態開頭的模型標誌(24 × 24 描邊圖形);預設是火花圖示。請傳入你的服務供應商的標誌。 + + + The model credited in the status line: its name is the idle label. Null credits the `Simulation`'s model while one plays, or none. + 狀態列中標示的模型:其名稱是閒置時的標籤。為 null 時,播放 `Simulation` 期間標示其模型,否則不標示。 + + + A message to play while the card has no `Text`: its text, byline, model and ready translations, streamed as a simulation (nothing is sent). Your own `Text` wins over it; null stops it and shows the empty card. + 卡片沒有 `Text` 時要播放的訊息:它的文字、署名、模型和現成譯文,以模擬方式串流輸出(不會送出任何內容)。你自己的 `Text` 優先於它;為 null 時停止播放並顯示空卡片。 + + + The language of `Text`. Null detects it from the text with a light check and the chip reads Detected; set, the chip reads Written in. + `Text` 的語言。為 null 時透過簡單檢查從文字中偵測,標籤顯示 Detected;設定後標籤顯示 Written in。 + + + What the card shows: the original, a translation streaming in, a translation, or a failure. + 卡片顯示的內容:原文、正在串流輸出的譯文、譯文,或失敗。 + + + The text to translate, in its original language. Changing it clears the cached translations and shows the new original. Null plays `Simulation`, or shows the empty card without one. + 要翻譯的文字,使用其原始語言。變更它會清除快取的譯文並顯示新的原文。為 null 時播放 `Simulation`,沒有模擬時顯示空卡片。 + + + Shown after the role in the byline, for example "Oct 27, 09:14". + 顯示在署名列中職稱之後,例如 "Oct 27, 09:14"。 + + + Raised once a translation has fully arrived, before it is cached. Persist it here and pass it back in `Translations`. Bubbles. + 譯文完整送達後、快取之前引發。在此儲存譯文,並透過 `Translations` 傳回。會向上傳遞。 + + + Raised when a translation fails. Cancelled requests never raise it. Bubbles. + 翻譯失敗時引發。已取消的要求從不引發它。會向上傳遞。 + + + Ready translations by language, streamed by the simulation (content you translated ahead of time). A missing language shows the failure. + 依語言提供的現成譯文,由模擬串流輸出(預先翻譯好的內容)。缺少的語言會顯示失敗狀態。 + + + Your translation call: returns the translation as an async stream of text deltas. Pass the request's cancellation token on to your network call; it is cancelled when the reader picks another language, shows the original or the card leaves the window. A thrown exception's message is announced and kept as the status tooltip. Null simulates the stream. + 你的翻譯呼叫:以文字增量的非同步串流傳回譯文。請把要求的取消權杖傳給網路呼叫;讀者選擇其他語言、顯示原文或卡片離開視窗時它會被取消。擲回的例外訊息會被朗讀,並保留為狀態列的提示。為 null 時模擬串流輸出。 + + + The language on screen (two-way); null or the source language means the original. Set it to translate, for example to a reader's preferred language: a language other than the source translates as soon as the card is shown. + 畫面上的語言(雙向);null 或來源語言代表原文。設定它即可翻譯,例如設為讀者的偏好語言:來源語言以外的語言會在卡片一顯示時就開始翻譯。 + + + Raised when the reader picks a language or toggles Show original (not when the app sets `Value` or autoplay moves). Bubbles. + 讀者選擇語言或切換 Show original 時引發(應用程式設定 `Value` 或自動播放切換時不引發)。會向上傳遞。 + + + Who wrote the text of an `AiTranslate` card, shown in its byline. + `AiTranslate` 卡片文字的作者,顯示在其署名列中。 + + + Every word `AiTranslate` shows or says, in English by default. Replace the whole object to localize or reword; the phrases that name a language are format functions. + `AiTranslate` 顯示或朗讀的每個字詞,預設為英文。替換整個物件即可在地化或改寫;包含語言名稱的片語是格式化函式。 + + + One translation the card asks your `Translator` for. + 卡片向你的 `Translator` 要求的一次翻譯。 + + + A message an `AiTranslate` plays without a translator, from data you give it: the text, its byline, the model credited and translations made ahead of time. Set it as `Simulation` while the card has no `Text`: picking a language streams the ready translation in place after a short wait (a language without one shows the failure), the status says it is simulated and nothing was sent, and unless `AutoPlay` is false the card tours `Tour` by itself until someone presses a pointer or a key inside it. + `AiTranslate` 在沒有翻譯函式時依你提供的資料播放的訊息:文字、署名、標示的模型以及預先準備好的譯文。在卡片沒有 `Text` 時把它設為 `Simulation`:選擇一種語言後,卡片會在短暫等待後原地串流輸出現成譯文(沒有譯文的語言會顯示失敗狀態),狀態會說明這是模擬、沒有送出任何內容,並且除非 `AutoPlay` 為 false,卡片會自動依序播放 `Tour`,直到有人在卡片內按下指標或按鍵。 + + + A compact automation builder card with an AI step (Arc AI workflow, design reference). Steps are 32px rows on one 1px hairline rail: a trigger, an AI step, a branch whose paths switch with small tabs, and actions. Test run glides a small accent dot down each connector while the line behind it fills; each step crossfades to Running with a shimmering activity line, then shows its duration and an inline output preview in small muted text (the email, the label with a streamed reason, the posted message, the created issue). The path the run does not take is marked Skipped. A failing step shows its error and Retry, which resumes the run from that step. Steps are added from a compact command menu (search, rows with app marks and number shortcuts) and removed with a small button. + 一張帶有 AI 步驟的精簡自動化建構卡片(Arc AI workflow,設計參考)。步驟是排在同一條 1px 細線軌道上的 32px 列:觸發條件、AI 步驟、可用小分頁切換路徑的分支,以及動作。Test run 讓一個強調色小圓點沿著每段連接線滑下,身後的線隨之填滿;每個步驟交叉淡化為 Running 並顯示閃爍的活動列,接著以小號淡化文字顯示耗時與內嵌的輸出預覽(郵件、帶有串流理由的標籤、送出的訊息、建立的議題)。執行未走的路徑會標為 Skipped。失敗的步驟會顯示錯誤與 Retry,Retry 會從該步驟繼續執行。步驟從精簡的指令選單(搜尋、帶有應用程式標記與數字快速鍵的列)中新增,用一個小按鈕移除。 + + + Plays a test run once the card is on screen and repeats it after a short pause, until someone presses or types inside the card. Nothing is scheduled while the card is off screen. Keep it off in real products. + 卡片出現在螢幕上後播放一次測試執行,短暫停頓後重複,直到有人在卡片內按下滑鼠或按鍵。卡片不在螢幕上時不安排任何播放。正式產品中請關閉。 + + + The entries of the add step menu. Triggers are offered in the empty state, everything else in the menu. Null uses the simulation's catalog while its flow is shown, and no entries otherwise; an empty list hides the menu. + 新增步驟選單的項目。觸發條件在空狀態中提供,其餘都在選單中。為 null 時,顯示模擬的流程期間使用模擬的目錄,否則沒有項目;空清單會隱藏選單。 + + + One line of context under the name. Null shows the simulation's line while its flow is shown. + 名稱下方的一行說明。為 null 時,顯示模擬的流程期間會顯示模擬的說明。 + + + Shows the add step menu and the remove buttons. Triggers cannot be removed. + 顯示新增步驟選單與移除按鈕。觸發條件無法移除。 + + + Shows three skeleton steps while your flow loads; Test run and the add menu are unavailable. + 在流程載入時顯示三個骨架步驟;此時 Test run 與新增選單無法使用。 + + + True while a test run is in progress. + 測試執行進行中時為 true。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個字。 + + + Raised when a test run passes or stops on a failed step (not after Stop). Bubbles. + 測試執行通過或停在失敗步驟上時觸發(Stop 之後不觸發)。會向上傳遞。 + + + Runs one step for real and returns its output; throw to fail the step with the message. Null simulates each step from its `Output` and `Duration` (the card then says "Simulated"). + 實際執行一個步驟並傳回其輸出;擲回例外則以該訊息讓步驟失敗。為 null 時,每一步都依其 `Output` 與 `Duration` 模擬(卡片會顯示 "Simulated")。 + + + The flow the card shows and test-runs on its own while `Steps` is null: your name, line, steps and catalog, played exactly like a flow of your own (autoplay, pausing off screen, stopping for good after a press). Null shows the empty state instead. + `Steps` 為 null 時卡片自行顯示並測試執行的流程:你的名稱、說明、步驟與目錄,播放方式與你自己的流程完全相同(自動播放、離開螢幕時暫停、按下後永久停止)。為 null 時改為顯示空狀態。 + + + The steps, in order (two-way). A branch carries its paths and must be the last top-level step. Null plays the `Simulation`'s flow, or shows the empty state without one; an empty list shows the empty state. Adding or removing a step puts a new list here. + 依序排列的步驟(雙向)。分支帶有自己的路徑,而且必須是最後一個頂層步驟。為 null 時播放 `Simulation` 的流程,沒有模擬時顯示空狀態;空清單顯示空狀態。新增或移除步驟時會在這裡放入一個新清單。 + + + Raised after a step is added or removed, with the new list (already in `Steps`). Bubbles. + 新增或移除步驟後觸發,附帶新清單(已寫入 `Steps`)。會向上傳遞。 + + + The workflow's name in the header. Null shows the simulation's name while its flow is shown. + 標題列中的工作流程名稱。為 null 時,顯示模擬的流程期間會顯示模擬的名稱。 + + + The app a step of an `AiWorkflow` belongs to: a name read to screen readers and shown in the add step menu, and a mark (a 24 × 24 stroked glyph) drawn in a small hairline square on the rail. The built-in apps are generic (Mail, AI, Chat, Tracker, ...) with Lucide glyphs; pass your own name and mark for real products (for a model step, the model's vendor with `isAccent: true`). + `AiWorkflow` 中步驟所屬的應用程式:一個供螢幕閱讀器朗讀、並顯示在新增步驟選單中的名稱,以及一個畫在軌道上細線小方框裡的標記(24 × 24 描邊圖形)。內建應用程式都是通用的(Mail、AI、Chat、Tracker 等),使用 Lucide 圖示;真實產品請傳入你自己的名稱與標記(模型步驟請使用模型廠商,並設定 `isAccent: true`)。 + + + One path of a branch step: a label for its tab and the steps that run when the branch picks it. + 分支步驟的一條路徑:分頁上的文字,以及分支選中它時執行的步驟。 + + + An entry of the add step menu of an `AiWorkflow`: the same shape as a step without an id. Triggers are offered in the empty state; everything else is in the menu. + `AiWorkflow` 新增步驟選單中的一個項目:與步驟的形狀相同,只是沒有 id。觸發條件在空狀態中提供;其餘都在選單中。 + + + An email: the sender and subject on one line and a two line preview. + 一封郵件:一行寄件者與主旨,以及兩行預覽。 + + + A label a model chose: a hairline label with a status dot, the confidence in monospace and a streamed reason. + 模型選定的標籤:一個帶狀態圓點的細線標籤、等寬字型的信心值,以及串流出現的理由。 + + + Every word `AiWorkflow` shows or says, in English by default. Replace the whole object to localize or reword; the sentences that carry a value are format functions. + `AiWorkflow` 顯示或朗讀的每一個字,預設為英文。替換整個物件即可在地化或改寫;帶有數值的句子是格式化函式。 + + + A posted chat message. + 一則已送出的聊天訊息。 + + + The output preview of a step of an `AiWorkflow`, rendered inline in small muted text under the step, aligned with its title. Use one of `AiWorkflowEmailOutput`, `AiWorkflowLabelOutput`, `AiWorkflowRouteOutput`, `AiWorkflowMessageOutput`, `AiWorkflowRecordOutput` or `AiWorkflowTextOutput`. + `AiWorkflow` 中步驟的輸出預覽,以小號淡化文字內嵌在步驟下方,與標題對齊。請使用 `AiWorkflowEmailOutput`、`AiWorkflowLabelOutput`、`AiWorkflowRouteOutput`、`AiWorkflowMessageOutput`、`AiWorkflowRecordOutput` 或 `AiWorkflowTextOutput` 其中之一。 + + + A created or updated record: an issue key, a row, a ticket or a page. + 一筆建立或更新的紀錄:議題編號、表格列、客服單或頁面。 + + + The path a branch took. Return it from a branch step to pick the path. + 分支走的路徑。從分支步驟傳回它即可選擇路徑。 + + + Arguments of `RunCompleted`. + `RunCompleted` 的參數。 + + + What `RunStep` gets with each step. + `RunStep` 隨每個步驟收到的內容。 + + + A flow an `AiWorkflow` shows and test-runs on its own while the app sets no `Steps`: a name, a line of context, the steps and the add step catalog, all supplied by the caller. Each simulated run plays the steps' `Output`, `Duration` and `Error`; with `AutoPlay` the card repeats the run while it is on screen until someone presses or types inside it. Nothing is sent. Set the data before handing the simulation to the card; to show another flow, give the card a new simulation. + 在應用程式沒有設定 `Steps` 時,`AiWorkflow` 自行顯示並測試執行的流程:名稱、一行說明、步驟與新增步驟目錄,全部由呼叫端提供。每次模擬執行都會播放步驟的 `Output`、`Duration` 與 `Error`;開啟 `AutoPlay` 時,只要卡片在螢幕上就會重複執行,直到有人在卡片內按下滑鼠或按鍵。不會送出任何內容。請在把模擬交給卡片之前設定好資料;要顯示另一個流程,就給卡片一個新的模擬。 + + + One step of an `AiWorkflow`. The workflow treats steps as values: to change one, put a new list in `Steps`. `Output`, `Duration` and `Error` are what a simulated run shows; a run through `RunStep` shows what your code returns. + `AiWorkflow` 的一個步驟。工作流程把步驟當成值:要修改某一步,就在 `Steps` 中放入一個新清單。`Output`、`Duration` 與 `Error` 是模擬執行顯示的內容;透過 `RunStep` 執行時則顯示你的程式碼傳回的內容。 + + + Arguments of `StepsChanged`. + `StepsChanged` 的參數。 + + + Free text, such as a drafted reply; it streams in fast chunks. + 自由文字,例如草擬的回覆;以快速的分塊串流出現。 + Arc alert: a persistent message that helps people recover or continue. Keep one instance and change `Tone`, `Title` and the content to morph it between states: the icon pops into the new tone, new text rises out of a soft blur and the height springs to fit. Hiding it (`IsOpen`) collapses the height while it fades, so the content below closes the gap. A string `Content` is the description line; any other content is shown as is. Arc 警示:持續顯示的訊息,協助使用者恢復或繼續操作。保持同一執行個體,改變 `Tone`、`Title` 和內容即可在各狀態間變形:圖示以彈出方式切換為新色調,新文字從柔和模糊中升起,高度以彈簧適配內容。隱藏(`IsOpen`)時高度在淡出的同時收縮,使下方內容填補空隙。字串類型的 `Content` 為描述行;其他類型的內容按原樣顯示。 @@ -2424,6 +3288,110 @@ The whole prompt sent to both models. 傳送給兩個模型的完整提示。 + + Natural language to chart in one card (Arc Ask your data, design reference). A question field with the data source; a collapsible Generated SQL panel where the query streams in with syntax tints; a running state with a rolling count of the rows scanned over a breathing skeleton; then a result that builds as stacked bars, lines or a table, with a legend, a pointer and keyboard readout, and a one line insight streamed underneath. Failures show the database's message with Try again; a query that matches nothing offers the suggestions. Wire `GenerateSql` (a whole string, revealed four tokens a tick at a fixed final size) or `StreamSql` (chunks from your model; the panel grows with them) to your model, and `RunQuery` to a read only database role. Without them the card plays its `Simulation`, answers you supply, and asks its question once it is 35% on screen; with neither, it waits in its empty state and Ask stays unavailable. + 在一張卡片裡把自然語言變成圖表(Arc Ask your data,設計參考)。帶資料來源的問題輸入框;可收合的 Generated SQL 面板,查詢帶著語法著色串流寫入;執行狀態中,已掃描列數在呼吸的骨架上方滾動計數;接著結果以堆疊長條、折線或表格逐步呈現,附帶圖例、指標與鍵盤讀數,以及在下方串流出現的一行洞察。失敗時顯示資料庫的訊息與「重試」;沒有相符結果的查詢會提供建議問題。把 `GenerateSql`(整段字串,以固定的最終尺寸每個節拍顯示四個語彙單元)或 `StreamSql`(來自模型的分段,面板隨之增高)接上你的模型,把 `RunQuery` 接上唯讀資料庫角色。都不接上時,卡片會播放它的 `Simulation`(由你提供的答案),並在有 35% 出現在螢幕上時提出它的問題;兩者都沒有時,卡片會停在空狀態,提問按鈕無法使用。 + + + Asks the question once the card is 35% on screen. Null: on when a `Simulation` writes the SQL, off when you wire a real model. + 卡片有 35% 出現在螢幕上時提出問題。為 null 時:由 `Simulation` 撰寫 SQL 則開啟,接上真實模型則關閉。 + + + The view on show (two-way). Null leaves it to the card: each result opens on its own view or `DefaultChart`, and the switch changes it until the next result. + 目前顯示的檢視(雙向)。為 null 時交由卡片決定:每個結果依自己的檢視或 `DefaultChart` 開啟,切換器在下一個結果到來前可改變它。 + + + Raised when the person switches the view with the chart switch. Bubbles. + 使用者透過圖表切換器切換檢視時引發。向上傳遞。 + + + The view for results that do not name one. + 未指定檢視的結果所使用的檢視。 + + + Raised when writing or running fails; not when the person stops a run. Bubbles. + 撰寫或執行失敗時引發;使用者停止執行時不引發。向上傳遞。 + + + Writes the SQL for a question as one string. The card reveals it four tokens every 34ms at its final size. Honour the request's cancellation token: Stop cancels it. Without this and `StreamSql` the `Simulation` writes the SQL. + 以一整段字串為問題撰寫 SQL。卡片以其最終尺寸每 34ms 顯示四個語彙單元。請遵守請求的取消權杖:「停止」會取消它。此項與 `StreamSql` 都未設定時,由 `Simulation` 撰寫 SQL。 + + + Keeps the SQL panel open after the query starts instead of folding it away. Once the person opens or closes the panel during a run, their choice wins. + 查詢開始後讓 SQL 面板保持展開,而不是收合起來。一旦使用者在執行中展開或收合了面板,就以使用者的選擇為準。 + + + Every word the card shows or says. + 卡片顯示或朗讀的所有文字。 + + + The placeholder of the empty field. + 輸入框為空時的預留位置文字。 + + + The question (two-way); the field writes it as the person types. Null shows the simulation's question, or an empty field without one. + 問題(雙向);使用者輸入時由輸入框寫入。為 null 時顯示模擬的問題,沒有模擬時輸入框為空。 + + + The result on show, or null. + 目前顯示的結果,或 null。 + + + Raised once a result is charted (for saving the question to a dashboard or logging). Bubbles. + 結果繪製成圖表後引發(用於把問題儲存到儀表板或記錄日誌)。向上傳遞。 + + + Runs the SQL (read only) and returns a chart ready result. Call `ReportProgress` to roll the count in the running state; throw to show the database error. Without it the `Simulation` answers. + 執行 SQL(唯讀)並傳回可直接繪圖的結果。呼叫 `ReportProgress` 讓執行狀態中的計數滾動;擲回例外以顯示資料庫錯誤。未設定時由 `Simulation` 回答。 + + + The model and database the card plays while you wire none: its question (asked on its own once the card is on screen), suggestions, source and answers, run exactly like your own delegates (the SQL revealed, the rows scanned rolling, the chart building, the insight streaming). It stands in only for what you have not wired. Null leaves the card in its empty state. + 你一個委派都沒有接上時卡片播放的模型與資料庫:它的問題(卡片出現在螢幕上後自行提出)、建議、資料來源與答案,執行方式與你自己的委派完全相同(SQL 逐步顯示、掃描列數滾動、圖表建立、洞察逐字出現)。它只替代你沒有接上的部分。為 null 時卡片停在空狀態。 + + + The database the question runs against: its name shows in the field and the empty state, its dialect leads the SQL meta. Null uses the simulation's source, or names none. + 問題所針對的資料庫:其名稱顯示在輸入框與空狀態中,其方言位於 SQL 中繼資訊的開頭。為 null 時使用模擬的資料來源,否則不顯示名稱。 + + + The SQL of the current run, as written so far. + 本次執行的 SQL,即目前已寫出的部分。 + + + Where the run is: idle, writing, running, done, empty, failed or stopped. + 執行所處的階段:閒置、撰寫中、執行中、完成、無資料、失敗或已停止。 + + + Streams the SQL for a question as chunks from your model; each chunk fades in and the panel grows with them. Takes precedence over `GenerateSql`. Honour the cancellation token. + 以來自模型的分段串流輸出問題的 SQL;每個分段淡入出現,面板隨之增高。優先於 `GenerateSql`。請遵守取消權杖。 + + + Questions offered in the empty state and after a question that matches no data. Null offers the simulation's suggestions, or none; an empty list hides them. + 在空狀態以及問題沒有相符資料之後提供的問題。為 null 時提供模擬的建議,沒有模擬時不提供;空清單會隱藏它們。 + + + Every word `AskYourData` shows or says, in English by default. Replace the whole object to localize or reword; counts and times are format functions. + `AskYourData` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;計數與時間是格式化函式。 + + + What `RunQuery` returns, shaped for the chart. + `RunQuery` 傳回的內容,結構為圖表而設計。 + + + One series of an `AskYourDataResult`: a plan, a region or a metric with one value per category. + `AskYourDataResult` 中的一個數列:一個方案、地區或指標,每個類別一個值。 + + + A model and a database an `AskYourData` plays while the app wires neither: the question it asks on its own, the suggestions, the source, and the answers it knows. A question that contains one of an answer's `Keywords` gets that answer (the first match in order); any other gets `Fallback`. The SQL is revealed at the card's own pace after a short pause, the rows scanned roll up in ten steps, then the result builds and its insight streams in. Nothing is queried, and the live region says so. Set the data before handing the simulation to the card; to play other answers, give the card a new simulation. + 應用程式一個都沒有接上時 `AskYourData` 播放的模型與資料庫:它自行提出的問題、建議、資料來源以及它知道的答案。問題包含某個答案的 `Keywords` 之一時得到該答案(依序取第一個相符者);其他問題得到 `Fallback`。SQL 在短暫停頓後依卡片自己的節奏逐步顯示,掃描列數分十步滾動上升,接著結果建立、洞察逐字出現。不會查詢任何內容,即時區域會說明這一點。請在把模擬交給卡片之前設定好資料;要播放其他答案,就給卡片一個新的模擬。 + + + One answer of an `AskYourDataSimulation`: the words that pick it, the SQL, the rows scanned and the result. + `AskYourDataSimulation` 的一個答案:選中它的字詞、SQL、掃描的列數與結果。 + + + The database an `AskYourData` question runs against. + `AskYourData` 的問題所針對的資料庫。 + Arguments of `ConfirmRequested`: set `Cancel` to keep the selection unconfirmed. `ConfirmRequested` 的引數:設定 `Cancel` 可讓選擇保持未確認。 @@ -3088,6 +4056,70 @@ The built-in text of `BudgetVariance`. Replace the whole object to localize the block; parameterized entries are functions that receive already formatted amounts. `BudgetVariance` 的內建文字。替換整個物件即可在地化區塊;帶參數的項目是接收已格式化金額的函式。 + + An animated "AI is building your app" visual (Arc Building blocks, design reference). Rounded blocks for a nav bar, a hero card, a chart, an avatar row, a settings row and a button fly in and snap into a tiny app layout with no bounce, land as tinted placeholders, fill with real-looking content and pick up the tint in a styling pass, while the status line steps through Laying out, Adding components, Styling and Ready. Map your request to `Status`: `Building` while it runs, `Done` when the result arrives (the rest fast forwards), `Idle` before the first prompt. Leave `Status` unset to play a simulated build. The frame grows to `Size` (or `StageWidth`) and never past its container; everything inside scales with the width. + 「AI 正在搭建你的應用程式」的動畫畫面(Arc Building blocks,設計參考)。導覽列、主卡片、圖表、頭像列、設定列和按鈕這些圓角積木飛入,無回彈地吸附進一個小小的應用程式版面,先以著色的預留位置落定,再填上逼真的內容,並於樣式階段染上主題色;狀態列依序顯示 Laying out、Adding components、Styling 和 Ready。把請求對應到 `Status`:進行中為 `Building`,結果抵達為 `Done`(其餘階段快轉),第一次提示之前為 `Idle`。不設定 `Status` 時播放模擬建置。框架長到 `Size`(或 `StageWidth`)的寬度且不超出容器;內部一切隨寬度縮放。 + + + Shows Replay on Ready; it raises `ReplayRequested`. Start the generation again and set `Status` back to building. + 在 Ready 時顯示 Replay;按下會觸發 `ReplayRequested`。請重新開始產生,並把 `Status` 設回 building。 + + + One build, from the first block to the end of styling, split evenly over the three phases; never under 2.4 seconds. + 一次建置的時長,從第一塊積木到樣式結束,平均分配給三個階段;最少 2.4 秒。 + + + Shows the status line under the frame: the mark, the phase label and Replay. Turn it off when the surrounding UI already says what happens. + 在框架下方顯示狀態列:標記、階段標籤和 Replay。周圍介面已經說明正在發生什麼時可以關閉。 + + + Without a status: replay the build after a short hold on Ready (off plays once and shows Replay). Building: re-assemble until the status turns done (off holds on Styling). Reduced motion always plays once. + 沒有狀態時:在 Ready 上短暫停留後重播建置(關閉則只播放一次並顯示 Replay)。building 時:持續重新組裝直到狀態變為 done(關閉則停在 Styling)。減少動態效果時一律只播放一次。 + + + Every word the component shows or says. + 元件顯示或朗讀的所有文字。 + + + The phase the status line shows. + 狀態列顯示的階段。 + + + Raised when the visible phase changes, for analytics or to keep other UI in step. Bubbles. + 可見階段變化時觸發,可用於分析或讓其他介面保持同步。冒泡。 + + + Raised when the person presses Replay. Bubbles. Without a status the simulated build also starts again. + 使用者按下 Replay 時觸發。冒泡。沒有狀態時模擬建置也會重新開始。 + + + The width the frame grows to: Small 280, Medium 400, Large 560 (never past the container). + 框架能長到的寬度:Small 280、Medium 400、Large 560(不超出容器)。 + + + A width in pixels that takes the place of `Size`; NaN (the default) uses `Size`. + 以像素指定的寬度,取代 `Size`;NaN(預設)時使用 `Size`。 + + + What the app says about its generation: idle clears the frame to dashed slots, building runs the phases (and re-assembles while `IsLooping`), done fast forwards and settles on Ready. Null plays the built-in simulated build. + 應用程式對產生過程的說明:idle 把框架清空為虛線槽位,building 播放各個階段(`IsLooping` 時重新組裝),done 快轉並停在 Ready。為 null 時播放內建的模擬建置。 + + + Any color, taking the place of `Tone`, for a brand or per-product tint. + 任意顏色,取代 `Tone`,用於品牌色或依產品著色。 + + + The tint of the placeholders, the button, the chart, the switch and the status mark: the accent or the foreground. + 預留位置、按鈕、圖表、開關和狀態標記的顏色:強調色或前景色。 + + + Every word `BuildingBlocks` shows or says, in English by default. Replace the whole object to localize or to name the thing being built (`Ready = "Your dashboard is ready"`). + `BuildingBlocks` 顯示或朗讀的所有文字,預設為英文。替換整個物件來在地化,或寫明正在建置的東西(`Ready = "Your dashboard is ready"`)。 + + + Arguments of `PhaseChanged`. + `PhaseChanged` 的參數。 + Arc button: a clear, responsive action with quiet secondary states. Presses scale on a snappy spring, label changes crossfade with a short blur while the width springs to fit, and `IsLoading` shows a spinner while keeping keyboard focus (clicks are swallowed instead of disabling the button). Arc 按鈕:清晰、靈敏的操作,次要狀態安靜低調。按下時以輕快的彈簧縮放;文字變化時以短暫模糊交叉淡化,寬度以彈簧適配內容。`IsLoading` 顯示載入指示器,同時保留鍵盤焦點(點按會被忽略,而不是停用按鈕)。 @@ -3100,6 +4132,14 @@ Anchors of popups and menus must not scale on press (the popup measures the pressed size); they answer with color only. Defaults to true. 彈層與選單的錨點按鈕在按下時不得縮放(彈層需量測按下時的尺寸);它們僅以顏色作出回應。預設為 true。 + + Spring the button's width follows when its label changes: Morph (default, a slight bounce), or Quick for a bounce-free 0.25s glide that pairs with the Label swap. + 標籤變化時按鈕寬度跟隨的彈簧:Morph(預設,帶輕微回彈),或 Quick,即 0.25 秒無回彈的滑動,與 Label 切換搭配使用。 + + + How a new label replaces the old one: BlurSwap (default, a short blur), or Label for the quiet crossfade of Arc's AI blocks (100ms out, 160ms in rising 2px), or any other content swap mode. + 新標籤取代舊標籤的方式:BlurSwap(預設,短暫模糊),或 Label,即 Arc AI 區塊的安靜交叉淡化(淡出 100ms,淡入 160ms 並上升 2px),也可以是其他任意內容切換模式。 + Button height: Small, Medium (default) or Large. 按鈕高度:Small、Medium(預設)或 Large。 @@ -4136,6 +5176,142 @@ Header text shown when `FileName` is empty (default "Source code"). `FileName` 為空時顯示的標題文字(預設為「原始碼」)。 + + An AI code edit suggestion in a small editor panel, like an inline edit in an AI code editor. The header shows the model's mark, the instruction and the model; the panel shows a file tab with its language mark, a running +/− count, and the code with each proposed change as an inline diff hunk: removed lines tinted red with a minus, new lines tinted green with a plus. New lines stream in, then every hunk gets Reject and Accept under it, and the footer offers Reject all and Accept all (Cmd or Ctrl + Enter). Accepting a hunk folds its removed lines away and settles the new lines into the file; rejecting folds the new lines away and restores the original. Give it the file in `Code`, the changes in `Hunks` and set `Status`; write the file in `Resolved` (or progressively in `DecisionMade`): the control never writes anything itself. With no code and no hunks it shows an empty file, or plays a `Simulation` you pass in, which reviews itself and replays until someone touches it. + 在小型編輯器面板中呈現的 AI 程式碼修改建議,類似 AI 程式碼編輯器中的行內修改。標題列顯示模型標記、使用者要求與模型;面板顯示附語言標記的檔案分頁、即時的 +/− 計數,以及程式碼,其中每處建議的變更都是一個行內差異區塊:刪除的行以紅色底並帶減號,新增的行以綠色底並帶加號。新行逐行串流進來,接著每個差異區塊下方會出現「拒絕」與「接受」,頁尾提供「全部拒絕」與「全部接受」(Cmd 或 Ctrl + Enter)。接受一個差異區塊會摺疊其刪除行並讓新行併入檔案;拒絕則摺疊新行並還原原文。把檔案放進 `Code`、變更放進 `Hunks` 並設定 `Status`;在 `Resolved` 中(或在 `DecisionMade` 中逐步)寫入檔案:控制項本身從不寫入任何內容。既沒有程式碼也沒有差異區塊時,它顯示一個空白檔案,或播放你傳入的 `Simulation`,後者會自行審閱並重播,直到有人操作它。 + + + Shows Regenerate once the review is done (or there were no changes) and Retry on error; both raise `RegenerateRequested`. A simulation always has them and replays on its own. + 審閱完成後(或沒有變更時)顯示「重新產生」,出錯時顯示「重試」;兩者都會引發 `RegenerateRequested`。模擬一律帶有它們,並會自行重播。 + + + Shows Stop while thinking or streaming; it raises `StopRequested`. A simulation always has it. + 思考或串流時顯示「停止」;它會引發 `StopRequested`。模擬一律帶有它。 + + + Cmd or Ctrl + Enter accepts all and Cmd or Ctrl + Backspace rejects all while focus is inside the card; Accept all shows the hint. + 焦點在卡片內時,Cmd 或 Ctrl + Enter 接受全部,Cmd 或 Ctrl + Backspace 拒絕全部;「全部接受」顯示提示。 + + + Simulation only: after streaming, it accepts and rejects hunks on its own (one by one and Accept all on alternate runs) and replays about 2.6s after it finishes. Any pointer, key or focus inside the card hands control to the person for that run. Off under reduced motion. + 僅用於模擬:串流結束後自行接受與拒絕差異區塊(隔輪交替為逐一決定與全部接受),結束約 2.6 秒後重播。卡片內的任何指標、按鍵或焦點都會把這一輪交給使用者控制。減少動態效果時關閉。 + + + The original file contents, lines split on `\n`. Leave it and `Hunks` unset to play the `Simulation`. + 檔案的原始內容,依 `\n` 分行。與 `Hunks` 都不設定時播放 `Simulation`。 + + + Raised for every hunk accepted or rejected (Accept all raises it once per hunk), with the file as it now stands. Bubbles. + 每接受或拒絕一個差異區塊時引發(「全部接受」對每個差異區塊各引發一次),並附上檔案目前的內容。會冒泡。 + + + The message of the error status. + 錯誤狀態顯示的訊息。 + + + Your own language mark for the tab (any content); null shows two letters from the file's extension. + 分頁中你自己的語言標記(任意內容);為 null 時顯示取自副檔名的兩個字母。 + + + The file name in the tab (empty by default); its extension picks the highlighting language and the language mark. + 分頁中的檔名(預設為空);其副檔名決定醒目提示語言與語言標記。 + + + The folder shown after the file name from a 520px card, such as "src/billing". + 卡片寬度達 520px 時顯示在檔名之後的資料夾,例如 "src/billing"。 + + + What the person asked for, shown as the header title (empty by default). + 使用者提出的要求,顯示為標題列的標題(預設為空)。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個字詞。 + + + Caps the code area's height; longer files scroll inside it. Infinity (the default) never scrolls. + 限制程式碼區域的高度;更長的檔案會在其中捲動。預設值為無限大,從不捲動。 + + + The model that wrote the suggestion: its name under the title and its mark (a 24 × 24 stroked glyph, in `MarkBrush` or the text color) in the header. Null (the default) hides the model line. + 寫出這個建議的模型:其名稱顯示在標題下方,其標記(24 × 24 的描邊圖形,顏色為 `MarkBrush` 或文字顏色)顯示在標題列中。為 null(預設)時隱藏模型那一行。 + + + How many hunks still wait for a decision. + 仍在等待決定的差異區塊數量。 + + + Raised when the person presses Regenerate or Retry. Bubbles. + 使用者按下「重新產生」或「重試」時引發。會冒泡。 + + + Raised once every hunk has a decision, with the final code and the hunk ids. Write the file here. Bubbles. + 每個差異區塊都有決定後引發一次,附上最終程式碼與差異區塊的 id。在這裡寫入檔案。會冒泡。 + + + Whether the tab shows a language mark at all. + 分頁是否顯示語言標記。 + + + A simulated suggestion to play while the card has no `Code` and no `Hunks` (demos and prototypes): it streams its hunks over its file and, with `AutoPlay`, reviews itself and replays. Null shows the card's own state. + 卡片沒有 `Code` 與 `Hunks` 時要播放的模擬建議(示範與原型):它在自己的檔案上串流進差異區塊,並在 `AutoPlay` 下自行審閱並重播。為 null 時顯示卡片自己的狀態。 + + + Where the suggestion is: thinking shows a shimmering label over the plain file, streaming shows hunks as they grow, ready shows Accept and Reject, error shows the message and Retry. Going back to thinking clears earlier decisions. + 建議所處的階段:thinking 在原始檔案上顯示閃爍的標籤,streaming 顯示正在增長的差異區塊,ready 顯示「接受」與「拒絕」,error 顯示訊息與「重試」。回到 thinking 會清除先前的決定。 + + + Raised when the person presses Stop. Abort your request, then set `Status`. Bubbles. + 使用者按下「停止」時引發。中止你的請求,然後設定 `Status`。會冒泡。 + + + Data of `DecisionMade`. + `DecisionMade` 的事件資料。 + + + One proposed change of a `CodeSuggestion`: it replaces `Remove` lines starting at the zero based line `Start` with the `Insert` lines (remove 0 inserts before `Start`; an empty insert deletes). To stream, replace `Insert` with a longer list (or call `AppendLine`) as tokens arrive. + `CodeSuggestion` 中的一處建議變更:從由零起算的第 `Start` 行開始,用 `Insert` 行取代 `Remove` 行(remove 為 0 時在 `Start` 之前插入;insert 為空時表示刪除)。串流時,隨著詞元抵達把 `Insert` 取代為更長的清單(或呼叫 `AppendLine`)。 + + + The hunk's id, unique within the suggestion; decisions are kept by it. + 差異區塊的 id,在建議中唯一;決定依它保存。 + + + The new lines (empty for a pure deletion). Replace the list to stream more lines in. + 新增的行(純刪除時為空)。取代清單即可串流進更多行。 + + + How many original lines the hunk removes (0 for a pure insertion). + 差異區塊刪除的原始行數(純插入時為 0)。 + + + The zero based line of the first removed line, or the line the new lines go before. + 第一行刪除行由零起算的行號,或新行插入位置之後的那一行。 + + + A two to six word summary shown in the review bar and in the names of its buttons. + 兩到六個字詞的摘要,顯示在審閱列中,也用於其按鈕的名稱。 + + + Every word `CodeSuggestion` shows or says, in English by default. Replace the whole object to localize or reword; texts that carry a name or a count are format functions. + `CodeSuggestion` 顯示或朗讀的每一個字詞,預設為英文。取代整個物件即可在地化或改寫;帶名稱或數量的文字是格式化函式。 + + + Data of `Resolved`. + `Resolved` 的事件資料。 + + + A simulated suggestion for a `CodeSuggestion`, for demos, prototypes and tests: the card reads `Code` for 0.8s, then each of `Hunks` opens (its removed lines tint red in 240ms) and its new lines stream in one every 38ms. With `AutoPlay` the card then reviews it itself (one hunk at a time and Accept all on alternate runs) and replays 2.6s after it finishes. Any pointer, key or focus inside the card hands control to the person for that run; Stop keeps the hunks that finished and Regenerate replays. Reduced motion and Off open on the finished suggestion with no streaming and no autoplay. Nothing is sent to a model. Set it as the card's `Simulation`: it plays while the card has no `Code` and no `Hunks` of its own, from the top each time the card comes into a window. The file name, the instruction and the model stay on the card. Its values take effect at the next run; the hunks are copied, so one list can be shared between simulations. Use one simulation per card. + 為 `CodeSuggestion` 提供的模擬建議,用於示範、原型和測試:卡片讀取 `Code` 0.8 秒,接著 `Hunks` 中的每個差異區塊依序展開(其刪除行在 240ms 內染成紅色),新行每 38ms 串流進一行。在 `AutoPlay` 下,卡片隨後自行審閱(隔輪交替為逐一決定與全部接受),結束 2.6 秒後重播。卡片內的任何指標、按鍵或焦點都會把這一輪交給使用者控制;「停止」保留已完成的差異區塊,「重新產生」會重播。減少動態效果與關閉動態效果時直接顯示完成的建議,不串流也不自動播放。不會向任何模型傳送內容。把它設為卡片的 `Simulation`:卡片沒有自己的 `Code` 與 `Hunks` 時播放它,每次卡片進入視窗時從頭開始。檔名、指令與模型仍設定在卡片上。它的值在下一輪生效;差異區塊會被複製,所以多個模擬可以共用一份清單。每張卡片使用一個模擬。 + + + The original file contents, lines split on `\n`. + 檔案的原始內容,依 `\n` 分行。 + + + The complete hunks the simulated model streams, in file order. + 模擬模型要串流輸出的完整差異區塊,依檔案中的順序排列。 + One cell of a `CohortRetention`: a cohort (or the average row) and a month since the cohort started. `CohortRetention` 的一個儲存格:一個世代(或平均列)和自該世代開始以來的某個月。 @@ -5076,6 +6252,130 @@ The legend text of the healthy zone. 健康區域的圖例文字。 + + One app a `ConnectorSetup` lists. Write the scopes as plain-language capabilities ("Post replies as your assistant"), not raw OAuth scope strings. + `ConnectorSetup` 列出的一個應用程式。用淺白的能力描述來寫權限範圍("Post replies as your assistant"),不要寫原始的 OAuth scope 字串。 + + + What a connect handler gets besides the app: a token that is cancelled when the person presses Cancel or the card leaves the window, and the report calls that move the row through its steps. The report calls may come from any thread. + 連結處理常式除應用程式之外取得的內容:一個在使用者按下 Cancel 或卡片離開視窗時取消的權杖,以及推動這一列經過各個步驟的回報呼叫。回報呼叫可以來自任何執行緒。 + + + What a finished connect reports back: the account, the granted scopes and how many items the first sync read. + 連結完成後回報的內容:帳號、取得的權限範圍,以及首次同步讀取了多少項。 + + + An app's mark as `ConnectorSetup` draws it: a neutral Lucide glyph for the kind of app (or your own glyph, or the first letter of the name) in a quiet rounded tile. No brand logos are drawn; pass the app's own mark as a 24 × 24 stroked `Glyph`. Use it anywhere else in your settings UI to match the card. + `ConnectorSetup` 繪製的應用程式標記:在一個安靜的圓角圖塊中,顯示代表應用程式類型的中性 Lucide 圖示(或你自己的圖示,或名稱的首字母)。不繪製品牌標誌;應用程式自己的標記以 24 × 24 描邊 `Glyph` 傳入。可在設定介面的其他位置使用,與卡片保持一致。 + + + Your own 24 × 24 stroked glyph; it takes the place of the built-in one. + 你自己的 24 × 24 描邊圖示;它取代內建圖示。 + + + The glyph's color; the foreground by default. + 圖示的顏色;預設為前景色。 + + + The built-in glyph for the kind of app. + 代表應用程式類型的內建圖示。 + + + The name whose first letter is shown when there is no glyph. + 沒有圖示時顯示其首字母的名稱。 + + + The glyph's size in pixels; the tile adds its padding around it. + 圖示的大小(像素);圖塊在其周圍加上內距。 + + + A "Connect your tools" card for an AI assistant (Arc Connector setup, design reference). Each app connects inline: its row opens the sign-in, waits for the person to approve and runs a first sync with a progress hairline and a rolling count, then lists the scopes it was granted behind a Connected button and offers Disconnect with an inline confirmation. A search field filters the list by name, description, account and keywords. The OAuth flow is the app's: set `ConnectHandler` to run it, report the steps through the `ConnectorConnectContext` and return the account, scopes and sync count (throw to show the reason with Retry); set `DisconnectHandler` to revoke. Without handlers the flow is simulated and nothing is sent. With no `Connectors` the card lists its `Simulation`'s apps and, once on screen, plays its scripted run; with neither it lists no apps. + AI 助理的「Connect your tools」卡片(Arc Connector setup,設計參考)。每個應用程式都在行內連結:它的列開啟登入頁面,等待使用者核准,並執行首次同步,同時顯示進度細線和滾動的計數;接著在 Connected 按鈕後列出取得的權限範圍,並提供附帶行內確認的 Disconnect。搜尋欄位依名稱、描述、帳號和關鍵字篩選清單。OAuth 流程由應用程式負責:設定 `ConnectHandler` 來執行它,透過 `ConnectorConnectContext` 回報各步驟,並傳回帳號、權限範圍和同步數量(擲回例外即可顯示原因和 Retry);設定 `DisconnectHandler` 來撤銷存取。沒有處理常式時流程是模擬的,不會傳送任何內容。沒有 `Connectors` 時,卡片列出它的 `Simulation` 中的應用程式,並在出現在螢幕上後播放它的腳本示範;兩者都沒有時不列出任何應用程式。 + + + Raised once a connection completes, with the account, scopes and sync count. Bubbles. + 連結完成後引發,附帶帳號、權限範圍和同步數量。會向上傳遞。 + + + Raised once access to an app was revoked. Bubbles. + 對某個應用程式的存取被撤銷後引發。會向上傳遞。 + + + The assistant's name, used in the description, the scopes heading, the confirmation and the footer. Null uses the simulation's while it plays; with no name the texts say "your assistant". + 助理的名稱,用於描述、權限範圍標題、確認問題和頁尾。為 null 時,播放模擬期間使用模擬中的名稱;沒有名稱時文字會寫成「your assistant」。 + + + Plays the `Simulation`'s scripted run once the card is on screen. It stops at the first pointer or key press. Null plays it while the simulation's apps show and there is no connect handler. + 卡片出現在螢幕上後播放 `Simulation` 的腳本示範。首次指標或按鍵操作時停止。為 null 時,在顯示模擬的應用程式且沒有連結處理常式時播放。 + + + Runs your OAuth flow for an app. Null simulates it (nothing is sent). + 為某個應用程式執行你的 OAuth 流程。為 null 時進行模擬(不傳送任何內容)。 + + + The ids of the connected apps (two-way). The card writes the new list after each connect or disconnect finishes. Null at first connects nothing, or the simulation's connected apps while it plays. + 已連結應用程式的 id(雙向)。每次連結或中斷完成後,卡片寫回新的清單。初始為 null 時不連結任何應用程式;播放模擬期間則連結模擬中已連結的應用程式。 + + + Raised with the new list of connected ids after a connect or disconnect finishes. Bubbles. + 連結或中斷完成後,以新的已連結 id 清單引發。會向上傳遞。 + + + The apps to list. Null lists the `Simulation`'s apps, or none without one. + 要列出的應用程式。為 null 時列出 `Simulation` 中的應用程式,沒有模擬時不列出任何應用程式。 + + + The line under the heading; built from the assistant's name when null. + 標題下的一行文字;為 null 時依助理名稱產生。 + + + Revokes access after the inline confirmation. Null simulates it. + 在行內確認之後撤銷存取。為 null 時進行模擬。 + + + The footer under the list (text or any content); a short privacy note when null. + 清單下方的頁尾(文字或任意內容);為 null 時顯示一段簡短的隱私說明。 + + + Shows the footer. + 顯示頁尾。 + + + Shows the search field. + 顯示搜尋欄位。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個詞。 + + + The search field's placeholder; "Search apps" when null. + 搜尋欄位的預留位置文字;為 null 時為 "Search apps"。 + + + The search text (two-way); it matches name, description, account and keywords. + 搜尋文字(雙向);比對名稱、描述、帳號和關鍵字。 + + + The apps the card lists and plays on its own while `Connectors` is null: the assistant's name, the apps, the ones already connected and the scripted run, played exactly like your own apps would connect (simulated without a connect handler). Null lists no apps instead. + `Connectors` 為 null 時卡片自行列出並播放的應用程式:助理的名稱、應用程式、已連結的應用程式和腳本示範,播放方式與你自己的應用程式連結完全相同(沒有連結處理常式時為模擬)。為 null 時改為不列出任何應用程式。 + + + The card's heading; "Connect your tools" when null. + 卡片的標題;為 null 時為 "Connect your tools"。 + + + Every word `ConnectorSetup` shows or says, in English by default. Replace the whole object to localize or reword; texts that hold a name, the assistant or a count are format functions. + `ConnectorSetup` 顯示或朗讀的每一個詞,預設為英文。取代整個物件即可在地化或改寫;包含名稱、助理或數量的文字是格式化函式。 + + + The apps a `ConnectorSetup` lists and plays on its own while the app sets no `Connectors`: the assistant's name, the apps, the ones already connected and a scripted run of connects (one of them can fail first, then succeed on retry). The run starts once the card is on screen and stops for good at the first pointer or key press inside it; each connect is simulated (sign-in, approval, the first sync counting up) and nothing is sent. Set the data before handing the simulation to the card; to show other apps, give the card a new simulation. + 應用程式沒有設定 `Connectors` 時 `ConnectorSetup` 自行列出並播放的應用程式:助理的名稱、應用程式、已連結的應用程式,以及一段連結的腳本示範(其中一次可以先失敗,重試後成功)。示範在卡片出現在螢幕上後開始,並在卡片內第一次出現指標按下或按鍵時永久停止;每次連結都是模擬的(登入、核准、首次同步計數上升),不會傳送任何內容。請在把模擬交給卡片之前設定好資料;要顯示其他應用程式,就給卡片一個新的模擬。 + + + One step of the scripted run of a `ConnectorSetupSimulation`. + `ConnectorSetupSimulation` 腳本示範中的一步。 + Secondary actions kept close to the selected object (Arc context menu). Wraps its `Content` and opens a menu at the pointer on right-click, or under the target (12px in, 8px below) on a plain click, a tap, the ContextMenu key or Shift+F10. The menu grows from the pointer on a snappy spring, rows press with a tint, and it closes on any scroll, resize or outside press instead of repositioning. Avoid it on targets that need their own click. To give any control a right-click menu without wrapping it, attach a surface: `<Border><arc:ContextMenu.Menu><arc:MenuSurface>…</arc:MenuSurface></arc:ContextMenu.Menu></Border>`. 靠近所選物件的次要動作(Arc 內容選單)。包裹其 `Content`,在右鍵時於指標處開啟選單,或在一般按一下、輕觸、ContextMenu 鍵或 Shift+F10 時在目標下方(向內 12px、向下 8px)開啟。選單以輕快的彈簧從指標處展開,選單列按下時會著色,捲動、調整大小或外部按壓時會關閉而不是重新定位。避免用於本身需要按一下功能的目標。若要讓任意控制項擁有右鍵選單而無需包裹,可附加一個表面:`<Border><arc:ContextMenu.Menu><arc:MenuSurface>…</arc:MenuSurface></arc:ContextMenu.Menu></Border>`。 @@ -5117,8 +6417,8 @@ 在目標上一般按一下或輕觸是否開啟選單(Arc 為 true)。 - A model's context window as a fixed budget of 200 cells (20 × 10) that fills in reading order: the system prompt, tool definitions, files and memory, the summary and the conversation, then free space, with the output reserve held as outlined cells at the end. A dashed line marks the compaction threshold. The header reads the tokens used, the window, the percentage and the free tokens, with a pill to switch models; a legend lists every part with its tokens and share; Send message, Attach file and Compact now change the window, and a status line says what happened. Crossing the threshold after a send or an attachment, or switching to a model the window overfills, folds every message but the newest two into one summary (about 6% of them, between 4K and 24K), and the space comes back. The card rewrites `Items` as it goes and raises `ItemsChanged`, `Compacted` and `ModelChanged`. Given no data at all it shows an invented sample session with three sample models (`ContextWindowSamples`). - 以 200 個格子(20 × 10)組成的固定預算來呈現模型的上下文視窗,依閱讀順序填入:系統提示、工具定義、檔案與記憶、摘要與對話,然後是剩餘空間,輸出保留量以輪廓格子保留在末端。虛線標示壓縮門檻。標題列顯示已用權杖、視窗大小、百分比與剩餘權杖,並附一個切換模型的膠囊;圖例列出每個部分的權杖數與佔比;傳送訊息、附加檔案與立即壓縮會改變視窗,狀態列說明發生了什麼。傳送或附加後超過門檻,或切換到視窗容納不下的模型時,除最新兩則外的所有訊息會合併為一則摘要(約佔其中的 6%,介於 4K 與 24K 之間),空間因此釋出。此卡片會在執行時改寫 `Items`,並引發 `ItemsChanged`、`Compacted` 與 `ModelChanged`。若完全沒有資料,它會顯示內建的虛構範例工作階段與三個範例模型(`ContextWindowSamples`)。 + A model's context window as a fixed budget of 200 cells (20 × 10) that fills in reading order: the system prompt, tool definitions, files and memory, the summary and the conversation, then free space, with the output reserve held as outlined cells at the end. A dashed line marks the compaction threshold. The header reads the tokens used, the window, the percentage and the free tokens, with a pill to switch models; a legend lists every part with its tokens and share; Send message, Attach file and Compact now change the window, and a status line says what happened. Crossing the threshold after a send or an attachment, or switching to a model the window overfills, folds every message but the newest two into one summary (about 6% of them, between 4K and 24K), and the space comes back. The card rewrites `Items` as it goes and raises `ItemsChanged`, `Compacted` and `ModelChanged`. Without models or items the card is empty: no model pill, every cell free, and no Send message or Attach file without `Turns` and `Files`. + 以 200 個格子(20 × 10)組成的固定預算來呈現模型的上下文視窗,依閱讀順序填入:系統提示、工具定義、檔案與記憶、摘要與對話,然後是剩餘空間,輸出保留量以輪廓格子保留在末端。虛線標示壓縮門檻。標題列顯示已用權杖、視窗大小、百分比與剩餘權杖,並附一個切換模型的膠囊;圖例列出每個部分的權杖數與佔比;傳送訊息、附加檔案與立即壓縮會改變視窗,狀態列說明發生了什麼。傳送或附加後超過門檻,或切換到視窗容納不下的模型時,除最新兩則外的所有訊息會合併為一則摘要(約佔其中的 6%,介於 4K 與 24K 之間),空間因此釋出。此卡片會在執行時改寫 `Items`,並引發 `ItemsChanged`、`Compacted` 與 `ModelChanged`。沒有模型或項目時,卡片為空:沒有模型膠囊,所有格子都空著;沒有 `Turns` 與 `Files` 時也沒有傳送訊息與附加檔案。 Defines the `AutoCompact` property. @@ -5480,6 +6780,94 @@ Replaces the Split shape's setup card (any control or content); null shows the setup card. 取代分割形態的設定卡片(可以是任意控制項或內容);為 null 時顯示設定卡片。 + + An isometric cube character that shows what an assistant is doing (Arc cube mascot), drawn from real 3D math: a rounded silhouette, three faces shaded by how they face the light, and a face painted on the front side through its projected plane. Idle tilts, floats and blinks; thinking turns the cube a quarter turn in eased steps and the face fades in on the side that arrives at the front; writing tips the face down while lines print onto the ground in front of it; done settles with a happy squint and a sparkle; error shakes once and shows worried brows. Status changes blend from the pose on screen over 420ms, never a hard cut. Leave `Status` unset and the cube plays through every state on a loop (for showcases). + 一個表示助理正在做什麼的等軸測立方體角色(Arc 立方體吉祥物),由真實的三維數學繪製:圓潤的輪廓、依朝向光線的程度著色的三個面,以及透過投影平面畫在正面上的一張臉。閒置時傾斜、浮動和眨眼;思考時立方體以緩動的步伐轉四分之一圈,臉在轉到正面的那一面上淡入;書寫時臉向下低,線條印在它前方的地面上;完成時帶著開心的瞇眼和閃光安頓下來;出錯時搖晃一次並露出擔憂的眉毛。狀態切換從螢幕上的姿態在 420ms 內過渡,從不生硬切換。不設定 `Status` 時,立方體會循環播放每種狀態(用於展示)。 + + + Overrides the whole accessible label; null (default) builds it from the name and the status. + 取代整個無障礙標籤;null(預設)時由名字和狀態組成。 + + + Announces status changes in a polite live region. Turn it on for the one mascot that carries status, not for decorative copies. + 在禮貌(polite)即時區域中朗讀狀態變化。只為承載狀態的那個吉祥物開啟,裝飾性的副本不要開啟。 + + + The assistant's name, used in the accessible label ("Assistant, thinking"). + 助理的名字,用於無障礙標籤("Assistant, thinking")。 + + + The cube's body color; the theme sets the accent. The top and the shaded side are mixed from it. + 立方體主體的顏色;主題設為強調色。頂面和背光側面由它混合而成。 + + + The status the cube shows: `Status`, or the current self-play step while it is unset. + 立方體顯示的狀態:`Status`,未設定時為自動播放的目前步驟。 + + + The color of the eyes, brows and mouth; the theme sets the accent foreground. + 眼睛、眉毛和嘴巴的顏色;主題設為強調色上的前景色。 + + + Overrides the front face's shading anchor; null uses `BodyBrush`. + 覆寫正面的著色錨點;null 時使用 `BodyBrush`。 + + + Freezes the cube on the current frame. It also pauses by itself off screen and while hidden, and resumes without a jump. + 把立方體凍結在目前的影格。它在螢幕外或被隱藏時也會自動暫停,並無跳動地繼續。 + + + Every word the cube speaks. + 立方體朗讀的所有文字。 + + + The color of the lines printed while writing; the theme sets the muted text color. + 書寫時印出的線條的顏色;主題設為弱化文字色。 + + + The color of the contact shadow (drawn translucent); null (default) uses the theme's shadow tone. + 接觸陰影的顏色(半透明繪製);null(預設)時使用主題的陰影色調。 + + + Draws the contact shadow and the lines printed while writing (at 56px and up). + 繪製接觸陰影和書寫時印出的線條(56px 以上)。 + + + Overrides the shaded side's anchor; null mixes it from `BodyBrush` toward black. + 覆寫背光側面的錨點;null 時由 `BodyBrush` 向黑色混合而成。 + + + Width and height in px, from 24 (inline next to a message) to 160 (empty state). Below 56 a compact drawing takes over: a larger cube in the frame, bolder eyes, no mouth, shadow or printed lines. + 寬和高(px),從 24(內嵌在訊息旁)到 160(空狀態)。小於 56 時改用精簡畫法:立方體在畫框中更大,眼睛更粗,沒有嘴巴、陰影和印出的線條。 + + + The color of the done sparkle; the theme sets the accent. + 完成時閃光的顏色;主題設為強調色。 + + + What the assistant is doing. Leave it unset (null) and the cube plays through idle, thinking, writing, done and error on a loop, which is meant for showcases; `StatusChanged` reports each step. + 助理正在做什麼。不設定(null)時立方體會循環播放閒置、思考、書寫、完成和出錯,用於展示;`StatusChanged` 回報每一步。 + + + Raised when the self-playing loop (status unset) moves to the next state. + 自動播放的循環(未設定狀態)進入下一個狀態時觸發。 + + + Overrides the top face's shading anchor; null mixes it from `BodyBrush` toward white. + 覆寫頂面的著色錨點;null 時由 `BodyBrush` 向白色混合而成。 + + + Every word a `CubeMascot` speaks, in English by default: the status words and the pattern that joins them with the assistant's name into the accessible label ("Assistant, thinking"). Replace the instance (or set its properties) to translate or reword. + `CubeMascot` 朗讀的所有文字,預設是英文:狀態詞,以及把狀態詞與助理名字連成無障礙標籤的格式("Assistant, thinking")。取代這個執行個體(或設定它的屬性)即可翻譯或改寫。 + + + What the assistant behind a `CubeMascot` is doing; each status has its own pose, face and motion. + `CubeMascot` 背後的助理正在做什麼;每種狀態都有自己的姿態、表情和動效。 + + + Arguments of `StatusChanged`: the status the self-playing loop moved to. + `StatusChanged` 的參數:自動播放的循環進入的狀態。 + A customer galaxy: every account a dot, its area by monthly recurring revenue and its color by health, slowly turning as a tilted two-armed spiral. Group the accounts by any dimension and the dots spring into one packed cluster per value, labelled with its MRR and size; pan, zoom about the pointer, double-click a cluster to fly into it, hover a dot for its card and click it to select it. A totals panel beside the stage (under it in narrow containers) shows the MRR, accounts, growing and at-risk MRR of everything or of the focused cluster, and a list of the clusters by MRR or by accounts whose rows fly to their cluster. 客戶星系:每個帳戶是一個點,面積由月度經常性收入(MRR)決定,顏色由健康度決定,以傾斜的雙臂螺旋緩慢旋轉。依任意維度將帳戶分組後,這些點會彈跳聚合成每個分組值對應的一個緊密叢集,並標示其 MRR 與規模;可平移、以指標為中心縮放、雙擊叢集飛入其中,懸停點可查看其卡片,點選即可選取。舞台旁(窄容器中在其下方)是彙總面板,顯示全部或所選叢集的 MRR、帳戶數、成長與風險 MRR,以及依 MRR 或帳戶數排序的叢集清單,清單中各列可飛至對應叢集。 @@ -6380,6 +7768,114 @@ Raised when a range is applied. 套用範圍時引發。 + + A screenshot-to-code card (Arc Design to code, design reference). The person drops a UI screenshot or imports a frame from a design file; a scan line crosses it and the layout regions it finds appear as boxes with tags; then code streams in beside it, linked region by region (pointing at a box or a tag highlights its lines and the other way round), with a working live preview one tab away. Generation comes from `Generator`, an async delegate that adds regions and writes code chunks through a `DesignToCodeContext`. Without one the card plays a built-in simulation on a sample pricing card and, while it is on screen, loops it (generate, hold on the code, show the preview, start again) until the first pointer or key press inside the card. Dropping a file uses Avalonia drag and drop; Choose file raises `ChooseFileRequested` for your picker (hand the file to `LoadFileAsync`). + 截圖轉程式碼卡片(Arc Design to code,設計參考)。使用者拖入介面截圖或從設計檔匯入一個畫框;掃描線掃過它,找到的版面區域以帶標籤的方框出現;接著程式碼在旁邊串流寫出,並與區域一一對應(指向方框或標籤會反白它的程式碼行,反之亦然),切換一個分頁即可看到可實際操作的即時預覽。產生來自 `Generator`,這是一個透過 `DesignToCodeContext` 新增區域並寫入程式碼片段的非同步委派。沒有產生器時,卡片在範例價格卡片上播放內建模擬,並在可見期間循環播放(產生、停留在程式碼上、顯示預覽、重新開始),直到卡片內第一次出現指標按下或按鍵。拖入檔案使用 Avalonia 拖放;Choose file 會引發 `ChooseFileRequested`,供你開啟檔案選擇器(把檔案交給 `LoadFileAsync`)。 + + + The file types accepted, as in an input's accept attribute (MIME types, `image/*` or extensions). Other files show an error. + 接受的檔案類型,寫法同輸入框的 accept 屬性(MIME 類型、`image/*` 或副檔名)。其他檔案會顯示錯誤。 + + + Plays the `Simulation` on a loop while the card is on screen (generate, hold on the code, show the preview, start again), and stops at the first pointer or key press inside the card. Null plays it when there is no `Generator`. Under reduced motion it plays once. + 卡片在畫面上時循環播放 `Simulation`(產生、停留在程式碼上、顯示預覽、重新開始),並在卡片內第一次出現指標按下或按鍵時停止。為 null 時,在沒有 `Generator` 的情況下播放。在減少動態效果下只播放一次。 + + + Raised when the person presses Choose file. Bubbles. Open your file picker, pass the file to `LoadFileAsync` and set Handled; when nobody handles it the card uses the sample and says nothing was opened. + 使用者按下 Choose file 時引發。會冒泡。開啟你的檔案選擇器,把檔案交給 `LoadFileAsync` 並設定 Handled;沒有人處理時,卡片改用範例,並說明沒有開啟任何檔案。 + + + The code written so far (the finished code once `Phase` is Done). + 目前已寫出的程式碼(`Phase` 為 Done 時即完成的程式碼)。 + + + Raised with the finished code when a run completes. Bubbles. + 一次執行完成時引發,附帶完成的程式碼。會冒泡。 + + + Runs your design file picker or link flow for Import design file: return the frame (with its rendered image) or null when cancelled. Null simulates an import of the simulation's design, if any. + 為「匯入設計檔」執行你的設計檔選擇器或連結流程:回傳畫框(帶算繪後的影像),取消時回傳 null。為 null 時模擬匯入模擬的設計(如果有)。 + + + The problem the status names (a rejected file, a failed import or run), or null. + 狀態中指出的問題(被拒絕的檔案、失敗的匯入或執行),沒有時為 null。 + + + The file name shown in the code header, in the status while writing and in the copy button's name. Empty uses the simulation's while it plays. + 顯示在程式碼標題列、撰寫時的狀態以及複製按鈕名稱中的檔名。為空時,播放模擬期間使用模擬的檔名。 + + + Turns a source into code: call `AddRegion` as parts are detected, then `Write` as code streams, with a region id to link a chunk to its box. Complete the task when done, throw to show the error, and stop when the context's token is cancelled. Null plays the `Simulation`, if any. + 把來源轉換為程式碼:辨識出部分時呼叫 `AddRegion`,程式碼串流抵達時呼叫 `Write`,並用區域 id 把程式碼片段與它的方框連結。完成時結束工作,擲出例外以顯示錯誤,並在內容的權杖取消時停止。為 null 時播放 `Simulation`(如果有)。 + + + Every word the card shows or announces. + 卡片顯示或朗讀的每一個詞。 + + + The language the code is highlighted as (a TextMate grammar name or alias such as tsx, jsx, html or vue). + 程式碼醒目標示所用的語言(TextMate 語法名稱或別名,例如 tsx、jsx、html 或 vue)。 + + + The largest file accepted, in bytes (10 MB by default). + 接受的最大檔案,單位為位元組(預設 10 MB)。 + + + The model credited in the code header: its mark and name. Null credits the simulation's model while it plays, and none otherwise. + 在程式碼標題列中註明的模型:它的標誌與名稱。為 null 時,播放模擬期間註明模擬的模型,否則不註明。 + + + Where the card is in its run. + 卡片在本次執行中所處的階段。 + + + Renders the finished code in the Preview tab (for example in a sandboxed web view; never run generated code in your app). Return null for no preview. Null uses the simulation's preview after a simulated run, and a short note otherwise. + 在「預覽」分頁中算繪完成的程式碼(例如放在沙箱化的網頁檢視中;切勿在你的應用程式內執行產生的程式碼)。回傳 null 表示沒有預覽。為 null 時,模擬執行之後使用模擬的預覽,其他情況顯示一則簡短說明。 + + + The design and code the card plays while there is no `Generator`: its design shows while `Source` is unset, Use sample brings it back, and Generate (or the autoplay loop) scans it and streams its code exactly like a generator would. Null leaves the card on its drop zone. + 沒有 `Generator` 時卡片播放的設計與程式碼:`Source` 未設定期間顯示它的設計,「Use sample」會把它帶回來,「產生」(或自動播放循環)會掃描它並串流輸出它的程式碼,方式與產生器完全相同。為 null 時卡片停在拖放區。 + + + The design shown (two-way). Leave it unset to show the `Simulation`'s design (or the drop zone without one, or when `Generator` is set); set it to null to open on the drop zone. A source without an image draws its `Sketch`. + 顯示的設計(雙向)。不設定時顯示 `Simulation` 的設計(沒有模擬或設定了 `Generator` 時顯示拖放區);設為 null 則以拖放區開啟。沒有影像的來源繪製它的 `Sketch`。 + + + The layer open on the output side (two-way). Each new run returns to Code so the stream is visible. + 輸出那一側開啟的圖層(雙向)。每次新的執行都會回到「程式碼」,讓串流輸出可見。 + + + Arguments of `Completed`: the finished code and what it was made from. + `Completed` 的引數:完成的程式碼以及它的來源。 + + + What a `Generator` gets besides the source: a token that is cancelled when the person stops the run (Escape or Stop) or replaces the design, `AddRegion` for each layout region as it is detected, and `Write` for each chunk of code as it streams. Both may be called from any thread; calls after the run ended are ignored. + `Generator` 除來源之外取得的內容:一個權杖,在使用者停止執行(Escape 或 Stop)或取代設計時取消;每辨識出一個版面區域時呼叫的 `AddRegion`;以及程式碼串流抵達時為每個程式碼片段呼叫的 `Write`。兩者都可以在任何執行緒呼叫;執行結束後的呼叫會被忽略。 + + + Every word `DesignToCode` shows or announces, in English by default. Replace the whole object to localize or reword; sentences with values are format functions so the word order can change. + `DesignToCode` 顯示或朗讀的每一個詞,預設為英文。取代整個物件即可在地化或改寫;帶數值的句子是格式化函式,因此可以調整語序。 + + + A layout region found on the design: an id that code chunks refer to, a short label for its tag, and its box in percent of the design's width and height (0 to 100, from the top left). + 在設計上找到的一個版面區域:程式碼片段參照的 id、標籤上的簡短文字,以及以設計寬度與高度百分比表示的方框(0 到 100,從左上角起算)。 + + + A design and the code a model could write for it, which a `DesignToCode` plays while the app wires no `Generator`: the design (an image or a `Sketch`), the layout regions the scan finds, the code part by part with the region each part implements, and optionally the model credited, the file name and a preview. With `AutoPlay` the card loops it while it is on screen (generate, hold on the code, show the preview, start again) until someone presses or types inside it. Nothing is uploaded or sent. Set the data before handing the simulation to the card; to play another design, give the card a new simulation. + 一個設計以及模型可能為它寫出的程式碼,在應用程式沒有接上 `Generator` 時由 `DesignToCode` 播放:設計(影像或 `Sketch`)、掃描找到的版面區域、逐段的程式碼及每段實作的區域,以及可選的署名模型、檔名與預覽。開啟 `AutoPlay` 時,卡片在畫面上會循環播放它(產生、停留在程式碼上、顯示預覽、重新開始),直到有人在卡片內按下或輸入。不會上傳或傳送任何內容。請在把模擬交給卡片之前設定好資料;要播放另一個設計,就給卡片一個新的模擬。 + + + One part of the code of a `DesignToCodeSimulation`: its text and the region it implements. + `DesignToCodeSimulation` 程式碼中的一段:它的文字以及它實作的區域。 + + + The theme colors, font and corner radius a `DesignToCodeSketch` draws with. + `DesignToCodeSketch` 繪製時使用的主題色彩、字型與圓角。 + + + The design a `DesignToCode` card turns into code: a name, where it came from and its image, or a `Sketch` that draws it in code in the card's colors. + `DesignToCode` 卡片轉換為程式碼的設計:名稱、來源以及影像,或者一個以卡片的色彩用程式碼繪製它的 `Sketch`。 + A focused surface for decisions that need attention: confirmations, short forms, step flows whose title crossfades. Declare it anywhere in XAML; it renders nothing in place and shows its panel above a blurred scrim while `IsOpen` is true: The overlay fades in while the panel rises 8px from 0.96 on the smooth spring; closing is shorter and starts from wherever the entrance is. Escape and a press on the scrim close it (cancellable through `Closing`); Tab cycles inside; focus returns to the opener. Code can await a result with `ShowAsync` or `ConfirmAsync`. 用於需要注意之決策的聚焦表面:確認、簡短表單、標題淡入切換的分步流程。可在 XAML 中任意位置宣告;它不會在原位置渲染任何內容,當 `IsOpen` 為 true 時,會在模糊遮罩之上顯示其面板。覆蓋層淡入,面板以平滑彈簧從 0.96 縮放並上升 8px;關閉比進入更短,並從進入動畫當前所在的位置開始。Escape 與在遮罩上按下都會關閉(可透過 `Closing` 取消);Tab 在內部循環;焦點返回至開啟它的元素。程式碼可透過 `ShowAsync` 或 `ConfirmAsync` 等待結果。 @@ -6392,6 +7888,118 @@ Panel width: Small 360, Medium 440 (Arc), Large 560 — always within the viewport minus 32px. 面板寬度:Small 360、Medium 440(Arc)、Large 560——始終不超過視埠寬度減去 32px。 + + A chat composer with voice input (Arc Dictation button, design reference): the field, a model label, the dictation mic and send. Pressing the mic springs it into a recording pill with a live waveform, a timer, cancel and Insert; words stream into the field as the person speaks, the recognizer's guesses muted until they settle. Insert appends the text and puts the caret at its end; Enter sends. Speech comes from `Recognizer` (an `IDictationRecognizer` you write for your platform or service). For demos, a `Simulation` speaks the scripts you give it without touching the microphone and, unless `AutoPlay` says otherwise, loops a scripted dictation until the person first touches the card. With neither, the composer is a plain field whose mic says dictation is not available. Handle `SendRequested` to send; without a handler sending is simulated. + 具備語音輸入的聊天輸入框(Arc Dictation button,設計參考):輸入框、模型標籤、聽寫麥克風與傳送按鈕。按下麥克風,它以彈簧動態展開成附即時波形、計時器、取消與插入的錄音膠囊;使用者說話時文字流入輸入框,辨識器的猜測以淡色顯示,確定後轉為內文顏色。插入會附加文字並把游標放在末端;按 Enter 傳送。語音來自 `Recognizer`(你為所在平台或服務撰寫的 `IDictationRecognizer`)。用於展示時,`Simulation` 會在不存取麥克風的情況下朗讀你提供的腳本,並且除非 `AutoPlay` 另有設定,會循環播放一段腳本化聽寫,直到使用者第一次操作卡片。兩者都沒有時,輸入框只是一個一般欄位,按下麥克風會提示聽寫無法使用。處理 `SendRequested` 以傳送;沒有處理常式時傳送是模擬的。 + + + Loops a scripted dictation (listen, insert, send) until the first pointer, key or focus inside the card; it pauses off screen. Null plays it only with a `Simulation` and no `Recognizer`. For docs and demos, not production. + 循環播放一段腳本化聽寫(聆聽、插入、傳送),直到卡片內第一次出現指標、按鍵或焦點;不在畫面上時暫停。為 null 時只在有 `Simulation` 且沒有 `Recognizer` 時播放。供文件與展示使用,不適用於正式環境。 + + + Raised with the final text of a dictation, after it has been appended to the field. Bubbles. + 攜帶聽寫的最終文字,在它附加到輸入框之後引發。會向上傳遞。 + + + True while a send is pending (the send button shows a spinner). + 傳送進行中時為 true(傳送按鈕顯示轉圈)。 + + + Every word the composer shows or says. + 輸入框顯示或朗讀的所有文字。 + + + Seconds before listening stops and inserts on its own; 0 turns the limit off. + 聆聽自動停止並插入之前的秒數;0 表示不限制。 + + + The model label at the start of the toolbar (the model the composer sends to); null (the default) hides it. + 工具列開頭的模型標籤(輸入框傳送到的模型);為 null(預設)時隱藏。 + + + The field's placeholder, such as "Message" and the assistant's name; null (the default) shows none. + 輸入框的預留位置文字,例如 "Message" 加上助理的名稱;為 null(預設)時不顯示。 + + + The speech source. Null uses `Simulation`; with neither, the mic says dictation is not available. + 語音來源。為 null 時使用 `Simulation`;兩者都沒有時,麥克風會提示聽寫無法使用。 + + + Raised to send the message (Enter in the field or the send button). Set `Completion` to your sending task: the button spins until it completes and the field clears when it succeeds. Without a handler the send is simulated. Bubbles. + 傳送訊息時引發(在輸入框中按 Enter 或按下傳送按鈕)。把 `Completion` 設為你的傳送工作:按鈕會轉圈直到工作完成,成功後輸入框清空。沒有處理常式時傳送是模擬的。會向上傳遞。 + + + A simulated voice to dictate with while `Recognizer` is null: it never touches the microphone and speaks its scripts, and with `AutoPlay` left null the composer loops it on its own. Its clock stands still while the card is off screen. Null (the default) plays nothing. Give each composer its own simulation. + 在 `Recognizer` 為 null 時用於聽寫的模擬聲音:它從不存取麥克風,只朗讀自己的腳本;`AutoPlay` 維持 null 時輸入框會自行循環播放它。卡片不在畫面上時它的時鐘會停止。為 null(預設)時不播放任何內容。請給每個輸入框各自的模擬執行個體。 + + + Where the dictation is. + 聽寫所處的狀態。 + + + Raised on every status change, with the error code for denied and error. Bubbles. + 每次狀態變化時引發;遭拒與發生錯誤時附帶錯誤代碼。會向上傳遞。 + + + The field's text (two-way): typing, an inserted dictation and the clear after sending change it. + 輸入框的文字(雙向):輸入、插入的聽寫內容與傳送後的清空都會改變它。 + + + Raised whenever the live transcript of the current dictation changes. Bubbles. + 目前聽寫的即時轉寫內容每次變化時引發。會向上傳遞。 + + + Every word `DictationButton` and `DictationControl` show or say, in English by default. Replace the whole object to localize or reword; counts, times and errors are format functions. + `DictationButton` 與 `DictationControl` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;計數、時間與錯誤是格式化函式。 + + + Just the mic and its recording pill, for a composer you already have (Arc Dictation button, design reference). Pressing the mic springs it leftward into a 216px pill: cancel, a live waveform, the elapsed time and Insert, which sits where the mic was. Words arrive through `TranscriptChanged` (render them in your field, the interim part muted) and the final text through `Committed`. A blocked microphone or a failure turns the slot into Try again. Speech comes from `Recognizer`; for demos, a `Simulation` speaks the scripts you give it and records nothing. With neither, pressing the mic says dictation is not available. Same state machine as `DictationButton`, no field. + 只有麥克風與它的錄音膠囊,用於你已有的輸入框(Arc Dictation button,設計參考)。按下麥克風,它向左以彈簧動態展開成 216px 的膠囊:取消、即時波形、已錄時間與插入,插入位於麥克風原本的位置。文字透過 `TranscriptChanged` 送達(在你的輸入框中呈現,暫定部分用淡色),最終文字透過 `Committed` 送達。麥克風遭封鎖或發生錯誤時,該位置會變成 Try again。語音來自 `Recognizer`;用於展示時,`Simulation` 會朗讀你提供的腳本,不錄下任何內容。兩者都沒有時,按下麥克風會提示聽寫無法使用。與 `DictationButton` 使用同一個狀態機,但沒有輸入框。 + + + Raised when the person cancels; drop any live text you rendered. Bubbles. + 使用者取消時引發;請捨棄你呈現的即時文字。會向上傳遞。 + + + Raised with the final text of a dictation, to append to your field. Bubbles. + 攜帶聽寫的最終文字引發,供附加到你的輸入框。會向上傳遞。 + + + Every word the control shows or says. + 控制項顯示或朗讀的所有文字。 + + + Seconds before listening stops and inserts on its own; 0 turns the limit off. + 聆聽自動停止並插入之前的秒數;0 表示不限制。 + + + The speech source. Null uses `Simulation`; with neither, starting fails as unavailable. + 語音來源。為 null 時使用 `Simulation`;兩者都沒有時,啟動會以無法使用失敗。 + + + A simulated voice to dictate with while `Recognizer` is null: it never touches the microphone and speaks its scripts, and the control says nothing is recorded. Null (the default) leaves the mic unavailable. Give each control its own simulation. + 在 `Recognizer` 為 null 時用於聽寫的模擬聲音:它從不存取麥克風,只朗讀自己的腳本,控制項會說明沒有錄下任何內容。為 null(預設)時麥克風無法使用。請給每個控制項各自的模擬執行個體。 + + + Raised when the recognizer detects the end of an utterance. Do nothing, or confirm hands-free. Bubbles. + 辨識器偵測到一句話結束時引發。可以不做任何事,或免手動確認。會向上傳遞。 + + + Where the dictation is. + 聽寫所處的狀態。 + + + Raised on every status change, with the error code for denied and error. Bubbles. + 每次狀態變化時引發;遭拒與發生錯誤時附帶錯誤代碼。會向上傳遞。 + + + The live transcript of the current dictation. + 目前聽寫的即時轉寫內容。 + + + Raised whenever the live transcript changes (render it in your field, the interim part muted). Bubbles. + 即時轉寫內容每次變化時引發(在你的輸入框中呈現,暫定部分用淡色)。會向上傳遞。 + Turns a document into typed data and shows where each value came from. A quiet viewer of the document sits on the left and an editable form on the right; while a model reads, the fields fill in one by one and the text each value came from is tinted. Pointing at a field or at its text lights both and draws a curved link between them; values the model is unsure of carry its reason and a "Looks right" button; "Accept all" hands the typed result to the app. 文件擷取會把文件轉為具型別的資料,並顯示每個值的來源。左側是文件的唯讀檢視器,右側是可編輯的表單;模型讀取時,欄位會逐一填入,每個值的來源文字會被上色。指向欄位或其文字會同時點亮兩者,並畫出一條曲線連線;模型不確定的值會附上理由和「看起來正確」按鈕;「全部接受」把具型別的結果交給應用程式。 @@ -6516,6 +8124,122 @@ The zoom steps of the buttons and the keys. 按鈕和按鍵的縮放級距。 + + The certificate that was issued once every record was valid. + 憑證簽發後的資訊:所有記錄有效之後簽發的憑證。 + + + Who hosts the domain's DNS, shown as "DNS hosted at {name}" with a small mark. + 代管網域 DNS 的服務商,顯示為帶一個小標記的「DNS hosted at {name}」。 + + + A DNS record the customer has to create. + 客戶需要建立的一筆 DNS 記錄。 + + + What one check found for one record. + 一次檢查對一筆記錄的發現。 + + + A custom domain setup card. The customer adds a domain and gets the DNS records to create, each with a copy button; a live checker then polls the resolvers, marks every record valid, not found or misconfigured (with what was found and a one-line fix) and finishes with the domain verified and its SSL certificate issued. The three server steps are async delegates: `GetRecordsAction` lists the records, `VerifyAction` is one check and `IssueCertificateAction` runs once every record is valid. The card does the polling, stops on "all valid" or any conflicting record and cancels everything when it is removed or leaves the window. With none of them set it plays a simulated flow (add, check, a conflicting record, fix, verified) until someone clicks or types in it. + 自訂網域設定卡片。客戶新增網域並取得需要建立的 DNS 記錄(每筆都附有複製按鈕);即時檢查器隨後輪詢解析器,把每筆記錄標為有效、找不到或設定錯誤(附帶找到的內容和一行修正建議),最後完成網域驗證並簽發 SSL 憑證。三個伺服器步驟是非同步委派:`GetRecordsAction` 列出記錄,`VerifyAction` 做一次檢查,`IssueCertificateAction` 在所有記錄有效後執行。卡片負責輪詢,在「全部有效」或出現衝突記錄時停止,並在被移除或離開視窗時取消一切。三個委派都沒設定時,它會播放一段模擬流程(新增、檢查、衝突記錄、修正、驗證通過),直到有人在其中點擊或輸入。 + + + Plays the simulated flow once the card is on screen: add, check, fix, verified. Any click or key press inside the card hands control to the person. Null plays only while no action is set. + 卡片出現在畫面上後播放模擬流程:新增、檢查、修正、驗證通過。卡片內的任何點擊或按鍵都會把控制權交給使用者。為 null 時僅在沒有設定任何動作時播放。 + + + The certificate once it was issued; null before. + 憑證簽發後的憑證;簽發之前為 null。 + + + The padding of a record row in the compact layout; the theme sets it from the token. + 緊湊版面下一筆記錄列的內距;由主題根據 Token 設定。 + + + Below this width of the card the records stack and the column header leaves (544). + 卡片寬度低於此值時,記錄改為堆疊,欄標題消失(544)。 + + + The domain the field starts with. A protocol, path, port and trailing dot are stripped before the domain is checked. Empty starts with an empty field. + 輸入框的初始網域。檢查之前會先去掉通訊協定、路徑、連接埠和結尾的點。為空則輸入框初始為空。 + + + The line under the heading (at most 520 wide); null leaves it out. + 標題下方的一行文字(最寬 520);為 null 則不顯示。 + + + The domain that was added (host name only); null before one was added and after it was removed. + 已新增的網域(僅主機名稱);新增之前和移除之後為 null。 + + + Raised when one of the actions threw; the card shows the message and can be tried again. + 某個動作丟出例外時觸發;卡片顯示訊息並可再次嘗試。 + + + Lists the records for a domain: awaited when the domain is added, with a token that removing the domain or the card leaving the window cancels. An exception returns to the field with the message under it and raises `Failed`. Null simulates a server (a CNAME, or an A record on an apex domain, an ownership TXT and a CAA). + 列出網域的記錄:新增網域時等待它,權杖會在移除網域或卡片離開視窗時取消。例外會回到輸入框並在其下顯示訊息,同時觸發 `Failed`。為 null 時模擬伺服器(一筆 CNAME,頂級網域則為 A 記錄,加一筆擁有權 TXT 和一筆 CAA)。 + + + Draws a bordered card with rounded corners (true); false lays the block flat on the page. + 繪製帶圓角的邊框卡片(true);false 則讓區塊平鋪在頁面上。 + + + Issues the SSL certificate once every record is valid; the domain row says "Issuing SSL certificate" until it returns. Null simulates an issuer. + 所有記錄有效後簽發 SSL 憑證;回傳之前,網域列顯示「Issuing SSL certificate」。為 null 時模擬簽發。 + + + Every word the card shows or announces. + 卡片顯示或播報的每一個詞。 + + + Where the card is in its flow. + 卡片所處的流程階段。 + + + Raised on every phase change. + 每次階段變化時觸發。 + + + Time between two checks; null uses 800ms while the check is simulated and 5s with a `VerifyAction`. + 兩次檢查之間的間隔;為 null 時,模擬檢查用 800 毫秒,設定了 `VerifyAction` 則用 5 秒。 + + + Who hosts the domain's DNS, shown as "DNS hosted at {name}" with a small mark; null hides the line. + 代管網域 DNS 的服務商,顯示為帶一個小標記的「DNS hosted at {name}」;為 null 則隱藏這一行。 + + + Raised when the domain is removed (the close button or "Add another"). + 網域被移除時觸發(關閉按鈕或「Add another」)。 + + + Run with the `DomainRemovedEventArgs` when the domain is removed. + 網域被移除時,以 `DomainRemovedEventArgs` 為參數執行。 + + + The padding of a record row; the theme sets it from the token. + 一筆記錄列的內距;由主題根據 Token 設定。 + + + The card's heading; null leaves it out. + 卡片的標題;為 null 則不顯示。 + + + Raised once every record is valid and the certificate was issued. + 所有記錄有效且憑證簽發後觸發。 + + + Run with the `DomainVerifiedEventArgs` once the certificate is issued. + 憑證簽發後,以 `DomainVerifiedEventArgs` 為參數執行。 + + + One check of the records: awaited on every poll with the domain, the records and which attempt of which run this is. Return a verdict per record. Return `Missing` while a record propagates and `Invalid` only for a conflicting value. The card polls while any record is missing and stops when all are valid or any is invalid. An exception stops the run, shows the message and raises `Failed`. Null plays a scripted check. + 對記錄做一次檢查:每次輪詢都會等待它,參數是網域、記錄,以及這是哪一輪的第幾次。為每筆記錄回傳結論。記錄仍在生效時回傳 `Missing`,只有值衝突時才回傳 `Invalid`。只要有記錄缺失,卡片就繼續輪詢,全部有效或任一無效時停止。例外會中止這一輪、顯示訊息並觸發 `Failed`。為 null 時播放腳本化的檢查。 + + + The result of one check: a verdict per record and the propagation count. + 一次檢查的結果:每筆記錄的結論和生效統計。 + Arc's donut chart: a few parts of one whole. The legend and the ring show and hide segments together, the center reads the active segment (or the total at rest) and rolls like a drum between readouts, small parts fold into "Other", and new data moves every arc from where it is on screen, keeping its velocity. The first time the chart is seen all segments sweep out of the top together, each trailing edge a beat behind. Arc interpolates start and end angles rather than paths; every angle is a `MotionValue` written into one self-drawn ring, which ControlTheme transitions cannot express. Arc 環形圖:表示一個整體的若干部分。圖例與圓環同步顯示和隱藏分段;中心顯示目前啟用的分段(靜止時顯示總計),並在讀數之間像滾筒一樣滾動;過小的部分合併為「Other」;新資料會讓每段弧線從其在畫面上的目前位置出發,並保留其速度。首次看到此圖表時,所有分段會一同從頂部掃出,每段的後緣比前緣晚一拍。Arc 對起始角與結束角進行插值,而非路徑;每個角度都是寫入一個自繪圓環的 `MotionValue`,這是 ControlTheme 過渡無法表達的。 @@ -6816,6 +8540,122 @@ The title in the Idle phase. Null uses Arc's copy for the scene. Idle 階段的標題。為 null 時使用 Arc 針對該場景的文案。 + + A table card with an AI column (Arc Enrich column, design reference). One prompt runs down every row; each cell shimmers while it works, then settles into a value with its source, or a failure with a retry. The top bar holds the table's name, the row count and the column button (Run column, Stop, Re-run); the AI column's header shows its name over its prompt with an edit control that opens an editor; the footer names the model and counts the results. Give it `Rows` and an `Enrich` delegate that fills one cell: it gets the row and an `EnrichColumnContext` (the prompt, a cancellation token, the attempt and `SetActivity` for the shimmering label) and returns the value and its source, or throws to fail the cell with the exception's message. At most `Concurrency` cells run at once and the workers start 140ms apart so the column fills top to bottom. Persist values from `CellChanged` and feed them back through `InitialCells`. With no rows of its own the column plays its `Simulation`, a table you supply, and nothing is sent; with neither it shows the empty frame (no rows, Run column unavailable). The rows are a virtualizing list: give the control a height (or a `MaxHeight`) for long tables and only the rows on screen are built; without one every row shows. Below 560px wide the domain, the source text and the model detail fold away; below 440px the gutters tighten, values wrap to two lines and failure reasons hide behind "Failed". + 帶一欄 AI 欄的表格卡片(Arc Enrich column,設計參考)。同一個提示詞逐列執行;每個儲存格在處理時顯示微光,隨後落定為附來源的值,或顯示可重試的失敗。頂列包含表格名稱、列數和欄位按鈕(Run column、Stop、Re-run);AI 欄的表頭在欄名下方顯示提示詞,並附有開啟編輯器的編輯控制項;頁尾顯示模型並統計結果。提供 `Rows` 和一個填入儲存格的 `Enrich` 委派:它收到資料列和一個 `EnrichColumnContext`(提示詞、取消權杖、嘗試次數,以及設定微光標籤的 `SetActivity`),傳回值及其來源,或擲出例外,以例外訊息把儲存格標為失敗。同時最多執行 `Concurrency` 個儲存格,各工作者相隔 140ms 啟動,讓這一欄由上到下依序填入。在 `CellChanged` 中儲存值,並透過 `InitialCells` 傳回。這一欄自己沒有資料列時,會播放它的 `Simulation`(由你提供的表格),不傳送任何內容;兩者都沒有時顯示空框架(沒有資料列,Run column 無法使用)。資料列是虛擬化清單:長表格請為控制項設定高度(或 `MaxHeight`),只會建立畫面上的資料列;未設定時顯示全部資料列。寬度小於 560px 時,網域、來源文字和模型說明收起;小於 440px 時,左右留白收緊,值換成兩行,失敗原因隱藏在「Failed」後面。 + + + Runs the column once when it first scrolls into view, if no cell has a value yet. Null (the default) means on while the `Simulation` plays and off with your own `Enrich`. + 在這一欄第一次捲入檢視區時執行一次,前提是尚未有任何儲存格有值。為 null(預設)時,播放 `Simulation` 期間開啟,使用你自己的 `Enrich` 則關閉。 + + + Raised whenever a cell changes state (queued, running, done, failed). Use it to persist values. Bubbles. + 每當儲存格狀態變化(排隊、執行、完成、失敗)時觸發。用它儲存值。會向上傳遞。 + + + The AI column's name and prompt (two-way). The person can edit both from the column header; saving re-runs every row. Null shows the simulation's column while it plays, and an unnamed column otherwise. + AI 欄的名稱和提示詞(雙向)。使用者可以在欄標題中編輯兩者;儲存後會重新執行每一列。為 null 時,播放模擬期間顯示模擬的欄,否則顯示一個未命名的欄。 + + + Raised when the person saves an edited column (the column then re-runs). Bubbles. + 使用者儲存編輯後的欄位時觸發(隨後這一欄會重新執行)。會向上傳遞。 + + + How many cells run at once. Workers start 140ms apart so the column fills top to bottom. + 同時執行的儲存格數。各工作者相隔 140ms 啟動,讓這一欄由上到下依序填入。 + + + Fills one cell: it gets the row and its context (prompt, column, cancellation token, attempt, `SetActivity`) and returns the value with its source. Throw to mark the cell failed with the exception's message. Null plays the `Simulation`'s answers while its rows are shown; with rows of your own and no delegate nothing runs. + 填入一個儲存格:它收到資料列及其內容物件(提示詞、欄位定義、取消權杖、嘗試次數、`SetActivity`),傳回值及其來源。擲出例外即以例外訊息把儲存格標為失敗。為 null 時,顯示 `Simulation` 的資料列期間播放它的答案;使用你自己的資料列而沒有委派時不執行任何內容。 + + + Values you already have, keyed by row id, such as cells saved from an earlier run. + 已有的值,依資料列 id 索引,例如上一次執行儲存下來的儲存格。 + + + Shows the edit control on the AI column's header. + 在 AI 欄的表頭上顯示編輯控制項。 + + + Whether any cell is queued or running. + 是否有儲存格正在排隊或執行。 + + + Every word the column shows or says. + 這一欄顯示或朗讀的所有文字。 + + + The model shown in the footer. Null shows the simulation's model while it plays, and hides it otherwise. + 頁尾中顯示的模型。為 null 時,播放模擬期間顯示模擬的模型,否則隱藏。 + + + A short note after the row count. Null shows "sample data" while the simulation plays and nothing otherwise. + 列數後面的簡短備註。為 null 時,播放模擬期間顯示「sample data」,否則不顯示。 + + + The rows to enrich. Null shows the `Simulation`'s rows, or no rows without one. + 要補全的資料列。為 null 時顯示 `Simulation` 的資料列,沒有模擬時不顯示任何資料列。 + + + Raised when a run settles (finished or stopped), with every cell. Bubbles. + 一次執行落定(完成或停止)時觸發,並附上所有儲存格。會向上傳遞。 + + + The table the column plays on its own while `Rows` is null: your name, column, model, rows and the answer each row settles into, run exactly like your own delegate would (queued, shimmering, settled, a failure to retry). Null shows the empty frame instead. + `Rows` 為 null 時這一欄自行播放的表格:你的名稱、欄、模型、資料列以及每一列最後得到的答案,執行方式與你自己的委派完全相同(排隊、微光、落定、可重試的失敗)。為 null 時改為顯示空框架。 + + + Raised when the person opens a value's source. Set Handled to open it yourself; otherwise the system opens the address. Bubbles. + 使用者開啟某個值的來源時觸發。設定 Handled 即可自行開啟;否則由系統開啟該網址。會向上傳遞。 + + + The table's name in the top bar. Null shows the simulation's name while it plays. + 頂列中的表格名稱。為 null 時,播放模擬期間顯示模擬的名稱。 + + + One cell of the AI column: its status and, once settled, its value and source or the reason it failed. Immutable; the column hands out a new cell on every change. + AI 欄的一個儲存格:它的狀態,以及落定後的值和來源或失敗原因。不可變;每次變化時這一欄都會給出一個新的儲存格。 + + + What the enrich delegate gets with each row: the column and its prompt, a cancellation token that fires when the person stops the run or the column is re-run, the attempt number and `SetActivity` for the shimmering label. + 補全委派隨每一列收到的內容:欄位定義及其提示詞、在使用者停止執行或重新執行整欄時觸發的取消權杖、嘗試次數,以及設定微光標籤的 `SetActivity`。 + + + The AI column: its name in the header and the prompt that runs down every row. + AI 欄:表頭中的名稱,以及逐列執行的提示詞。 + + + Every word `EnrichColumn` shows or says, in English by default. Replace the whole object to localize or reword; counts and names are format functions. + `EnrichColumn` 顯示或朗讀的每個詞語,預設為英文。替換整個物件即可在地化或改寫;數量和名稱是格式化函式。 + + + The model shown in the footer of an `EnrichColumn`. + `EnrichColumn` 頁尾中顯示的模型。 + + + What the enrich delegate returns for one row: the value and, optionally, its source. + 補全委派為一列傳回的結果:值,以及選用的來源。 + + + One row of an `EnrichColumn`: the record the prompt runs on (a company, a lead, a ticket). + `EnrichColumn` 的一列:提示詞所作用的紀錄(一家公司、一筆潛在客戶、一張工單)。 + + + A table an `EnrichColumn` plays on its own while the app sets no `Rows`: a name, the AI column, the model in the footer, the rows and, for each row, the answer a simulated run settles into. When the column runs (on its own the first time it comes into view, or from Run column), each cell waits through its answer's `Steps`, showing their activities, then shows the value and its source. An answer with an `Error` fails on its first attempt, so the failed state and Retry show. Nothing is sent, and the top bar says the data is a sample. Set the data before handing the simulation to the column; to show another table, give the column a new simulation. + 應用程式沒有設定 `Rows` 時 `EnrichColumn` 自行播放的表格:名稱、AI 欄、頁尾中的模型、資料列,以及每一列模擬執行後得到的答案。這一欄執行時(第一次進入檢視區時自動執行,或透過 Run column),每個儲存格依序經過其答案的 `Steps` 並顯示它們的活動,然後顯示值及其來源。帶有 `Error` 的答案在第一次嘗試時失敗,因而顯示失敗狀態和重試。不會傳送任何內容,頂列會說明這是範例資料。請在把模擬交給這一欄之前設定好資料;要顯示另一張表格,就給這一欄一個新的模擬。 + + + What one row of an `EnrichColumnSimulation` settles into, and how it gets there. + `EnrichColumnSimulation` 中一列最後得到的結果,以及得到它的過程。 + + + One stage of a simulated cell of an `EnrichColumn`: the shimmering activity and how long it lasts. + `EnrichColumn` 模擬儲存格的一個階段:顯示微光的活動以及它持續的時間。 + + + Where a value came from: a short label and the address it opens. + 值的出處:一個簡短的標籤和它開啟的網址。 + An environment of `EnvVariables`: a column of the matrix and a choice in the pills and selects. `EnvVariables` 的一個環境:矩陣中的一欄,也是膠囊與下拉選單中的一個選項。 @@ -7572,9 +9412,93 @@ The text. 文字。 - - The parsed value (date, number, text); null when invalid. - 解析後的值(日期、數字、文字);無效時為 null。 + + The parsed value (date, number, text); null when invalid. + 解析後的值(日期、數字、文字);無效時為 null。 + + + The feedback control of an AI reply (Arc Response feedback, design reference), for your own message rows: thumbs up and down after any `Leading` actions and `Trailing` content pinned to the end of the row. Thumbs down opens an inline panel with reason chips and an optional note; thumbs up fills and asks what was good; a submit shows a short thank-you before the panel folds back into the row. Pressing the active thumb clears the rating. Every thumb press sends a `Rating` submission and the panel sends a `Details` one, through `FeedbackHandler` (a task that throws shows the inline error and Try again; its cancellation token fires when the panel closes or the rating changes) and `FeedbackSubmitted`. Without a handler the sends are simulated and the control says so. + AI 回覆的意見回饋控制項(Arc Response feedback,設計參考),用於你自己的訊息列:在任何 `Leading` 操作之後是讚與倒讚,`Trailing` 內容固定在列尾。按倒讚會在行內展開一個附原因標籤與可選備註的面板;按讚會填滿圖示並詢問哪裡做得好;送出後顯示簡短的感謝,隨後面板收回到操作列中。再次按下目前啟用的按鈕會清除評分。每次按下讚或倒讚都會傳送一筆 `Rating` 送出,面板會傳送一筆 `Details` 送出,經由 `FeedbackHandler`(擲回例外的工作會顯示行內錯誤與 Try again;面板關閉或評分改變時其取消權杖觸發)與 `FeedbackSubmitted`。沒有處理常式時傳送是模擬的,控制項會如實說明。 + + + Shows the optional note field after a thumbs down. + 按倒讚後顯示可選的備註欄。 + + + The note's maximum length in characters. + 備註的最大長度(字元數)。 + + + The note field's placeholder. + 備註欄的預留位置文字。 + + + Persists feedback: called with a `Rating` submission on every thumb press and a `Details` one when reasons or a note are submitted. Throw (or fault the task) to show the inline error and Try again; the token is cancelled when the panel closes or the rating changes. Null simulates sends. + 保存意見回饋:每次按下讚或倒讚時以 `Rating` 送出呼叫,送出原因或備註時以 `Details` 送出呼叫。擲回例外(或讓工作失敗)即可顯示行內錯誤與 Try again;面板關閉或評分改變時權杖會被取消。為 null 時模擬傳送。 + + + Raised for every submission, before `FeedbackHandler` runs (and when the send is simulated). Bubbles. + 每次送出時引發,在 `FeedbackHandler` 執行之前(模擬傳送時也會引發)。會向上傳遞。 + + + Whether the thumbs can be pressed; turn it off while the reply is still streaming. + 是否可以按下讚與倒讚;回覆仍在串流時將其關閉。 + + + Every word the control shows or announces. + 控制項顯示或播報的每一個字詞。 + + + Actions rendered before the thumbs, such as copy or regenerate. Use 16px icons in small (36px) buttons to match. + 繪製在讚與倒讚之前的操作,例如複製或重新產生。在小型(36px)按鈕中使用 16px 圖示以保持一致。 + + + Passed back on every submission so feedback can be stored against a message. + 在每次送出時傳回,以便把意見回饋與某則訊息一起儲存。 + + + Reason chips offered after a thumbs down. A chip labelled Other moves focus to the note and changes its placeholder. + 按倒讚後提供的原因標籤。標為 Other 的標籤會把焦點移到備註並更換其預留位置文字。 + + + Chips offered after a thumbs up. An empty list skips the follow-up row. + 按讚後提供的標籤。空清單會略過後續列。 + + + A quiet line next to Submit about what gets shared. Hidden below 400px of width; null hides it. + Submit 旁一行低調的文字,說明會分享什麼。寬度低於 400px 時隱藏;為 null 時不顯示。 + + + The rating (two-way). Set it to show a rating loaded from your database; bind it to own the state. A thumb press sets it and pressing the active thumb sets `None`. + 評分(雙向)。設定它可顯示從資料庫讀取的評分;繫結它可自行掌管狀態。按下讚或倒讚會設定它,再次按下目前啟用的按鈕會設為 `None`。 + + + Raised when a thumb press changes the rating, including clearing it. Bubbles. + 按下讚或倒讚改變評分時引發,包括清除評分。會向上傳遞。 + + + How long the thank-you state holds before the panel folds away. + 感謝狀態停留多久後面板收起。 + + + The second line of the thank-you state. Null reads "It helps improve future replies." with a `FeedbackHandler`, and says nothing was sent without one. + 感謝狀態的第二行。為 null 時,有 `FeedbackHandler` 則顯示 "It helps improve future replies.",沒有則說明沒有傳送任何內容。 + + + Content pinned to the end of the row, such as a timestamp or a token count. + 固定在列尾的內容,例如時間戳記或權杖數。 + + + Arguments of `RatingChanged`. + `RatingChanged` 的引數。 + + + One piece of feedback to store: sent on every thumb press (`Rating`) and when reasons or a note are submitted (`Details`). Store the rating on the first stage and attach the reasons and the note on the second, both against `MessageId`. + 一筆要儲存的意見回饋:每次按下讚或倒讚時傳送(`Rating`),送出原因或備註時傳送(`Details`)。在第一個階段儲存評分,在第二個階段附加原因與備註,兩者都與 `MessageId` 關聯。 + + + Arguments of `FeedbackSubmitted`. + `FeedbackSubmitted` 的引數。 Helper copy under the frame (rewrites changed words in place; counts roll). @@ -7980,6 +9904,290 @@ A band that starts at `From` (in value units). The highest band the value reaches sets the ring's tone and names the state under the number, so the state never rests on color alone. 從 `From`(以數值單位計)開始的一個區間。數值所達到的最高區間決定環形的色調,並在數字下方標明狀態,因此狀態不只依靠顏色來表達。 + + The model that answered: its name as people know it, your key for its provider, its API id and, optionally, its mark. + 回答問題的模型:大家熟知的名稱、你為其服務商設定的鍵、API id,以及選用的標記。 + + + Prices in US dollars per million tokens. Reasoning tokens bill at the output rate. + 每百萬 token 的美元價格。推理 token 按輸出單價計費。 + + + A live stats strip for a streaming LLM response (Arc Generation stats, design reference). The head shows the model's mark, its name and a status with an icon and a word (Waiting, Streaming, Done in 4.18s, Stopped at 1.90s, Failed). Below it four cells split by hairlines: time to first token, speed in tokens per second with a small sparkline, output tokens and cost. While the response streams every number counts up in tabular digits and the sparkline draws; when it ends the numbers settle on exact values. Details opens a breakdown with a latency bar (first token, generation, total) and a token table (input, cached input, output, reasoning, total) with rates and cost. A failed response gets a row with the reason and Retry. Feed it from your streaming handler: set `StartedAt` when the request leaves, `Status` Waiting until the first chunk, then Streaming while you set a new `Usage` with the growing output tokens, then Done with the duration. Speed, latency and cost follow. Leave `Status` unset and set `Simulation` to play simulated runs of your own; with neither the strip is idle and every value reads a dash. + 串流 LLM 回覆的即時統計列(Arc Generation stats,設計參考)。頂部顯示模型的標記、名稱,以及由圖示和文字組成的狀態(Waiting、Streaming、Done in 4.18s、Stopped at 1.90s、Failed)。下方是以細線分隔的四個儲存格:首個 token 時間、附迷你折線的每秒 token 速度、輸出 token 與費用。回覆串流輸出時,每個數字以等寬數字逐步累加,迷你折線逐步繪出;結束後數字定格在精確值上。Details 會展開明細,其中有延遲條(首個 token、生成、總計)與 token 表格(輸入、快取輸入、輸出、推理、總計)及其單價與費用。失敗的回覆會多出一列,顯示原因與 Retry。用你的串流處理程式提供資料:請求送出時設定 `StartedAt`,在第一個資料區塊之前 `Status` 為 Waiting,之後為 Streaming,同時每次傳入帶有持續增加之輸出 token 的新 `Usage`,最後以 Done 並附上時長結束。速度、延遲與費用隨之得出。不設定 `Status` 並設定 `Simulation`,即可播放你自己的模擬執行;兩者都未設定時,統計列處於閒置狀態,每個值都顯示短橫線。 + + + Shows Retry in the error row; it raises `RetryRequested`. + 在錯誤列中顯示 Retry;按下時引發 `RetryRequested`。 + + + Raised when the person presses Details (not when `IsExpanded` is set in code). Bubbles. + 使用者按下 Details 時引發(在程式碼中設定 `IsExpanded` 時不會引發)。會向上傳遞。 + + + The reason shown in the error row when `Status` is Error. + 當 `Status` 為 Error 時,錯誤列中顯示的原因。 + + + Whether the breakdown is open (two-way). Set it in XAML to open it on first render. + 明細是否展開(雙向繫結)。在 XAML 中設定可在首次轉譯時展開。 + + + Every word the strip shows or says. + 統計列顯示或朗讀的所有文字。 + + + The model that answered: its name, your provider key, its API id and, optionally, its mark. + 回答問題的模型:名稱、你的服務商鍵、API id,以及選用的標記。 + + + A footnote under the breakdown, for example where the rates come from (text or any content). A playing `Simulation` says its runs are simulated when it is not set. + 明細下方的註腳,例如單價的來源(文字或任意內容)。未設定時,正在播放的 `Simulation` 會說明其執行是模擬的。 + + + Prices in US dollars per million tokens. Enables the Rate and Cost columns and the live cost. Leave it out when the usage carries the provider's cost; the Rate column then hides. + 每百萬 token 的美元價格。啟用 Rate 與 Cost 欄以及即時費用。用量中已帶有服務供應商的費用時可省略;此時 Rate 欄隱藏。 + + + Raised when the person presses Retry on a failed response. Bubbles. Send the request again. + 使用者在失敗的回覆上按下 Retry 時引發。會向上傳遞。請重新送出請求。 + + + Simulated runs to play while `Status` is null: they loop while the strip is on screen, hold while the breakdown is open, settle at once under reduced motion and retry from the error row. Null (the default) shows the idle strip. Give each strip its own simulation. + `Status` 為 null 時播放的模擬執行:統計列位於畫面上時循環播放,明細展開時停留,減弱動態效果下立即定格,並可從錯誤列重試。為 null(預設)時顯示閒置的統計列。每個統計列使用自己的模擬。 + + + Speed readings in tokens per second, oldest first, for the sparkline. Null (the default) reads the speed every 160ms while the output grows, smoothed, keeping the whole run's shape in at most 48 points. + 供迷你折線使用的速度讀數(每秒 token),由舊到新。為 null(預設)時,輸出增加期間每 160ms 讀取一次速度並平滑,最多以 48 個點保留整個執行的形狀。 + + + When the request was sent. While waiting, time to first token counts live from here; while streaming, total latency does. + 請求送出的時間。等待期間,首個 token 時間從這裡開始即時計時;串流輸出期間,總延遲從這裡開始計時。 + + + Where the response is. Null (the default) plays `Simulation`, or shows the idle strip without one. + 回覆所處的階段。為 null(預設)時播放 `Simulation`;沒有模擬時顯示閒置的統計列。 + + + The token usage and timing. Set a new one as chunks arrive (output tokens), on the first token (`Ttft`) and on the last (`Duration`). Speed and cost are derived unless it carries them. + token 用量與耗時。每個資料區塊抵達時(輸出 token)、首個 token 時(`Ttft`)與最後一個 token 時(`Duration`)各設定一個新的值。除非其中已包含,速度與費用會自動推算。 + + + A bordered card with four cells, or one quiet wrapping line for under a chat message. + 附四個儲存格的有框線卡片,或用於聊天訊息下方的一行安靜、可換行的文字。 + + + Every word `GenerationStats` shows or says, in English by default. Replace the whole object to localize or reword; the parts that hold a number or a name are format functions that receive it already formatted. + `GenerationStats` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;包含數字或名稱的部分是格式化函式,接收已格式化好的值。 + + + One response a `GenerationStatsSimulation` plays: the model and its prices, the token counts it ends on, how long the first token takes and the peak speed. Give `FailAtTokens` to make it fail once at that output count and succeed when retried. + `GenerationStatsSimulation` 播放的一次回覆:模型及其價格、最終的 token 數、首個 token 所需的時間與峰值速度。設定 `FailAtTokens` 可讓它在該輸出數量處失敗一次,重試後成功。 + + + A simulated stream for a `GenerationStats` that has no `Status`: it plays `Runs` in order on its own virtual clock, which the strip advances in 40ms steps while it is on screen (full motion only), so leaving the screen pauses the run. A run waits for its first token, then streams with a ramp up and a few percent of waver, in chunks of four or more tokens, and settles; a run with `FailAtTokens` fails there once and succeeds when retried (by Retry, or on its own after a short wait). A settled run stays on screen while the breakdown is open. Nothing is sent anywhere. Create one per strip: it keeps the playback state. + 供沒有 `Status` 的 `GenerationStats` 使用的模擬串流:它依序在自己的虛擬時鐘上播放 `Runs`,統計列位於畫面上時以 40ms 為步長推進該時鐘(僅完整動態效果),因此離開畫面會暫停執行。一次執行先等待首個 token,然後以爬升與幾個百分點的波動、每塊四個或更多 token 的方式串流輸出,最後定格;帶有 `FailAtTokens` 的執行會在該處失敗一次,重試後成功(透過 Retry,或在短暫等待後自動重試)。明細展開時,已定格的執行會停留在畫面上。不會傳送任何內容。每個統計列建立一個:它保存播放狀態。 + + + Raised whenever the frame (status, usage, error) changes. + 每當畫面(狀態、用量、錯誤)變化時引發。 + + + The token usage and timing of one response, all optional. Make a new one (or a copy with `with`) whenever a value changes: update `OutputTokens` as chunks arrive, set `Ttft` on the first token and `Duration` on the last. Speed and cost are derived unless you pass `TokensPerSecond` or `CostUsd`. + 一次回覆的 token 用量與耗時,皆為選用。任何值變化時都建立一個新的(或用 `with` 複製):資料區塊抵達時更新 `OutputTokens`,首個 token 時設定 `Ttft`,最後一個時設定 `Duration`。除非你傳入 `TokensPerSecond` 或 `CostUsd`,速度與費用會自動推算。 + + + A floating ghost mascot that shows what an assistant is doing (Arc ghost mascot): a soft sheet whose hem ripples while it hovers, a quiet form shade and two simple eyes, drawn in one control. Idle floats, blinks and glances around; listening comes forward with wide eyes and a small head tilt; thinking drifts up, turns translucent and looks up at a swirl that curls in and out; writing leans toward a line it writes with a tiny pen, eyes on the tip; done lifts with happy eyes, a smile and a sparkle; error wobbles once, sinks and droops its eyes. Every status change blends from the pose on screen over 400ms, never a hard cut. Leave `Status` unset and the ghost plays through every state on a loop (for showcases); `StatusChanged` reports each step. + 一隻表示助理正在做什麼的漂浮幽靈吉祥物(Arc 幽靈吉祥物):懸停時下襬起伏的柔軟布單、低調的形體陰影和兩隻簡單的眼睛,繪製在一個控制項中。閒置時漂浮、眨眼、四處張望;傾聽時向前靠近,睜大眼睛,頭微微歪向一側;思考時向上飄起,變得半透明,抬頭看著一個畫入又擦去的漩渦;書寫時傾向它用小筆寫出的一行,眼睛盯著筆尖;完成時升起,眼睛變成開心的弧線,帶著微笑和閃光;出錯時搖晃一次,下沉,眼睛垂下。每次狀態切換都從畫面上的姿態在 400ms 內過渡,從不生硬切換。不設定 `Status` 時,幽靈會循環示範每一種狀態(用於展示);`StatusChanged` 回報每一步。 + + + Overrides the whole accessible label (null builds it from `AssistantName` and the status). + 取代整個無障礙標籤(為 null 時由 `AssistantName` 和狀態組成)。 + + + Announces status changes as a polite live region. Turn it on for the one ghost that carries status, not for decorative copies. + 以禮貌的即時區域朗讀狀態變化。只為承載狀態的那一隻幽靈開啟,裝飾用的副本不要開啟。 + + + The assistant's name used in the accessible label, for example "Assistant, thinking" (default "Assistant"). + 用於無障礙標籤的助理名字,例如「Assistant, thinking」(預設「Assistant」)。 + + + The status the ghost shows: `Status`, or the current self-play step while it is unset. + 幽靈顯示的狀態:`Status`,未設定時為目前自動示範的步驟。 + + + Any color for the eyes, the happy eyes and the smile. + 眼睛、開心的眼睛和微笑的任意顏色。 + + + Any color for the floating ground shadow (null uses a translucent foreground). + 漂浮地面陰影的任意顏色(為 null 時使用半透明的前景色)。 + + + Freezes the ghost on the current frame; resuming continues from the same frame. The ghost also pauses by itself while it is off screen or hidden. + 把幽靈凍結在目前影格;恢復後從同一影格繼續。幽靈在畫面外或隱藏時也會自行暫停。 + + + Every word of the accessible label. + 無障礙標籤中的所有文字。 + + + Any color for the marks: the thinking swirl, the pen and its line, and the sparkle. + 標記的任意顏色:思考的漩渦、筆和它寫出的線,以及閃光。 + + + Any color for the form shade clipped to the sheet (null uses a darker, translucent tone of the sheet). + 裁切在布單內的形體陰影的任意顏色(為 null 時使用布單更深、半透明的色調)。 + + + Any color for the sheet (a solid brush; a gradient uses its first stop). Overrides `Tone`; the eyes become a dark mix of it unless `EyeBrush` is set. + 布單的任意顏色(純色筆刷;漸層取其第一個色標)。覆寫 `Tone`;除非設定了 `EyeBrush`,眼睛會變成它的深色混合。 + + + The soft floating shadow under the ghost, which shrinks and fades as the ghost rises. Drawn only at 56px and up. + 幽靈下方柔和的漂浮陰影,幽靈升高時會縮小變淡。只在 56px 以上繪製。 + + + Width and height in px (default 96), from 24 inline next to a message to 160 in an empty state. Below 56 a compact drawing takes over: tighter framing, bigger eyes, heavier swirl, pen and line, and no shadow. + 寬度和高度(px,預設 96),從訊息旁內嵌的 24 到空狀態中的 160。小於 56 時改用精簡畫法:取景更緊,眼睛更大,漩渦、筆和線更粗,而且沒有陰影。 + + + What the assistant is doing. Leave it unset (null) to play through every state on a loop, which is meant for showcases; `StatusChanged` then reports each step. + 助理正在做什麼。不設定(null)時循環示範每一種狀態,用於展示;此時 `StatusChanged` 回報每一步。 + + + Raised when the self-playing ghost (`Status` unset) moves to the next state. Use it to sync a caption or a control. + 自動示範的幽靈(未設定 `Status`)進入下一個狀態時引發。可用它同步說明文字或控制項。 + + + The sheet color from the theme: the accent with accent-foreground eyes (default), a pale raised surface with a hairline and foreground eyes (the marks keep the accent), or the foreground ink with background-colored eyes. + 來自主題的布單顏色:強調色搭配強調前景色的眼睛(預設);淺色的凸起表面加細線和前景色的眼睛(標記保持強調色);或前景色搭配背景色的眼睛。 + + + Every word a `GhostMascot` speaks, in English by default: the status words of its accessible label and the pattern that joins the assistant's name to them ("Assistant, thinking"). Replace the instance (or set its properties) to translate or reword. + `GhostMascot` 朗讀的所有文字,預設為英文:無障礙標籤中的狀態詞,以及把助理名字與狀態詞連接起來的格式(「Assistant, thinking」)。取代實例(或設定其屬性)即可翻譯或改寫。 + + + Arguments of `StatusChanged`: the status the self-playing ghost moved to. + `StatusChanged` 的參數:自動示範的幽靈進入的狀態。 + + + Inline AI autocomplete for a text field (Arc Ghost text, design reference). When the person pauses, muted ghost text continues their sentence right at the caret, streamed from your `Suggest` function. Tab accepts the whole suggestion and it settles from ghost ink to text ink in place; Cmd+Right (Ctrl+Right, Option or Alt+Right too) accepts one word at a time; Escape fades it away; typing the same characters as the ghost keeps it in place instead of asking again. A small chip at the end shows the model's mark and name, a shimmering Thinking while it waits, a Tab key cap and "to accept" when a suggestion is ready (a tap target on touch screens), Accepted with a check after Tab, and No suggestion with Retry when your function throws. The field is a real Avalonia `TextBox` (caret, selection, IME, undo, clipboard and spellcheck stay native); the ghost is the muted tail of the same text layout, so text and ghost wrap identically at any width. A multi-line field grows with its text from `Rows` to `MaxRows` lines on a 180ms tween and has a label row and a footer row; a single-line field scrolls sideways and keeps the chip inline at its end. With no `Suggest` and no text it is a plain field, or plays a `Simulation` you pass in that types, pauses, accepts and loops; clicking it takes over. + 文字欄位的行內 AI 自動完成(Arc Ghost text,設計參考)。使用者停頓時,淡色的幽靈文字會剛好在游標處接著句子寫下去,內容由你的 `Suggest` 函式串流提供。Tab 接受整段建議,並讓它原地從幽靈墨色過渡到正文墨色;Cmd+Right(Ctrl+Right,Option 或 Alt+Right 也可以)一次接受一個字詞;Escape 讓它淡出;輸入與幽靈文字相同的字元會保留它,而不會重新請求。末端的小標籤顯示模型的標誌與名稱,等待時顯示閃爍的 Thinking,建議就緒時顯示 Tab 鍵帽與 "to accept"(在觸控螢幕上可點按),按 Tab 後顯示帶勾號的 Accepted,函式擲回例外時顯示 No suggestion 與 Retry。欄位是真正的 Avalonia `TextBox`(游標、選取、輸入法、復原、剪貼簿與拼字檢查都維持原生);幽靈文字是同一文字版面中淡色的尾端,因此文字與幽靈文字在任何寬度下都以相同方式換行。多行欄位以 180ms 補間從 `Rows` 行長高到 `MaxRows` 行,並有標籤列與頁尾列;單行欄位水平捲動,標籤保持在行尾。沒有 `Suggest` 也沒有文字時,它是一般的欄位,或播放你傳入的 `Simulation`,自動打字、停頓、接受並循環;點一下即可接手。 + + + Raised with the text that was accepted: the whole suggestion or the next word. Bubbles. + 以被接受的文字引發:整段建議或下一個字詞。會向上傳遞。 + + + The pause before `Suggest` is called (450ms). Every keystroke restarts it. + 呼叫 `Suggest` 前的停頓(450ms)。每次按鍵都會重新計時。 + + + Raised with the suggestion the person dismissed with Escape. Bubbles. + 以使用者用 Escape 關閉的建議引發。會向上傳遞。 + + + Content at the start of the footer row, such as a character count, a note or a send button. Multi-line only. + 頁尾列開頭的內容,例如字數、備註或傳送按鈕。僅限多行。 + + + Shows the chip, the word and Escape key hints next to it on wide fields, and the keys description for screen readers. + 顯示標籤、寬欄位上標籤旁的字詞與 Escape 按鍵提示,以及供螢幕閱讀器使用的按鍵說明。 + + + A growing multi-line field with a label row and a footer row (true), or a single-line field with the chip inline at its end. Line breaks typed or pasted into a single-line field become spaces. + 帶標籤列與頁尾列、會長高的多行欄位(true),或標籤位於行尾的單行欄位。輸入或貼上到單行欄位的換行會變成空格。 + + + True while a streamed suggestion is still arriving. + 串流建議仍在傳入時為 true。 + + + A quiet row above the text, such as a field name with an icon; a string also names the field. Multi-line only. + 文字上方安靜的一列,例如帶圖示的欄位名稱;字串也會作為欄位的名稱。僅限多行。 + + + Every word the field shows or says. + 欄位顯示或朗讀的每一個字。 + + + Lines before a multi-line field stops growing and scrolls, keeping the caret in view (10). + 多行欄位停止長高、開始捲動(並保持游標可見)前的行數(10)。 + + + Trimmed characters needed before suggestions start (3). + 開始提供建議所需的去除前後空白後的字元數(3)。 + + + The model behind the suggestions: its name shows in the idle chip and its mark at 12px in every chip state. + 建議背後的模型:名稱顯示在閒置狀態的標籤中,12px 的標誌顯示在每種標籤狀態中。 + + + A one-off message for screen readers, said through the field's own polite announcements. + 給螢幕閱讀器的一次性訊息,透過欄位自己的禮貌播報朗讀。 + + + Hint shown while the field is empty. + 欄位為空時顯示的提示。 + + + Minimum visible lines of a multi-line field (3). + 多行欄位的最少可見行數(3)。 + + + A simulated session to play while the field has no `Suggest` and no text (demos and prototypes): it types its script, lets the suggestions stream and accepts them, looping; clicking the field takes it over. Null leaves the field to the person. + 欄位沒有 `Suggest` 也沒有文字時要播放的模擬工作階段(示範與原型):它依劇本輸入,讓建議串流出現並接受,循環播放;點一下欄位即可接手。為 null 時欄位完全交給使用者。 + + + Where the suggestion is: idle, loading, ready or failed. + 建議所處的狀態:閒置、載入中、就緒或失敗。 + + + Raised when `Status` changes. Bubbles. + `Status` 變化時引發。會向上傳遞。 + + + Returns a continuation for the text before the caret: a string, a task of one, or a stream of chunks (`GhostTextResponse`); null skips without showing Thinking. Cancel your work when the token fires. + 為游標前的文字回傳接續內容:字串、回傳字串的工作,或分塊串流(`GhostTextResponse`);回傳 null 表示略過,且不顯示 Thinking。權杖觸發時請取消你的工作。 + + + The suggestion at the caret (what Tab would accept), or empty. + 游標處的建議(Tab 會接受的內容),或為空。 + + + The text (two-way). Programmatic changes run through the same logic as typing, so a parent can drive the field. + 文字(雙向)。以程式碼修改與打字走相同的邏輯,因此父層可以驅動這個欄位。 + + + A bordered field, or plain text that sits flush inside a card you already have. + 帶邊框的欄位,或平貼在你既有卡片中的純文字。 + + + Every word a `GhostText` shows or says, in English by default: the chip in each state, the key hints, the keys description read with the field and the polite announcements. Replace the whole object to localize or reword; sentences that carry text are functions so word order can change. + `GhostText` 顯示或朗讀的每一個字,預設為英文:各狀態下的標籤、按鍵提示、隨欄位朗讀的按鍵說明以及禮貌播報。替換整個物件即可在地化或改寫;包含文字的句子是函式,以便調整詞序。 + + + The answer of a `GhostTextSuggest` function: a whole suggestion now, a task that completes with one, or a stream of chunks that appear one after another (each fades in on its own). A string or a `Task<string?>` converts implicitly; wrap an async iterator with `FromStream`. A null or empty result shows nothing. + `GhostTextSuggest` 函式的回答:立即給出的整段建議、完成時給出建議的工作,或依序出現的分塊串流(每塊各自淡入)。字串或 `Task<string?>` 可隱含轉換;非同步迭代器請用 `FromStream` 包裝。結果為 null 或空時不顯示任何內容。 + + + A simulated session for a `GhostText`, for demos, prototypes and tests: it types each line of the `Script`, pauses, lets the scripted suggestion stream in 3 to 6 word chunks after a short think, and accepts it; lines alternate between Tab and a word at a time with Cmd+Right, then it rests and starts over. Every step goes through the field's own handling (typing, asking, Tab, the word key), so it shows exactly what a person would see. Set it as the field's `Simulation`: it plays while the field has no `Suggest` and no text, at every motion level, pausing off screen; with reduced motion (or none) it plays once and stops. Clicking or focusing the field hands it to the person, who then gets the scripted suggestions and, for anything else, `Suggest`. Label, placeholder, footer and model stay on the field. Use one simulation per field. + 為 `GhostText` 提供的模擬工作階段,用於示範、原型和測試:它逐行輸入 `Script`,停頓,讓劇本中的建議在短暫思考後以 3 到 6 個字詞的分塊串流出現並接受;各行在 Tab 與 Cmd+Right 逐字詞接受之間交替,然後休息並從頭開始。每一步都經過欄位自己的處理(輸入、請求、Tab、逐字詞鍵),所以顯示的正是使用者會看到的樣子。把它設為欄位的 `Simulation`:欄位沒有 `Suggest` 也沒有文字時播放它,在所有動態效果等級下執行,離開畫面時暫停;減少動態效果(或關閉動態效果)時只播放一次就停止。點一下或聚焦欄位會把它交給使用者,使用者隨後得到劇本中的建議,其他內容則由 `Suggest` 回答。標籤、預留位置文字、頁尾與模型仍設定在欄位上。每個欄位使用一個模擬。 + + + The lines to type and the suggestions that continue them, in order. + 要輸入的各行以及接續它們的建議,依順序排列。 + + + Suggestions for text the script does not cover, such as what the person types after taking the field over; null suggests nothing there. + 為劇本未涵蓋的文字提供建議,例如使用者接手欄位後輸入的內容;為 null 時這些內容不會得到建議。 + + + One line of a `GhostTextSimulation`: the words it types, and the suggestion that continues them. + `GhostTextSimulation` 中的一行:它輸入的字詞,以及接續這些字詞的建議。 + A grid of square tiles that bends toward the pointer like spacetime around a mass (Arc Gravity well, design reference). Tiles near the well lean in, shrink and dim on their own springs; a click, a tap, Space or Enter sends a shockwave rolling outward through the grid. Each item becomes one tile: photos, avatars, icons or any small content. 一格格正方形圖塊組成的網格,像時空圍繞質量一樣朝指標彎曲(Arc 重力井,設計參考)。重力井附近的圖塊沿各自的彈簧向內傾斜、縮小並變暗;點擊、輕觸、Space 或 Enter 會發出一道向外滾過網格的衝擊波。每個項目成為一個圖塊:照片、頭像、圖示或任何小型內容。 @@ -8308,6 +10516,10 @@ A number shown in a `HoverCardProfile` (value over its label). 在 `HoverCardProfile` 中顯示的數字(數值位於標籤之上)。 + + The playback of fetched speech, supplied by the app (an audio element, a platform player, or an adapter over `IAudioPlayer`). The `SpeechPlayer` drives it and reads `Position` on every frame while it plays, so its position is the player's clock. All calls come from the UI thread. + 由應用程式提供的已取得語音的播放(音訊元素、平台播放器,或以 `IAudioPlayer` 為基礎的轉接器)。`SpeechPlayer` 會驅動它,並在播放時每個影格讀取 `Position`,所以它的位置就是播放器的時鐘。所有呼叫都來自 UI 執行緒。 + Icons that morph between two states instead of swapping: lines rotate and slide into place, outlines interpolate (P16 `PathMorph`), and a few carry a moment of their own (the bell rings, the heart bursts). Every morph rides an interruptible spring, so toggling quickly reverses from wherever the icon is. Drawn in one `Render` on the 24-unit grid. The icon is decorative: put it in a button that carries the name and the pressed or expanded state. 在兩種狀態之間變形而非直接切換的圖示:線條旋轉並滑入定位,輪廓進行插值(P16 `PathMorph`),部分圖示還帶有自身的動態效果(鈴鐺會響,愛心會迸發)。每次變形都沿可被打斷的彈簧運行,因此快速切換時會從圖示當前位置反向。圖示在 24 單位網格上透過一次 `Render` 繪製。此圖示僅作裝飾:請將其放入帶有名稱以及按下或展開狀態的按鈕中。 @@ -8432,6 +10644,258 @@ Shows the captions; they fade as the divider reaches them (Arc `labels={false}` hides them). 顯示標註;隨分隔線靠近而淡出(Arc `labels={false}` 可將其隱藏)。 + + An image generation card: a prompt, model and aspect chips with Generate, a 2 × 2 grid where each image resolves as a pixel mosaic (coarse colour blocks to full detail under a soft light band) with its own progress ring, and a footer with the status and Upscale, Vary and Download. Pick an image to continue with it: Upscale grows it to fill the grid and sweeps a band of crisp pixels across it, Vary makes four new images from it and Download hands it to the app. Give it a `Generator` that makes one image (it is called once per tile, in parallel, for Generate, Vary and Retry, and reports progress through its `ImageGenerationContext`) and optionally an `Upscaler`. With no generator it plays its `Simulation`, pictures painted by your `Painter`, nothing downloaded, and generates once when it first scrolls into view; with neither, Generate stays unavailable. + 一張圖像生成卡片:提示詞、模型與畫面比例標籤和 Generate,一個 2 × 2 網格,每張圖像以像素馬賽克的方式成形(在柔和的光帶下從粗略色塊到完整細節),並帶有自己的進度環,以及一個顯示狀態和 Upscale、Vary、Download 的頁尾。選取一張圖像繼續操作:Upscale 把它放大到鋪滿網格並讓一道清晰像素帶掃過,Vary 以它為基礎產生四張新圖,Download 把它交給應用程式。為它提供一個產生單張圖像的 `Generator`(每個格子呼叫一次,平行執行,用於 Generate、Vary 和 Retry,並透過其 `ImageGenerationContext` 回報進度),以及選用的 `Upscaler`。沒有產生器時,它會播放它的 `Simulation`:由你的 `Painter` 繪製圖片,不下載任何內容,並在第一次捲動進入可視範圍時自動生成一次;兩者都沒有時,Generate 無法使用。 + + + The shapes offered as chips. Null or empty offers 1:1, 4:5 and 16:9. + 作為標籤提供的畫面比例。為 null 或空時提供 1:1、4:5 和 16:9。 + + + Generates once when the card first scrolls into view, then picks image 1 if nobody has touched the card. Null (the default) does so with the simulation only, not with your `Generator`. + 在卡片第一次捲動進入可視範圍時自動生成一次,如果沒有人操作過卡片,再選取第 1 張圖像。為 null(預設)時只在使用模擬時這樣做,使用你的 `Generator` 時不會。 + + + Raised when the person presses Download, with the image and a file name built from the prompt, its number and its size. Save it from here (for example through the window's storage provider). Bubbles. + 使用者按下 Download 時觸發,附帶圖像和一個由提示詞、編號與尺寸組成的檔名。請在這裡儲存它(例如透過視窗的儲存提供者)。會向上傳遞。 + + + Makes one image. Called once per tile, in parallel, for Generate, Vary (the request's VariationOf is set) and Retry. Report progress from 0 to 1 through the context, optionally with a preview the mosaic resolves from; throw to mark the tile failed. The context's token is cancelled on Stop. Null uses the `Simulation`'s, if any. + 產生一張圖像。每個格子呼叫一次,平行執行,用於 Generate、Vary(請求的 VariationOf 已設定)和 Retry。透過內容物件以 0 到 1 回報進度,可附帶供馬賽克成形的預覽;擲出例外即把該格子標記為失敗。按下 Stop 時內容物件的權杖會被取消。為 null 時使用 `Simulation` 的產生器(如果有)。 + + + True while the picked image is expanded over the grid. + 選取的圖像展開覆蓋網格時為 true。 + + + True while a batch runs (Generate reads Stop). + 批次執行時為 true(Generate 顯示為 Stop)。 + + + Every word the card shows or announces. + 卡片顯示或播報的每一個詞。 + + + The models offered as chips. Null or empty offers the simulation's models, or one unnamed model. + 作為標籤提供的模型。為 null 或空時提供模擬中的模型,沒有模擬時提供一個未命名的模型。 + + + The prompt field's placeholder; null uses `Placeholder`. + 提示詞欄位的預留文字;為 null 時使用 `Placeholder`。 + + + The prompt (two-way); the field and the suggestions write it. Null starts with the simulation's prompt, if any. + 提示詞(雙向);輸入欄位和建議會寫入它。為 null 時以模擬的提示詞開始(如果有)。 + + + The chosen shape's id (two-way); null or unknown chooses the first. It applies to the next generation; the grid keeps the shape of its batch. + 所選畫面比例的 id(雙向);為 null 或未知時選擇第一個。它對下一次生成生效;網格維持本批次的比例。 + + + The picked image (the upscaled one after an upscale), or null. + 選取的圖像(放大後為放大後的圖像),或 null。 + + + The chosen model's id (two-way); null or unknown chooses the first model. + 所選模型的 id(雙向);為 null 或未知時選擇第一個模型。 + + + Raised when the person picks or clears an image, and with the upscaled image once an upscale finishes. Bubbles. + 使用者選取或取消選取圖像時觸發,放大完成後也會以放大後的圖像觸發。會向上傳遞。 + + + The generator the card plays while `Generator` is null: its prompt, models, suggestions and painter, run exactly like your own generator (four tiles resolving from coarse blocks, Upscale, Vary). Null leaves Generate unavailable instead. + `Generator` 為 null 時卡片播放的產生器:它的提示詞、模型、建議與繪製器,執行方式與你自己的產生器完全相同(四個格子從粗略色塊成形、Upscale、Vary)。為 null 時 Generate 無法使用。 + + + Prompts offered in the empty grid before the first generation; picking one fills the prompt and generates. Null offers the simulation's, or none. + 第一次生成之前在空網格中提供的提示詞;選擇其中一個會填入提示詞並開始生成。為 null 時提供模擬中的建議,沒有模擬時不提供。 + + + Upscales the picked image while it is expanded; return the new image with its width and height. The token is cancelled when the expanded image closes. Null uses the `Simulation`'s, or keeps the image as it is. + 在選取的圖像展開時將其放大;傳回帶有寬度和高度的新圖像。展開的圖像關閉時權杖會被取消。為 null 時使用 `Simulation` 的放大器,沒有模擬時保持圖像原樣。 + + + An output shape offered as a chip on an `ImageGeneration` card. + 在 `ImageGeneration` 卡片上作為標籤提供的輸出畫面比例。 + + + Handed to a generator or upscaler: the cancellation of the run and a way to report progress. + 交給產生器或放大器的物件:本次執行的取消,以及回報進度的方法。 + + + Arguments of `DownloadRequested`. + `DownloadRequested` 的引數。 + + + One generated image. + 一張生成的圖像。 + + + Every word `ImageGeneration` shows or says, in English by default. Replace the whole object to localize or reword; text with numbers in it comes from format functions. Image numbers start at 1. + `ImageGeneration` 顯示或播報的每一個詞,預設為英文。取代整個物件即可在地化或改寫;帶數字的文字來自格式化函式。圖像編號從 1 開始。 + + + A model offered as a chip on an `ImageGeneration` card. + 在 `ImageGeneration` 卡片上作為標籤提供的模型。 + + + Paints a picture for an `ImageGenerationSimulation`: `width` × `height` opaque BGRA bytes, row by row (width × height × 4). The same prompt and seed should paint the same picture. Called off the UI thread. + 為 `ImageGenerationSimulation` 繪製一張圖片:`width` × `height` 的不透明 BGRA 位元組,逐列排列(width × height × 4)。相同的提示詞與種子應繪製相同的圖片。在 UI 執行緒之外呼叫。 + + + What `Generator` is asked to make: one image of a batch. + 要求 `Generator` 產生的內容:一個批次中的一張圖像。 + + + Arguments of `SelectionChanged`. + `SelectionChanged` 的引數。 + + + A generator and an upscaler an `ImageGeneration` plays while the app wires no `Generator`, fed by the caller: the prompt it starts with, the models and suggestions on offer, and a `Painter` that paints a picture for a prompt (the same prompt and seed always paint the same picture). Each tile reports progress frame by frame over two to three seconds and resolves from the painted preview; Upscale paints it again at twice the detail over about two seconds. Nothing is downloaded or sent anywhere. Set the data before handing the simulation to the card; to offer other pictures, give the card a new simulation. + 在應用程式沒有接上 `Generator` 時由 `ImageGeneration` 播放的產生器與放大器,資料由呼叫端提供:起始的提示詞、可選的模型與建議,以及一個為提示詞繪製圖片的 `Painter`(相同的提示詞與種子總是繪製相同的圖片)。每個格子在兩到三秒內逐影格回報進度,並從繪製的預覽中逐步成形;Upscale 在約兩秒內以兩倍細節重新繪製。不會下載或傳送任何內容。請在把模擬交給卡片之前設定好資料;要提供其他圖片,就給卡片一個新的模擬。 + + + An image generation canvas (Arc Image reveal, design reference): the way current image models show a picture forming. A soft, heavily blurred preview made from the image's own colors, with fine film grain, sharpens top to bottom behind a soft-edged band while the rest gently comes into focus; a translucent pill shows a progress ring and the step count; queued and failed jobs get a calm surface with one soft shimmer, or an alert, the message and Retry. Drive it with `Status`, `Progress` and `Source` (coarse progress updates are smoothed into one continuous reveal; without progress it estimates from `Duration`), or hand it a `Generator` that reports progress and returns the image. A `Source` alone is shown finished. For demos, a `Simulation` plays a looping, simulated generation over the pictures it is given; with nothing at all the canvas rests on its plain surface. The canvas fills its container up to 560px wide and keeps `AspectRatio`. + 影像生成畫布(Arc Image reveal,設計參考):以當今影像模型的方式呈現圖片逐漸成形。一張由影像本身顏色產生、帶細緻底片顆粒的高度模糊柔和預覽,在一條邊緣柔和的光帶後方由上而下變清晰,其餘部分慢慢對焦;半透明的膠囊顯示進度環與步數;排隊中與失敗的工作顯示一塊安靜的表面與一道柔和的微光,或警示圖示、錯誤訊息與 Retry。以 `Status`、`Progress` 與 `Source` 驅動它(粗略的進度更新會被平滑成一段連續的顯現;沒有進度時依 `Duration` 估算),或交給它一個回報進度並傳回圖片的 `Generator`。只設定 `Source` 時,圖片直接以完成狀態顯示。用於展示時,`Simulation` 會在傳入的圖片上播放循環的模擬生成;什麼都沒有設定時,畫布停在它的素色表面上。畫布填滿容器,最寬 560px,並維持 `AspectRatio`。 + + + The status on screen, in every mode (the simulation's and the generator's too). + 畫面上顯示的狀態,適用於所有模式(包括模擬模式與產生器模式)。 + + + Shows a Retry button in the error state; it raises `RetryRequested` (and reruns the `Generator`). + 在錯誤狀態下顯示 Retry 按鈕;它會引發 `RetryRequested`(並重新執行 `Generator`)。 + + + The image's name once it is done (the canvas's accessible name); "Generated image" by default. + 圖片完成後的名稱(畫布的無障礙名稱);預設為 "Generated image"。 + + + The canvas's width over its height (1.5, or 0.8 for 4 / 5); the image is cover-cropped to it. + 畫布的寬度除以高度(1.5,或 4 / 5 對應的 0.8);圖片以覆蓋方式裁切到該比例。 + + + Simulation and generator: starts a run as soon as the control is shown. Otherwise it waits in the queued look for `Restart`. + 模擬模式與產生器模式:控制項一顯示就開始執行。否則它以排隊外觀等待 `Restart`。 + + + Raised once per run when the final image is fully resolved and crisp. Bubbles. + 每次執行中最終圖片完全顯現並變清晰時引發一次。會向上傳遞。 + + + How long one generation takes when progress is not given (the simulation's length, or the estimate's). + 未提供進度時一次生成所需的時間(模擬的時長,或估算的時長)。 + + + The message of the error state; "Generation failed" by default. + 錯誤狀態的訊息;預設為 "Generation failed"。 + + + Runs one image job: it reports progress (and the image as soon as it has one) and returns the final image. Used when `Status` is null. + 執行一次影像工作:回報進度(以及一拿到就附上的圖片),並傳回最終圖片。在 `Status` 為 null 時使用。 + + + Simulation only: how long the finished image holds before a loop restarts. + 僅限模擬:完成的圖片在循環重新開始前停留的時間。 + + + Simulation only: after `HoldDuration` the image blurs back out and generates again. + 僅限模擬:經過 `HoldDuration` 後,圖片重新模糊淡出並再次生成。 + + + Every word the control shows or says. + 控制項顯示或朗讀的每一個詞。 + + + Controlled progress from 0 to 1 while generating; coarse updates (one per model step) are smoothed into continuous motion. Null estimates it from `Duration`. + 生成期間受控的進度,取值 0 到 1;粗略的更新(每個模型步驟一次)會被平滑成連續的動態。為 null 時依 `Duration` 估算。 + + + Simulation only: how long each run waits in the queue. + 僅限模擬:每次執行在佇列中等待的時間。 + + + The chip while queued, such as "Queued, 2 ahead"; "Queued" by default. + 排隊時的標籤,例如 "Queued, 2 ahead";預設為 "Queued"。 + + + Raised when the person presses Retry in the error state. Bubbles. Restart your job, then set `Status`. + 在錯誤狀態下按下 Retry 時引發。會向上傳遞。請重新啟動你的工作,然後設定 `Status`。 + + + The progress ring's color; the accent by default. + 進度環的顏色;預設為強調色。 + + + Simulation and generator: change it to blur back out and run again (a Generate again button can bind its count here). + 模擬模式與產生器模式:變更它會讓圖片重新模糊淡出並再次執行("Generate again" 按鈕可以把計數繫結到這裡)。 + + + Seeds the film grain and the drift of the soft preview (and which of the simulation's pictures comes first), so tiles in a grid differ. + 為底片顆粒與柔和預覽的漂移(以及模擬中最先出現的是哪張圖片)提供種子,讓格線中的圖塊各不相同。 + + + Shows the translucent progress pill. It fades out shortly after the image resolves. + 顯示半透明的進度膠囊。圖片顯現完成後不久它會淡出。 + + + Plays one soft light sheen across the image when it resolves. Skipped with reduced motion. + 圖片顯現完成時讓一道柔和的光澤橫掃而過。減少動態效果時略過。 + + + A simulated job to play while `Status`, `Generator` and `Source` are null: it reveals its pictures in turn on the timing of `Duration`, `QueueDuration` and `HoldDuration`. Null (the default) leaves the canvas on its plain surface. Give each control its own simulation. + 在 `Status`、`Generator` 與 `Source` 都為 null 時播放的模擬工作:依 `Duration`、`QueueDuration` 與 `HoldDuration` 的節奏輪流揭示它的圖片。為 null(預設)時畫布停在素色表面上。請給每個控制項各自的模擬執行個體。 + + + The overlay's scale and the corner radius: Small (grids, thumbnails) shows only the ring and the count; Medium and Large add the Generating label. + 疊加層的比例與圓角:Small(格線、縮圖)只顯示進度環與步數;Medium 與 Large 另加 Generating 標籤。 + + + The final image (any `IImage`, usually a decoded `Bitmap`); it is cover-cropped to the canvas. With no `Status` and no `Generator` it is simply shown, finished (a simulation is set aside). + 最終圖片(任何 `IImage`,通常是已解碼的 `Bitmap`);以覆蓋方式裁切到畫布。沒有 `Status` 也沒有 `Generator` 時,它直接以完成狀態顯示(模擬會讓位)。 + + + Simulation only: extra queue time before each run, for staggered grids. + 僅限模擬:每次執行前額外的排隊時間,用於錯開格線中的圖塊。 + + + The job's status when the app controls it. Null runs the generator, shows `Source`, or plays the `Simulation` (queued, generating over `Duration`, done). Every change transitions from the current pose. + 由應用程式控制時工作的狀態。為 null 時執行產生器、顯示 `Source`,或播放 `Simulation`(排隊、在 `Duration` 內生成、完成)。每次變化都從目前姿態轉場。 + + + Raised when the status on screen changes, including the simulation's transitions. Bubbles. + 畫面上顯示的狀態變化時引發,包括模擬中的狀態切換。會向上傳遞。 + + + The total steps the pill counts, as in 18 / 30 (tabular numerals, reserved width). + 膠囊計數的總步數,如 18 / 30(等寬數字,寬度預留)。 + + + Every word `ImageReveal` shows or says, in English by default. Replace the whole object to localize or reword; the counts are format functions. `QueueLabel` and `ErrorMessage` override the queued chip and the error message for one job. + `ImageReveal` 顯示或朗讀的每一個詞,預設為英文。替換整個物件即可在地化或改寫;計數是格式化函式。`QueueLabel` 與 `ErrorMessage` 可為單一工作覆寫排隊標籤與錯誤訊息。 + + + One progress report from an `ImageRevealGenerator`: how far the job is (0 to 1) and, when the service has one, the image so far (the soft preview is sampled from it, so pass it as soon as it exists). + 來自 `ImageRevealGenerator` 的一次進度回報:工作進行到哪裡(0 到 1),以及服務已有圖片時的目前圖片(柔和預覽從它取樣,所以一有圖片就傳入)。 + + + A simulated image job for an `ImageReveal` that has no `Status`, `Generator` or `Source`, for demos and prototypes. Each run waits in the queue for `QueueDuration` (plus `StartDelay`), generates over `Duration` and reveals one of `Pictures`; with `IsLooping` it holds for `HoldDuration`, blurs back out and runs again with the next picture. The first run shows the picture `Seed` picks (seed 1 the first, seed 2 the second, wrapping), so tiles in a grid differ. Announcements say "simulated". Nothing is generated or downloaded. Create one per control: it keeps its place in the pictures. + 用於展示和原型的 `ImageReveal` 模擬影像工作,適用於沒有 `Status`、`Generator` 或 `Source` 的控制項。每次執行先在佇列中等待 `QueueDuration`(加上 `StartDelay`),再於 `Duration` 內生成並揭示 `Pictures` 中的一張;開啟 `IsLooping` 時停留 `HoldDuration`,然後重新模糊淡出,並以下一張圖片再次執行。第一次執行顯示 `Seed` 選中的圖片(種子 1 為第一張,種子 2 為第二張,循環取用),讓格線中的圖塊各不相同。播報會註明 "simulated"。不會生成或下載任何內容。請為每個控制項建立一個執行個體:它會記住自己播放到哪張圖片。 + + + One picture of an `ImageRevealSimulation`: the image a simulated run reveals and its name. + `ImageRevealSimulation` 中的一張圖片:模擬執行所揭示的圖片及其名稱。 + + + Where an `ImageReveal` job is. + `ImageReveal` 工作所處的階段。 + + + Arguments of `StatusChanged`. + `StatusChanged` 的引數。 + A section title that reveals itself when 45% of it scrolls into view (Arc InViewTitle, P18 + P20): opacity leads, the blur clears next and the movement settles last, so the title is readable before it fully arrives. Titles already scrolled past show at once. With `IsReplayEnabled` it hides again (a quick fade with no stagger) when it leaves and replays on every entry. Reduced motion shows the title without movement, blur or masks. Drawn as one control: no per-word controls. 當其 45% 捲動進入視野時自行顯現的章節標題(Arc InViewTitle,P18 + P20):不透明度先行,模糊隨後消退,位移最後收尾,因此標題在完全到位之前即可閱讀。已捲過的標題會立即顯示。設定 `IsReplayEnabled` 後,標題離開時會再次隱藏(快速淡出,無錯開),並在每次進入時重新播放。減少動態效果時標題直接顯示,不帶位移、模糊或遮罩。它以單一控制項繪製:沒有逐字控制項。 @@ -8628,6 +11092,146 @@ Arc input: a single-line field with a visible label, helper and error rows that open on a spring and rewrite only the words that change, and the Arc frame states (hover and focus darken the border, an error turns it danger, disabled fades). Everything from Avalonia's TextBox is kept: caret, selection, IME, undo, clipboard and a two-way `Text`. Arc 輸入框:帶可見標籤的單行欄位,輔助文字列與錯誤列以彈簧展開,只改寫變化的詞;外框狀態與 Arc 一致(懸停與聚焦加深邊框,錯誤時變為危險色,停用時變淡)。Avalonia TextBox 的能力全部保留:游標、選取、輸入法、復原、剪貼簿與雙向繫結的 `Text`。 + + A "get started in 30 seconds" card for SDK onboarding (Arc Install snippet, design reference): framework tabs, three numbered steps (Install, Add your key, Ship) and highlighted snippets that take a developer from install to the first event. Step 1 offers npm, pnpm, yarn and bun when the framework installs packages; every snippet has a copy button; Generate key wipes a fresh key into the placeholder in place. Snippets, file names and step lines crossfade in space that is reserved for the longest of them, so switching frameworks never moves anything. Give it your `Frameworks` (write `{{key}}` where the key goes) and a `KeyGenerator` that creates a real key, or pass `ApiKey` for a reader who already has one. With no frameworks it shows the SDK of its `Simulation`, if any, and otherwise the three steps with empty snippets; with no generator it invents keys (nothing is sent, and step 2 says so). + 用於 SDK 引導的「30 秒上手」卡片(Arc Install snippet,設計參考):框架分頁、三個編號步驟(安裝、新增金鑰、上線)和語法標示的程式碼片段,帶開發者從安裝走到第一個事件。當框架透過套件安裝時,步驟 1 提供 npm、pnpm、yarn 和 bun;每段片段都有複製按鈕;「產生金鑰」會把新金鑰就地擦入預留位置。片段、檔名和步驟說明在為其中最長者預留的空間裡交叉淡化,因此切換框架不會移動任何東西。傳入你的 `Frameworks`(在金鑰所在位置寫 `{{key}}`)和能建立真實金鑰的 `KeyGenerator`,或為已有金鑰的讀者傳入 `ApiKey`。沒有框架時顯示其 `Simulation` 的 SDK(如果有),否則顯示三個步驟與空白的程式碼片段;沒有產生器時會編造金鑰(不傳送任何內容,步驟 2 會據實說明)。 + + + The reader's own key. When set, the snippets show it and the Generate key button is hidden, for signed-in readers who already have a key. + 讀者自己的金鑰。設定後,各片段顯示該金鑰,「產生金鑰」按鈕隱藏,適用於已登入且已有金鑰的讀者。 + + + Below this width the framework tabs drop their marks and the version label hides. + 寬度低於此值時,框架分頁拿掉標記,版本標籤隱藏。 + + + Raised after a copy with the step and the exact clipboard text (onboarding analytics). Bubbles. + 複製完成後引發,帶有步驟和放入剪貼簿的確切文字(用於引導流程分析)。會冒泡。 + + + The key in the snippets: `ApiKey`, else the last generated key, else null (the placeholder shows). + 片段中的金鑰:`ApiKey`,否則為最近產生的金鑰,否則為 null(顯示預留位置)。 + + + One line under the title. + 標題下方的一行說明。 + + + Raised when the reader picks another framework tab. Bubbles. + 讀者選取另一個框架分頁時引發。會冒泡。 + + + Raised with each new key once it is in the snippets. Bubbles. + 每把新金鑰進入片段後引發,帶有該金鑰。會冒泡。 + + + Creates a real key. Return the key and it wipes into the snippets; throw to show the error with Try again. The token is cancelled when the card leaves the window or a new key is requested. Null invents a demo key (nothing is sent). + 建立真實金鑰。傳回金鑰後它會擦入各片段;擲出例外則顯示錯誤和「重試」。卡片離開視窗或再次要求金鑰時,權杖會被取消。為 null 時編造展示金鑰(不傳送任何內容)。 + + + Shown, tinted, wherever `{{key}}` appears until a key exists; copies as written. "YOUR_API_KEY" by default. + 在金鑰出現之前,顯示在每個 `{{key}}` 處(帶底色);依原樣複製。預設為 "YOUR_API_KEY"。 + + + Where the Generate key button is: idle, generating, ready or failed. + 「產生金鑰」按鈕所處的狀態:閒置、產生中、就緒或失敗。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個詞。 + + + Below this width the padding tightens and the package manager tabs show marks only. + 寬度低於此值時,內距收緊,套件管理工具分頁只顯示標記。 + + + The package manager of step 1 (two-way). Persist it to remember the reader's choice across pages. + 步驟 1 使用的套件管理工具(雙向)。持久保存它,即可在不同頁面間記住讀者的選擇。 + + + Raised when the reader picks another package manager. Bubbles. + 讀者選取另一個套件管理工具時引發。會冒泡。 + + + The selected framework's id (two-way); null selects the first. + 所選框架的 id(雙向);為 null 時選取第一個。 + + + A simulated SDK to show while `Frameworks` is empty: its framework tabs and the prefix of the keys the simulated Generate key invents. Null (the default) shows the three steps with empty snippets. Give each card its own simulation. + 在 `Frameworks` 為空白時顯示的模擬 SDK:它的框架分頁,以及模擬的「產生金鑰」所編造金鑰的前置字串。為 null(預設)時顯示三個步驟與空白的程式碼片段。請給每張卡片各自的模擬執行個體。 + + + The card's title. + 卡片的標題。 + + + A muted label at the top right, such as the SDK version; null (the default) or empty hides it. + 右上角低調的標籤,例如 SDK 版本;為 null(預設)或空白時隱藏。 + + + One snippet of an `InstallSnippetFramework`: the file it goes in, its code and one line saying what to do with it. Write `{{key}}` where the API key goes. + `InstallSnippetFramework` 中的一段片段:它所屬的檔案、程式碼,以及一行說明該怎麼用。在 API 金鑰所在位置寫 `{{key}}`。 + + + The code; `{{key}}` marks where the API key goes. Keep it under ten lines. + 程式碼;`{{key}}` 標出 API 金鑰所在位置。保持在十行以內。 + + + The line under the step's title; null uses the step's default from the labels. + 步驟標題下方的一行說明;為 null 時使用標籤文字中該步驟的預設說明。 + + + The file name in the code block's header, such as `.env` or `app.py`; "Terminal" for a command. + 程式碼區塊標題列中的檔名,例如 `.env` 或 `app.py`;指令使用 "Terminal"。 + + + The highlighting language (`bash`, `ts`, `python`, `go`, `html`, ...); null infers it from the file name. + 語法標示所用的語言(`bash`、`ts`、`python`、`go`、`html` 等);為 null 時依檔名推斷。 + + + One framework tab of an `InstallSnippet`: its id and label, an optional mark, and the three snippets. Step 1 is a package install when `Packages` is set (the reader picks npm, pnpm, yarn or bun) and `Install` otherwise (a command such as `pip install`, or a script tag). + `InstallSnippet` 的一個框架分頁:它的 id 和名稱、可選的標記,以及三段片段。設定了 `Packages` 時,步驟 1 是套件安裝(讀者可選 npm、pnpm、yarn 或 bun),否則使用 `Install`(例如 `pip install` 之類的指令,或指令碼標籤)。 + + + Step 3: the code that sends the first event. + 步驟 3:傳送第一個事件的程式碼。 + + + A stable id, such as `node`. The ids `node`, `python`, `go` and `html` get a mark of their own. + 穩定的 id,例如 `node`。id 為 `node`、`python`、`go` 和 `html` 時有各自的標記。 + + + Step 1 when there are no `Packages`: a command or a script tag. + 沒有 `Packages` 時的步驟 1:一道指令或一個指令碼標籤。 + + + Step 2: where the key goes, with `{{key}}` in the code. + 步驟 2:金鑰放在哪裡,程式碼中寫 `{{key}}`。 + + + The tab's name. + 分頁的名稱。 + + + The tab's mark, a 24 × 24 stroked glyph; null uses the built-in mark of a known id, or none. + 分頁的標記,24 × 24 的描邊圖形;為 null 時使用已知 id 的內建標記,否則不顯示。 + + + Package names for step 1, separated by spaces (`@acme/sdk`); the command follows the package manager. + 步驟 1 的套件名稱,以空格分隔(`@acme/sdk`);指令隨套件管理工具變化。 + + + The description of step 1 for a package install; null uses the label's default. + 套件安裝時步驟 1 的說明;為 null 時使用標籤文字的預設值。 + + + Every word `InstallSnippet` shows or says, in English by default (the card's title, description and version are properties of the card). Replace the whole object to localize or reword; announcements that name something are format functions. + `InstallSnippet` 顯示或朗讀的每一個詞,預設為英文(卡片的標題、說明和版本是卡片本身的屬性)。替換整個物件即可在地化或改寫措辭;需要代入名稱的播報是格式化函式。 + + + A simulated SDK for an `InstallSnippet` that has no `Frameworks`, for demos and prototypes: the framework tabs it shows and the prefix of the keys its simulated Generate key invents (nothing is created or sent, and step 2 says so). The app's own frameworks set it aside. Create one per card. + 用於展示和原型、供沒有 `Frameworks` 的 `InstallSnippet` 使用的模擬 SDK:它顯示的框架分頁,以及模擬的「產生金鑰」所編造金鑰的前置字串(不會建立或傳送任何內容,步驟 2 會據實說明)。應用程式自己的框架會讓它讓位。請為每張卡片建立一個執行個體。 + A filter of `Integrations`: the items whose `Category` is its key. `Integrations` 的篩選項:`Category` 等於其鍵的那些項目。 @@ -10364,57 +12968,233 @@ The heading of the block; null hides it. 區塊的標題;為 null 時隱藏。 - - Writes a value wherever one is shown or spoken; without one, values are whole numbers like "2,316". - 在顯示或朗讀數值的地方輸出文字;未提供時,數值為整數,形如 "2,316"。 + + Writes a value wherever one is shown or spoken; without one, values are whole numbers like "2,316". + 在顯示或朗讀數值的地方輸出文字;未提供時,數值為整數,形如 "2,316"。 + + + The name of the measured quantity ("Active"), above the current value and in the slider's spoken value. + 被量測的量的名稱("Active"),顯示在目前值上方,也用於滑桿朗讀的數值。 + + + The readout's ratio label; {0} is `BaselineLabel` with a lower-case first letter. + 讀數中比值的標籤;{0} 為首字母小寫的 `BaselineLabel`。 + + + An event to point out in a `LineReplay`, timed on the same clock as the data. It gets a row in the list of moments, a tick on the slider, and a dot and a card on the chart once the replay gets there. + 在 `LineReplay` 中要指出的事件,與資料使用同一個時鐘。它在時刻清單中佔一列,在滑桿上有一個刻度,重播抵達時在圖表上顯示一個圓點與一張卡片。 + + + One optional line of detail under the title. + 標題下方選填的一行說明。 + + + A stable id, reported back by `MomentReached`. + 穩定的識別碼,會由 `MomentReached` 回報。 + + + The sort of event, which decides the icon (problems also use the warning color). + 事件的類型,決定圖示(問題類事件還會使用警告色)。 + + + The clock time of the event in minutes; it may fall between two samples. + 事件的時鐘時間,以分鐘計;可以落在兩個取樣之間。 + + + A short name for the event ("Release notes published"). + 事件的簡短名稱("Release notes published")。 + + + The sort of event a `LineReplayMoment` marks, which decides its icon. + `LineReplayMoment` 所標記的事件類型,決定其圖示。 + + + A sample for a `LineReplay`: when it was taken (`Time`, minutes on the block's clock), what it measured (`Value`), and optionally what a normal day measured then (`Baseline`), which the chart shows as a dashed line. + `LineReplay` 的一個取樣:取樣時間(`Time`,區塊時鐘上的分鐘數)、量得的值(`Value`),以及選填的、平常日子在該時刻的值(`Baseline`),圖表會把它畫成虛線。 + + + How a `LineReplay` looks before anyone has played it. + `LineReplay` 在還沒有人播放之前的樣子。 + + + Shows the cell as an accent-colored link with an underline that strengthens on hover. A click on the text raises `CellInvoked` with `Part`; the cell is not selected by that click. + 把儲存格顯示為強調色連結,底線在滑鼠移上時加深。點擊文字會以 `Part` 觸發 `CellInvoked`;該次點擊不會選取儲存格。 + + + A realtime visitors card (Arc Live visitors, design reference): a pulsing live status, the number of people on the site rolling digit by digit, a dotted world map where new arrivals ping in and fade, the top pages right now with bars that reorder as counts change, and a ticker of signups and upgrades with the country and how long ago. A pause button holds the last snapshot. Feed it from your realtime source: set `Count`, `Pages`, `Visitors` and `Events` to new snapshots as they arrive (the card works out what moved from the ids), or push single arrivals and conversions with `PushVisitor` and `PushEvent`. Reflect the socket's state in `Connection`. Without `Count` the card plays its `Simulation`, a `LiveVisitorsSimulation` of a site you describe; with neither, it holds placeholders and says there is no data. + 即時訪客卡片(Arc Live visitors,設計參考):脈動的即時狀態、逐位滾動的站內人數、新訪客閃現後淡去的點陣世界地圖、帶長條且隨人數重新排序的目前熱門頁面,以及顯示國家和多久之前的註冊與升級動態。暫停按鈕保留最後一份快照。從你的即時資料來源取得資料:在新快照抵達時設定 `Count`、`Pages`、`Visitors` 和 `Events`(卡片根據 id 判斷哪些發生了變化),或用 `PushVisitor` 和 `PushEvent` 逐筆推送訪客與轉換。用 `Connection` 反映連線狀態。沒有 `Count` 時,卡片播放它的 `Simulation`,也就是一個描述你所指定網站的 `LiveVisitorsSimulation`;兩者都沒有時,卡片維持預留位置狀態並說明沒有資料。 + + + The stream's state: Connecting shows placeholders in the same space, Reconnecting keeps the last data with a warning dot. + 資料串流的狀態:Connecting 在同樣的位置顯示預留位置,Reconnecting 保留最後的資料並顯示警告色圓點。 + + + People on the site right now. Leave it null to play the `Simulation`; once set, the card shows only the data you pass. + 此刻站內的人數。保持為 null 時播放 `Simulation`;一旦設定,卡片只顯示你傳入的資料。 + + + Recent conversions. The card lists the newest few; with a position the newest is labelled on the map for 2s. + 最近的轉換。卡片列出最新的幾次;帶有位置時,最新的一次會在地圖上標註 2 秒。 + + + Shows the pause button. + 顯示暫停按鈕。 + + + Whether updates are paused (two-way). While paused the card holds its last snapshot, whatever you keep passing. + 是否暫停更新(雙向)。暫停期間卡片保留最後一份快照,不論你繼續傳入什麼。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每個詞。 + + + How many conversions to list. + 列出多少次轉換。 + + + How many pages to list. + 列出多少個頁面。 + + + Pages with people on them. The card sorts them and lists the busiest few with bars scaled to the busiest page; rows slide when the order changes. + 有人正在瀏覽的頁面。卡片為它們排序,列出最熱門的幾個,長條依最熱門頁面縮放;順序變化時各列滑動。 + + + Raised when the person presses the pause button. Bubbles. Pause or close your own subscription to match. + 使用者按下暫停按鈕時引發。會向上傳遞。相應地暫停或關閉你自己的訂閱。 + + + A simulated stream to play while `Count` is null: its count, pages, arrivals and conversions, advanced by the card's clock while it is on screen, live and not paused. Null (the default) leaves the card empty: placeholders and a "No data" status. Give each card its own simulation. + 在 `Count` 為 null 時播放的模擬資料串流:它的人數、頁面、訪客與轉換,在卡片位於螢幕上、處於 Live 且未暫停時由卡片的時鐘推進。為 null(預設)時卡片為空白:顯示預留位置內容與 "No data" 狀態。請給每張卡片各自的模擬執行個體。 + + + Recent arrivals as map dots. New ids ping in, ids no longer passed fade out; keep it to the last few seconds. + 最近抵達的訪客,顯示為地圖上的圓點。新的 id 閃現,不再傳入的 id 淡出;只保留最近幾秒的訪客。 + + + A recent conversion in the `LiveVisitors` ticker: what happened (past tense, "Signed up"), where and when. Never put personal data such as an email address in it. + `LiveVisitors` 動態中的一次最近轉換:發生了什麼(過去式,"Signed up")、在哪裡、何時。絕不要在其中放入個人資料,例如電子郵件地址。 + + + Every word `LiveVisitors` shows or says, in English by default. Replace the whole object to localize or reword; counts, times and sentences are format functions. + `LiveVisitors` 顯示或朗讀的每個詞,預設為英文。替換整個物件即可在地化或改寫;人數、時間和句子是格式化函式。 + + + A page with people on it, listed by `LiveVisitors` with a bar scaled to the busiest page. + 有人正在瀏覽的頁面,由 `LiveVisitors` 列出,長條依最熱門頁面縮放。 + + + A recent arrival drawn on the `LiveVisitors` map. A new `Id` pings in; an id that is no longer passed fades out. Send arrivals as a rolling window of the last few seconds. + 繪製在 `LiveVisitors` 地圖上的一位最近訪客。新的 `Id` 閃現;不再傳入的 id 淡出。以最近幾秒的滾動視窗傳送訪客。 + + + A simulated visitors stream for a `LiveVisitors` card that has no `Count`, for demos, prototypes and tests. It plays the site you describe: about `TypicalCount` people spread over `SitePages`, a new arrival from one of `Cities` every 600ms, page counts that drift, and one of `Conversions` every 2.4s; it starts with a few conversions from the last minutes. Nothing is fetched or tracked. It has no timer of its own: call `Advance` with the time that passed (the card does this from its clock while it is on screen and not paused). Setting any data property starts the stream over. Create one per card. + 用於展示、原型和測試,供沒有 `Count` 的 `LiveVisitors` 卡片使用的模擬訪客資料串流。它播放你描述的網站:約 `TypicalCount` 人分布在 `SitePages` 上,每 600ms 有一位來自 `Cities` 之一的新訪客,頁面人數會漂移,每 2.4s 發生一次 `Conversions` 之一;開始時已有幾筆最近幾分鐘的轉換。不會擷取或追蹤任何內容。它沒有自己的計時器:用經過的時間呼叫 `Advance`(卡片在螢幕上且未暫停時由自己的時鐘呼叫)。設定任何資料屬性都會讓資料串流從頭開始。請為每張卡片建立一個執行個體。 + + + Raised after `Advance` changed the data. + `Advance` 變更資料後引發。 + + + A city a `LiveVisitorsSimulation` sends people from. + `LiveVisitorsSimulation` 中訪客來自的一座城市。 + + + A kind of conversion the simulated site records, with how often it happens. + 模擬網站記錄的一種轉換,以及它發生的頻率。 + + + A page of the simulated site, with how much of the traffic it gets. + 模擬網站的一個頁面,以及它獲得的流量份額。 + + + A pricing calculator for LLM workloads in one panel (Arc LLM cost calculator, design reference). The header shows the selected model's monthly total in a large rolling figure. Below it come your workload presets, three sliders for input tokens per request, output tokens per request and requests per day, and a Cached input switch that bills part of the input at the cache read rate. Every model sits in a ranked list, lowest cost first, with its mark, a bar relative to the most expensive model and a rolling monthly cost; rows glide to their new rank as values change. Picking a row selects that model for the total, the footer breakdown (input, cached, output, per request) and its price line. Prices are inputs: pass your own `Models` with list prices in US dollars per 1M tokens and say where they came from in `Description`. With no models the panel shows its empty state. `ComputeCost` is the arithmetic the panel uses, for server code and invoices. + 一個面板中的 LLM 工作負載價格試算器(Arc LLM cost calculator,設計參考)。頁首以大型滾動數字顯示所選模型的每月總額。其下是你的工作負載預設、三個滑桿(每次請求的輸入 token 數、每次請求的輸出 token 數和每天的請求數),以及一個 Cached input 開關,開啟後部分輸入依快取讀取價格計費。所有模型依費用由低到高排在一份清單中,每列有其標誌、一根相對於最貴模型的長條和滾動的每月費用;數值改變時各列滑動到新的名次。選取一列,總額、底部明細(輸入、快取、輸出、每次請求)和價格列就切換為該模型。價格是輸入:傳入你自己的 `Models` 及其牌價(每 1M token 的美元價格),並在 `Description` 中說明來源。沒有模型時,面板顯示空狀態。`ComputeCost` 是面板使用的演算法,可用於伺服器程式碼和發票。 + + + Adds a Try again button to the error state; it raises `RetryRequested`. + 在錯誤狀態中加入 Try again 按鈕;它會觸發 `RetryRequested`。 + + + The share of input tokens billed at the cached rate while Cached input is on, from 0 to 1 (default 0.6). + 開啟 Cached input 時依快取價格計費的輸入 token 比例,取值 0 到 1(預設 0.6)。 + + + Days in a billing month (default 30). + 一個計費月的天數(預設 30)。 + + + The line under the heading: say where your prices come from and when they were checked. Null uses the label. + 標題下方的一行:說明價格的來源和核對日期。為 null 時使用標籤中的文字。 + + + Shows "Prices did not load" with this reason in place of the list. Null or empty shows the list. + 在清單的位置顯示 "Prices did not load" 和此原因。為 null 或空值時顯示清單。 + + + Shows placeholder rows with the list's exact geometry and disables the controls while prices load. + 價格載入期間顯示與清單幾何完全一致的佔位列,並停用各個控制項。 + + + Every word the panel shows or says. + 面板顯示或朗讀的每一個詞。 + + + The models to compare, each with prices in US dollars per 1M tokens. Null or an empty list shows the empty state. + 要比較的模型,每個都帶有每 1M token 的美元價格。為 null 或空清單時顯示空狀態。 + + + The fine print at the bottom of the panel. Null uses the label. + 面板底部的小字。為 null 時使用標籤中的文字。 - - The name of the measured quantity ("Active"), above the current value and in the slider's spoken value. - 被量測的量的名稱("Active"),顯示在目前值上方,也用於滑桿朗讀的數值。 + + Workload shortcuts shown as a segmented control; a Custom segment lights up when the sliders match none. Null or an empty list hides the row. + 以分段控制項顯示的工作負載捷徑;滑桿與任何預設都不相符時,Custom 分段會亮起。為 null 或空清單時隱藏這一列。 - - The readout's ratio label; {0} is `BaselineLabel` with a lower-case first letter. - 讀數中比值的標籤;{0} 為首字母小寫的 `BaselineLabel`。 + + Raised when the person presses Try again in the error state. Bubbles. + 在錯誤狀態中按下 Try again 時觸發。會向上傳遞。 - - An event to point out in a `LineReplay`, timed on the same clock as the data. It gets a row in the list of moments, a tick on the slider, and a dot and a card on the chart once the replay gets there. - 在 `LineReplay` 中要指出的事件,與資料使用同一個時鐘。它在時刻清單中佔一列,在滑桿上有一個刻度,重播抵達時在圖表上顯示一個圓點與一張卡片。 + + What the selected model costs for the workload. + 所選模型在目前工作負載下的費用。 - - One optional line of detail under the title. - 標題下方選填的一行說明。 + + The id of the selected model (two-way). Null selects the second model; an id that is not in the models falls back to the cheapest model. + 所選模型的 id(雙向)。為 null 時選擇第二個模型;不在模型中的 id 會退回最便宜的模型。 - - A stable id, reported back by `MomentReached`. - 穩定的識別碼,會由 `MomentReached` 回報。 + + The model the total and the breakdown are for, or null when no model is shown. + 總額和明細所對應的模型;沒有顯示模型時為 null。 - - The sort of event, which decides the icon (problems also use the warning color). - 事件的類型,決定圖示(問題類事件還會使用警告色)。 + + Raised when someone picks a row by pointer or with the arrow keys, with the model and its cost. Bubbles. + 有人透過指標或方向鍵選取一列時觸發,帶有該模型及其費用。會向上傳遞。 - - The clock time of the event in minutes; it may fall between two samples. - 事件的時鐘時間,以分鐘計;可以落在兩個取樣之間。 + + The heading of the panel. Null uses the label ("Estimated monthly cost"). + 面板的標題。為 null 時使用標籤中的文字("Estimated monthly cost")。 - - A short name for the event ("Release notes published"). - 事件的簡短名稱("Release notes published")。 + + The workload (two-way): input and output tokens per request, requests a day and whether caching is on. Values between slider stops still compute exactly; the sliders show the nearest stop. Defaults to 16,000 tokens in, 500 out, 5,000 requests a day, cached. + 工作負載(雙向):每次請求的輸入與輸出 token 數、每天的請求數,以及是否開啟快取。介於滑桿刻度之間的值仍依精確值計算;滑桿顯示最接近的刻度。預設為每次輸入 16,000 個 token、輸出 500 個,每天 5,000 次請求,開啟快取。 - - The sort of event a `LineReplayMoment` marks, which decides its icon. - `LineReplayMoment` 所標記的事件類型,決定其圖示。 + + Raised on every slider step, preset and cache toggle with the whole workload. Bubbles. + 滑桿的每一步、每個預設和每次切換快取時觸發,帶有完整的工作負載。會向上傳遞。 - - A sample for a `LineReplay`: when it was taken (`Time`, minutes on the block's clock), what it measured (`Value`), and optionally what a normal day measured then (`Baseline`), which the chart shows as a dashed line. - `LineReplay` 的一個取樣:取樣時間(`Time`,區塊時鐘上的分鐘數)、量得的值(`Value`),以及選填的、平常日子在該時刻的值(`Baseline`),圖表會把它畫成虛線。 + + Every word `LlmCostCalculator` shows or says, in English by default. Replace the whole object to localize or reword; counts, prices and sentences are format functions. Money reaches them already formatted ("$1,234.56"). + `LlmCostCalculator` 顯示或朗讀的每一個詞,預設為英文。替換整個物件即可在地化或改寫;數量、價格和句子是格式化函式。金額在傳入時已經格式化("$1,234.56")。 - - How a `LineReplay` looks before anyone has played it. - `LineReplay` 在還沒有人播放之前的樣子。 + + One model an `LlmCostCalculator` compares: its id, name and provider, an optional 16px mark, and its list prices in US dollars per 1M tokens. + `LlmCostCalculator` 比較的一個模型:其 id、名稱和供應商、可選的 16px 標誌,以及每 1M token 的美元牌價。 - - Shows the cell as an accent-colored link with an underline that strengthens on hover. A click on the text raises `CellInvoked` with `Part`; the cell is not selected by that click. - 把儲存格顯示為強調色連結,底線在滑鼠移上時加深。點擊文字會以 `Part` 觸發 `CellInvoked`;該次點擊不會選取儲存格。 + + A workload shortcut of an `LlmCostCalculator`: an id, a short label and the workload it sets. + `LlmCostCalculator` 的一個工作負載捷徑:id、簡短的標籤和它設定的工作負載。 The Login block as one control: it shows one of the five sign in and sign up layouts, chosen by `Variant`. Each layout is also a control of its own (`SignIn`, `LoginCentered`, `LoginSplit`, `LoginImmersive`, `SignupForm`) with its own properties and events; `Form` is the one being shown, so a host that switches layouts at run time configures it after each switch. @@ -11140,6 +13920,86 @@ The track's name: the player's heading while it is current, and its row in the playlist. 曲目名稱:作為目前曲目時是播放器的標題,也是它在播放清單中的那一列。 + + One task from the call: an owner, a due date and whether it is done. + 通話中產生的一項任務:一位負責人、一個到期日,以及是否已完成。 + + + AI notes for one call (Arc Meeting summary, design reference): a header with the platform's mark, the title, the platform, date and length and the attendee stack, then Summary, Decisions and Action items that stream in after a short summarizing pass. Hovering a line, or focusing its timestamp, shows the transcript quote it came from. Action items can be checked off and sent to an issue tracker or a docs app (`SendRequested`); the footer credits the model and offers copy and regenerate. Give it the notes in `Meeting` and drive `Status` yourself, or leave `Status` unset and the card plays its own summarizing pass when it scrolls into view. With `Summarizer` the card calls your async streaming delegate instead and streams each snapshot it yields. With neither it plays a `Simulation` (a call's notes you pass); with no data at all it shows the no transcript state. A send with no handler is simulated. + 一次通話的 AI 會議紀錄(Arc Meeting summary,設計參考):頁首包含平台標誌、標題、平台、日期與時長以及與會者頭像組,接著是經過一段簡短的彙整過程後串流出現的摘要、決議和待辦事項。滑鼠停在一行上或聚焦它的時間戳記,會顯示它出自的逐字稿原話。待辦事項可以勾選完成,也可以傳送到議題追蹤工具或文件應用程式(`SendRequested`);頁尾署名模型,並提供複製和重新產生。把紀錄放進 `Meeting` 並自己驅動 `Status`,或者不設定 `Status`,讓卡片在捲動進入畫面時自己播放彙整過程。設定 `Summarizer` 後,卡片改為呼叫你的非同步串流委派,並串流顯示它產出的每一個快照。兩者都沒有時,它播放 `Simulation`(你傳入的一次通話的紀錄);完全沒有資料時,顯示「無逐字稿」狀態。沒有處理常式的傳送是模擬的。 + + + Raised when the person checks or unchecks an action item. The card keeps the checked state itself. Bubbles. + 使用者勾選或取消勾選一個待辦事項時觸發。勾選狀態由卡片自己保存。向上傳遞。 + + + Raised when a send succeeded (or a simulated send finished). Bubbles. + 傳送成功(或模擬傳送完成)時觸發。向上傳遞。 + + + When the card drives its own status: start summarizing and play the pass. False starts on the finished notes. + 卡片自己驅動狀態時:開始彙整並播放彙整過程。為 false 時一開始就顯示完成的紀錄。 + + + The message under the error title when the status is Error. + 狀態為 Error 時顯示在錯誤標題下方的訊息。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個文字。 + + + The notes of the call. Null plays `Simulation`, or shows the no transcript state without one. + 這次通話的紀錄。為 null 時播放 `Simulation`,沒有模擬時顯示「無逐字稿」狀態。 + + + The model credited in the footer. Null credits the `Simulation`'s model while one plays, or hides the credit. + 頁尾中署名的模型。為 null 時,播放 `Simulation` 期間署名其模型,否則隱藏署名。 + + + Raised by Regenerate in the footer and Try again in the error state. Bubbles. A card that drives its own status also replays its pass (or runs `Summarizer` again). + 由頁尾的重新產生和錯誤狀態中的 Try again 觸發。向上傳遞。自己驅動狀態的卡片還會重新播放它的彙整過程(或再次執行 `Summarizer`)。 + + + Raised when the person sends an action item. Bubbles. Set `Completion` to your request before the handler returns; leave it null and the card simulates the send. + 使用者傳送一個待辦事項時觸發。向上傳遞。在處理常式返回之前把 `Completion` 設為你的請求;保持為 null 時,卡片會模擬這次傳送。 + + + Where action items can be sent (Issues and Docs by default). An empty list hides the send buttons. + 待辦事項可以傳送到的位置(預設是 Issues 和 Docs)。空清單會隱藏傳送按鈕。 + + + A call to play while the card has no `Meeting` and no `Summarizer`: its notes and model, with the summarizing pass, the notes streaming in and a tick and a send played once (sends are simulated). Your own data wins over it; null stops it and shows the no transcript state. + 卡片既沒有 `Meeting` 也沒有 `Summarizer` 時要播放的通話:它的紀錄和模型,包括彙整過程、串流顯示紀錄,以及只播放一次的勾選和傳送(傳送是模擬的)。你自己的資料優先於它;為 null 時停止播放並顯示「無逐字稿」狀態。 + + + The status, when you drive it. Null (the default) lets the card play its own summarizing pass when it scrolls into view, or run `Summarizer`. Ready with empty lists shows the no transcript state. + 由你驅動時的狀態。為 null(預設)時,卡片在捲動進入畫面後播放自己的彙整過程,或執行 `Summarizer`。Ready 且清單為空時顯示沒有逐字稿的狀態。 + + + Stream the notes in, a few words at a time, when the status moves from Summarizing to Ready. + 狀態從 Summarizing 變為 Ready 時,以每次幾個字詞的方式串流顯示紀錄。 + + + An async streaming delegate that produces the notes: the card calls it when it drives its own status (on first view and on regenerate), shows the summarizing pass until the first snapshot arrives, then streams every snapshot it yields (each one the notes so far). A fault shows the error state with its message. Takes the place of `Meeting` while it runs. + 產生紀錄的非同步串流委派:卡片自己驅動狀態時(第一次進入畫面和重新產生時)呼叫它,在第一個快照抵達之前顯示彙整過程,之後串流顯示它產出的每一個快照(每一個都是截至目前的紀錄)。出錯時顯示帶有錯誤訊息的錯誤狀態。執行期間取代 `Meeting`。 + + + The notes of one call: what it was, who was on it, and the Summary, Decisions and Action items lists. Map your model's output to it; keep `At` as the time from the start of the recording. + 一次通話的紀錄:通話內容、與會者,以及摘要、決議和待辦事項三個清單。把模型的輸出對應到它;`At` 保持為從錄音開始算起的時間。 + + + Every word `MeetingSummary` shows or says, in English by default. Replace the whole object to localize or reword; counts, dates and names are format functions. + `MeetingSummary` 顯示或朗讀的每一個文字,預設為英文。若要在地化或改寫,請替換整個物件;數量、日期和姓名是格式化函式。 + + + Somewhere an action item can be sent: an issue tracker, a notes app. A name and a 24 × 24 stroked mark; the defaults are `Issues` and `Docs`. Pass your own mark for a real product. + 待辦事項可以傳送到的地方:議題追蹤工具、筆記應用程式。一個名稱和一個 24 × 24 的描邊標誌;預設值是 `Issues` 和 `Docs`。對於真實的產品,請傳入你自己的標誌。 + + + A call a `MeetingSummary` plays without a summarizer, from data you give it: the notes, the model credited and how simulated sends are numbered. Set it as `Simulation` while the card has no `Meeting` and no `Summarizer`: when the card scrolls into view it plays its summarizing pass, streams the notes in and, unless someone uses the card first, ticks off the second action item and sends the first one. Sends are simulated and announce that nothing was sent. + `MeetingSummary` 在沒有彙整器時依你提供的資料播放的通話:會議紀錄、署名的模型,以及模擬傳送的編號方式。在卡片既沒有 `Meeting` 也沒有 `Summarizer` 時把它設為 `Simulation`:卡片捲動進入畫面後會播放彙整過程、串流顯示紀錄,並且只要沒有人先操作卡片,就會勾選第二個待辦事項並傳送第一個。傳送是模擬的,並會播報什麼都沒有傳送。 + A filing group of memories (About you, Preferences, Projects, People by default). 記憶的一個歸檔群組(預設為「關於你」「偏好」「專案」「人物」)。 @@ -12416,6 +15276,142 @@ Run when a vote is cast, with the `ArenaVote` as the parameter. 投票時執行,參數為 `ArenaVote`。 + + A model details card for a model picker, docs or a marketplace that keeps one size in every state (Arc Model card, design reference). The header shows the model's mark, name and provider with a Compare switch. The details view has a two line summary, all seven modality chips (unsupported ones dashed and muted), the context window with a gauge that reads as pages, max output and knowledge cutoff, input, output and cached prices per million tokens, a monthly volume slider that rolls the estimated cost live, and benchmark bars against two rivals where hovering or focusing a row shows the exact score, its rank and the gap to the leader. The footer shows the API model id with Copy model id. Compare crossfades to a side by side table against any rival, with a check and the margin on the better value and a tally of who leads. Details, compare, loading and error are stacked in one grid cell, so switching models, views or benchmarks never moves the card: every price, limit and score rolls in place in tabular digits and every text value crossfades. Without a `Model` the card keeps its frame, says no model is selected and reads every value as not listed. + 用於模型選擇器、文件或市集的模型詳情卡片,在任何狀態下都維持同一尺寸(Arc Model card,設計參考)。頁首顯示模型的標誌、名稱和供應商,以及比較開關。詳情檢視包含兩行簡介、全部七個模態標籤(不支援的為虛線並變淡)、附有以頁數表示之容量條的上下文視窗、最大輸出和知識截止時間,每百萬 token 的輸入、輸出和快取價格,即時捲動估算費用的每月用量滑桿,以及與兩個對手比較的基準測試長條:游標停留或聚焦某一列會顯示確切分數、排名以及與領先者的差距。頁尾顯示 API 模型 id 和 Copy model id。比較以交叉淡化切換到與任一對手並排的表格,較佳的值附勾號和領先幅度,並統計誰領先。 詳情、比較、載入和錯誤疊放在同一個方格儲存格中,因此切換模型、檢視或基準測試都不會移動卡片:每個價格、上限和分數都以等寬數字在原位捲動,每個文字值都交叉淡化。沒有 `Model` 時,卡片保留其框架,說明未選擇模型,並把每個值顯示為未列出。 + + + The id of the benchmark the bars show (two-way). Null or one the model has no score for uses the first scored benchmark. + 基準測試長條顯示的基準測試 id(雙向)。為 null 或模型在該基準上沒有分數時,使用第一個有分數的基準測試。 + + + Raised when the person picks a benchmark in the switcher. Bubbles. + 使用者在切換器中選擇基準測試時觸發。會向上傳遞。 + + + Benchmarks in order. Only those the model has a score for are offered in the switcher; with none the benchmark section hides. Null shows no benchmarks. + 依序排列的基準測試。切換器只提供模型有分數的基準測試;一個都沒有時隱藏基準測試區塊。為 null 時不顯示基準測試。 + + + Adds a Retry button to the error state; it raises `RetryRequested`. + 在錯誤狀態中加入 Retry 按鈕;它會觸發 `RetryRequested`。 + + + Adds a primary Use model button to the footer, for model pickers; it raises `SelectRequested`. + 在頁尾加入一個主要的 Use model 按鈕,用於模型選擇器;它會觸發 `SelectRequested`。 + + + The id of the rival in the compare view (two-way). Null or an unknown id uses the first rival. + 比較檢視中對手的 id(雙向)。為 null 或未知 id 時使用第一個對手。 + + + Raised when the person picks a rival from the compare menu. Bubbles. + 使用者從比較選單中選擇對手時觸發。會向上傳遞。 + + + The ISO 4217 currency of the prices ("USD" writes "$"). + 價格使用的 ISO 4217 貨幣("USD" 顯示為 "$")。 + + + Replaces the body with this error message (announced as an alert). The header still shows the model. + 以此錯誤訊息取代卡片主體(作為警示朗讀)。頁首仍顯示模型。 + + + Raised after Copy model id has written the id to the clipboard. Bubbles. + Copy model id 把 id 寫入剪貼簿後觸發。會向上傳遞。 + + + Share of the monthly volume billed at the input price; the rest is billed at the output price. + 每月用量中依輸入價格計費的比例;其餘依輸出價格計費。 + + + Shows a skeleton of the card and disables Compare; the status region says the details are loading. + 顯示卡片的骨架畫面並停用比較;狀態區域說明詳情正在載入。 + + + Labels prices and scores as samples in the benchmark caption and the compare footnote. Turn it on while your figures are placeholders. + 在基準測試說明和比較註腳中把價格和分數標示為範例。資料仍是預留位置時開啟它。 + + + Shows the select button as Selected, with a check. + 把選擇按鈕顯示為 Selected,並附勾號。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每個字詞。 + + + The model the card describes. Null shows the empty card: no model selected, every value not listed. + 卡片描述的模型。為 null 時顯示空卡片:未選擇模型,每個值都未列出。 + + + The monthly token volume of the cost estimate (two-way). It snaps to the nearest stop: 1M, 5M, 10M, 25M, 50M, 100M, 250M, 500M or 1B. + 費用估算的每月 token 用量(雙向)。會貼齊最近的級距:1M、5M、10M、25M、50M、100M、250M、500M 或 1B。 + + + Raised with the new volume as the person moves the volume slider. Bubbles. + 使用者移動用量滑桿時以新用量觸發。會向上傳遞。 + + + The footnote under the compare table: say where the prices and scores come from and when they were checked. Null shows the sample footnote while `IsSample` is on, and nothing otherwise. + 比較表下方的註腳:說明價格和分數的來源以及核對日期。為 null 時,`IsSample` 開啟則顯示範例註腳,否則不顯示。 + + + Raised when the person presses Retry in the error state. Bubbles. + 使用者在錯誤狀態中按下 Retry 時觸發。會向上傳遞。 + + + Other models. The first two appear under the model in the benchmark rows, ranks count every rival with a score, and every rival can be picked in the compare view. The model itself is filtered out, so one shared pool works for every model. Null or an empty list hides the Compare switch and the rival rows. + 其他模型。前兩個顯示在基準測試列中模型的下方,排名計入每個有分數的對手,比較檢視中可以選擇任一對手。模型本身會被排除,因此所有模型可共用同一組對手。為 null 或空清單時隱藏比較開關和對手列。 + + + The select button's label; null uses `UseModel`. + 選擇按鈕的文字;為 null 時使用 `UseModel`。 + + + Raised when the person presses Use model. Bubbles. + 使用者按下 Use model 時觸發。會向上傳遞。 + + + The view on screen (two-way): the details or the compare table. + 螢幕上的檢視(雙向):詳情或比較表格。 + + + Raised when the person flips the Compare switch. Bubbles. + 使用者切換比較開關時觸發。會向上傳遞。 + + + A benchmark the card can show bars for. + 卡片可以為其顯示長條的基準測試。 + + + Arguments of the card's events that carry an id: the rival picked, the benchmark chosen or the id copied. + 卡片中攜帶 id 的事件的參數:所選的對手、所選的基準測試或複製的 id。 + + + Every word `ModelCard` shows or says, in English by default. Replace the whole object to localize or reword; sentences that carry values are format functions. + `ModelCard` 顯示或朗讀的每個字詞,預設為英文。替換整個物件即可在地化或改寫;帶數值的句子是格式化函式。 + + + One model a `ModelCard` describes or compares against. Every value but `Id` and `Name` is optional; a missing one reads as "Not listed". + `ModelCard` 描述或比較的一個模型。除 `Id` 和 `Name` 外每個值都是選用的;缺少的值顯示為 "Not listed"。 + + + Arguments of `SelectRequested`. + `SelectRequested` 的參數。 + + + Arguments of `MonthlyTokensChanged`. + `MonthlyTokensChanged` 的參數。 + + + Per token prices of a model, per million tokens, in the card's `Currency`. + 模型的按 token 計費價格,單位為每百萬 token,以卡片的 `Currency` 計。 + + + Arguments of `ViewChanged`. + `ViewChanged` 的參數。 + Chooses a model and its reasoning effort from one compact pill. The pill shows the current choice; its menu lists a few featured models with a one-line note each, an effort setting, and every other model grouped by provider. Under the pill a summary shows what the model can hold and costs, and confirms each change for a moment. A composition of `ModelPill`, `ModelMenu` and `ModelSummary`: the catalog, the selection and the effort are properties, so bind them or read them back from the events. The pill opens on a pointer press or on Enter, Space and ArrowDown; the menu has a featured list, an Effort row and a More models row that open submenus (or pages in a narrow window); choosing closes the menu and returns focus to the pill; the effort moves to the nearest level a new model offers. Set `IsSummaryVisible` to false for the pill alone (a toolbar, a header). Screen readers hear a polite announcement after each change ("Now using Aurora Prime 3, medium effort"); the visible confirmation under the summary stays for `NoticeDuration`. 從一個緊湊的膠囊中選擇模型及其推理強度。膠囊顯示目前的選擇;其選單列出數個精選模型(各附一行說明)、一項推理強度設定,以及依提供者分組的所有其他模型。膠囊下方的概覽顯示該模型能容納的內容與費用,並在每次變更後短暫給出確認。它由 `ModelPill`、`ModelMenu` 和 `ModelSummary` 組合而成:目錄、選取與推理強度皆為屬性,因此可以繫結,或從事件中讀回。膠囊在指標按下,或按 Enter、Space 與 ArrowDown 時開啟選單;選單包含精選清單、Effort 列和 More models 列,它們會開啟子選單(視窗較窄時改為頁面);選擇後選單關閉,焦點回到膠囊;推理強度會移到新模型所提供的最接近的一檔。把 `IsSummaryVisible` 設為 false,就只剩膠囊(用於工具列或頁首)。每次變更後,螢幕閱讀器會聽到一則禮貌式朗讀("Now using Aurora Prime 3, medium effort");概覽下方的確認提示會保留 `NoticeDuration` 的時間。 @@ -13672,6 +16668,90 @@ Arguments of `StepChanged`. `StepChanged` 的參數。 + + + The category the logo belongs to. The category chips are built from these. + 標誌所屬的類別。類別膠囊由這些類別產生。 + + + The color a path-data mark is painted in. Leave it out for black-and-white brands so the mark follows the text color. Image and control marks keep their own colors. + 路徑資料標誌所用的繪製顏色。黑白品牌可以不設定,讓標誌跟隨文字顏色。圖片和控制項標誌保留自己的顏色。 + + + The line of text in the card. + 卡片中的一行文字。 + + + Address of the documentation. When set, the card has a Docs link. + 文件位址。設定後卡片中會出現 Docs 連結。 + + + The mark: SVG path data (24 by 24), an image address, an image source or a control. + 標誌:SVG 路徑資料(24 × 24)、圖片位址、圖片來源或控制項。 + + + Stable identifier. When empty, `Name` identifies the logo. + 穩定的識別碼。為空時由 `Name` 識別該標誌。 + + + Whether the integration is connected. It starts the status and is set when a connection succeeds. + 整合是否已連線。它決定初始狀態,連線成功後會被設定。 + + + The brand or product name, used in the chip's name tag, the card and the accessible name. + 品牌或產品名稱,用於標誌的名稱標籤、卡片和無障礙名稱。 + + + An integrations orbit (Arc Orbit logos, design reference): your product mark at the centre with brand logos on two flat rings that turn slowly and hold still whenever the pointer or keyboard focus is inside. Category chips turn the matching logos toward the detail side and dim the rest. Choosing a logo springs its ring so the logo glides to a fixed spot, draws an accent connector from the centre and morphs a detail card out of the chip, with a description, the status, a Connect action, a Docs link and previous and next. + 整合軌道(Arc Orbit logos,設計參考):你的產品標誌位於中央,品牌標誌排在兩條平面環上,環緩慢轉動,指標或鍵盤焦點在內時保持靜止。類別膠囊會把相符的標誌轉向詳情一側並讓其餘標誌變暗。選中一個標誌時,它所在的環以彈簧轉動,使標誌滑到固定位置,從中央畫出強調色連線,並從標誌中長出詳情卡片,包含說明、狀態、Connect 操作、Docs 連結以及上一個和下一個。 + + + Label of the first category chip, the one that shows every logo. + 第一個類別膠囊的文字,該膠囊顯示所有標誌。 + + + Content of the centre disc, such as your product mark. Defaults to a simple mark in the accent color. + 中央圓盤的內容,例如你的產品標誌。預設是強調色的簡單圖形。 + + + Raised when the Connect button is pressed. A handler can set `Completion`. + 按下 Connect 按鈕時引發。處理常式可以設定 `Completion`。 + + + Color of the connector from the centre to the chosen logo. The theme uses the accent. + 從中央到所選標誌的連線顏色。主題使用強調色。 + + + Id (or name) of the logo open on first display, when `SelectedId` is not set. + 首次顯示時開啟的標誌的 id(或名稱),在未設定 `SelectedId` 時使用。 + + + Holds the orbit still, for a page-level motion control. + 讓軌道保持靜止,用於頁面層級的動態控制。 + + + Accessible name of the logo list, and the title of the idle panel. + 標誌清單的無障礙名稱,也是閒置面板的標題。 + + + Color of the two ring lines. The theme uses the subtle border. + 兩條環線的顏色。主題使用弱邊框色。 + + + Id (or name) of the logo whose card is open. Null closes the card. Two-way. + 卡片已開啟的標誌的 id(或名稱)。為 null 時關閉卡片。雙向。 + + + Raised when a logo is opened, when previous or next steps to another, and with no logo and index -1 when the card closes. + 開啟標誌、用上一個或下一個切換到另一個標誌時引發;卡片關閉時沒有標誌且索引為 -1。 + + + Multiplies the rotation speed. At 1 the inner ring turns once every 64 seconds and the outer ring, the other way, every 88 seconds. 0 holds the orbit still. + 轉速倍數。為 1 時內環每 64 秒轉一圈,外環反向每 88 秒轉一圈。0 讓軌道保持靜止。 + A row of single-character slots for one-time codes (Arc's OTP input block): a visible label, one slot per character, helper and error copy under the row. Typing fills a slot and moves on; a paste fills the slots from the focused one; arrow keys move between slots and Backspace on an empty slot clears the one before it. Characters rise into their slot, a paste lands as a short left to right wave, one focus ring glides between the slots, and a new error nudges the row side to side once. `Value` is two-way; `Completed` fires when an edit fills the last slot, which is where a host checks the code. 用於一次性驗證碼的單字元格子列(Arc 的 OTP 輸入區塊):可見的標籤、每個字元一個格子,以及列下方的輔助文字與錯誤文字。輸入時填入格子並移至下一格;貼上時從目前聚焦的格子開始依序填入;方向鍵可在格子之間移動,在空白格子上按 Backspace 會清除前一格。字元會向上升入格子,貼上時出現一道由左至右的短暫波浪動畫,一個焦點環在格子之間滑動,新的錯誤會讓整列左右晃動一次。`Value` 為雙向繫結;編輯填滿最後一格時觸發 `Completed`,宿主應在此檢查驗證碼。 @@ -14080,6 +17160,182 @@ Defines the `ValueChanged` event. 定義 `ValueChanged` 事件。 + + Arguments of `Redacted`: the safe prompt and every entity it replaced. + `Redacted` 的參數:安全提示詞以及它取代的每個實體。 + + + A guardrail card that shows personal data being redacted before a prompt reaches the model (Arc PII redaction, design reference). Scan marks each name, email, phone number and card number in the message with its type, then swaps each one for a numbered placeholder such as `[EMAIL_1]` in a single crossfade. Send carries the safe prompt along a small flow (you, the PII guard, the model), the reply streams back in quick chunks with its placeholders, and the guard puts the original values back for the person. Show original flips the message between the safe prompt and what was typed, and the message can be edited before a scan. Detection comes from `Detector` (your DLP or NER service) or the built-in `PiiRedactor`; the reply from `Sender` or a simulation, which the card says. Without a `Message` it plays a `Simulation` (a conversation you pass) on a loop while at least a quarter of it is on screen, until someone interacts; with neither it shows an empty message field. + 一張防護卡片,展示個人資料在提示詞送到模型之前被遮蔽的過程(Arc PII redaction,設計參考)。掃描會在訊息中標出每個姓名、電子郵件、電話號碼與卡號及其類型,接著在一次交叉淡化中把它們各自換成帶編號的預留位置,例如 `[EMAIL_1]`。傳送會讓安全提示詞沿著一條小流程(你、PII 防護、模型)傳遞,回覆帶著預留位置分小塊快速串流傳回,防護再為使用者放回原始值。顯示原文會在安全提示詞與輸入的內容之間切換,掃描前訊息可以編輯。偵測來自 `Detector`(你的 DLP 或 NER 服務)或內建的 `PiiRedactor`;回覆來自 `Sender` 或模擬,卡片會註明模擬。沒有 `Message` 時,只要卡片至少四分之一在畫面內,它就循環播放 `Simulation`(你傳入的一段對話),直到有人操作;兩者都沒有時顯示空的訊息框。 + + + Plays scan, send and restore on a loop while at least a quarter of the card is on screen. Stops for good as soon as someone edits, toggles or presses a button. Off with reduced motion. + 只要卡片至少四分之一在畫面內,就循環播放掃描、傳送與還原。一旦有人編輯、切換或按下按鈕就永久停止。減少動態效果時關閉。 + + + The color of card numbers (theme: Series4). + 卡號的顏色(主題:Series4)。 + + + Replaces the built-in detector, for example with your DLP or NER service. Return spans with an exclusive end; the token is cancelled when the person presses Stop or Reset. A thrown exception shows its message with Retry. + 取代內建偵測器,例如換成你的 DLP 或 NER 服務。傳回終點不含在內的區間;使用者按下 Stop 或 Reset 時權杖會被取消。擲出的例外會顯示其訊息並提供 Retry。 + + + The color of emails (theme: Series2). + 電子郵件的顏色(主題:Series2)。 + + + Whether people can edit the message before a scan. Cmd or Ctrl plus Enter scans from the field. + 掃描前使用者能否編輯訊息。在輸入框中按 Cmd 或 Ctrl 加 Enter 即開始掃描。 + + + Every word the card shows or announces. + 卡片顯示或朗讀的每個詞語。 + + + The message to check (two-way). Null shows the `Simulation`'s message, or a blank field without one. + 要檢查的訊息(雙向)。為 null 時顯示 `Simulation` 的訊息,沒有模擬時顯示空白輸入框。 + + + The model's mark, a 24 × 24 stroked glyph drawn at 16px; the sparkle by default. + 模型的標記,一個 24 × 24 的描邊圖形,以 16px 繪製;預設為火花。 + + + The mark's color; the accent by default. + 標記的顏色;預設為強調色。 + + + The model the safe prompt goes to, on the flow and above the reply; null names the `Simulation`'s model, or none. + 安全提示詞送往的模型,顯示在流程列上與回覆上方;為 null 時顯示 `Simulation` 的模型,沒有則不顯示。 + + + The color of names: their marks, placeholders and cell (theme: Series1). + 姓名的顏色:用於其標記、預留位置與類型格(主題:Series1)。 + + + Names the built-in detector always flags, on top of introductions like "I'm", "this is" and "my name is". Pass the signed-in person's name and their contacts. Null uses the `Simulation`'s names, or none. + 內建偵測器一律標出的姓名,此外還會辨識「I'm」、「this is」與「my name is」之類的自我介紹。傳入已登入使用者的姓名及其聯絡人。為 null 時使用 `Simulation` 的姓名,沒有則不標。 + + + Where the card is in its flow (read only). + 卡片位於流程中的哪個階段(唯讀)。 + + + Raised on every phase change. Bubbles. + 每次階段變化時觸發。會向上傳遞。 + + + The color of phone numbers (theme: Series3). + 電話號碼的顏色(主題:Series3)。 + + + Raised once the message is redacted, with the safe prompt and every entity. Bubbles. + 訊息遮蔽完成時觸發,附帶安全提示詞與每個實體。會向上傳遞。 + + + The restored reply once it is back, otherwise null (read only). + 回覆傳回後為還原後的回覆,否則為 null(唯讀)。 + + + Raised when the reply is restored, with the model's raw reply and the text the person sees. Bubbles. + 回覆還原後觸發,附帶模型的原始回覆與使用者看到的文字。會向上傳遞。 + + + The safe prompt once the message is redacted, otherwise null (read only). + 訊息遮蔽後為安全提示詞,否則為 null(唯讀)。 + + + Sends the safe prompt to your model and returns its reply, placeholders included; the card restores them. Without it the reply is simulated (the `Simulation`'s reply, or a short stand-in) and the card says so. A thrown exception shows its message with Retry. + 把安全提示詞傳給你的模型並傳回其回覆(含預留位置);卡片會將它們還原。沒有它時回覆是模擬的(`Simulation` 的回覆,或一則簡短的替身回覆),卡片會註明。擲出的例外會顯示其訊息並提供 Retry。 + + + Shows the original message instead of the safe prompt once it is redacted (two-way). + 訊息遮蔽後顯示原始訊息而非安全提示詞(雙向)。 + + + A conversation to play: its message (while the card has no `Message`), the names to flag, the model and its written reply. Your own values win over the simulation's; null stops it and shows the empty field. + 要播放的對話:它的訊息(卡片沒有 `Message` 時)、要標記的姓名、模型以及寫好的回覆。你自己的值優先於模擬的值;為 null 時停止播放並顯示空的訊息框。 + + + Every word a `PiiRedaction` shows or announces, in English by default. Replace the whole object to translate or reword; sentences with a number or a name are functions so the word order can change. + `PiiRedaction` 顯示或朗讀的每個詞語,預設為英文。替換整個物件即可翻譯或改寫;含數字或名稱的句子是函式,以便調整語序。 + + + Arguments of `PhaseChanged`: the phase the card left and the one it entered. + `PhaseChanged` 的參數:卡片離開的階段與進入的階段。 + + + A conversation a `PiiRedaction` plays without a model, from data you give it: the message, the names the built-in detector should always flag, the model the safe prompt goes to and the model's reply with its placeholders. Set it as `Simulation`: while the card has no `Message` it shows this message and, unless `AutoPlay` is false, plays scan, send and restore on a loop. Without a `Sender` the reply is simulated and the card says so; nothing is sent. + `PiiRedaction` 在沒有模型時依你提供的資料播放的對話:訊息、內建偵測器應一律標出的姓名、安全提示詞送往的模型,以及附預留位置的模型回覆。把它設為 `Simulation`:卡片沒有 `Message` 時會顯示這則訊息,並且除非 `AutoPlay` 為 false,會循環播放掃描、傳送和還原。沒有 `Sender` 時回覆是模擬的,卡片會註明;不會傳送任何內容。 + + + Arguments of `ReplyRestored`: the model's reply and the text the person sees. + `ReplyRestored` 的參數:模型的回覆以及使用者看到的文字。 + + + A pixel-art robot on a 24 × 24 grid that shows what an assistant is doing (Arc Pixel mascot, design reference). It idles, thinks, types, celebrates and errors in frame-stepped sprite animations on a fixed 40ms clock, and status changes morph from the current pose one pixel at a time. Every sprite pixel is `Scale` device-independent pixels, snapped to whole device pixels, so it stays crisp at every size from 24px to 192px. Set `Status` from your request lifecycle, or leave it null to play the built-in loop through every state. The palette follows the theme neutrals and `AccentBrush` (the theme's accent by default). + 一個畫在 24 × 24 網格上的像素機器人,呈現助理正在做什麼(Arc Pixel mascot,設計參考)。它以固定 40ms 時脈上的逐格精靈動畫待機、思考、打字、慶祝和出錯,狀態切換時從目前姿勢一次一個像素地變形過去。每個精靈像素佔 `Scale` 個裝置無關像素,並對齊到整數裝置像素,因此在 24px 到 192px 的每個尺寸下都保持清晰。依請求生命週期設定 `Status`,或留空以循環播放內建的全部狀態。調色盤跟隨主題中性色和 `AccentBrush`(預設為主題強調色)。 + + + The accent of the antenna, chest light, typed bar, sparkles and the eyes' glow; the theme's accent by default. + 天線、胸燈、打字進度列、閃光和眼睛光暈使用的強調色;預設為主題強調色。 + + + Announces status changes politely to assistive technology. Null (the default) announces when `Status` is set and stays quiet for the built-in loop. Turn it off when nearby text already announces progress. + 以禮貌方式向輔助科技播報狀態變化。為 null(預設)時,設定了 `Status` 就播報,內建循環則保持安靜。附近文字已播報進度時請關閉。 + + + Who the mascot stands for, used in the accessible label ("Assistant is thinking"). + 吉祥物代表的助理名稱,用於無障礙標籤("Assistant is thinking")。 + + + Freezes the animation on the current frame. The mascot also pauses by itself while off screen. + 把動畫凍結在目前這一格。離開螢幕時吉祥物也會自動暫停。 + + + Replaces the generated accessible label. + 取代自動產生的無障礙標籤。 + + + Every word the mascot says. + 吉祥物說出的所有文字。 + + + Pixel scale from 1 to 8: each sprite pixel is this many device-independent pixels, so 1 renders 24px, 2 renders 48px, 4 renders 96px and 8 renders 192px. Other values clamp. + 像素縮放,1 到 8:每個精靈像素佔這麼多個裝置無關像素,所以 1 算繪為 24px,2 為 48px,4 為 96px,8 為 192px。其他值會被限制在範圍內。 + + + The status the mascot shows: `Status`, or the built-in loop's current step when it is null. + 吉祥物正在顯示的狀態:`Status`,為 null 時則是內建循環的目前這一步。 + + + Raised when `ShownStatus` changes, with the old and new status. Bubbles. + `ShownStatus` 變化時觸發,帶有舊狀態和新狀態。會向上傳遞。 + + + What the assistant is doing. Leave it null to play the built-in loop through every state. + 助理正在做什麼。留空則循環播放內建的全部狀態。 + + + Every word `PixelMascot` says, in English by default: the accessible label for each status, where `{0}` is the assistant's name. Replace the whole object to localize or reword. + `PixelMascot` 說出的所有文字,預設為英文:每種狀態的無障礙標籤,其中 `{0}` 是助理名稱。取代整個物件即可在地化或改寫措辭。 + + + The built-in status loop of `PixelMascot` (idle, thinking, writing, done, idle, thinking, error) as a shared clock. A mascot without a `Status` plays this loop on its own; put one `PixelMascotLoop` in resources and bind several mascots' `Status` to its `Status` to keep them in step. It switches only at step boundaries (one timer tick every few seconds), never per frame. + `PixelMascot` 的內建狀態循環(待機、思考、撰寫、完成、待機、思考、出錯),作為共用時脈。沒有 `Status` 的吉祥物會自己播放這個循環;在資源中放一個 `PixelMascotLoop`,把多個吉祥物的 `Status` 繫結到它的 `Status`,即可讓它們步調一致。它只在步與步的邊界切換(每隔幾秒一次計時器觸發),從不逐格執行。 + + + Whether the loop moves on. False holds the current status. + 循環是否繼續推進。為 false 時停在目前狀態。 + + + The status the loop is on. + 循環目前所處的狀態。 + + + Arguments of `ShownStatusChanged`. + `ShownStatusChanged` 的參數。 + A small surface anchored to a trigger and opened by a click, for interactive content that should not block the page: share settings, a small filter form, a picker built from your own controls. Two ways to use it: Code can open any content anchored to any control with `ShowAt`. The panel starts 5px toward its trigger at scale 0.97, fades in fast and settles on a spring from the side it actually landed on; it leaves in about 160ms at half the travel, and an open and a close reverse each other mid-flight. Escape and a press outside close it and return focus to the trigger; focus moves into the panel on open. The trigger answers presses with color only (no press scale), since the panel is measured against it. 一個錨定在觸發器上、由點擊開啟的小型浮層,適用於不應阻擋頁面的互動內容:分享設定、小型篩選表單、由您自己的控制項組成的選取器。有兩種用法:程式碼可透過 `ShowAt` 將任意內容錨定到任意控制項上開啟。面板從觸發器方向偏移 5px 開始,縮放比例為 0.97,快速淡入並以彈簧從實際落位的一側穩定下來;關閉時約 160ms 內完成,移動距離為一半,且開啟與關閉在過程中可相互反轉。按下 Escape 或在外部按下會關閉面板,並將焦點返回觸發器;開啟時焦點會移入面板。觸發器僅以顏色回應按下(無按壓縮放),因為面板是相對於它量測的。 @@ -14944,6 +18200,178 @@ The user message of this version. 此版本的使用者訊息。 + + One provider's stored key in `ProviderKeys`. Pass `Value` to enable reveal and copy, or only `Last4` when your server never returns full keys (the row then shows the masked key and disables both). + `ProviderKeys` 中某個供應商已儲存的金鑰。傳入 `Value` 以啟用顯示與複製;如果伺服器從不傳回完整金鑰,則只傳 `Last4`(該列隨後顯示遮罩金鑰,並停用這兩項操作)。 + + + A provider row's identity in `ProviderKeys`: its id, name, mark and the shape of its keys (a prefix and a minimum length), which the add and rotate form checks as the person types. + `ProviderKeys` 中一個供應商列的身分:其 id、名稱、標誌以及金鑰的形式(前綴與最小長度),新增與輪替表單會在使用者輸入時據此檢查。 + + + A settings card for bring-your-own-key AI products (Arc Provider keys, design reference). One row per provider with its mark, the masked key (`sk-••••4f2a`), a status as icon plus label (Connected, Invalid, Not set), when the key was last used, and four actions: reveal for five seconds with a countdown ring, copy, rotate, and remove with undo. Pressing a status tests the stored key again. Add key and Rotate open an inline form under the row that checks the key's format as you type, then shows Testing connection and Connected before the row takes the new key. Bind `Keys` (two-way) and handle `KeySaved` and `KeyRemoved` to persist; set `TestKey` to test keys on your server. Providers without a key read Not set. For a demo, set `Simulation` instead of `TestKey` to simulate every test; with neither, a test fails with `NoKeyTest`, so no key reads Connected without a check. + 為自備金鑰 AI 產品設計的設定卡片(Arc Provider keys,設計參考)。每個供應商一列,顯示其標誌、遮罩金鑰(`sk-••••4f2a`)、由圖示加文字組成的狀態(Connected、Invalid、Not set)、金鑰最近一次使用的時間,以及四個操作:帶倒數圓環的五秒顯示、複製、輪替與可復原的移除。按下狀態會重新測試已儲存的金鑰。Add key 與 Rotate 會在該列下方開啟內嵌表單,在輸入時檢查金鑰格式,然後顯示 Testing connection 與 Connected,再讓該列採用新金鑰。繫結 `Keys`(雙向)並處理 `KeySaved` 與 `KeyRemoved` 以保存;設定 `TestKey` 以在你的伺服器上測試金鑰。沒有金鑰的供應商顯示為 Not set。用於展示時,設定 `Simulation` 而非 `TestKey` 來模擬每次測試;兩者都沒有時,測試會以 `NoKeyTest` 失敗,因此任何金鑰都不會未經檢查就顯示為 Connected。 + + + A line under the title; null or empty hides it. + 標題下方的一行文字;為 null 或空白時隱藏。 + + + A quiet line under the list, an encryption note by default; null or empty hides it. + 清單下方一行低調的文字,預設是加密說明;為 null 或空白時隱藏。 + + + Shows placeholder rows with the real row geometry while keys load. + 在金鑰載入期間顯示預留列,幾何尺寸與真實的列一致。 + + + Raised when a key was copied to the clipboard, for an audit log. Bubbles. + 金鑰被複製到剪貼簿時引發,可用於稽核記錄。會向上傳遞。 + + + Raised when the undo window of a removal closes, so a removal only reaches your server once it sticks. Bubbles. + 移除的復原視窗關閉時引發,所以移除只有在確定後才會傳到你的伺服器。會向上傳遞。 + + + Raised when a new key passed its test; persist it here (the row already shows it as connected). Bubbles. + 新金鑰通過測試時引發;在這裡保存(該列已顯示為已連線)。會向上傳遞。 + + + The keys, one per provider (two-way): the card writes the new list on every add, rotation, removal, undo and retest. Providers without an entry read Not set; null or an empty list is the empty state, every row Not set. + 金鑰,每個供應商一把(雙向):每次新增、輪替、移除、復原與重新測試時,卡片都會寫回新的清單。沒有項目的供應商顯示為 Not set;為 null 或空清單時即為空白狀態,每一列都是 Not set。 + + + Raised on every change to the keys: added, rotated, removed, restored with undo, or retested. Bubbles. + 金鑰每次變更時引發:新增、輪替、移除、復原或重新測試。會向上傳遞。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個詞。 + + + The card width under which each row stacks (status beside the name, the key on its own line) and the form stacks. + 卡片寬度低於此值時,每一列改為堆疊排列(狀態在名稱旁邊,金鑰獨佔一行),表單也會堆疊。 + + + Which providers get a row, in order. Null or an empty list shows no rows. + 哪些供應商有一列,以及它們的順序。為 null 或空清單時不顯示任何列。 + + + How long a revealed key stays visible before it masks itself again. + 顯示的金鑰在重新遮罩之前保持可見的時間長度。 + + + Simulated key tests while `TestKey` is null, for demos: stored invalid keys fail again, everything else connects, and announcements say nothing was sent. Null (the default) runs no simulated test. + `TestKey` 為 null 時使用的模擬金鑰測試,用於展示:已儲存的無效金鑰會再次失敗,其餘金鑰都連線成功,朗讀會說明沒有傳送任何內容。為 null(預設)時不執行模擬測試。 + + + Tests a key and returns whether the provider accepted it. The request carries the pasted key when adding or rotating, and no key when retesting a stored one (test it on your server by provider); its token is cancelled when the form closes. Null uses `Simulation`; with neither, every test fails with `NoKeyTest`. + 測試金鑰並傳回供應商是否接受它。新增或輪替時,請求帶有貼上的金鑰;重新測試已儲存的金鑰時不帶金鑰(請在伺服器上依供應商測試);其權杖會在表單關閉時取消。為 null 時使用 `Simulation`;兩者都沒有時,每次測試都以 `NoKeyTest` 失敗。 + + + The card's title, also the name of the section. + 卡片的標題,也是該區塊的名稱。 + + + How long Undo stays after a removal; `KeyRemoved` is raised when it closes. + 移除後 Undo 保留的時間長度;它關閉時會引發 `KeyRemoved`。 + + + Every word `ProviderKeys` shows or says, in English by default. Replace the whole object to localize or reword; words that name a provider, a prefix, a time or an error are format functions. + `ProviderKeys` 顯示或朗讀的每一個詞,預設為英文。替換整個物件即可在地化或改寫;涉及供應商、前綴、時間或錯誤的詞是格式化函式。 + + + Simulated key tests for a `ProviderKeys` without `TestKey`, for demos and prototypes: after `Delay`, retesting a stored key whose status is Invalid fails again with its own error (or `RejectedError`), and every other test connects. Nothing is sent anywhere, and the card adds "Simulated, nothing was sent." to what it announces. + 供沒有 `TestKey` 的 `ProviderKeys` 使用的模擬金鑰測試,用於展示與原型:等待 `Delay` 之後,重新測試狀態為 Invalid 的已儲存金鑰會以其自身的錯誤(或 `RejectedError`)再次失敗,其餘測試都連線成功。不會傳送任何內容,卡片會在朗讀中附加 "Simulated, nothing was sent."。 + + + A status card for the AI providers a product depends on (Arc Provider status, design reference). Each row shows the provider's mark, its health as an icon and label (Operational, Degraded, Outage), the median latency in tabular numerals and a 30 day uptime bar of small ticks with a tooltip per day. A row with an incident is a button that opens its updates (Investigating, Identified, Monitoring, Resolved), newest first; new updates grow the panel smoothly. The header sums up health across providers and checks again on a visible countdown ring, with a refresh button, a Checking state and an error state that keeps the last results. Cards narrower than 560px take two lines per row. Give it `Providers` and a `RefreshHandler` that fetches fresh status (from your own aggregation endpoint). With neither, a `Simulation` plays its story on simulated checks; without one the card says there are no providers. + 一張呈現產品所依賴的 AI 服務供應商狀態的卡片(Arc Provider status,設計參考)。每一列顯示供應商的標誌、以圖示和標籤表示的健康狀態(Operational、Degraded、Outage)、以等寬數字顯示的中位延遲,以及由小刻度組成、每天都附提示的 30 天可用率條。帶有事件的列是一個按鈕,依從新到舊展開其更新(Investigating、Identified、Monitoring、Resolved);新的更新會讓面板平順變高。標題列彙總所有供應商的健康狀態,並依可見的倒數計時環再次檢查,附有重新整理按鈕、Checking 狀態,以及保留上次結果的錯誤狀態。寬度小於 560px 的卡片每列佔兩行。傳入 `Providers` 和一個取得最新狀態的 `RefreshHandler`(來自你自己的彙整端點)。兩者都沒有時,由 `Simulation` 在模擬檢查中播放其故事;沒有模擬時,卡片說明沒有供應商。 + + + An error to show in the header, such as a failed first load. Errors thrown by the refresh handler show on their own. + 在標題列中顯示的錯誤,例如第一次載入失敗。重新整理處理常式擲回的錯誤會自行顯示。 + + + Raised when an incident row is opened or closed by the person. Bubbles. + 使用者展開或收合事件列時觸發。會向上傳遞。 + + + The provider whose incident is open (two-way), or null. + 目前展開事件的供應商(雙向),或 null。 + + + A quiet line under the list, such as how latency and uptime are measured. A playing simulation shows a sample data note. + 清單下方一行低調的說明,例如延遲與可用率是如何量測的。正在播放的模擬會顯示一則範例資料說明。 + + + Raised by the Status page link of an open incident. Bubbles. When nothing handles it, the link opens the incident's `Url`; with neither a handler nor a URL the link is hidden. + 由展開事件中的 Status page 連結觸發。會向上傳遞。沒有任何處理常式處理它時,連結會開啟事件的 `Url`;既沒有處理常式也沒有 URL 時,連結會隱藏。 + + + True while a check runs. + 檢查進行期間為 true。 + + + Shows skeleton rows with the same row height, for the first load. + 第一次載入時顯示列高相同的骨架列。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個字詞。 + + + A one-off message for screen readers, such as the result of an action; it is read each time it changes. + 給螢幕閱讀器的一次性訊息,例如某個操作的結果;每次變化時都會朗讀。 + + + The providers to show. Leave it null (with no `RefreshHandler`) to play `Simulation`. A check that returns providers writes them here. + 要顯示的供應商。保持為 null(且沒有 `RefreshHandler`)即播放 `Simulation`。回傳供應商的檢查會把結果寫回這裡。 + + + Fetches fresh status on the countdown and on the refresh button. Return the new providers, or update `Providers` yourself and return null. Throw to show the error state: the last results stay and the countdown turns into a retry. A newer check cancels the older one through the token. Without it the refresh controls are hidden. + 在倒數結束和按下重新整理按鈕時取得最新狀態。回傳新的供應商,或自行更新 `Providers` 並回傳 null。擲回例外即顯示錯誤狀態:上次的結果保留,倒數變為重試。新的檢查會透過取消權杖取消較早的檢查。沒有它時,重新整理控制項會隱藏。 + + + Time between automatic checks (30 seconds by default). The countdown pauses while the card is off screen or hidden; retries after an error wait at most 10 seconds. Null turns automatic checks off and keeps the refresh button. + 兩次自動檢查之間的時間(預設 30 秒)。卡片不在畫面上或被隱藏時倒數暫停;出錯後的重試最多等待 10 秒。為 null 時關閉自動檢查,但保留重新整理按鈕。 + + + A simulated status feed to play while `Providers` and `RefreshHandler` are null: its first step shows at once, and each simulated check on the countdown moves its story on. Null (the default) shows no providers. Give each card its own simulation. + `Providers` 和 `RefreshHandler` 為 null 時播放的模擬狀態來源:其第一步立即顯示,倒數計時上的每次模擬檢查都會推進故事。為 null(預設)時不顯示供應商。每張卡片使用自己的模擬。 + + + The card's title. + 卡片的標題。 + + + One day in a provider's history. + 供應商歷史記錄中的一天。 + + + An incident of a provider: a title and its updates. + 供應商的一個事件:標題及其更新。 + + + One AI provider in a `ProviderStatus` card: its mark, health, latency, daily history and incident. + `ProviderStatus` 卡片中的一個 AI 服務供應商:它的標誌、健康狀態、延遲、逐日歷史記錄和事件。 + + + Every word `ProviderStatus` shows or says, in English by default. Replace the whole object to localize or reword; numbers, dates and sentences that carry values are format functions. + `ProviderStatus` 顯示或朗讀的每一個字詞,預設為英文。替換整個物件即可在地化或改寫措辭;數字、日期以及帶有數值的句子是格式化函式。 + + + A simulated status feed for a `ProviderStatus` that has no `Providers` and no `RefreshHandler`, for demos and prototypes. The card shows the first step's providers, then runs a simulated check every `Interval` on its countdown (or when the refresh button is pressed): each check takes `CheckDelay` and moves to the next step, looping, so incidents can open, post updates and resolve. Nothing is fetched. Create one per card: it keeps its place in the story. + 供沒有 `Providers` 和 `RefreshHandler` 的 `ProviderStatus` 使用的模擬狀態來源,用於展示與原型。卡片先顯示第一步的供應商,然後依倒數計時每隔 `Interval`(或按下重新整理按鈕時)進行一次模擬檢查:每次檢查耗時 `CheckDelay`,並進入下一步、循環播放,因此事件可以開啟、發布更新並解決。不會取得任何資料。每張卡片建立一個:它會記住故事進行到哪裡。 + + + One simulated check of a `ProviderStatusSimulation`: what it returns, or that it fails. + `ProviderStatusSimulation` 的一次模擬檢查:它回傳的內容,或者它失敗。 + + + One update of an incident. + 事件的一則更新。 + One option of `RadioCards` (Arc `RadioCardOption`): a label with optional description, a meta value (price, estimate), a decorative icon, and a reason shown in place of the description when the option is disabled. `RadioCards` 的一個選項(Arc 的 `RadioCardOption`):包含標籤(可選描述)、中繼資料值(價格、預估時間)、裝飾性圖示,以及選項停用時代替描述顯示的原因。 @@ -15028,6 +18456,94 @@ Raised when the viewer adds or removes a reaction (Arc's onValueChange), after `Value` changed. 檢視者新增或移除回應時觸發(Arc 的 onValueChange),於 `Value` 變更之後。 + + The collapsible thinking block of an AI reply (Arc Reasoning block, design reference). While the model thinks, a shimmering label counts the seconds and the latest reasoning streams under it in muted text, two lines tall. When it finishes it folds to "Thought for 7s" with a chevron; opening it shows the full reasoning as steps with headings on a quiet rail, then a footer with the outcome and a copy button. Give it the reasoning as one growing string in `Reasoning` (blank lines split paragraphs and a line written as `**Heading**` or `## Heading` starts a step) or as `ReasoningStep`s in `Steps`, and set `Status`. The seconds are counted here unless you set `Elapsed`. With no data it reads "Thought process" and has nothing to open; a `Simulation` plays a reasoning you pass in instead. + AI 回覆中可摺疊的思考區塊(Arc Reasoning block,設計參考)。模型思考時,一個閃爍的標籤逐秒計時,最新的推理以淡色文字在其下方串流出現,最多兩行。完成後它摺疊為帶箭頭的 "Thought for 7s";展開後以軌道上的步驟呈現完整推理,底部是結果和複製按鈕。把推理作為一個不斷增長的字串傳給 `Reasoning`(空行分隔段落,寫成 `**Heading**` 或 `## Heading` 的行開始一個新步驟),或放入 `Steps`,並設定 `Status`。除非你自行設定 `Elapsed`,秒數由該區塊自己計時。沒有資料時,它顯示 "Thought process",也沒有可展開的內容;`Simulation` 則會播放你傳入的推理。 + + + Shows a Stop button while the model thinks; it raises `StopRequested`. + 在模型思考時顯示停止按鈕;它會引發 `StopRequested`。 + + + The time the model has thought. Null counts it here while thinking and keeps the count after. + 模型已思考的時間。為 null 時,在思考期間由該區塊自行計時,結束後保留計數。 + + + The reason shown at the end of the reasoning when `Status` is `Error`. + `Status` 為 `Error` 時,顯示在推理末尾的原因。 + + + Whether the steps are open (two-way). + 步驟是否展開(雙向)。 + + + Every word the block shows or says. + 該區塊顯示或朗讀的每一個文字。 + + + The 18px mark at the left of the head (a 24 × 24 stroked glyph); the sparkle by default. + 頭部左側 18px 的標記(24 × 24 的描邊圖示);預設為星芒。 + + + The mark's color; the accent by default. + 標記的顏色;預設為強調色。 + + + The reasoning so far as one string. Set it as it grows; it takes the place of `Steps`. + 到目前為止的推理,作為一個字串。隨其增長不斷設定它;它取代 `Steps`。 + + + Raised when the person presses Retry on a failed block. Bubbles. + 當人在失敗的區塊上按下重試時引發。會冒泡。 + + + A simulated model to play while the block has no reasoning, steps, status or time of its own (demos and prototypes). The block starts it over each time it takes it up; null shows the block's own state. + 要播放的模擬模型,在該區塊沒有自己的推理、步驟、狀態或時間時使用(展示與原型)。該區塊每次接手它時都會從頭開始;為 null 時顯示該區塊自己的狀態。 + + + Where the block is: thinking, done, stopped or failed. + 該區塊所處的狀態:思考中、已完成、已停止或失敗。 + + + Raised when the person presses Stop. Bubbles. Abort your request, then set `Status` to Stopped. + 當人按下停止時引發。會冒泡。終止你的請求,然後把 `Status` 設為 Stopped。 + + + A card or a plain block. + 卡片或精簡樣式的區塊。 + + + Every word `ReasoningBlock` shows or says, in English by default. Replace the whole object to localize or reword; the times are format functions. + `ReasoningBlock` 顯示或朗讀的每一個文字,預設為英文。若要在地化或改寫,請替換整個物件;時間是格式化函式。 + + + Plays a reasoning into a `ReasoningBlock` the way a model streams it, for demos, prototypes and tests: the block thinks, the seconds count, and `Reasoning` arrives three words every 45ms; then the block finishes with the time it took. Nothing is sent to a model. Set it as the block's `Simulation`. The block plays it while it has no reasoning, steps, status or time of its own, starts it over each time it takes it up, and advances it only while it is on screen; Stop on the block stops it. Use one simulation per block. It has no timer of its own: without a block, call `Advance` with the time that passed. + 像模型串流輸出那樣把一段推理播放到 `ReasoningBlock` 中,用於展示、原型和測試:該區塊進入思考、逐秒計時,`Reasoning` 以每 45ms 三個詞的速度到達;然後該區塊以所用的時間完成。不會向任何模型傳送內容。把它設為該區塊的 `Simulation`。該區塊在沒有自己的推理、步驟、狀態或時間時播放它,每次接手時從頭開始,只在顯示於螢幕上時推進;該區塊上的停止按鈕會停止它。每個區塊使用一個模擬。它沒有自己的計時器:沒有區塊時,呼叫 `Advance` 並傳入經過的時間。 + + + Raised when the text, the status or the shown second changes. + 文字、狀態或顯示的秒數變化時引發。 + + + Raised once the last word is in and the simulated model has stopped thinking. + 最後一個詞到達、模擬模型停止思考時引發一次。 + + + The whole reasoning to stream, written the way `Reasoning` reads it (blank lines split paragraphs, `**Heading**` starts a step). A change takes effect the next time the simulation starts. + 要串流輸出的完整推理,寫法與 `Reasoning` 的讀取方式相同(空行分隔段落,`**Heading**` 開始一個步驟)。修改會在模擬下次開始時生效。 + + + One step of a model's reasoning: an optional heading and the text under it. Change `Text` in place as the model streams; the `ReasoningBlock` follows every change. + 模型推理的一個步驟:一個選用的標題和其下的文字。模型串流輸出時就地修改 `Text`;`ReasoningBlock` 會跟隨每次修改。 + + + The step's heading (a few words); empty for a step that is only text. + 步驟的標題(幾個詞);僅有文字的步驟為空。 + + + The step's text. + 步驟的文字。 + One thought of a `ReasoningTree`: a short label for the node, the full sentence for the tag and screen readers, an illustrative score and the thoughts it leads to. The tree reads three levels: the thoughts given to `Thoughts` are depth 1, their children depth 2 and their grandchildren depth 3; deeper thoughts are ignored. Add a thought to one parent only. `ReasoningTree` 中的一個思路:節點的簡短標籤、用於標籤與螢幕閱讀器的完整句子、一個示意分數,以及它所通向的思路。樹會呈現三個層級:給予 `Thoughts` 的思路為深度 1,它們的子節點為深度 2,孫節點為深度 3;更深的思路將被忽略。請只把一個思路加入一個父節點下。 @@ -15092,9 +18608,129 @@ The arguments of `Answered`: the answer the search reached and how it got there. `Answered` 的參數:搜尋所到達的答案,以及它是如何得出的。 - - Every word a `ReasoningTree` shows or announces, in English by default: column headers, the header status, the beam width label, the tag and accessible names of the thoughts and the polite announcements. Replace the instance to translate or reword; sentences with numbers are functions so that word order can change. The playback buttons are named by `PlaybackLabels`. - `ReasoningTree` 顯示或宣讀的每一個文字,預設為英文:欄標題、標題狀態、束寬標籤、思路的標籤與無障礙名稱,以及即時區域的播報。替換整個實例即可翻譯或改寫;包含數字的句子為函數,以便調整詞序。播放按鈕的名稱由 `PlaybackLabels` 指定。 + + Every word a `ReasoningTree` shows or announces, in English by default: column headers, the header status, the beam width label, the tag and accessible names of the thoughts and the polite announcements. Replace the instance to translate or reword; sentences with numbers are functions so that word order can change. The playback buttons are named by `PlaybackLabels`. + `ReasoningTree` 顯示或宣讀的每一個文字,預設為英文:欄標題、標題狀態、束寬標籤、思路的標籤與無障礙名稱,以及即時區域的播報。替換整個實例即可翻譯或改寫;包含數字的句子為函數,以便調整詞序。播放按鈕的名稱由 `PlaybackLabels` 指定。 + + + One person invited through a `ReferralCard`. + 透過 `ReferralCard` 邀請的一個人。 + + + A give-and-get referral card (Arc Referral card, design reference). The ask on top ("Give $20, get $20"), the invite link in a quiet field with a Copy button and three share buttons (a post, a network and email, drawn with generic glyphs), and an invite-by-email field that turns each address into a chip. Below, the funnel as three rolling numbers (invited, signed up, earned), a segmented milestone bar with the reward it unlocks, and the people invited with their avatar and stage as a glyph plus a word. Everything is derived from `Referrals`: pass the list from your backend and replace it when sign-ups and payments come in. Stage changes crossfade in place, the earned total counts up, and reaching the goal turns the bar and its line into the unlocked state. Sending invites calls `InviteHandler`; without one the send is simulated. With no referrals the card plays its `Simulation` (your program) once when it is 40% on screen; with neither it is empty: zero counts and "No invites yet". + 雙向獎勵的推薦卡片(Arc Referral card,設計參考)。頂部是邀約("Give $20, get $20"),邀請連結放在一個安靜的欄位裡,附複製按鈕和三個分享按鈕(貼文、人脈和電子郵件,使用通用圖示),另有一個依電子郵件邀請的欄位,把每個地址變成一個標籤。下方是三個滾動數字組成的漏斗(已邀請、已註冊、已賺取)、顯示所解鎖獎勵的分段里程碑進度列,以及被邀請的人:頭像加上以圖示和文字表示的階段。一切都由 `Referrals` 推導:傳入後端的清單,並在有人註冊或付款時替換它。階段變化在原位交叉淡化,已賺取總額累加,達到目標時進度列及其文字變為已解鎖狀態。傳送邀請會呼叫 `InviteHandler`;沒有它時傳送是模擬的。沒有推薦資料時,卡片在有 40% 進入畫面時播放一次它的 `Simulation`(你的計畫);兩者都沒有時卡片為空:計數為零,並顯示 "No invites yet"。 + + + The ISO 4217 code used to format the reward and the total ("USD" writes "$20"). + 用於格式化獎勵和總額的 ISO 4217 貨幣代碼("USD" 寫作 "$20")。 + + + One or two lines under the heading. Null says what both sides get. + 標題下方的一兩行文字。為 null 時說明雙方各得什麼。 + + + The subject of the share email, or null for none. + 分享郵件的主旨,為 null 時沒有主旨。 + + + Sends the invites. Send stays busy until the task ends; a failed task keeps the chips and shows the exception's message; on success the addresses join the top of the list as Invited. The token is cancelled when the card leaves the screen. Null simulates the send. + 傳送邀請。工作結束前傳送按鈕保持忙碌狀態;工作失敗時保留標籤並顯示例外訊息;成功後這些地址以 Invited 狀態加入清單頂部。卡片離開畫面時取消權杖。為 null 時模擬傳送。 + + + Raised after a send succeeded (or was simulated), with the addresses. Bubbles. + 傳送成功(或模擬傳送)後引發,附上這些地址。會冒泡。 + + + Every word the card shows or says. + 卡片顯示或朗讀的每一個詞。 + + + The person's invite link. The domain shows muted and the code in full color; Copy copies the whole URL. + 此人的邀請連結。網域以弱化色顯示,邀請碼以內文色顯示;複製按鈕複製完整 URL。 + + + Raised after Copy has written the link to the clipboard. Bubbles. + 複製按鈕把連結寫入剪貼簿後引發。會冒泡。 + + + The most addresses one send can hold. Extra addresses are refused with a note. + 一次傳送最多可包含的地址數。多出的地址會被拒絕並附上說明。 + + + The bonus after a number of paid referrals: the goal (up to 12 segments are drawn), the reward in a few words and an optional line shown once unlocked. Null (the default) hides the milestone. + 達到一定數量的付費推薦後獲得的額外獎勵:目標(最多繪製 12 段)、用幾個詞說明的獎勵,以及解鎖後顯示的選用說明。為 null(預設)時隱藏里程碑。 + + + Raised once when paid referrals reach the milestone's goal. Bubbles. + 付費推薦達到里程碑目標時引發一次。會冒泡。 + + + The share URL of the network button, with `{url}` and `{text}` placeholders. Null passes the link itself. + 人脈按鈕的分享 URL,帶 `{url}` 和 `{text}` 預留位置。為 null 時直接傳遞連結本身。 + + + The share URL of the post button, with `{url}` and `{text}` placeholders (filled escaped), for example your social network's share intent. Null passes the link itself. + 貼文按鈕的分享 URL,帶 `{url}` 和 `{text}` 預留位置(以跳脫後的值填入),例如你的社群網路的分享網址。為 null 時直接傳遞連結本身。 + + + Everyone invited, newest first. The counts, the milestone and the list are derived from it, and the card follows it when you replace it. Null shows the `Simulation`'s program, or the empty card. + 所有被邀請的人,依由新到舊排列。計數、里程碑和清單都由它推導,替換它時卡片隨之更新。為 null 時顯示 `Simulation` 的計畫,否則顯示空卡片。 + + + The credit each side gets, in whole currency units. Drives the heading, the Earned total and the rows' Earned label; 0 (the default) leaves the amount out of the words. + 雙方各得的獎勵,以整數貨幣單位計。決定標題、已賺取總額以及各列的 Earned 標籤;為 0(預設)時文字中不寫金額。 + + + Text that goes with the link in a post, a network share and the email body. + 在貼文、人脈分享和郵件內文中與連結一起出現的文字。 + + + Raised when a share button is pressed, with the channel and a ready share URL. Open the URL and set `Handled`; when nobody does, the share is simulated and the card says so. Bubbles. + 按下分享按鈕時引發,附上管道和現成的分享 URL。開啟該 URL 並設定 `Handled`;沒有人這樣做時,分享是模擬的,卡片會說明這一點。會冒泡。 + + + Plays the `Simulation`'s beats once on screen and moves the first address of each simulated send through the stages. Null means true while a `Simulation` shows (no `Referrals`) and false otherwise. + 在畫面上播放一次 `Simulation` 的節拍,並讓每次模擬傳送的第一個地址依序經歷各個階段。為 null 時,顯示 `Simulation`(沒有 `Referrals`)則為 true,否則為 false。 + + + A program to play while `Referrals` is null: the card shows its list and plays its beats once when it is 40% on screen. Null (the default) leaves a card without referrals empty. Give each card its own. + 在 `Referrals` 為 null 時播放的計畫:卡片顯示它的清單,並在卡片有 40% 進入畫面時播放一次它的節拍。為 null(預設)時,沒有推薦資料的卡片為空。每張卡片各用一個。 + + + The heading. Null builds it from the reward: "Give $20, get $20" (or "Invite friends" without one). + 標題。為 null 時由獎勵產生:"Give $20, get $20"(沒有獎勵時為 "Invite friends")。 + + + Every word `ReferralCard` shows or says, in English by default. Replace the whole object to localize or reword; counts, amounts and dates are format functions. Amounts arrive already formatted ("$20"). + `ReferralCard` 顯示或朗讀的每一個詞,預設是英文。替換整個物件即可在地化或改寫措辭;數量、金額和日期是格式化函式。金額傳入時已經格式化("$20")。 + + + A referral program for a `ReferralCard` to play, written by your app (nothing ships with the package): the list it starts from and the beats that move friends through the stages. Give it to `Simulation`; while the card has no referrals of its own it shows this list and plays `Beats` once when it is 40% on screen (twice as fast under reduced motion), and moves the first address of each simulated send through the stages. Data only; the card keeps the playback state. + 供 `ReferralCard` 播放的推薦計畫,由你的應用程式撰寫(套件不附帶任何計畫):它起始的清單,以及讓好友依序經歷各個階段的節拍。把它交給 `Simulation`;卡片沒有自己的推薦資料時,會顯示這個清單,並在卡片有 40% 進入畫面時播放一次 `Beats`(減少動態效果時以兩倍速度播放),還會讓每次模擬傳送的第一個地址依序經歷各個階段。僅為資料;播放狀態由卡片保存。 + + + One beat of a `ReferralCardSimulation`: after `Delay`, the referral `Id` moves to `Stage` (taking `Name` when it has none yet, as a friend who signs up does). + `ReferralCardSimulation` 的一個節拍:經過 `Delay` 後,推薦 `Id` 進入 `Stage`(尚無名字時採用 `Name`,就像一位註冊的好友)。 + + + Arguments of `InvitesSent`. + `InvitesSent` 的參數。 + + + Arguments of `LinkCopied`. + `LinkCopied` 的參數。 + + + The bonus a `ReferralCard` unlocks after a number of paid referrals. + `ReferralCard` 在達到一定數量的付費推薦後解鎖的額外獎勵。 + + + Arguments of `MilestoneReached`. + `MilestoneReached` 的參數。 + + + Arguments of `ShareRequested`. Open `Url` and set `Handled`; when no handler does, the card treats the share as simulated and says so. + `ShareRequested` 的參數。開啟 `Url` 並設定 `Handled`;沒有處理常式這樣做時,卡片把這次分享視為模擬並加以說明。 One check of a `ReleaseReadiness` gate: a title, an optional line under it and whether it is done. Every required check must be done before the release can start; optional checks (`IsRequired` false) are shown and can be ticked, but do not hold the release back. @@ -15340,6 +18976,162 @@ A row of a `ReorderableList` as Arc models it: an id, a title and an optional detail line. Arc 建模的 `ReorderableList` 中的一列:包含一個 id、一個標題,以及一個可選的詳細資訊行。 + + An AI email reply drafter (Arc Reply draft, design reference): a received email with an AI reply streaming under it. Quick reply chips steer a fresh draft, which streams in chunks of 3 to 8 words that each fade in over 140ms while the draft box springs to its height. Tone (Friendly, Concise, Formal) and length (Short, Medium, Long) rewrite a finished draft with one crossfade: the old text dims, fades out in 100ms and the new one fades in over 160ms rising 2px. The finished draft is plain editable text; Regenerate writes a new one, and Insert (or Cmd/Ctrl+Enter anywhere in the card) hands the text, with the reader's edits, to `InsertHandler` and `InsertRequested`. Give it a `Message`, `Suggestions` and a `Generator` that streams your model's reply. Without a message it plays a `Simulation` (an email and the replies you wrote for it, nothing is sent) and, unless the reader touches it, tours tones, length, a second quick reply and Insert on a loop while it is on screen. With neither it shows an empty email and a draft box that cannot draft. + AI 郵件回覆起草器(Arc Reply draft,設計參考):一封收到的郵件,下方串流寫出 AI 回覆。快速回覆標籤引導一份新的草稿,草稿以每塊 3 到 8 個詞的片段串流出現,每塊在 140ms 內淡入,同時草稿框以彈簧過渡到新高度。語氣(Friendly、Concise、Formal)與長度(Short、Medium、Long)以一次交叉淡化改寫已完成的草稿:舊文字變暗,在 100ms 內淡出,新文字在 160ms 內淡入並上升 2px。完成的草稿是可編輯的純文字;Regenerate 重新寫一份,Insert(或在卡片內任意位置按 Cmd/Ctrl+Enter)把包含讀者修改的文字交給 `InsertHandler` 與 `InsertRequested`。為它提供 `Message`、`Suggestions` 以及串流輸出模型回覆的 `Generator`。沒有郵件時,它播放 `Simulation`(一封郵件和你為它寫好的回覆,不寄出任何內容),並且只要讀者沒有操作它,就會在可見時循環示範語氣、長度、第二個快速回覆與 Insert。兩者都沒有時,顯示一封空白郵件和一個無法起草的草稿框。 + + + Tours tones, length, a second quick reply and Insert on a loop. It stops for good on the first pointer or key press inside the card and pauses off screen. Null turns it on only while a `Simulation` plays; leave it off in products. + 循環示範語氣、長度、第二個快速回覆與 Insert。在卡片內第一次按下指標或按鍵後永久停止,離開畫面時暫停。為 null 時僅在播放 `Simulation` 時開啟;在產品中請關閉。 + + + Starts the first draft when the card scrolls into view. When false the draft box shows an empty state with a Draft reply button. + 卡片捲動進入檢視時開始寫第一份草稿。為 false 時草稿框顯示帶 Draft reply 按鈕的空狀態。 + + + The draft on screen, including the reader's edits; empty before the first draft. + 螢幕上的草稿,包含讀者的修改;第一份草稿之前為空。 + + + Raised each time a draft or rewrite finishes. Cancelled and failed requests do not raise it. Bubbles. + 每當一份草稿或改寫完成時引發。被取消或失敗的請求不會引發。會向上傳遞。 + + + Writes the draft as a stream of text deltas. Every new action cancels the previous request's token. Without it a message of yours cannot be drafted; a playing `Simulation` does not use it. + 以文字增量串流的形式寫出草稿。每個新操作都會取消上一個請求的權杖。沒有它時,你自己的郵件無法起草;正在播放的 `Simulation` 不會使用它。 + + + Puts the draft into your composer. While its task runs, Insert shows a spinner. With neither this nor a handler that marks `InsertRequested` handled, Insert is simulated and says that nothing was sent. + 把草稿放入你的編輯器。其工作執行期間,Insert 顯示載入圈。既沒有它,也沒有把 `InsertRequested` 標記為已處理的處理常式時,插入是模擬的,並會說明沒有寄出任何內容。 + + + Raised when Insert is pressed (or Cmd/Ctrl+Enter), with the final draft and the reader's edits, before `InsertHandler` runs. Mark it handled when you insert the text here, so the card does not say it was simulated. Bubbles. + 按下 Insert(或 Cmd/Ctrl+Enter)時,在 `InsertHandler` 執行之前引發,帶有最終草稿與讀者的修改。如果你在這裡插入文字,請將它標記為已處理,這樣卡片就不會說是模擬的。會向上傳遞。 + + + Adds "Simulated, nothing was sent" to announcements and errors. Null turns it on while a `Simulation` plays or there is no `Generator`. + 在播報與錯誤中加上 "Simulated, nothing was sent"。為 null 時,在播放 `Simulation` 或沒有 `Generator` 時開啟。 + + + Every word the card shows or says. + 卡片顯示或播報的每個詞。 + + + The length (two-way). Works like `Tone`. + 長度(雙向)。行為與 `Tone` 相同。 + + + Raised when the reader picks a length (not when the app or the tour sets it). Bubbles. + 讀者選擇長度時引發(應用程式或示範設定時不引發)。會向上傳遞。 + + + The received email; null plays `Simulation`, or shows an empty email without one. + 收到的郵件;為 null 時播放 `Simulation`,沒有模擬時顯示一封空白郵件。 + + + The model credited on the reply, with its 16px mark; null credits the `Simulation`'s model while one plays, or none. + 回覆上署名的模型及其 16px 標記;為 null 時,播放 `Simulation` 期間署名其模型,否則不署名。 + + + The id of the quick reply steering the draft (two-way). Null, or an id that is not in the list, means the first suggestion. Setting it after the first draft writes a fresh one. + 正在引導草稿的快速回覆的 id(雙向)。為 null 或不在清單中的 id 時表示第一個快速回覆。在第一份草稿之後設定它會寫出一份新的草稿。 + + + An email to play while the card has no `Message`: the message, quick replies, model and the replies written for them, streamed as a simulation (nothing is sent). Your own `Message` wins over it; null stops it and shows the empty card. + 卡片沒有 `Message` 時要播放的郵件:郵件、快速回覆、模型以及為它們寫好的回覆,以模擬方式串流輸出(不寄出任何內容)。你自己的 `Message` 優先於它;為 null 時停止播放並顯示空卡片。 + + + The 24 × 24 stroked mark before the subject; null draws a generic mail glyph. + 主旨前的 24 × 24 描邊標記;為 null 時繪製通用的郵件圖示。 + + + Where the card is: idle, drafting, rewriting, ready, inserting, inserted or failed. + 卡片所處的狀態:閒置、起草中、改寫中、就緒、插入中、已插入或失敗。 + + + Raised when the reader chooses a quick reply chip, before its draft starts. Bubbles. + 讀者選擇快速回覆標籤時、在其草稿開始之前引發。會向上傳遞。 + + + The quick replies shown as chips above the draft; choosing one streams a fresh draft steered by it. Null shows the `Simulation`'s while one plays; null or an empty list hides the row. + 以標籤顯示在草稿上方的快速回覆;選擇其中一個會串流寫出由它引導的新草稿。為 null 時,播放 `Simulation` 期間顯示其快速回覆;為 null 或空清單時隱藏這一列。 + + + The tone (two-way). Changing it rewrites a finished draft with one crossfade, or restarts a draft that is still streaming. + 語氣(雙向)。變更它會以一次交叉淡化改寫已完成的草稿,或重新開始仍在串流輸出的草稿。 + + + Raised when the reader picks a tone (not when the app or the tour sets it). Bubbles. + 讀者選擇語氣時引發(應用程式或示範設定時不引發)。會向上傳遞。 + + + Arguments of `DraftCompleted` and `InsertRequested`: the text and how it was written. + `DraftCompleted` 與 `InsertRequested` 的引數:文字及其寫法。 + + + Writes a `ReplyDraft` reply as a stream of text deltas. Yield chunks from your model's stream, or wrap a whole text with `FromText` to have it streamed locally in chunks. Throw to show the error state; honour `CancellationToken`. + 以文字增量串流的形式寫出 `ReplyDraft` 的回覆。從模型的串流中逐塊產出,或用 `FromText` 包裝整段文字,讓它在本機分塊串流輸出。擲回例外即顯示錯誤狀態;請遵守 `CancellationToken`。 + + + Puts a finished `ReplyDraft` reply into your composer. While the returned task runs, Insert shows a spinner; a fault shows the error in the status line and keeps the draft. + 把完成的 `ReplyDraft` 回覆放入你的編輯器。傳回的工作執行期間,Insert 顯示載入圈;出錯時在狀態列顯示錯誤並保留草稿。 + + + Every word `ReplyDraft` shows or says, in English by default. Replace the whole object to localize or reword; the words that carry a name or a count are format functions. + `ReplyDraft` 顯示或播報的每個詞,預設為英文。替換整個物件即可在地化或改寫;帶有名稱或數量的詞是格式化函式。 + + + How long a `ReplyDraft` reply is. + `ReplyDraft` 回覆的長度。 + + + Arguments of `LengthChanged`. + `LengthChanged` 的引數。 + + + The received email a `ReplyDraft` answers: who sent it, the subject, the body (blank lines split paragraphs) and when it arrived. Data only. + `ReplyDraft` 回覆的那封收到的郵件:寄件人、主旨、內文(空行分隔段落)以及送達時間。僅為資料。 + + + What a `ReplyDraftRequest` asks the generator for. + `ReplyDraftRequest` 要求產生器寫什麼。 + + + What a `ReplyDraftGenerator` is asked to write. + 要求 `ReplyDraftGenerator` 寫出的內容。 + + + An email a `ReplyDraft` plays without a generator, from data you give it: the received message, the quick replies, the model credited and the replies you wrote for each quick reply, tone and length. Set it as `Simulation` while the card has no `Message`: the card shows the email, streams the matching reply after a short read (3 to 8 words every 34ms), says it is simulated (nothing is sent) and, unless `AutoPlay` is false, tours tones, length, a second quick reply and Insert on a loop. + `ReplyDraft` 在沒有產生器時依你提供的資料播放的郵件:收到的郵件、快速回覆、署名的模型,以及你為每個快速回覆、語氣和長度寫好的回覆。在卡片沒有 `Message` 時把它設為 `Simulation`:卡片顯示郵件,在短暫的閱讀之後串流輸出相符的回覆(每 34ms 3 到 8 個詞),說明這是模擬(不寄出任何內容),並且除非 `AutoPlay` 為 false,會循環示範語氣、長度、第二個快速回覆與 Insert。 + + + Where a `ReplyDraft` is. + `ReplyDraft` 所處的狀態。 + + + Streams for `ReplyDraft`: a wrapper that streams a whole text locally and the chunker it and `ReplyDraftSimulation` use (3 to 8 words every 34ms). Nothing is sent. + `ReplyDraft` 的串流:在本機串流輸出整段文字的包裝器,以及它和 `ReplyDraftSimulation` 共用的分塊器(每 34ms 3 到 8 個詞)。不寄出任何內容。 + + + A quick reply chip of a `ReplyDraft`: a stable id the prompt understands and the label readers see. + `ReplyDraft` 的一個快速回覆標籤:提示詞能理解的穩定 id,以及讀者看到的標籤文字。 + + + Arguments of `SuggestionSelected`. + `SuggestionSelected` 的引數。 + + + The voice of a `ReplyDraft` reply. + `ReplyDraft` 回覆的語氣。 + + + Arguments of `ToneChanged`. + `ToneChanged` 的引數。 + + + One reply of a `ReplyDraftSimulation`: the quick reply, tone and length it answers, and its text. + `ReplyDraftSimulation` 的一則回覆:它回應的快速回覆、語氣和長度,以及它的文字。 + Fails an empty value (null, empty text). 空值(null、空文字)不通過。 @@ -15392,6 +19184,106 @@ Each pane's share in percent. Updated after every resize settles; set it (for example from saved settings) to restore a layout. Two-way. 每個窗格的比例(百分比)。每次調整大小完成後更新;設定它(例如從儲存的設定中讀取)即可還原版面配置。雙向繫結。 + + An AI reply card with its feedback (Arc Response feedback, design reference): the person's message in a bubble, the model's mark and name, the reply and an action row with copy, an optional regenerate and the thumbs of `FeedbackActions`. Thumbs down opens an inline panel with reason chips and an optional note, thumbs up fills and asks what was good, and a submit shows a short thank-you before the panel folds back into the row. With no data it shows the frame alone: the model's mark, an empty reply and the action row, whose feedback is simulated until you set a `FeedbackHandler`. Every property of `FeedbackActions` except `Leading` is available here. + 附意見回饋的 AI 回覆卡片(Arc Response feedback,設計參考):泡泡中的使用者訊息、模型的標誌與名稱、回覆,以及包含複製、可選的重新產生與 `FeedbackActions` 讚與倒讚的操作列。按倒讚會在行內展開一個附原因標籤與可選備註的面板,按讚會填滿圖示並詢問哪裡做得好,送出後顯示簡短的感謝,隨後面板收回到操作列中。沒有資料時只顯示框架:模型標誌、空白的回覆與操作列;在你設定 `FeedbackHandler` 之前,意見回饋為模擬傳送。除 `Leading` 外,`FeedbackActions` 的每個屬性在這裡都可用。 + + + Shows the optional note field after a thumbs down. + 按倒讚後顯示可選的備註欄。 + + + Shows a regenerate action in the row; it raises `RegenerateRequested`. + 在操作列中顯示重新產生操作;它會引發 `RegenerateRequested`。 + + + The note's maximum length in characters. + 備註的最大長度(字元數)。 + + + The note field's placeholder. + 備註欄的預留位置文字。 + + + Plain text for the copy action; null (the default) hides copy. + 複製操作使用的純文字;為 null(預設)時隱藏複製。 + + + Persists feedback: called with a `Rating` submission on every thumb press and a `Details` one when reasons or a note are submitted. Throw (or fault the task) to show the inline error and Try again; the token is cancelled when the panel closes or the rating changes. Null simulates sends. + 保存意見回饋:每次按下讚或倒讚時以 `Rating` 送出呼叫,送出原因或備註時以 `Details` 送出呼叫。擲回例外(或讓工作失敗)即可顯示行內錯誤與 Try again;面板關閉或評分改變時權杖會被取消。為 null 時模擬傳送。 + + + Raised for every submission (the event of the inner `FeedbackActions`, which bubbles here). + 每次送出時引發(內部 `FeedbackActions` 的事件,向上傳遞到這裡)。 + + + Whether the thumbs can be pressed; turn it off while the reply is still streaming. + 是否可以按下讚與倒讚;回覆仍在串流時將其關閉。 + + + Every word the control shows or announces. + 控制項顯示或播報的每一個字詞。 + + + Passed back on every submission so feedback can be stored against a message. + 在每次送出時傳回,以便把意見回饋與某則訊息一起儲存。 + + + The model's mark, a 24 × 24 stroked glyph drawn at 16px; a neutral sparkle by default. + 模型的標誌,一個以 16px 繪製的 24 × 24 描邊圖形;預設為中性的閃光圖示。 + + + The name of the model that wrote the reply; null (the default) shows the mark alone. + 寫出這則回覆的模型名稱;為 null(預設)時只顯示標誌。 + + + Reason chips offered after a thumbs down. A chip labelled Other moves focus to the note and changes its placeholder. + 按倒讚後提供的原因標籤。標為 Other 的標籤會把焦點移到備註並更換其預留位置文字。 + + + Chips offered after a thumbs up. An empty list skips the follow-up row. + 按讚後提供的標籤。空清單會略過後續列。 + + + A quiet line next to Submit about what gets shared. Hidden below 400px of width; null hides it. + Submit 旁一行低調的文字,說明會分享什麼。寬度低於 400px 時隱藏;為 null 時不顯示。 + + + The person's message shown above the reply; null (the default) hides it. + 顯示在回覆上方的使用者訊息;為 null(預設)時隱藏。 + + + The rating (two-way). Set it to show a rating loaded from your database; bind it to own the state. A thumb press sets it and pressing the active thumb sets `None`. + 評分(雙向)。設定它可顯示從資料庫讀取的評分;繫結它可自行掌管狀態。按下讚或倒讚會設定它,再次按下目前啟用的按鈕會設為 `None`。 + + + Raised when a thumb press changes the rating (the event of the inner `FeedbackActions`, which bubbles here). + 按下讚或倒讚改變評分時引發(內部 `FeedbackActions` 的事件,向上傳遞到這裡)。 + + + Raised when the person presses regenerate. Bubbles. + 使用者按下重新產生時引發。會向上傳遞。 + + + The reply body: text or any control. Null leaves the reply empty. + 回覆內文:文字或任何控制項。為 null 時回覆為空白。 + + + How long the thank-you state holds before the panel folds away. + 感謝狀態停留多久後面板收起。 + + + The second line of the thank-you state. Null reads "It helps improve future replies." with a `FeedbackHandler`, and says nothing was sent without one. + 感謝狀態的第二行。為 null 時,有 `FeedbackHandler` 則顯示 "It helps improve future replies.",沒有則說明沒有傳送任何內容。 + + + Content pinned to the end of the row, such as a timestamp or a token count. + 固定在列尾的內容,例如時間戳記或權杖數。 + + + Every word `FeedbackActions` and `ResponseFeedback` show or announce, in English by default. Replace the whole object to localize or reword. The reasons, the note's placeholder, the privacy note and the second line of the thank-you state are properties of the control. + `FeedbackActions` 與 `ResponseFeedback` 顯示或播報的每一個字詞,預設為英文。替換整個物件即可在地化或改寫。原因、備註的預留位置文字、隱私說明與感謝狀態的第二行是控制項的屬性。 + One cohort: people (or accounts, workspaces) who started in the same period, and how many of them were still active in each month after that. The number of values in `Retained` is the number of months measured so far, so newer cohorts have shorter rows and the heatmap becomes a triangle. 一個世代:在同一週期開始的人(或帳戶、工作區),以及其中有多少人在之後每個月仍然活躍。`Retained` 中值的個數就是目前已測量的月數,因此較新的世代列更短,熱力圖呈三角形。 @@ -16244,6 +20136,10 @@ Where the save of a `RolesPermissions` matrix stands. `RolesPermissions` 矩陣的儲存目前所處的狀態。 + + A ticket that landed in a lane of a `TicketRouter`. + 落入 `TicketRouter` 某條泳道的工單。 + One axis of a `ConnectedScatter`. The range and the step are used exactly as given (no rounding to nice numbers, not forced to start at zero); leave them unset to derive them from the data of every series. `ConnectedScatter` 的一條座標軸。範圍和步長按給定值原樣使用(不會取整為好看的數字,也不會強制從零開始);不設定則由所有系列的資料推算。 @@ -18228,6 +22124,10 @@ Arguments of `ProviderSelectedEvent` (Arc's `onProvider`). `ProviderSelectedEvent`(Arc 的 `onProvider`)的引數。 + + A recognizer that never touches the microphone: it speaks the `Scripts` you give it as scripted speech with interim guesses, corrections and speech-like levels. Set it as the `Simulation` of a `DictationControl` or a `DictationButton` (where the composer can loop it) for demos and design reviews, or use it as a recognizer in tests. Each start plays the next script; with no scripts the voice stays silent. + 從不存取麥克風的辨識器:把你提供的 `Scripts` 以附暫定猜測、更正與類似語音音量的腳本化語音朗讀出來。可把它設為 `DictationControl` 或 `DictationButton` 的 `Simulation`(輸入框可以循環播放它),用於展示與設計審查,也可以在測試中當作辨識器使用。每次啟動會播放下一段腳本;沒有腳本時聲音保持靜默。 + A website footer in three layouts: link columns with a newsletter signup (`Columns`), a single quiet row (`Minimal`), and a closing layout where the Arc arch rises in, oversized and cropped by the bottom edge, fading into the page (`Logo`). The newsletter validates in place, keeps its width while the button morphs, and confirms without a toast. Every text, link, column and the status line can be replaced; with nothing set the footer shows Arc's sample footer. 網站頁尾,提供三種版面:帶電子報訂閱的連結欄(`Columns`)、單行簡潔版面(`Minimal`),以及收尾版面(`Logo`),在此版面中 Arc 的拱形圖案從底部升起,尺寸放大並被底邊裁切,漸隱於頁面之中。電子報訂閱會就地驗證,按鈕變形時保持寬度,並在不跳出提示的情況下確認。所有文字、連結、欄和狀態列都可以替換;未設定時顯示 Arc 的範例頁尾。 @@ -18596,6 +22496,86 @@ Placeholder text lines (Arc clamps to 1–6). Defaults to 3. 佔位文字行數(Arc 會限制在 1–6 之間)。預設為 3。 + + An image generation visual that paints the way an artist does (Arc Sketch to image, design reference). Pencil lines sketch the scene stroke by stroke with a pen tip that lifts and travels between strokes, a round brush sweeps flat color blocks in region by region from back to front, two soft detail passes, film grain and a warm light refine it, and it resolves into the finished picture. Under the canvas a stage label with an icon crossfades above the prompt, beside a thin progress line, a percent and a Replay button. Give it a `Scene` (what gets drawn) and, optionally, the finished `Image`. With no `Status` it loops on its own over them; set `Status` (or `Progress`) to follow a real generation. Nothing is generated: it is a simulated preview over your scene. Without a scene or an image it rests on blank paper, waiting. One clock drives a single 0 → 1 timeline; a frame redraws the ink layer and sets layer opacities, and allocates nothing. + 像畫家一樣作畫的影像生成視覺效果(Arc Sketch to image,設計參考)。鉛筆線條一筆一筆勾出場景,筆尖在筆畫之間抬起並移動;圓頭畫筆由後往前逐個區域掃過,鋪上平塗色塊;兩層柔和的細節、底片顆粒與暖光加以潤飾,最後呈現完成的畫面。畫布下方,帶圖示的階段標籤在提示詞上方交叉淡入淡出,旁邊是一條細進度線、百分比和「重播」按鈕。給它一個 `Scene`(要繪製的內容),也可以再給完成的 `Image`。不設定 `Status` 時它在這些內容上自行循環;設定 `Status`(或 `Progress`)即可跟隨真實的生成。這裡並不生成任何東西:它是在你的場景上播放的模擬預覽。沒有場景也沒有圖片時,它停在空白畫紙上等待。一個時鐘驅動唯一一條 0 → 1 的時間軸;每一影格重繪墨跡層並設定各層的不透明度,不配置任何記憶體。 + + + Raised each time the picture is fully revealed. Bubbles. + 每次畫面完全呈現時引發。會向上傳遞。 + + + One full pass from blank paper to the picture. Sketching takes about 38%, blocking 24%, refining 26% and the final crossfade 12%. + 從空白畫紙到完成畫面的完整一遍所需時間。勾線約占 38%,鋪色 24%,潤飾 26%,最後的交叉淡入 12%。 + + + The finished picture (your real result), stretched to fill the canvas; it overrides the scene's own. + 完成的畫面(你的真實結果),延展以填滿畫布;它會取代場景自帶的畫面。 + + + When it plays on its own, holds the picture for about two seconds, fades back to blank paper and paints again. + 自行播放時,在畫面上停留約兩秒,淡回空白畫紙後再畫一遍。 + + + Every word the card shows or says. + 卡片顯示或朗讀的所有文字。 + + + Where the pass should be, from 0 (blank paper) to 1 (the picture), in place of `Status`'s stage ends (sketching ends at 0.38, blocking at 0.62, refining at 0.88). The painting still travels there at its own pace. + 這一遍應到達的位置,從 0(空白畫紙)到 1(畫面),用來取代 `Status` 的各階段終點(勾線在 0.38 結束,鋪色在 0.62,潤飾在 0.88)。繪畫仍依自己的節奏前往那裡。 + + + The prompt under the stage label, on one line with the full text in a tooltip. Empty or null (the default) hides it. + 階段標籤下方的提示詞,單行顯示,完整文字在提示框中。為空或 null(預設)時隱藏。 + + + Raised when Replay is pressed, for example to rerun a real request. Bubbles. + 按下「重播」時引發,例如用於重新發出真實的請求。會向上傳遞。 + + + What gets drawn and the picture it resolves into. Null (with no `Image`) leaves blank paper: the card waits and does not loop. + 要繪製的內容及其最終呈現的畫面。為 null(且沒有 `Image`)時維持空白畫紙:卡片等待,不會循環。 + + + Shows the Replay button, which fades to blank paper and paints again up to the current stage. + 顯示「重播」按鈕,它會淡回空白畫紙,再重新畫到目前階段。 + + + The card's maximum width (340, 520 or 720px) and pencil weight. Small shows only Replay's icon. + 卡片的最大寬度(340、520 或 720px)與鉛筆粗細。Small 只顯示「重播」的圖示。 + + + The stage on screen (it can trail `Status` while the painting travels there). + 螢幕上顯示的階段(繪畫前往目標期間可能落後於 `Status`)。 + + + Raised once each time the stage on screen changes, never per frame. Bubbles. + 螢幕上的階段每變化一次引發一次,從不逐影格引發。會向上傳遞。 + + + The generation stage. Null (default) plays the whole pass on its own and loops (a card with no scene and no image waits on blank paper instead). A stage plays to its end and holds there with a shimmering label until the next one arrives; moving back (for example to idle for a new request) fades the canvas to blank paper first. Showing the control with done shows the picture straight away. + 生成階段。null(預設)時自行播放完整一遍並循環(沒有場景也沒有圖片的卡片則停在空白畫紙上等待)。每個階段播放到結尾並停在那裡,標籤保持閃爍,直到下一個階段到來;往回走(例如為新請求回到 idle)時會先把畫布淡回空白畫紙。以 done 顯示控制項時直接呈現畫面。 + + + The color of the pencil lines, pen tip, stage icon and progress line. + 鉛筆線條、筆尖、階段圖示與進度線的顏色。 + + + Every word `SketchToImage` shows or says, in English by default. Replace the whole object to localize or reword; the percent, the canvas name and the stage announcement are format functions. + `SketchToImage` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;百分比、畫布名稱與階段播報是格式化函式。 + + + One region of a `SketchToImageScene`: shapes the brush sweeps in together through one rectangle. + `SketchToImageScene` 中的一個區域:畫筆在同一個矩形內一起掃入的若干形狀。 + + + What a `SketchToImage` draws and the picture it resolves into, on a 600 × 400 canvas: the pencil `Lines` drawn in order, the `Regions` a brush paints back to front, the point the warm light comes from, and the finished picture. Trace the lines and regions from your picture so the sketch matches it. + `SketchToImage` 在 600 × 400 畫布上繪製的內容及其最終呈現的畫面:依序繪製的鉛筆 `Lines`、畫筆由後往前上色的 `Regions`、暖光的來源位置,以及完成的畫面。依你的畫面描出線條與區域,讓草圖與之吻合。 + + + One shape of a `SketchToImageRegion`. + `SketchToImageRegion` 中的一個形狀。 + Arc slider: a value or a range on a track. The thumb follows the pointer 1:1, rubber-bands up to 9px past its limits and settles on the step grid with the velocity of the release (on a coarse grid the release may carry it one step further); pressing the track springs the nearest thumb there. A bubble with rolling digits rides the thumb while it is dragged or keyboard focused and lingers 700ms after a release. Arrows, Shift+arrows, PageUp / PageDown and Home / End work as in a native range input; a key at a limit makes the thumb strain toward the press and spring home. Unlike `Slider`, `SmallChange` is the step the value snaps to. Set `IsRange` for two thumbs (`LowerValue` / `UpperValue`). Arc 滑桿:在軌道上選擇一個值或一個範圍。滑塊跟隨指標 1:1 移動,超出限制時會有 9px 的回彈,並以放開時的速度落到步進網格上(在粗網格上,放開時可能再多移動一步);按下軌道會使最近的滑塊彈到該處。拖曳或鍵盤聚焦時,一個帶滾動數字的氣泡會跟隨滑塊,放開後再保留 700ms。方向鍵、Shift+方向鍵、PageUp / PageDown 以及 Home / End 的行為與原生範圍輸入一致;在極限處按鍵時,滑塊會朝按壓方向拉伸,然後彈回原位。與 `Slider` 不同,`SmallChange` 是數值吸附的步長。設定 `IsRange` 可使用兩個滑塊(`LowerValue` / `UpperValue`)。 @@ -18932,6 +22912,402 @@ The order of the panels of a `SmallMultiples`. `SmallMultiples` 中面板的順序。 + + A paste-to-fill form card (Arc Smart form fill, design reference). People paste an email signature, a forwarded message or messy notes into the well; AI reads it and fills the contact form field by field. Each value fades in with a confidence dot, the text it came from lights up in the pasted text, fields below `ReviewThreshold` ask for review (and hold Save until they are checked or edited), and Undo puts the values from before the fill back. Give it your model through `Extract`: it receives the text and a cancellation token (Stop, Escape and a new paste cancel) and returns a `SmartFormFillResult` with a value, a confidence, the exact source substring and an optional note per field id. With no extractor the card uses a simulated reader (a sample's known result, a small rule-based parser for anything else) and plays a `Simulation` if you give it one: its samples behind Try sample and, when motion is full, a loop until someone touches it. `Submit` saves the form. + 貼上即填的表單卡片(Arc Smart form fill,設計參考)。使用者把電子郵件簽名、轉寄的郵件或零散筆記貼到貼上區;AI 讀取後逐一填寫聯絡人表單的欄位。每個值淡入時附上一個信心度圓點,它所來自的文字在貼上的內容中醒目標示;信心度低於 `ReviewThreshold` 的欄位需要複核(在核對或修改之前無法儲存),復原會還原填寫之前的值。透過 `Extract` 接入你的模型:它接收文字和一個取消權杖(Stop、Escape 和新的貼上會取消),並回傳一個 `SmartFormFillResult`,依欄位 id 提供值、信心度、原文子字串和選用的備註。沒有擷取器時,卡片使用模擬讀取器(範例的已知結果,其他文字由以規則為基礎的小型剖析器處理),並在你提供 `Simulation` 時播放它:Try sample 背後的範例,以及動態效果為完整時的自動循環示範,直到有人操作它。`Submit` 儲存表單。 + + + Loops paste, fill, review and undo while the card is on screen, and stops for good on the first pointer or key press inside it. Null (default) plays while a `Simulation` plays. Never plays with reduced motion. + 卡片在畫面上時循環示範貼上、填寫、複核和復原,卡片內第一次指標按下或按鍵後永久停止。為 null(預設)時,在播放 `Simulation` 時播放。減少動態效果時從不播放。 + + + Whether Undo can restore the values from before the last fill. + 復原是否能還原上一次填寫之前的值。 + + + Starting values keyed by field id, applied when the card is built and whenever this changes. + 依欄位 id 指定的初始值,在卡片建立時以及此屬性變更時套用。 + + + One line under the title; an empty string hides it. + 標題下方的一行說明;空字串會隱藏它。 + + + Reads the pasted text: receives it and a token that is cancelled by Stop, Escape, a new paste or the card leaving the tree, and returns a value per field id. Null uses the simulated reader (a sample's known result, a local parser for anything else) and plays `Simulation` when one is set. + 讀取貼上的文字:接收文字和一個權杖(Stop、Escape、新的貼上或卡片離開視覺樹時取消),並依欄位 id 回傳值。為 null 時使用模擬讀取器(範例的已知結果,其他文字由本機剖析器處理),並在設定了 `Simulation` 時播放它。 + + + Raised once every field from a read has been filled. Bubbles. + 一次讀取的所有欄位都填寫完成後觸發一次。會向上傳遞。 + + + Every other word the card shows or says. + 卡片顯示或朗讀的其他所有文字。 + + + The model's mark beside its name (a 24 × 24 stroked glyph); the sparkle by default. + 模型名稱旁的標誌(24 × 24 描邊圖示);預設為閃光圖示。 + + + The model named beside the paste area; null names the `Simulation`'s model while one plays, or none. + 貼上區旁顯示的模型名稱;為 null 時,播放 `Simulation` 期間顯示其模型,否則不顯示。 + + + Where the card is: idle, reading, filling, filled, saving, saved or failed. + 卡片目前所處的階段:閒置、讀取中、填寫中、已填寫、儲存中、已儲存或失敗。 + + + + How many filled fields still ask for review. + 仍需複核的已填寫欄位數量。 + + + Fields whose confidence is below this ask for review and hold Save until checked or edited. Default 0.8. + 信心度低於此值的欄位需要複核,在核對或修改之前無法儲存。預設 0.8。 + + + The texts behind Try sample, cycled in order. Null uses the `Simulation`'s while one plays; null or an empty list hides the button and the autoplay. + Try sample 背後的文字,依序循環。為 null 時,播放 `Simulation` 期間使用其範例;為 null 或空清單時隱藏該按鈕並關閉自動示範。 + + + Texts to play while the card has no `Extract`: the samples behind Try sample with the results a read returns, and the model to name (nothing is sent). Your own `Extract` wins over it; null stops it and leaves the empty form. + 卡片沒有 `Extract` 時要播放的文字:Try sample 背後的範例及讀取時傳回的結果,以及要顯示的模型(不會傳送任何內容)。你自己的 `Extract` 優先於它;為 null 時停止播放並留下空白表單。 + + + Saves the form with its values. The button shows a loading state while the task runs; a fault shows an error status. Null simulates a save. + 以表單的值儲存表單。工作執行期間按鈕顯示載入狀態;工作出錯時顯示錯誤狀態。為 null 時模擬儲存。 + + + The primary action's label. + 主要操作按鈕的文字。 + + + Raised after the form was saved; carries the saved values. Bubbles. + 表單儲存後觸發;附帶儲存的值。會向上傳遞。 + + + The pasted text (two-way). Setting it while the paste area shows does not start a read; call `ReadAsync`. + 貼上的文字(雙向)。在顯示貼上框時設定它不會開始讀取;請呼叫 `ReadAsync`。 + + + The card's title. + 卡片的標題。 + + + Raised after Undo restored the values from before the last fill; carries the restored values. Bubbles. + 復原還原上一次填寫之前的值後觸發;附帶還原的值。會向上傳遞。 + + + The form's values by field id (a new snapshot on every change). + 依欄位 id 提供的表單值(每次變更都是新的快照)。 + + + Raised on every change of the values, typed or filled. Bubbles. + 每次值變更時觸發,無論是輸入或填寫。會向上傳遞。 + + + One field of a `SmartFormFill`: its id (the key of values and of extraction results), its label, what it holds, a placeholder and how much of the row it takes. + `SmartFormFill` 的一個欄位:它的 id(值與擷取結果中的鍵)、標籤、所存內容的類型、預留位置提示,以及在一列中所佔的寬度。 + + + The key of the field in values and extraction results ("email"). + 欄位在值與擷取結果中的鍵("email")。 + + + What the field holds: text, an email, a phone number, a web address or a company (which adds a logo slot). + 欄位所存的內容:文字、電子郵件、電話號碼、網址或公司(公司會增加一個標誌位置)。 + + + The visible label; also the input's accessible name. + 可見的標籤;也是輸入框的無障礙名稱。 + + + The hint shown while the field is empty. + 欄位為空時顯示的提示。 + + + Half a row (default) or the full row. + 半列(預設)或整列。 + + + Every word `SmartFormFill` shows or says, in English by default (the title, the description and the submit button are properties of the control). Replace the whole object to localize or reword; counts and names are format functions. + `SmartFormFill` 顯示或朗讀的每一個文字,預設為英文(標題、說明和送出按鈕是控制項的屬性)。取代整個物件即可在地化或改寫;計數和名稱是格式化函式。 + + + A text behind the Try sample button: a short label and the text it pastes. + Try sample 按鈕背後的文字:一個簡短的標籤和它貼上的文字。 + + + Texts a `SmartFormFill` plays without an extractor, from data you give it: the samples behind Try sample, each with the result a read returns, and the model named beside the paste area. Set it as `Simulation` while the card has no `Extract`: Try sample pastes the samples in turn, a read returns the sample's result after a short wait (other text is read by a few plain rules), and unless `AutoPlay` is false the card loops paste, fill, review and undo until someone touches it. Nothing is sent. + `SmartFormFill` 在沒有擷取器時依你提供的資料播放的文字:Try sample 背後的範例(每個都附有讀取時傳回的結果),以及貼上區旁顯示的模型。在卡片沒有 `Extract` 時把它設為 `Simulation`:Try sample 依序貼上這些範例,讀取在短暫等待後傳回範例的結果(其他文字由幾條簡單規則讀取),並且除非 `AutoPlay` 為 false,卡片會循環示範貼上、填寫、複核和復原,直到有人操作它。不會傳送任何內容。 + + + One value read from the pasted text: the value, how sure the reader is, the exact text it came from and why it is unsure. + 從貼上文字中讀取的一個值:值本身、讀取器的把握程度、它來自的確切文字,以及不確定的原因。 + + + One entry in a `SocialProofToast` feed: a person or a company, what they did, and when. Feed it real, recent events from your billing or analytics backend. + `SocialProofToast` 動態中的一筆紀錄:一個人或一家公司、他們做了什麼,以及何時發生。請使用來自計費或分析後端的真實、近期事件。 + + + What they did, read after the name: "started a Pro trial". + 他們做了什麼,緊接在名稱之後讀出:"started a Pro trial"。 + + + A person (round avatar) or a company (rounded square mark). Person by default. + 個人(圓形大頭貼)或公司(圓角方塊標識)。預設為個人。 + + + Stable id, used to match a toast to its event in callbacks. + 穩定的 id,用於在回呼中把通知對應到它的事件。 + + + Shows a check badge with `VerifiedLabel`, or "Verified". + 顯示一個勾選徽章,文字為 `VerifiedLabel` 或 "Verified"。 + + + + Short context before the time, such as a city or a seat count. + 顯示在時間之前的簡短脈絡,例如城市或席次數。 + + + A first name or a company, shown in medium weight. + 名字或公司名稱,以中等字重顯示。 + + + A portrait for a person. Initials show while it loads or when it fails. + 個人的大頭照。載入期間或載入失敗時顯示姓名縮寫。 + + + When it happened. Shown as relative time: Just now, 4 min ago, 2 h ago. + 事件發生的時間。顯示為相對時間:Just now、4 min ago、2 h ago。 + + + A time written out, shown as is (it takes the place of `Time`). + 寫好的時間文字,原樣顯示(取代 `Time`)。 + + + The verified badge's label, such as "Verified purchase". Setting it also shows the badge. + 已驗證徽章的文字,例如 "Verified purchase"。設定它也會顯示徽章。 + + + A stack of social proof notifications for landing and pricing pages (Arc Social proof toast, design reference): recent trials, upgrades and verified purchases from people and companies arrive every few seconds. A new toast rises in at the front while the others step back and shrink; hovering or keyboard focus pauses the cycle and fans the stack out, and visitors can dismiss any toast. Give it the feed in `Events` (oldest first) and it shows one every `Interval`, or call `Push` to show an event the moment it happens. With no events it plays its `Simulation`, if any, and otherwise shows nothing until you push one. Inline it fills its container's width; a corner `Position` pins the stack to the window instead. + 用於到達頁與定價頁的社會認同通知堆疊(Arc Social proof toast,設計參考):來自個人與公司的最新試用、升級與已驗證購買每隔幾秒出現一則。新通知從前方升起,其他通知後退並縮小;懸停或鍵盤焦點會暫停輪播並展開堆疊,訪客可以關閉任何一則通知。在 `Events` 中傳入動態(從舊到新),它會每隔 `Interval` 顯示一則;或呼叫 `Push` 在事件發生時立即顯示。沒有事件時播放它的 `Simulation`(如果有),否則在你推送之前不顯示任何內容。內嵌時它填滿容器寬度;設定角落 `Position` 則把堆疊固定在視窗上。 + + + Raised when a visitor dismisses a toast. Bubbles. + 訪客關閉一則通知時引發。會冒泡。 + + + Fans the stack out while it is hovered or holds keyboard focus. + 堆疊被懸停或持有鍵盤焦點時將其展開。 + + + Time before the first toast. + 第一則通知出現前的時間。 + + + Time between toasts. The countdown keeps its remaining time across hover and focus pauses. + 兩則通知之間的時間。懸停與聚焦暫停期間,倒數會保留剩餘時間。 + + + Pauses the cycle from outside, for example while a modal or cookie banner is open. + 從外部暫停輪播,例如在強制回應視窗或 Cookie 橫幅開啟期間。 + + + Makes each toast a button that raises `Selected`, for example to open pricing or a customer story. + 讓每則通知成為一個引發 `Selected` 的按鈕,例如用來開啟定價頁或客戶案例。 + + + Every word the stack shows or says. + 堆疊顯示或朗讀的所有文字。 + + + Stops after this many toasts in a session, so the page stays calm. Null shows toasts for as long as the feed lasts. + 一個工作階段顯示這麼多則通知後停止,讓頁面保持安靜。為 null 時只要動態還有內容就繼續顯示。 + + + Starts the feed over when it ends. False stops after the last event. + 動態播完後從頭開始。為 false 時在最後一個事件後停止。 + + + How many toasts stack at once. Older ones fade out from the back. + 同時堆疊的通知數量。較早的通知從後方淡出。 + + + Inline in the layout (default), or pinned to a window corner 24px from the edges (16px and full width on narrow windows). + 內嵌在版面中(預設),或固定在視窗角落、距離邊緣 24px(窄視窗上為 16px 並佔滿寬度)。 + + + How many toasts already show when the stack appears, without an entrance. Useful for previews and screenshots. + 堆疊出現時已經顯示的通知數量,不播放進場動畫。適合預覽與螢幕截圖。 + + + Raised when a visitor presses a toast while `IsSelectable` is set. Bubbles. + 設定 `IsSelectable` 時,訪客按下一則通知時引發。會冒泡。 + + + Raised each time a toast appears, for impression tracking. Bubbles. + 每當一則通知出現時引發,用於曝光統計。會冒泡。 + + + A simulated feed to play while `Events` is empty, with times measured back from each pass. Null (the default) leaves the stack empty until an event is pushed. Give each stack its own simulation. + 在 `Events` 為空白時播放的模擬動態,時間從每一輪開始時往回計算。為 null(預設)時,堆疊在推送事件之前維持空白。請給每個堆疊各自的模擬執行個體。 + + + Arguments of `Shown`, `Dismissed` and `Selected`. + `Shown`、`Dismissed` 與 `Selected` 的參數。 + + + Every word `SocialProofToast` shows or says, in English by default. Replace the whole object to localize or reword; the relative times and the names that include the toast's sentence are format functions. + `SocialProofToast` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;相對時間以及包含通知句子的名稱都是格式化函式。 + + + A simulated feed for a `SocialProofToast` that has no `Events`, for demos and prototypes: the events it plays, oldest first, with their times measured back from the start of each pass. The stack plays it like a feed of its own (prefill, interval, limit and loop included). Create one per stack. + 用於展示和原型、供沒有 `Events` 的 `SocialProofToast` 使用的模擬動態:它播放的事件依從舊到新排列,時間從每一輪開始時往回計算。堆疊會像播放自己的動態一樣播放它(包括預先填入、間隔、上限與循環)。請為每個堆疊建立一個執行個體。 + + + One entry of a `SocialProofToastSimulation`: an event and how long before each pass it happened. + `SocialProofToastSimulation` 中的一則:一個事件,以及它發生在每一輪開始之前多久。 + + + A row of cited sources under an AI answer (Arc Source cards, design reference). Each card shows the site's mark, its domain, the citation number and a two-line title. While searching, cards stream into reserved slots under "Searching 3 of 7"; past `Max` the rest collapse into a stack of overlapping marks ("+4 more"). Hovering or focusing a card lifts a preview with the passage, and a `SourceCitation` in the answer highlights its card. Give it the sources in citation order in `Sources` (append as results arrive), set `Status` and, while searching, `Total`. Put the answer in `Content`: it sits between the status line and the cards, and its height springs as it streams so the cards glide down. With `Sources` null the cards show the state you set (by default "No sources found"), or play a `Simulation` you pass in. + AI 回答下方的一排引用來源(Arc Source cards,設計參考)。每張卡片顯示網站標記、網域、引用編號和兩行標題。搜尋時,卡片在 "Searching 3 of 7" 下方流入預留的位置;超過 `Max` 的部分收攏為一疊相互重疊的標記("+4 more")。懸停或聚焦卡片會浮出帶原文片段的預覽,回答中的 `SourceCitation` 會醒目標示它的卡片。依引用順序在 `Sources` 中提供來源(結果抵達時附加),設定 `Status`,搜尋時還要設定 `Total`。把回答放進 `Content`:它位於狀態列和卡片之間,串流輸出時高度以彈簧變化,卡片隨之平滑下移。`Sources` 為 null 時,卡片顯示你設定的狀態(預設為 "No sources found"),或播放你傳入的 `Simulation`。 + + + Shows Retry after a failed search and Search again when nothing was found; both raise `RetryRequested`. + 搜尋失敗後顯示 Retry,找不到結果時顯示 Search again;兩者都會引發 `RetryRequested`。 + + + The search time, shown after the count when done ("7 sources · 2.1s"). + 搜尋耗時,完成時顯示在數量之後("7 sources · 2.1s")。 + + + The detail shown after "Search failed", such as "The search provider timed out after 8s." + 顯示在 "Search failed" 之後的詳細資訊,例如 "The search provider timed out after 8s." + + + Raised when the stack, the Show all toggle or a citation opens or closes the stack. Bubbles. + 疊放、Show all 開關或引用展開或收起疊放時引發。會向上傳遞。 + + + Raised when a citation highlights its card, and when that highlight ends. Bubbles. + 引用醒目標示它的卡片時,以及該醒目標示結束時引發。會向上傳遞。 + + + The highlighted source number (1-based), or null (two-way). A citation press sets it and clears it again after 2.6s unless it has changed meanwhile; set it yourself to hold a highlight. + 醒目標示的來源編號(從 1 開始),或 null(雙向繫結)。按下引用會設定它,並在 2.6 秒後清除(除非期間已改變);自行設定它可以保持醒目標示。 + + + Whether the stack is open and every card shows (two-way). The stack, the Show all toggle and a citation that points into the stack change it and raise `ExpandedChanged`. + 疊放是否展開、所有卡片是否都顯示(雙向繫結)。疊放、Show all 開關以及指向疊放內部的引用會改變它並引發 `ExpandedChanged`。 + + + Whether the status line shows; turn it off when your UI already shows the search progress (the Show all toggle and Retry stay). + 是否顯示狀態列;介面已顯示搜尋進度時可將其關閉(Show all 開關和 Retry 仍保留)。 + + + Every word the cards and their citations show or say. + 卡片及其引用顯示或朗讀的每一個詞。 + + + Cards shown before the rest collapse into the stack. The grid has Max + 1 columns on wide containers and two below 540px. + 收攏進疊放之前顯示的卡片數。寬容器中網格有 Max + 1 欄,窄於 540px 時為兩欄。 + + + Whether pressing a card opens its address in the browser when no `SourceInvoked` handler marks the event handled. Cards say so in their name. + 當沒有 `SourceInvoked` 處理常式把事件標記為已處理時,按下卡片是否在瀏覽器中開啟其網址。卡片的名稱會說明這一點。 + + + The search query, shown muted after the status while searching or when nothing was found. + 搜尋查詢,搜尋時或找不到結果時以淡化樣式顯示在狀態之後。 + + + Raised when the person presses Retry or Search again. Bubbles. + 使用者按下 Retry 或 Search again 時引發。會向上傳遞。 + + + A simulated search to play while `Sources` is null (demos and prototypes): its sources stream in and its answer streams with citations. Simulated cards open no address; null shows the cards' own state. + 沒有 `Sources` 時要播放的模擬搜尋(展示與原型):它的來源依序流入,回答帶著引用串流出現。模擬的卡片不會開啟任何網址;為 null 時顯示卡片自己的狀態。 + + + Raised when a card is pressed, before its address opens. Bubbles. Mark it handled to open the source in your own reader instead. + 按下卡片時、在其網址開啟之前引發。會向上傳遞。將其標記為已處理即可改在自己的閱讀器中開啟來源。 + + + The sources in citation order (source 1 is cited as [1]). Append as results arrive; each new card fades into its slot. An observable list is followed as it changes. While null, a `Simulation` plays. + 依引用順序排列的來源(第 1 個來源以 [1] 引用)。結果抵達時附加;每張新卡片淡入自己的位置。可觀察清單的變化會被跟隨。為 null 時播放 `Simulation`。 + + + Searching shows a spinner, a shimmering "Searching", a rolling count and reserved slots; Done shows "7 sources" and the time ("No sources found" with none); Error keeps what arrived and shows "Search failed". + Searching 顯示載入圈、閃爍的 "Searching"、捲動的數量和預留的位置;Done 顯示 "7 sources" 和耗時(沒有來源時為 "No sources found");Error 保留已抵達的結果並顯示 "Search failed"。 + + + How many sources the search will return. Drives "Searching 3 of 7" and reserves that many slots, so the row never shifts while it fills. + 搜尋將傳回的來源數量。它驅動 "Searching 3 of 7" 並預留同樣多的位置,因此填入過程中這一排不會跳動。 + + + Every word `SourceCards` and `SourceCitation` show or say, in English by default. Replace the whole object to localize or reword; counts and names are format functions. + `SourceCards` 和 `SourceCitation` 顯示或朗讀的每一個詞,預設為英文。替換整個物件即可在地化或改寫;數量與名稱是格式化函式。 + + + A simulated search for a `SourceCards`, for demos, prototypes and tests. In the `Live` scenario the `Sources` you pass stream into their slots (the first after 0.45s, then one every 0.33s), the search finishes, the `Answer` streams a word every 70ms with its [n] citations, the cards tour two citations, close the stack and start over, looping until someone interacts. In `Error` the search fails after two sources with `ErrorText`; in `Empty` it finds nothing for `EmptyQuery`. Nothing is searched, and simulated cards open no address. Set it as the cards' `Simulation`: it plays while `Sources` is null and only while the cards are on screen, and starts over when its `Scenario` changes or the person presses Retry or Search again. The other values take effect at the next start. Use one simulation per `SourceCards`. + 為 `SourceCards` 提供的模擬搜尋,用於展示、原型和測試。在 `Live` 情境中,你傳入的 `Sources` 依序流入各自的位置(第一個在 0.45s 後,之後每 0.33s 一個),搜尋完成,`Answer` 每 70ms 流出一個詞並帶有 [n] 引用,卡片依序醒目標示兩個引用,收起疊放後從頭開始,一直循環到有人操作為止。在 `Error` 中,搜尋在兩個來源之後以 `ErrorText` 失敗;在 `Empty` 中,它為 `EmptyQuery` 什麼也找不到。不會進行任何搜尋,模擬的卡片也不會開啟任何網址。把它設為卡片的 `Simulation`:它在 `Sources` 為 null 且卡片顯示在畫面上時播放,在 `Scenario` 改變或使用者按下重試、重新搜尋時從頭開始。其他值在下次開始時生效。每個 `SourceCards` 使用一個模擬。 + + + The answer the live scenario streams, words separated by spaces; a word written as `[n]` (optionally followed by punctuation, as in `[3].`) becomes a `SourceCitation` of source n. + 即時情境串流輸出的回答,詞與詞之間用空格分隔;寫成 `[n]` 的詞(後面可以帶標點,如 `[3].`)會變成來源 n 的 `SourceCitation`。 + + + Raised when the simulated search changes: a source arrives, the status changes or a word of the answer streams. + 模擬搜尋變化時引發:一個來源抵達、狀態改變,或回答流出一個詞。 + + + The search time the live scenario reports when it finishes ("6 sources · 2.1s"). + 即時情境完成時回報的搜尋用時("6 sources · 2.1s")。 + + + The query of the empty scenario, one that finds nothing; `Query` when not set. + 空結果情境的查詢,一個什麼也找不到的查詢;未設定時使用 `Query`。 + + + The detail the error scenario shows after "Search failed". + 錯誤情境在 "Search failed" 之後顯示的詳情。 + + + The query shown while searching. + 搜尋時顯示的查詢。 + + + Which story to tell: a live search with a cited answer, a failed search or an empty one. + 要呈現的情境:帶引用回答的即時搜尋、失敗的搜尋或空結果的搜尋。 + + + The sources the search finds, in citation order (source 1 is cited as [1]). + 搜尋找到的來源,依引用順序排列(第 1 個來源以 [1] 引用)。 + + + One source of a `SourceCards`: a page the answer cites. Sources are numbered by their place in `Sources`, so the first is cited as [1]. + `SourceCards` 的一個來源:回答引用的一個網頁。來源依其在 `Sources` 中的位置編號,因此第一個以 [1] 引用。 + + + An inline [n] chip for answer text, styled like the number on each card of a `SourceCards`. Inside the cards' content it names its source for screen readers ("Source 2: Request: text() method, webref.example"), turns accent while its card is highlighted, and pressing it highlights the card (opening the stack when the card is in it). Its hit area reaches a few pixels past the chip without pushing the text around it. Put it in a text block with `InlineUIContainer`; the regular `Click` event runs first, for example to scroll the cards into view. + 回答文字中的行內 [n] 標籤,樣式與 `SourceCards` 每張卡片上的編號相同。在卡片的內容中,它為螢幕閱讀器說出自己的來源("Source 2: Request: text() method, webref.example"),在其卡片醒目標示時變為強調色,按下它會醒目標示該卡片(卡片在疊放中時會先展開)。它的點擊區域比標籤多出幾個像素,但不會推開周圍的文字。用 `InlineUIContainer` 把它放進文字區塊;一般的 `Click` 事件先執行,例如用來把卡片捲動到可見範圍內。 + + + The 1-based number of the source it cites. + 它所引用的來源編號(從 1 開始)。 + + + The number as the chip shows it. + 標籤上顯示的編號。 + Arc's sparkline: a compact trend beside a value, for cards, table rows and KPI blocks. Scrub it with the pointer or the arrow keys: the cursor springs between points, the stretch after it dims as if it has not happened yet, and the headline and aside roll to the scrubbed point. The line draws once like a pen the first time it is half visible; the fill follows the tip, then the end dot lands. New data morphs the line from the shape on screen. Motion is code-driven because the pen tip, cursor and morph are coordinates inside the self-drawn plot. Arc 迷你趨勢圖:緊接數值旁的精簡趨勢圖,適用於卡片、表格列與 KPI 區塊。可用指標或方向鍵拖曳瀏覽:游標在各資料點之間彈動,其後的區段會變暗,彷彿尚未發生;標題與附註會滾動到所瀏覽的資料點。首次半數可見時,折線會像畫筆一樣繪製一次;填滿區隨筆尖跟進,然後末端圓點落定。新資料會從畫面上的形狀變形為新折線。由於筆尖、游標與變形都是自繪繪圖區內的座標,因此動態效果由程式碼驅動。 @@ -19060,6 +23436,162 @@ Every word `SpeculativeDecoding` shows or announces, apart from the title, the lane names (`DraftLabel` and the others) and the playback buttons (`PlaybackLabels`). English by default; replace the instance to translate or reword. Texts with numbers are functions so that word order can change. `SpeculativeDecoding` 顯示或朗讀的每個文字,標題、泳道名稱(`DraftLabel` 等)和播放按鈕(`PlaybackLabels`)除外。預設為英文;替換該執行個體即可翻譯或改寫。含數字的文字使用函式,以便調整詞序。 + + What your text to speech service returned: the playback, the word timings and the length. Each part is optional: without `Audio` the player keeps time itself (simulated, no sound); without `Timings` they are estimated from the text and fitted to the length. + 文字轉語音服務傳回的內容:播放、逐字時間與總長度。每個部分都是選用的:沒有 `Audio` 時播放器自行計時(模擬,無聲音);沒有 `Timings` 時會根據文字估算,並依總長度伸縮對齊。 + + + Arguments of the `SpeechPlayer` playback events: where playback is, with which voice and speed. + `SpeechPlayer` 播放事件的引數:播放到哪裡、使用哪個聲音與語速。 + + + Read aloud for an AI reply (Arc Speech player, design reference). The reply sits above an action row with Read aloud, Copy, Good response and Bad response. Read aloud crossfades the row into a compact player of the same height: play and pause, the elapsed time, a thin scrubbable progress bar, the total time, a speed button (1×, 1.5×, 2×), a voice picker and Stop reading. As it plays, one rounded highlight glides from word to word along a line and cuts to the next line, while the words still to come stay dimmed; a click on a word jumps there. Give it the reply in `Text` and your text to speech service in `Synthesize`: it receives a `SpeechRequest` (abort when its token is cancelled) and returns a `SpeechAudio` with the playback (`ISpeechAudio`, whose position becomes the player's clock), the word timings and the length. Without `Synthesize` the player is a timed simulation that plays no sound. With no text, a `Simulation` is read once the player is in view and loops through two voices until someone interacts; with neither, the action row waits with Read aloud disabled. + 為 AI 回覆朗讀(Arc Speech player,設計參考)。回覆下方是含有 Read aloud、Copy、Good response 與 Bad response 的操作列。Read aloud 會把這一列交叉淡化為同樣高度的精簡播放器:播放與暫停、已播放時間、可拖曳的細進度列、總長度、語速按鈕(1×、1.5×、2×)、聲音選擇與 Stop reading。播放時,一個圓角醒目標示會在一行內逐字滑動,到下一行時直接切換,還沒唸到的字詞維持淡化;點選一個字詞即跳到那裡。把回覆放在 `Text` 中,把文字轉語音服務放在 `Synthesize` 中:它會收到一個 `SpeechRequest`(權杖取消時中止),並傳回一個 `SpeechAudio`,包含播放(`ISpeechAudio`,其位置成為播放器的時鐘)、逐字時間與總長度。沒有 `Synthesize` 時,播放器是不發出聲音的定時模擬。沒有文字時,會在播放器進入可視範圍後朗讀 `Simulation`,並在兩個聲音之間循環,直到有人操作;兩者都沒有時,操作列等待,Read aloud 維持停用。 + + + Buttons after Read aloud in the action row, in place of the default Copy, Good response and Bad response. + 操作列中 Read aloud 之後的按鈕,取代預設的 Copy、Good response 與 Bad response。 + + + The last sentence said through the polite status region. + 透過禮貌模式狀態區域播報的最後一句話。 + + + The author's mark: a 24 × 24 stroked glyph drawn at 16px (the sparkle by default). + 作者標誌:一個 24 × 24 的描邊圖形,以 16px 繪製(預設為閃光圖示)。 + + + The author mark's color; the accent by default. + 作者標誌的顏色;預設為強調色。 + + + Who wrote the reply, shown above it with a 16px mark. Null shows no author line (a playing simulation shows its own). + 回覆的作者,搭配 16px 標誌顯示在回覆上方。為 null 時不顯示作者列(正在播放的模擬會顯示它自己的作者)。 + + + Starts reading once the player is in view. Null: on while a `Simulation` plays, off when `Text` is set. + 播放器進入可視範圍後開始朗讀。為 null 時:播放 `Simulation` 時開啟,設定了 `Text` 時關閉。 + + + Stop reading was pressed and the action row returned. Fetched speech is kept, so Read aloud starts again at once. Bubbles. + 按下了 Stop reading,操作列已恢復。已取得的語音會保留,因此 Read aloud 會立即重新開始。會冒泡。 + + + The index of the word being spoken (−1 before the first). + 目前朗讀字詞的索引(第一個字詞之前為 −1)。 + + + The last word finished. Bubbles. + 最後一個字詞已唸完。會冒泡。 + + + Whether the player shows in place of the action row. + 播放器是否顯示在操作列的位置上。 + + + Notes in the voice menu and the announcements that no audio plays. Null: on when `Synthesize` is not set. + 在聲音選單與播報中註明不會播放聲音。為 null 時:未設定 `Synthesize` 時開啟。 + + + Every word the player shows or says. + 播放器顯示或播報的每一個字詞。 + + + Playback paused. Bubbles. + 播放已暫停。會冒泡。 + + + Playback started or resumed. Bubbles. + 播放已開始或繼續。會冒泡。 + + + A scrub, an arrow key or a click on a word moved playback. Raised once when a drag ends, not on every move. Bubbles. + 拖曳、方向鍵或點選某個字詞移動了播放位置。拖曳結束時觸發一次,而不是每次移動都觸發。會冒泡。 + + + The selected voice's id (two-way). Null or an unknown id uses the first voice. A new voice while reading resumes from the word being spoken. + 所選聲音的 id(雙向)。為 null 或未知的 id 時使用第一個聲音。朗讀中換用新聲音時,會從目前唸到的字詞繼續。 + + + Shows Copy, Good response and Bad response after Read aloud when `Actions` is null (feedback stays local and is announced as simulated). False shows Read aloud alone. + 當 `Actions` 為 null 時,在 Read aloud 之後顯示 Copy、Good response 與 Bad response(回饋只在本機,並以模擬方式播報)。為 False 時只顯示 Read aloud。 + + + A self-playing reading to show while `Text` is null: its reply, author and voices, read once the player is in view and looped through two voices until someone interacts. Null (the default) leaves the player empty. Give each player its own simulation. + 在 `Text` 為 null 時顯示的自動朗讀:它的回覆、作者與聲音,在播放器進入可視範圍後朗讀,並在兩個聲音之間循環,直到有人操作。為 null(預設)時播放器為空白。請給每個播放器各自的模擬執行個體。 + + + The playback speed (two-way). The speed button cycles 1×, 1.5×, 2×. + 播放語速(雙向)。語速按鈕會在 1×、1.5×、2× 之間循環。 + + + The speed changed. Bubbles. + 語速已變更。會冒泡。 + + + Where the player is: the action row, preparing, playing, paused, finished or failed. + 播放器所處的狀態:操作列、準備中、播放中、已暫停、已結束或失敗。 + + + Fetches speech from your text to speech service. Return the playback, the word timings (one start and end per word, from the provider's alignment data) and the length; missing timings are estimated and fitted to the length. Throw with a short, human message to show it next to "Couldn't generate audio". Null: a simulation with no audio. + 從你的文字轉語音服務取得語音。傳回播放、逐字時間(每個字詞一組開始與結束時間,來自服務供應商的對齊資料)與總長度;缺少的逐字時間會被估算並依總長度伸縮對齊。擲回帶有簡短、易懂訊息的例外,即可顯示在 "Couldn't generate audio" 旁邊。為 null 時:不發出聲音的模擬。 + + + The reply to read. Blank lines start new paragraphs; each whitespace separated word is one highlight step. Null reads the `Simulation`, if any. + 要朗讀的回覆。空行開始新段落;每個以空白分隔的字詞是一個標示步驟。為 null 時朗讀 `Simulation`(如果有)。 + + + A bordered card, or plain to sit inside a chat column. + 有邊框的卡片,或融入聊天欄的 plain 樣式。 + + + A voice was picked. Bubbles. + 選擇了一個聲音。會冒泡。 + + + The voices in the picker. Null or empty offers the playing simulation's voices, else one default voice with an empty id (your service's default). + 聲音選擇中的聲音。為 null 或空白時提供正在播放的模擬的聲音,否則提供一個 id 為空的預設聲音(你的服務的預設聲音)。 + + + The spoken word changed; use it to sync a caption or analytics. Bubbles. + 目前朗讀的字詞已變更;可用於同步字幕或分析。會冒泡。 + + + Every word `SpeechPlayer` shows or says, in English by default. Replace the whole object to localize or reword; sentences that carry a value are format functions. + `SpeechPlayer` 顯示或播報的每一個字詞,預設為英文。替換整個物件即可在地化或改寫;帶有數值的句子是格式化函式。 + + + What a `SpeechPlayer` asks your text to speech service for. + `SpeechPlayer` 向你的文字轉語音服務請求的內容。 + + + A self-playing reading for the `Simulation` of a `SpeechPlayer`, for demos and prototypes: the reply to read, its author and the voices to offer. A player that has no `Text` reads it once it is in view and, with full motion, reads it again with the other of the first two voices after a short pause, until someone interacts. It is also the stand-in text to speech behind every player without `Synthesize`: `SynthesizeAsync` waits like a network round trip, then answers with estimated word timings and no audio. Nothing is sent anywhere. `EstimateTimings` and `FitTimings` are what the player uses when a real service returns audio without alignment data. + 用於展示和原型的 `SpeechPlayer` 的 `Simulation` 自動朗讀:要朗讀的回覆、它的作者與提供的聲音。沒有 `Text` 的播放器在進入可視範圍後朗讀它,在完整動效下,短暫停頓後會改用前兩個聲音中的另一個再唸一次,直到有人操作。它也是所有未設定 `Synthesize` 的播放器背後的替身文字轉語音:`SynthesizeAsync` 像一次網路往返那樣等待,然後傳回估算的逐字時間,不含音訊。不會向任何地方傳送內容。真實服務傳回的音訊沒有對齊資料時,播放器使用的就是 `EstimateTimings` 與 `FitTimings`。 + + + Options of `SynthesizeAsync`. + `SynthesizeAsync` 的選項。 + + + Arguments of `SpeedChanged`. + `SpeedChanged` 的引數。 + + + One voice in the `SpeechPlayer` voice picker. + `SpeechPlayer` 聲音選擇中的一個聲音。 + + + Arguments of `VoiceChanged`. + `VoiceChanged` 的引數。 + + + Arguments of `WordChanged`: the playback event plus the word now spoken. + `WordChanged` 的引數:播放事件加上目前朗讀的字詞。 + + + When one word of the text is spoken, on the 1× timeline. + 文字中一個字詞在 1× 時間軸上的朗讀時間。 + One default action joined to a chevron menu of nearby alternatives (Arc split button): Merge pull request with Squash and merge or Rebase and merge, an export with its usual format, Copy page that answers Copied in place. The whole pill presses in for the main action; the chevron half anchors the menu, so it answers in color only and turns while the menu is open. The main label morphs letter by letter (shared runs glide, changed letters rise out of a soft blur), a new `Icon` grows in while the old one shrinks away, and the main half springs to the new width. Menu rows are `MenuEntry` items declared inline or supplied with `ItemsSource`; the menu opens 4px below the chevron, aligned to the end. Keyboard: Enter or Space runs the main action on the main half and opens the menu on the first row from the chevron (ArrowDown too); arrow keys move through the rows and loop, Escape closes the menu and returns focus to the chevron. Label changes are announced politely. 將一個預設動作與一個相近替代項的下拉選單(Arc 分割按鈕)合併:例如「合併拉取要求」帶有「Squash 並合併」或「Rebase 並合併」,匯出操作帶有其常用格式,「複製頁面」操作則會就地顯示「已複製」。主操作時整個膠囊形按鈕會按下;下拉箭頭部分用於錨定選單,因此僅以顏色作出回應,並在選單開啟時旋轉。主標籤逐字母變形(相同的部分平滑滑動,變化的字母從柔和的模糊中升起),新的 `Icon` 會淡入而舊圖示縮小消失,主區域則以彈簧過渡到新寬度。選單行為內嵌宣告的 `MenuEntry` 項目,或由 `ItemsSource` 提供;選單在箭頭下方 4px 處開啟,並靠末端對齊。鍵盤:Enter 或 Space 在主區域執行主操作,從箭頭處開啟選單並定位到第一行(ArrowDown 同樣可以);方向鍵在各行之間移動並循環,Escape 關閉選單並將焦點返回箭頭。標籤變化時以禮貌方式朗讀。 @@ -19104,9 +23636,97 @@ Accessible name of the chevron half; null uses "{Label} more actions". 箭頭部分的無障礙名稱;為 null 時使用「{Label} 更多操作」。 - - Visual weight: Primary (default, a filled button) or Secondary (a compact outlined pill). - 視覺權重:Primary(預設,實心按鈕)或 Secondary(緊湊的描邊膠囊)。 + + Visual weight: Primary (default, a filled button) or Secondary (a compact outlined pill). + 視覺權重:Primary(預設,實心按鈕)或 Secondary(緊湊的描邊膠囊)。 + + + One starter card of `StarterPrompts`: a short verb-first `Title`, an optional quiet `Description`, and the full `Prompt` that is placed in the composer when the card is picked. + `StarterPrompts` 的一張起始卡片:簡短、以動詞開頭的 `Title`,選用的安靜說明 `Description`,以及選擇卡片時填入輸入框的完整 `Prompt`。 + + + A connected app a `StarterPrompt` uses, such as a mail, code or calendar integration. Its mark shows in a tinted square on the card and its name reads beside it and in the card's accessible name. The presets are neutral Lucide glyphs with plain names; pass your own `Mark` (a 24 × 24 stroked geometry) and `Name` for a real integration. + `StarterPrompt` 使用的已連結應用程式,例如郵件、程式碼或行事曆整合。它的標誌顯示在卡片上一個淺色底的方塊裡,名稱顯示在旁邊,也會讀入卡片的無障礙名稱。預設值是名稱樸素的中性 Lucide 圖示;真實的整合請傳入你自己的 `Mark`(24 × 24 描邊幾何圖形)和 `Name`。 + + + A tab of `StarterPrompts`: an id the starters refer to, a label and an optional 24 × 24 stroked icon. + `StarterPrompts` 的一個分頁:起始提示參照的 id、一個標籤和選用的 24 × 24 描邊圖示。 + + + Arguments of `PromptSelected`: the starter that was picked. + `PromptSelected` 的引數:被選擇的起始提示。 + + + The empty state of an AI app (Arc Starter prompts, design reference): a greeting, starter cards filtered by category tabs, a Shuffle that swaps the cards with a short staggered crossfade, and a composer the cards fill. Picking a card places its full prompt in the composer, focuses it and raises `PromptSelected`; Enter sends and raises `SubmitRequested`. Cards that use a connected app show its mark and name. With no `Prompts` it shows the greeting, the All tab and the composer with no cards, and with no handler that supplies `Sending` it simulates the send. Set `ShowsComposer` to false to pair the cards with a composer of your own: picks still set `Text` and raise the event. + AI 應用程式的空狀態(Arc Starter prompts,設計參考):問候語、依分類分頁篩選的起始卡片、以短暫錯開的交叉淡化切換卡片的「換一批」,以及由卡片填入的輸入框。選擇卡片會把它完整的提示詞填入輸入框、讓輸入框取得焦點並引發 `PromptSelected`;按 Enter 傳送並引發 `SubmitRequested`。使用已連結應用程式的卡片會顯示該應用程式的標誌與名稱。沒有 `Prompts` 時顯示問候語、「全部」分頁和輸入框,但沒有卡片;沒有處理常式提供 `Sending` 時模擬傳送。將 `ShowsComposer` 設為 false,可以把卡片與你自己的輸入框搭配:選擇卡片仍會設定 `Text` 並引發事件。 + + + Cards on screen at once. The grid keeps this many slots so nothing jumps. + 同時顯示在畫面上的卡片數。格線始終保留這麼多個位置,內容不會跳動。 + + + One quiet line under the heading; null or empty hides it. + 標題下方的一行安靜說明;為 null 或空白時隱藏。 + + + The heading. + 標題。 + + + Shows placeholder cards with the same size and rhythm while the starters load. + 起始提示載入期間顯示尺寸與節奏相同的預留位置卡片。 + + + Every word the control shows or says, beyond the greeting, description and placeholder. + 控制項顯示或朗讀的所有文字(問候語、說明和預留位置文字除外)。 + + + The model's mark beside its name (a 24 × 24 stroked glyph); the sparkle by default, null hides it. + 模型名稱旁的標誌(24 × 24 描邊圖示);預設為閃光圖示,為 null 時隱藏。 + + + The model named in the composer's footer; null (the default) shows the mark alone. + 輸入框頁尾中顯示的模型名稱;為 null(預設)時只顯示標誌。 + + + The composer's placeholder; null (the default) shows none. + 輸入框的預留位置文字;為 null(預設)時不顯示。 + + + Raised when a card is picked, after its prompt fills the composer. Bubbles. + 選擇卡片、其提示詞填入輸入框之後引發。會向上傳遞。 + + + The id of the selected tab (two-way): `"all"` or a `Id`. + 所選分頁的 id(雙向):`"all"` 或某個 `Id`。 + + + The message of the last failed send, or null. + 最近一次傳送失敗的訊息,或 null。 + + + Where the composer is with its message: idle, sending, sent, stopped or failed. + 輸入框中訊息的狀態:閒置、傳送中、已傳送、已停止或失敗。 + + + Shows the built-in composer. Turn it off to pair the cards with a composer of your own. + 顯示內建的輸入框。關閉後可以把卡片與你自己的輸入框搭配。 + + + Raised when the composer sends (Enter or the send button). Bubbles. Put your request's task in `Sending` and honor its cancellation token; without it the send is simulated. + 輸入框傳送時引發(Enter 或傳送按鈕)。會向上傳遞。把請求的工作放進 `Sending` 並遵守其取消權杖;不設定時為模擬傳送。 + + + The composer's text (two-way); changes from typing and from a picked card. + 輸入框的文字(雙向);隨輸入和所選卡片變化。 + + + Every word `StarterPrompts` shows or says, in English by default (the greeting, description and placeholder are properties of the control). Replace the whole object to localize or reword; sentences that carry a value are format functions. + `StarterPrompts` 顯示或朗讀的所有文字,預設為英文(問候語、說明和預留位置文字是控制項的屬性)。替換整個物件即可在地化或改寫;帶有數值的句子是格式化函式。 + + + Arguments of `SubmitRequested`: the message to send. Start your request and put its task in `Sending`; the composer shows Sending until it completes, the error state with Retry if it throws, and Stop cancels `CancellationToken`. Leave `Sending` null and the send is simulated. + `SubmitRequested` 的引數:要傳送的訊息。發出請求並把它的工作放進 `Sending`;工作完成前輸入框顯示傳送中,擲回例外時顯示附「重試」的錯誤狀態,停止會取消 `CancellationToken`。`Sending` 留空則為模擬傳送。 Shows where someone is in a strict multi-step flow and which steps are done (Arc Stepper). Drive it with `CurrentIndex` (the step count marks the whole flow complete). When progress jumps across several steps the connectors fill one after another (80ms apart) from the previous current step, then the halo grows around the new current step; numbers turn into drawn checks or alerts. With `IsNavigable` completed steps can be chosen by click or keyboard (arrows between reachable steps, Home / End, Enter / Space). Horizontal steppers narrower than 30rem fold into markers with the current step named underneath. Steps come from `ItemsSource` or the inline `Steps`. @@ -19172,6 +23792,22 @@ Step name, shown beside its marker. 步驟名稱,顯示在其標記旁。 + + A surface of stickers (Arc Sticker peel, design reference). Hovering lifts the nearest corner a few pixels, pulling folds the sticker back along a real fold with its paper backing showing, pulling far enough peels it off to carry around, and letting go sticks it down where it was dropped. It works with the keyboard and with touch, and `PositionsChanged` gives the layout to save. + 一面貼紙表面(Arc 貼紙撕起,設計參考)。懸停會把最近的一角翹起幾個像素,拉動會沿真實的摺線把貼紙折回並露出紙質背面,拉得夠遠就會整張撕下並可以拿著走,放手後貼紙貼在放下的位置。支援鍵盤和觸控,`PositionsChanged` 給出可儲存的版面。 + + + Accessible name of the surface. + 表面的無障礙名稱。 + + + Controlled placements by sticker id. Without it (or for ids it leaves out) the stickers start where their items say. + 依貼紙 id 受控的位置。沒有它(或它未包含的 id)時,貼紙從各自的項目所設的位置開始。 + + + Raised once a sticker has settled somewhere new, with every placement. + 貼紙在新位置落定後觸發一次,帶上每張貼紙的位置。 + Arc's streamgraph: layers stacked on a wiggle baseline, flowing over time. Hovering a layer isolates it and reads every layer at that moment; switching ranges blends every layer from the shape on screen on a shared 161-point grid, a hidden layer thins to nothing while the others close over it, and the first time the chart is seen the whole stream swells out of a hairline along its center. Motion is code-driven (a per-sample spring array and `MotionValue`) because every layer edge is recomputed from the stack each frame inside one self-drawn plot. Arc 的串流圖:各圖層堆疊在擺動基線上,隨時間流動。懸停某一圖層會將其單獨突出,並讀取該時刻每個圖層的數值;切換範圍時,所有圖層會從螢幕上的形狀出發,在共用的 161 點網格上過渡融合,隱藏的圖層會收縮至消失,其餘圖層隨之閉合;圖表首次出現時,整個串流從中線處的細線舒展開。動畫由程式碼驅動(每個取樣點的彈簧陣列加 `MotionValue`),因為每個圖層邊緣都要在單一自繪繪圖區內逐格根據堆疊結果重新計算。 @@ -19848,6 +24484,146 @@ What `SendInvitesAction` is asked to send. `SendInvitesAction` 被要求傳送的內容。 + + A workspace switcher for multi-tenant apps (Arc Team switcher, design reference). The trigger shows the current workspace's tile, its name and its plan; the menu under it has a search field, your role in each workspace and a Cmd/Ctrl+1 to 9 shortcut for each of the first nine, and lets you create or join a workspace inline. One highlight glides between rows, and switching crossfades the trigger to the new workspace through a short loading state. Give it your workspaces in `Workspaces` and the active id in `Value` (two-way). Loading a workspace, creating one and joining one are your async delegates (`SwitchHandler`, `CreateHandler`, `JoinHandler`); each one left out is simulated. With no workspaces, a `Simulation` supplies demo workspaces and the invites its simulated join accepts; with neither, the trigger says there is no workspace. The root is up to 360px wide and fills narrower containers. + 面向多租戶應用程式的工作區切換器(Arc Team switcher,設計參考)。觸發器顯示目前工作區的圖塊、名稱與方案;其下的選單有搜尋欄位、你在每個工作區中的角色,並為前九個工作區各提供一個 Cmd/Ctrl+1 到 9 的快速鍵,還能就地建立或加入工作區。一個醒目提示在列之間滑動,切換時觸發器會經過短暫的載入狀態交叉淡化到新的工作區。把你的工作區放進 `Workspaces`,把目前工作區的 id 放進 `Value`(雙向)。載入、建立與加入工作區都由你的非同步委派完成(`SwitchHandler`、`CreateHandler`、`JoinHandler`);未提供的委派會被模擬。沒有工作區時,`Simulation` 提供展示用的工作區以及模擬加入所接受的邀請;兩者都沒有時,觸發器顯示沒有工作區。根元素最寬 360px,在更窄的容器中填滿寬度。 + + + Creates a workspace from its name. Return it to add it and switch to it (or null to only close the menu); throw to show the exception's message under the field. Null simulates it. + 依名稱建立工作區。傳回它即可加入並切換過去(傳回 null 則只關閉選單);擲回例外會在欄位下方顯示例外訊息。為 null 時進行模擬。 + + + Whether the menu is open (two-way). Setting it opens the menu without moving focus. + 選單是否開啟(雙向)。設定它會開啟選單,但不移動焦點。 + + + Joins a workspace from an invite link or code. Return the workspace to add it and switch to it; throw to show the exception's message. Null simulates it: it accepts the invites of `Simulation`, if any. + 透過邀請連結或邀請碼加入工作區。傳回該工作區即可加入並切換過去;擲回例外會顯示例外訊息。為 null 時進行模擬:接受 `Simulation` 中的邀請(如果有)。 + + + Every word the switcher shows or says. + 切換器顯示或朗讀的所有文字。 + + + Raised when the menu opens or closes. Bubbles. + 選單開啟或關閉時引發。會反昇。 + + + How long the simulated switch, create and join take when no delegate is set. + 未設定委派時,模擬的切換、建立與加入所需的時間。 + + + Demo workspaces to show while `Workspaces` is empty, with the invites the simulated join accepts and what the simulated create makes. Null (the default) shows no workspace. Give each switcher its own simulation. + 在 `Workspaces` 為空白時顯示的展示工作區,以及模擬加入所接受的邀請與模擬建立的內容。為 null(預設)時不顯示工作區。請給每個切換器各自的模擬執行個體。 + + + Raised when `SwitchHandler` threw: the trigger went back to the previous workspace. Bubbles. + `SwitchHandler` 擲回例外時引發:觸發器已回到先前的工作區。會反昇。 + + + Loads a workspace. The trigger crossfades to it with a switching state until the task finishes; an exception reverts and shows its message. A newer switch cancels the token of the older one. Null simulates a load of `SimulatedDelay`. + 載入工作區。在工作完成之前,觸發器以切換狀態交叉淡化到該工作區;發生例外時恢復原狀並顯示例外訊息。較新的切換會取消較早切換的權杖。為 null 時模擬一次耗時 `SimulatedDelay` 的載入。 + + + Listens for Cmd/Ctrl+1 to 9 anywhere in the window. Off, the shortcuts work while focus is inside the switcher. + 在視窗的任何位置監聽 Cmd/Ctrl+1 到 9。關閉時,只有焦點在切換器內部時快速鍵才有效。 + + + The id of the active workspace (two-way). Null starts on the first workspace. + 目前工作區的 id(雙向)。為 null 時從第一個工作區開始。 + + + Raised once a switch has finished loading (and `Value` has changed). Bubbles. + 切換載入完成(且 `Value` 已改變)後引發。會反昇。 + + + Raised when someone created or joined a workspace from the menu. Add it to your list too; the switcher already shows it (de-duplicated by id) and switches to it. Bubbles. + 有人從選單建立或加入工作區時引發。也請把它加入你的清單;切換器已經顯示它(依 id 去除重複)並切換過去。會反昇。 + + + How a workspace joined the list of a `TeamSwitcher`. + 工作區是如何加入 `TeamSwitcher` 清單的。 + + + Every word `TeamSwitcher` shows or says, in English by default. Replace the whole object to localize or reword; texts with values in them are format functions. + `TeamSwitcher` 顯示或朗讀的所有文字,預設為英文。取代整個物件即可在地化或改寫;包含數值的文字是格式化函式。 + + + Arguments of `OpenChanged`. + `OpenChanged` 的引數。 + + + Simulated workspaces for a `TeamSwitcher` that has no `Workspaces`, for demos and prototypes: the list it shows, the invites its simulated join accepts (the join view notes them) and what its simulated create makes. The app's own workspaces set it aside entirely. Switching, creating and joining without the app's delegates take `SimulatedDelay` and are announced as simulated; nothing is sent. Create one per switcher: workspaces created or joined are added to that switcher. + 用於展示和原型、供沒有 `Workspaces` 的 `TeamSwitcher` 使用的模擬工作區:它顯示的清單、模擬加入所接受的邀請(加入檢視會註明)以及模擬建立的內容。應用程式自己的工作區會讓它完全讓位。沒有應用程式委派時,切換、建立與加入都耗時 `SimulatedDelay`,並播報為模擬;不會傳送任何內容。請為每個切換器建立一個執行個體:建立或加入的工作區會加入那個切換器。 + + + An invite the simulated join of a `TeamSwitcherSimulation` accepts, and the workspace it joins. + `TeamSwitcherSimulation` 的模擬加入所接受的一個邀請,以及它加入的工作區。 + + + Arguments of `SwitchFailed`: the workspace that could not load and why. + `SwitchFailed` 的引數:無法載入的工作區及原因。 + + + A workspace's avatar on its own: the app's logo image (`LogoImage`), the brand mark (`Logo`) on its brand color (`TileBackground`), or the first letter of the name in a square tinted from the theme's accent and series colors (the tint is picked from the name, so a workspace keeps its color). The corner radius and the mark scale with `TileSize`. Drawn in one pass; nothing loads. + 單獨的工作區頭像:應用程式提供的標誌圖片(`LogoImage`)、品牌色(`TileBackground`)上的品牌標誌(`Logo`),或在一個方塊中顯示名稱的第一個字母,方塊的色調取自主題的強調色與系列色(色調由名稱決定,所以同一個工作區永遠保持同一種顏色)。圓角與標誌隨 `TileSize` 縮放。一次繪製完成,不載入任何資源。 + + + The tile's width and height in pixels; the corner radius, letter and mark scale with it. + 圖塊的寬度與高度(像素);圓角、字母與標誌隨之縮放。 + + + The workspace to draw. + 要繪製的工作區。 + + + Arguments of `ValueChanged`: the workspace that is now active. + `ValueChanged` 的引數:目前生效的工作區。 + + + One workspace (organization, team, project or store) in a `TeamSwitcher`: its id and name, and optionally its plan, member count, your role in it and a logo (an image, or a mark on a brand color). Without a logo its tile shows the first letter of the name in a tinted square. + `TeamSwitcher` 中的一個工作區(組織、團隊、專案或商店):它的 id 與名稱,以及選用的方案、成員數、你在其中的角色與標誌(一張圖片,或品牌色上的品牌標誌)。沒有標誌時,它的圖塊在一個著色方塊中顯示名稱的第一個字母。 + + + The workspace's id; the switcher's `Value` and its de-duplication use it. + 工作區的 id;切換器的 `Value` 與去除重複都使用它。 + + + + An app-supplied logo image (a bitmap or a vector `DrawingImage`) drawn in the tile instead of `Logo` or the letter: it fills the tile, cropped to a square from its center and clipped to the tile's corners. Null falls back to `Logo`, then to the letter. + 應用程式提供的標誌圖片(點陣圖或向量 `DrawingImage`),在圖塊中取代 `Logo` 或字母繪製:它鋪滿圖塊,從中心裁成正方形,並依圖塊的圓角裁剪。為 null 時依序退回 `Logo` 與字母。 + + + How many people belong to it, shown after the plan; null hides the count. + 工作區中的人數,顯示在方案之後;為 null 時不顯示人數。 + + + The name people know it by. + 大家熟悉的名稱。 + + + The plan it is on ("Pro", "Enterprise"), shown under the name. + 所用的方案(「Pro」「Enterprise」),顯示在名稱下方。 + + + Your role in it ("Owner", "Admin", "Member"), shown at the end of its row. + 你在其中的角色(「Owner」「Admin」「Member」),顯示在所在列的結尾。 + + + The tile's brand color. Null tints the tile from the theme's accent and series colors, picked by the name. + 圖塊的品牌色。為 null 時,圖塊的色調取自主題的強調色與系列色,由名稱決定。 + + + The color of the letter or mark on `TileBackground`. Null picks white or near-black, whichever reads better on that color, in both themes. + `TileBackground` 上字母或標誌的顏色。為 null 時在白色與近黑色之間選擇在該顏色上較易讀的一種,兩種主題下皆是如此。 + + + Arguments of `WorkspaceAdded`: a workspace someone created or joined from the menu. Add it to your own list as well; the switcher de-duplicates by id. + `WorkspaceAdded` 的引數:有人從選單建立或加入的工作區。也請把它加入你自己的清單;切換器依 id 去除重複。 + Morphs one short label into the next in place (P4): letters both strings share glide to their new positions, new letters sharpen in with a short stagger, removed letters blur away, and the width follows on a spring. Single line; assistive technology reads the plain text. Rendered directly: no control per letter. 將一個短標籤原地變形為下一個標籤(P4):兩個字串共有的字母滑動到新位置,新字母以短暫錯開的節奏清晰浮現,被移除的字母模糊消失,寬度隨彈簧調整。單行;輔助技術讀取純文字。直接渲染:不為每個字母建立控制項。 @@ -19968,6 +24744,10 @@ Height chosen with the grip, in px; NaN follows the minimum height. 透過把手選定的高度(單位 px);NaN 表示遵循最小高度。 + + Smallest height of the field's frame, border and padding included, in px. The theme sets Arc's 110px; lower it for a short note or prompt box (two lines of text at the default size need 66px). The grip never drags below it. + 欄位外框的最小高度(含邊框和內距,單位 px)。佈景主題設為 Arc 的 110px;簡短的備註框或提示詞框可將其調低(預設尺寸下兩行文字需要 66px)。把手不會拖到它以下。 + Shows the corner grip that drags the field taller (Arc: `resize: vertical`). Default true. 顯示可拖曳欄位加高的右下角把手(Arc:`resize: vertical`)。預設為 true。 @@ -20004,6 +24784,122 @@ A comment and its replies (Arc's `ThreadComment`). Immutable: every change made in a `CommentThread` produces a new tree (unchanged comments are shared), so it can be stored as is. 一則註解及其回覆(Arc 的 `ThreadComment`)。不可變:在 `CommentThread` 中做出的每次變更都會產生新的樹狀結構(未變更的註解會被共用),因此可以原樣儲存。 + + What the classifier read in a `TicketMessage`: the intent, the sentiment and how sure it is. + 分類器從 `TicketMessage` 中讀出的內容:意圖、情緒以及有多確定。 + + + Arguments of `ClassificationFailed`: the classifier threw for a message. + `ClassificationFailed` 的參數:分類器在處理某則訊息時擲出了例外。 + + + A team lane of a `TicketRouter`, or the review lane that collects low confidence tickets. + `TicketRouter` 的團隊泳道,或收集低信賴度工單的複核泳道。 + + + One incoming support message for a `TicketRouter`. Data only, safe to read from any thread (your classifier may run off the UI thread). + `TicketRouter` 的一則傳入支援訊息。僅為資料,可在任何執行緒讀取(你的分類器可以在 UI 執行緒之外執行)。 + + + Arguments of `ReviewConfirmed`: a review ticket sent to its suggested lane. + `ReviewConfirmed` 的參數:一張複核工單被送往其建議的泳道。 + + + Arguments of `TicketRouted`: the ticket that landed in a lane. + `TicketRouted` 的參數:落入某條泳道的工單。 + + + A support ticket router card (Arc Ticket router, design reference). Messages from several channels (email, chat, messages, help desk) arrive one at a time, an AI classifier labels each with an intent, a sentiment and a confidence, and the ticket travels into a team lane where it gets the next assignee in turn. Tickets below `ReviewThreshold` wait in the review lane with the classifier's pick as a suggestion; one click routes them there. Give it `Tickets` and a `Classifier` (your model call); handle `TicketRouted` to assign in your help desk. With no tickets of its own it plays its `Simulation`, an inbox you supply, with a simulated classifier and loops; with neither it rests on "No tickets yet" in empty lanes. About two seconds per ticket at `Speed` 1; the flow pauses with `IsPaused`, off screen and when unloaded, and each pause cancels a running classifier call. + 支援工單路由卡片(Arc Ticket router,設計參考)。來自多個管道(電子郵件、聊天、訊息、服務台)的訊息逐則送達,AI 分類器為每則訊息標註意圖、情緒與信賴度,工單隨後移入團隊泳道,由輪到的下一位負責人接手。低於 `ReviewThreshold` 的工單在複核泳道中等待,以分類器的選擇作為建議;點一下即可送往該處。傳入 `Tickets` 與一個 `Classifier`(你的模型呼叫);處理 `TicketRouted` 以在你的服務台中指派。自己沒有工單時,它以模擬分類器播放它的 `Simulation`(由你提供的收件匣)並循環;兩者都沒有時,它會停在空泳道中的「No tickets yet」。`Speed` 為 1 時每張工單約兩秒;流程會在 `IsPaused`、離開畫面與卸載時暫停,每次暫停都會取消進行中的分類器呼叫。 + + + Raised when the classifier throws. Bubbles. + 分類器擲出例外時觸發。會冒泡。 + + + Your classifier: return the intent, sentiment and confidence, or throw to show the error state with Retry and Send to review. The token cancels on pause, off screen and unload. Null reads each message's `Sample`. + 你的分類器:傳回意圖、情緒與信賴度,或擲出例外以顯示帶有 Retry 與 Send to review 的錯誤狀態。權杖在暫停、離開畫面與卸載時取消。為 null 時讀取每則訊息的 `Sample`。 + + + Tickets already in the lanes on first show, newest first. Null uses the simulation's while it plays. + 首次顯示時已在泳道中的工單,最新的在前。為 null 時,播放模擬期間使用模擬中的工單。 + + + Whether the flow is paused (two-way); the Pause and Resume button sets it. + 流程是否已暫停(雙向);暫停與繼續按鈕會設定它。 + + + Every word the card shows or says. + 卡片顯示或播報的每一個字詞。 + + + The lanes left to right. Null uses the simulation's lanes while it plays, else the default lanes: Billing, Engineering, Success and Needs review (the ids `DefaultRoute` picks), with no assignees. + 由左到右的泳道。為 null 時,播放模擬期間使用模擬中的泳道,否則使用預設泳道:Billing、Engineering、Success 與 Needs review(`DefaultRoute` 挑選的 id),沒有負責人。 + + + Starts over when the queue runs out. Null loops a simulation and stops after your own tickets ("All caught up"). + 佇列用完時從頭開始。為 null 時循環播放模擬,而你自己的工單處理完後停止("All caught up")。 + + + Rows shown per lane. Narrow cards show at most 2. + 每條泳道顯示的列數。窄卡片最多顯示 2 列。 + + + The classifier model's name, in the header and the classifying label. Null shows the simulation's while it plays, and no name otherwise. + 分類器模型的名稱,顯示在標頭與分類中的標籤裡。為 null 時,播放模擬期間顯示模擬中的名稱,否則不顯示名稱。 + + + The mark before the title (a 24 × 24 stroked glyph); the sparkle by default. + 標題前的標記(24 × 24 描邊圖形);預設為閃光圖示。 + + + Raised when `IsPaused` changes, by the Pause and Resume button or by code. Bubbles. + `IsPaused` 變化時觸發,無論由暫停與繼續按鈕還是程式碼引起。會冒泡。 + + + Raised when someone confirms a review ticket's suggested lane. Bubbles. + 有人確認複核工單的建議泳道時觸發。會冒泡。 + + + Confidence below this sends a ticket to the review lane, with the picked lane as its suggestion. + 信賴度低於此值的工單進入複核泳道,並以挑選出的泳道作為建議。 + + + Picks the lane id for a classification; null uses `DefaultRoute`. An unknown id falls back to the first team lane. + 為一次分類挑選泳道 id;為 null 時使用 `DefaultRoute`。未知的 id 會回退到第一條團隊泳道。 + + + The inbox the router plays on its own while `Tickets` is null: the model's name, the lanes, the messages with their simulated answers and the tickets already routed, played exactly like your own stream (one ticket about every two seconds, pausing off screen, looping). Null rests on the empty state instead. + `Tickets` 為 null 時路由器自行播放的收件匣:模型名稱、泳道、附有模擬答案的訊息以及已路由的工單,播放方式與你自己的訊息流完全相同(約每兩秒一張工單、離開畫面時暫停、循環)。為 null 時改為停在空狀態。 + + + Playback speed multiplier of the flow (1 is about two seconds per ticket). + 流程的播放速度倍數(1 約為每張工單兩秒)。 + + + Raised when a ticket lands in a lane, with its classification, lane, assignee and suggested lane. Bubbles. + 工單落入某條泳道時觸發,附帶其分類、泳道、負責人與建議泳道。會冒泡。 + + + The incoming messages, processed in order. Null plays the `Simulation`'s inbox, or rests on "No tickets yet" without one; append to keep the flow going. + 傳入的訊息,依序處理。為 null 時播放 `Simulation` 的收件匣,沒有模擬時停在「No tickets yet」;追加訊息可讓流程繼續。 + + + The card's title. + 卡片的標題。 + + + Every word `TicketRouter` shows or says, in English by default. Replace the whole object to localize or reword; words built from data are format functions. + `TicketRouter` 顯示或播報的每一個字詞,預設為英文。替換整個物件即可在地化或改寫措辭;由資料組成的字詞是格式化函式。 + + + Arguments of `PausedChanged`. + `PausedChanged` 的參數。 + + + An inbox a `TicketRouter` plays on its own while the app sets no `Tickets`: the classifier model's name, the lanes, the stream of messages with the answer the simulated classifier gives for each (`Sample`) and the tickets already in the lanes. The router reads, labels and routes one message at a time, sends the ones below the review threshold to the review lane, and loops when the stream runs out; it pauses off screen and with `IsPaused`. Nothing is sent. Set the data before handing the simulation to the router; to play another inbox, give the router a new simulation. + 應用程式沒有設定 `Tickets` 時 `TicketRouter` 自行播放的收件匣:分類器模型的名稱、泳道、訊息流(每則附有模擬分類器給出的答案 `Sample`)以及已在泳道中的工單。路由器一次讀取、標註並路由一則訊息,把低於複核門檻的訊息送入複核泳道,訊息流用完時循環;離開畫面時以及 `IsPaused` 時暫停。不會送出任何內容。請在把模擬交給路由器之前設定好資料;要播放另一個收件匣,就給路由器一個新的模擬。 + Arc time field: a trigger over a scrolling list of times generated every `MinuteStep` minutes. The list opens centred on the chosen time, arrow keys move the highlight (the list glides to keep it in view, wrapping at both ends), Enter or a click chooses and closes, Escape closes. The shown time rolls like a clock face: a later time rises from below, an earlier one drops from above. 12- or 24-hour display follows the culture unless `ClockFormat` says otherwise; the value is always a 24-hour `TimeOnly`. Arc 時間欄位:觸發器下方是一個可捲動的時間清單,每隔 `MinuteStep` 分鐘產生一項。清單開啟時以所選時間為中心,方向鍵移動反白(清單會滑動以保持其可見,兩端循環),Enter 或按一下選取並關閉,Escape 關閉。顯示的時間如同鐘面一樣滾動:較晚的時間從下方升起,較早的時間從上方落下。除非 `ClockFormat` 另有指定,否則 12 小時制或 24 小時制顯示跟隨文化設定;值一律為 24 小時制的 `TimeOnly`。 @@ -20104,6 +25000,110 @@ One update in a `Timeline` (Arc's `TimelineEvent`): who did what and when, with optional context and a detail revealed in place. Events are read when they are added; replace an event to change it. `Timeline` 中的一則更新(Arc 的 `TimelineEvent`):誰在何時做了什麼,可附帶上下文,並可在原位展開詳細資訊。事件在新增時讀取;如需修改,請取代該事件。 + + The article a `TldrToggle` shows and summarizes: its title, its paragraphs (with stable ids) and the optional source, author, date and reading time of the header. Data only; give the card a new article to change it. + `TldrToggle` 顯示並摘要的文章:標題、段落(帶穩定的 id),以及頁首中選用的來源、作者、日期和閱讀時間。僅為資料;要更換文章,請給卡片一篇新文章。 + + + Arguments of `Completed`: a finished result. + `Completed` 的參數:一個完成的結果。 + + + Arguments of `Jumping`. + `Jumping` 的參數。 + + + A key point: its text and the id of the paragraph it came from. + 一則重點:它的文字和它所出自段落的 id。 + + + Arguments of `LengthChanged`. + `LengthChanged` 的參數。 + + + Arguments of `ModeChanged`. + `ModeChanged` 的參數。 + + + One paragraph of a `TldrArticle`. Its `Id` is what key points link back to. + `TldrArticle` 中的一個段落。重點透過它的 `Id` 連結回來。 + + + What a `TldrToggle` asks its summarizer for. + `TldrToggle` 向其摘要器請求的內容。 + + + One event of a summary stream: a piece of text, or the start of a key point with its source paragraph. + 摘要串流中的一個事件:一段文字,或一則重點的開始及其出處段落。 + + + Streams for a `TldrToggle`: the stand-in summarizer behind an article of yours with no summarizer, and pacing for a provider that returns whole results. Both send chunks of 3 to 8 words every 30 to 40ms and stop when the request's token is cancelled. Nothing is sent anywhere. + `TldrToggle` 的串流:沒有摘要器時你的文章背後的替身摘要器,以及為一次傳回完整結果的服務提供的節奏控制。兩者都每 30 到 40ms 傳送一塊 3 到 8 個字詞的內容,並在請求的權杖被取消時停止。不會向任何地方傳送內容。 + + + An article card with a Full, Summary and Key points switch and a length slider (Arc TL;DR toggle, design reference). Switching streams an AI summary in place of the article in chunks of 3 to 8 words while the body's height follows on a bounce-free spring. Key points arrive as a checklist: each point ticks when it is written, and once the list is done its ¶ jump link switches back to the article, scrolls the source paragraph into view, focuses it and highlights it. Results are kept per mode and length, so switching back is instant; Regenerate asks again, Stop or Escape ends a stream and keeps what arrived, and a failure shows the message with Try again. Give it an `Article` with stable paragraph ids and a `Summarizer` that streams `TldrStreamEvent`s; without a summarizer the summaries are simulated from the article's lead sentences. Without an article it plays a `Simulation` (an article and its written summaries you pass, opening the summary by itself once it has been half in view for a second); with neither it shows the quiet empty state. + 一張文章卡片,具有全文、摘要和重點切換以及長度滑桿(Arc TL;DR toggle,設計參考)。切換後,AI 摘要以每塊 3 到 8 個字詞的節奏在原文位置串流出現,內文高度以無回彈的彈簧跟隨。重點以清單形式出現:每則重點寫完時打勾,整份清單完成後,它的 ¶ 跳轉連結會切回文章,把出處段落捲動到視野中、聚焦並醒目提示。結果依模式和長度保留,所以切回去是即時的;重新產生會再次請求,停止或 Escape 會結束串流並保留已收到的內容,失敗時顯示訊息和「Try again」。為它提供段落 id 穩定的 `Article`,以及以串流傳回 `TldrStreamEvent` 的 `Summarizer`;沒有摘要器時,摘要由文章的首句模擬。沒有文章時它播放 `Simulation`(你傳入的文章及其寫好的摘要,在至少一半進入視野滿一秒後自動開啟摘要);兩者都沒有時顯示安靜的空狀態。 + + + The article to show and summarize; null plays `Simulation`, or shows the quiet empty state without one. + 要顯示並摘要的文章;為 null 時播放 `Simulation`,沒有模擬時顯示安靜的空狀態。 + + + Opens the summary by itself once the card has been half in view for a second, unless the reader has already touched it. Null turns it on only while a `Simulation` plays. + 卡片至少一半進入視野滿一秒後自動開啟摘要,除非讀者已經操作過卡片。為 null 時,僅在播放 `Simulation` 時開啟。 + + + Raised when a result finishes, for analytics or a server-side cache. Stopped and failed runs do not raise it. Bubbles. + 一個結果完成時引發,可用於分析或伺服器端快取。被停止或失敗的執行不會引發它。會冒泡。 + + + Adds "Simulated, nothing was sent" to announcements and error text. Null turns it on while a `Simulation` plays or there is no `Summarizer`; set it when you wrap `Simulate` or `Summarize` yourself. + 在播報和錯誤文字中加上 "Simulated, nothing was sent"。為 null 時,在播放 `Simulation` 或沒有 `Summarizer` 時開啟;自行包裝 `Simulate` 或 `Summarize` 時請設定它。 + + + Raised when a key point's jump link is used, before the card scrolls to the paragraph. Bubbles. + 某則重點的跳轉連結被使用時引發,在卡片捲動到段落之前。會冒泡。 + + + Every word the card shows or says. + 卡片顯示或播報的所有文字。 + + + The length from 1 to 5 (two-way): summaries aim for about 35, 60, 85, 100 and 130 words; key points return 3 to 7 items. + 長度,從 1 到 5(雙向):摘要分別以約 35、60、85、100 和 130 個字詞為目標;重點傳回 3 到 7 則。 + + + Raised when a slider drag is released or a key moves it (not on every step of a drag), so each change makes one request. Bubbles. + 滑桿拖曳放開或按鍵調整時引發(不會在拖曳的每一步引發),所以每次變更只發出一個請求。會冒泡。 + + + What the body shows (two-way): the article, a summary or key points. + 內文顯示的內容(雙向):文章、摘要或重點。 + + + Raised when the switch, a jump link or autoplay changes `Mode` (not when the app sets it). Bubbles. + 切換控制項、跳轉連結或自動播放變更 `Mode` 時引發(應用程式自行設定時不引發)。會冒泡。 + + + The model credited in the footer, with its 14px mark; null credits the `Simulation`'s model while one plays, or none. + 頁尾中署名的模型,附帶 14px 標誌;為 null 時,播放 `Simulation` 期間署名為其模型,否則不署名。 + + + An article to play while the card has no `Article`: the article, the model and the summaries and key points written for it, streamed as a simulation (nothing is sent). Your own `Article` wins over it; null stops it and shows the quiet empty state. + 卡片沒有 `Article` 時要播放的文章:文章、模型以及為它寫好的摘要和重點,以模擬方式串流輸出(不會傳送任何內容)。你自己的 `Article` 優先於它;為 null 時停止播放並顯示安靜的空狀態。 + + + Produces summaries and key points as a stream. Null simulates them from the article's lead sentences (`Simulate`), which sends nothing. The request's token is cancelled on Stop, Escape, a mode or length change, a new article, or when the card leaves the window. A playing `Simulation` does not use it. + 以串流的形式產生摘要和重點。為 null 時根據文章的首句模擬(`Simulate`),不會傳送任何內容。在停止、Escape、切換模式或長度、換了文章,或卡片離開視窗時,請求的權杖會被取消。正在播放的 `Simulation` 不會使用它。 + + + Every word `TldrToggle` shows or says, in English by default. Replace the whole object to localize or reword; counts and names go through format functions. + `TldrToggle` 顯示或播報的所有文字,預設為英文。取代整個物件即可在地化或改寫;計數和名稱透過格式化函式產生。 + + + An article a `TldrToggle` plays without a summarizer, from data you give it: the article, the model credited, a summary written for each length and key points ranked by weight. Set it as `Simulation` while the card has no `Article`: the card shows the article, streams the written summary or points after `ThinkTime` in chunks of 3 to 8 words, says it is simulated (nothing is sent) and, unless `AutoPlay` is false, opens the summary by itself once it has been half in view for a second. A length without a written summary gets the article's lead sentences, and without `KeyPoints` each paragraph's lead sentence is a point (the same stand-in as `Simulate`; not a model). + `TldrToggle` 在沒有摘要器時依你提供的資料播放的文章:文章、署名的模型、為每個長度寫好的摘要,以及依重要性排序的重點。在卡片沒有 `Article` 時把它設為 `Simulation`:卡片顯示文章,在 `ThinkTime` 之後以每塊 3 到 8 個字詞串流輸出寫好的摘要或重點,說明這是模擬(不會傳送任何內容),並且除非 `AutoPlay` 為 false,會在至少一半進入視野滿一秒後自動開啟摘要。沒有寫好摘要的長度會使用文章的首句;沒有 `KeyPoints` 時,每段的首句就是一則重點(與 `Simulate` 相同的替身,不是模型)。 + Arc toast: a brief confirmation for a completed background action (one locally controlled success message such as "Changes saved"). It rises from its edge on the morph spring while its check mark draws itself, closes after `Duration` (the timer does not pause, so keep the copy short), and can be swiped away in either direction with velocity. Text changes rise in place while the height springs to fit. For app-wide notifications (types, actions, updates, stacking) call `Show` from anywhere: it goes to the window's `ToastStack` (see `ToastHost`). Arc 通知提示(Toast):用於已完成背景操作的簡短確認(一則本地控制的成功訊息,如“Changes saved”)。它沿邊緣升起,依托形變彈簧運動,同時其勾號逐步繪出;經過 `Duration` 後關閉(計時器不會暫停,因此請保持文案簡短),並可依速度向任一方向滑動移除。文字變化時原位上浮,高度以彈簧方式調整至合適大小。若需應用程式層級的通知(類型、操作、更新、堆疊),請在任意位置呼叫 `Show`:它會進入視窗的 `ToastStack`(參見 `ToastHost`)。 @@ -20336,9 +25336,113 @@ Defines the `Symbol` property. 定義 `Symbol` 屬性。 + + A voice chip of a `ToneTuner`: pressing it sets `Voice` and moves both sliders to `Formality` and `Detail`; pressing it again returns to the neutral voice. + `ToneTuner` 的一個語氣標籤:按下時設定 `Voice`,並把兩個滑桿移到 `Formality` 與 `Detail`;再按一次則回到中性語氣。 + + + What a `ToneTuner` asks its `Rewrite` function for. + `ToneTuner` 向其 `Rewrite` 函式要求的內容。 + + + The tone of a `ToneTuner`: formality and detail on three levels each (0, 1, 2) and a voice. Values outside 0–2 are clamped where they are used. + `ToneTuner` 的語氣:正式程度與詳略各分三檔(0、1、2),外加一種語氣。超出 0–2 的值在使用時會被限制在範圍內。 + + + A brand voice card (Arc Tone tuner, design reference): a customer reply with Friendly, Confident and Empathetic voice chips and two sliders, formality and detail, that rewrite it. Each change waits `Debounce` and then asks `Rewrite` for a new version; the sentences that changed crossfade in place while the rest stay still, words that are new against the original can be highlighted (the Changes switch), and Copy takes the version on screen. The status says where the text comes from: Original draft, Rewriting with the model (shimmering), Rewritten, or Couldn't rewrite with Retry. Give it the draft in `Text` and your model call in `Rewrite`; without `Rewrite` the rewrites are simulated by a few plain rules. Without a draft it plays a `Simulation` (a draft and its versions you pass, toured by itself until someone touches it); with neither, it shows an empty card whose dials are off. + 一張品牌語氣卡片(Arc Tone tuner,設計參考):一封客戶回覆,配有 Friendly、Confident 與 Empathetic 語氣標籤以及正式程度與詳略兩個滑桿,用來改寫它。每次調整等待 `Debounce` 後向 `Rewrite` 要求新版本;改動過的句子原地交叉淡化,其餘保持不動,相對原稿新增的詞可以醒目提示(Changes 開關),複製按鈕取走螢幕上的版本。狀態說明文字的來源:Original draft、Rewriting(附模型名稱,閃爍)、Rewritten,或附重試按鈕的 Couldn't rewrite。把原稿傳給 `Text`,把模型呼叫傳給 `Rewrite`;沒有 `Rewrite` 時,改寫由幾條簡單規則模擬。沒有原稿時它播放 `Simulation`(你傳入的原稿及其各個版本,在有人操作之前自動輪流展示);兩者都沒有時,顯示一張空卡片,其中的調節控制項處於關閉狀態。 + + + Tours the voices while the card is on screen, until the first pointer press or focus inside it. Null (default) tours only while a `Simulation` plays. Reduced motion never tours. + 卡片在螢幕上時自動輪流展示各種語氣,直到卡片內第一次指標按下或取得焦點。為 null(預設)時只在播放 `Simulation` 時輪播。減弱動態效果時從不輪播。 + + + How many words of the version on screen are new against the original draft. + 螢幕上的版本中,相對原稿新增的詞數。 + + + Raised after Copy put the version on screen on the clipboard. Bubbles. + 複製按鈕把螢幕上的版本放入剪貼簿後引發。會冒泡。 + + + Quiet time after the last change before a rewrite starts (default 220ms). + 最後一次調整後、開始改寫前的靜默時間(預設 220ms)。 + + + The version on screen as plain text (what Copy takes). + 螢幕上的版本,純文字形式(即複製按鈕取走的內容)。 + + + Every word the card shows or says. + 卡片顯示或朗讀的所有文字。 + + + The model named in the status while a rewrite runs; null names the `Simulation`'s model while one plays, or none. + 改寫進行時狀態中顯示的模型名稱;為 null 時,播放 `Simulation` 期間顯示其模型,否則不顯示。 + + + The voice chips; null shows Friendly, Confident and Empathetic (`Defaults`). + 語氣標籤;為 null 時顯示 Friendly、Confident 與 Empathetic(`Defaults`)。 + + + The recipient's picture; without it the avatar shows their initials. + 收件人的頭像圖片;沒有時頭像顯示其姓名首字母。 + + + Who the message is for, shown in the header with an avatar. Null while a `Simulation` plays shows its recipient. + 訊息的收件人,與頭像一起顯示在頁首。播放 `Simulation` 時為 null 會顯示其收件人。 + + + The subject under the recipient's name. + 收件人姓名下方的主旨。 + + + Calls your model and returns the rewritten plain text. The request carries the draft, the tone and a ready made instruction; the token is cancelled when a newer change supersedes the request. Null simulates the rewrites of your draft with a few plain rules (nothing is sent). + 呼叫你的模型並回傳改寫後的純文字。要求包含原稿、語氣和一條現成的指示;當較新的調整取代這次要求時,權杖會被取消。為 null 時,用幾條簡單規則模擬你原稿的改寫(不會送出任何內容)。 + + + Raised when a rewrite lands, with the new plain text and its tone. Bubbles. + 改寫落定時引發,附有新的純文字及其語氣。會冒泡。 + + + Highlights the words that differ from the original draft (two-way; the Changes switch). + 醒目提示與原稿不同的詞(雙向;即 Changes 開關)。 + + + A tuning session to play while the card has no `Text`: its draft, recipient, model and versions, with simulated rewrites (nothing is sent). Your own `Text` wins over it; null stops it and shows the empty card. + 卡片沒有 `Text` 時要播放的調整過程:它的原稿、收件人、模型和各個版本,改寫是模擬的(不會送出任何內容)。你自己的 `Text` 優先於它;為 null 時停止播放並顯示空卡片。 + + + Where the text on screen comes from: the original, a rewrite on its way, a landed rewrite, or a failure. + 螢幕上文字的來源:原稿、正在進行的改寫、已落定的改寫,或一次失敗。 + + + The original draft, the source of every rewrite. Blank lines split paragraphs; single line breaks keep lines (a sign off). Null plays `Simulation`, or shows the empty card without one. + 原稿,每次改寫的來源。空行分隔段落;單一換行保留為行(例如落款)。為 null 時播放 `Simulation`,沒有模擬時顯示空卡片。 + + + The tone (two-way): formality and detail (0–2) and the voice. The original draft shows at `Default`. + 語氣(雙向):正式程度與詳略(0–2)以及語氣。原稿在 `Default` 下顯示。 + + + Raised on every slider step or chip press (and when code sets `Value`), before the debounce. Bubbles. + 每次滑桿步進或標籤按下時(以及程式碼設定 `Value` 時)引發,早於防彈跳。會冒泡。 + + + Every word `ToneTuner` shows or says, in English by default. Replace the whole object to localize or reword; counts and announcements are format functions. + `ToneTuner` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫措辭;計數與播報是格式化函式。 + + + A tuning session a `ToneTuner` plays without a model, from data you give it: a draft, who it is for, the model the status names and the rewritten versions per tone. Set it as `Simulation` while the card has no `Text`: the card then tunes `Text`, answers each tone with its version after `Latency`, says the rewrites are simulated (nothing is sent) and, unless `AutoPlay` is false, tours the voices by itself until someone touches it. A tone with no version in `Versions` is written by a few plain rules (contractions and greetings for formality, fewer sentences or a closing offer for detail, an opening line for the voice). That is a stand-in so the dials do something in a demo, not a language model. + `ToneTuner` 在沒有模型時依你提供的資料播放的調整過程:一份原稿、收件人、狀態中顯示的模型,以及每種語氣改寫後的版本。在卡片沒有 `Text` 時把它設為 `Simulation`:卡片會調整 `Text`,在 `Latency` 之後用對應版本回應每種語氣,說明改寫是模擬的(不會送出任何內容),並且除非 `AutoPlay` 為 false,會在有人操作之前自動輪流展示各種語氣。`Versions` 中沒有版本的語氣會用幾條簡單規則改寫(正式程度對應縮寫和問候語,詳略對應減少句子或加一句結尾提議,語氣對應一句開場白)。這只是讓調節控制項在示範中有所反應的替身,不是語言模型。 + + + One rewritten version of a `ToneTunerSimulation`'s draft: the tone it is written in and its text. + `ToneTunerSimulation` 原稿的一個改寫版本:它所用的語氣及其文字。 + - One tool call of an agent transcript (Arc Tool call, design reference): the tool's mark, the call as a verb phrase, quiet meta ("4 results · 1.2s") and a status icon. The whole row is a button: it springs open into Input (the arguments as a mono key / value list) and Output (the result as a list, a table, tinted JSON or text, folding long ones behind "Show N more lines"), each with a copy button. A running call shimmers its title next to a spinner; a failed one shows its reason and Retry; one that waits for approval sits on an accent outline with Deny and Allow; a denied one is muted. Bind `Item` and change the item in place as the call moves on. Allow, Deny and Retry raise `ApproveRequested`, `DenyRequested` and `RetryRequested` (they bubble, so a `ToolCallGroup` or any parent can handle them): update the call's status yourself and set `Handled`. When nothing handles them, Deny marks the call denied here; Allow and Retry only simulate a run on the built-in sample data (`ToolCallSamples`) and change nothing otherwise. With no item the row shows a sample web search. On its own the row sits in a 1px bordered Surface frame (radius 16, 4px in); inside a group it is flush. - 代理轉錄中的一次工具呼叫(Arc Tool call,設計參考):工具的標記、以動詞片語表示的呼叫、低調的中繼資訊("4 results · 1.2s")以及狀態圖示。整列是一個按鈕:點選後以彈性動效展開為輸入(引數,等寬的鍵/值列表)和輸出(結果,以列表、表格、著色的 JSON 或文字呈現,過長內容摺疊在"Show N more lines"之後),兩者都附複製按鈕。執行中的呼叫會在旋轉指示器旁讓標題閃爍;失敗的呼叫會顯示原因與重試;等待核准的呼叫帶強調色輪廓,並顯示拒絕與允許;被拒絕的呼叫會變為淡化顯示。繫結 `Item`,呼叫推進時就地修改該項。允許、拒絕與重試會引發 `ApproveRequested`、`DenyRequested` 與 `RetryRequested`(它們會冒泡,因此 `ToolCallGroup` 或任何父層都可以處理):由你自行更新呼叫的狀態並設定 `Handled`。若無人處理,拒絕會在此處將呼叫標記為已拒絕;允許與重試只會在內建範例資料(`ToolCallSamples`)上模擬執行,其他情況下不做任何改變。沒有項目時,該列顯示一個範例網頁搜尋。單獨使用時,該列位於 1px 帶邊框的 Surface 外框中(圓角 16,內縮 4px);在群組中則與邊緣齊平。 + One tool call of an agent transcript (Arc Tool call, design reference): the tool's mark, the call as a verb phrase, quiet meta ("4 results · 1.2s") and a status icon. The whole row is a button: it springs open into Input (the arguments as a mono key / value list) and Output (the result as a list, a table, tinted JSON or text, folding long ones behind "Show N more lines"), each with a copy button. A running call shimmers its title next to a spinner; a failed one shows its reason and Retry; one that waits for approval sits on an accent outline with Deny and Allow; a denied one is muted. Bind `Item` and change the item in place as the call moves on. Allow, Deny and Retry raise `ApproveRequested`, `DenyRequested` and `RetryRequested` (they bubble, so a `ToolCallGroup` or any parent can handle them): update the call's status yourself and set `Handled`. When nothing handles them, Deny marks the call denied here; Allow and Retry go to the `ToolCallSimulation` that appended the call, and change nothing otherwise. With no item the row is empty: a wrench and "No tool call" (`EmptyTitle`) without a status, and it does not open. On its own the row sits in a 1px bordered Surface frame (radius 16, 4px in); inside a group it is flush. + 代理轉錄中的一次工具呼叫(Arc Tool call,設計參考):工具的標記、以動詞片語表示的呼叫、低調的中繼資訊("4 results · 1.2s")以及狀態圖示。整列是一個按鈕:點選後以彈性動效展開為輸入(引數,等寬的鍵/值列表)和輸出(結果,以列表、表格、著色的 JSON 或文字呈現,過長內容摺疊在"Show N more lines"之後),兩者都附複製按鈕。執行中的呼叫會在旋轉指示器旁讓標題閃爍;失敗的呼叫會顯示原因與重試;等待核准的呼叫帶強調色輪廓,並顯示拒絕與允許;被拒絕的呼叫會變為淡化顯示。繫結 `Item`,呼叫推進時就地修改該項。允許、拒絕與重試會引發 `ApproveRequested`、`DenyRequested` 與 `RetryRequested`(它們會冒泡,因此 `ToolCallGroup` 或任何父層都可以處理):由你自行更新呼叫的狀態並設定 `Handled`。若無人處理,拒絕會在此處將呼叫標記為已拒絕;允許與重試會交給加入該呼叫的 `ToolCallSimulation`,其他情況下不做任何改變。沒有項目時,該列為空:一個扳手與 "No tool call"(`EmptyTitle`),沒有狀態,也無法展開。單獨使用時,該列位於 1px 帶邊框的 Surface 外框中(圓角 16,內縮 4px);在群組中則與邊緣齊平。 Raised when the person presses Allow on a call waiting for approval. Bubbles. @@ -20349,8 +25453,8 @@ 當使用者在等待核准的呼叫上按下「拒絕」時引發。會冒泡。 - The call on screen: `Item`, or the sample when it is null. - 螢幕上顯示的呼叫:即 `Item`;為 null 時則顯示範例。 + The call on screen: `Item`, or an empty call when it is null. + 螢幕上顯示的呼叫:即 `Item`;為 null 時則是一個空呼叫。 Raised when Input and Output open or close. @@ -20365,8 +25469,8 @@ 移除該列自身的外框(群組會為其列設定此項)。 - The call shown. Change it in place; the row follows every change. Null shows a sample web search. - 展示的呼叫。直接修改它,列會隨每次修改同步更新。為 null 時展示一個範例網頁搜尋。 + The call shown. Change it in place; the row follows every change. Null shows the empty row. + 展示的呼叫。直接修改它,列會隨每次修改同步更新。為 null 時顯示空列。 Every word the row shows or says. @@ -20393,8 +25497,8 @@ `ExpandedChanged` 與 `ExpandedChanged` 的資料。 - Every tool call of one assistant turn in one quiet card (Arc Tool call group, design reference): a header with the tools' overlapping marks, "Used 4 tools" (or "Using 3 tools" with a shimmer while the agent works, or "Waiting for approval") and, on the right, "1 failed" or the total duration; below it, one `ToolCall` row per call. Append calls to `Items` (or an observable `ItemsSource`) as the agent starts them and change them in place: a new row grows in from zero height, a new tool's mark joins the stack on a spring. Opening the header staggers the rows in. While the agent works the group stays open; 450ms after every call finished it folds itself away (`AutoCollapse`) unless the person opened or closed the group or a row. A call that needs a decision (a failure, an approval) opens a folded group. An empty group takes no space; its first call grows it in. With neither `Items` nor `ItemsSource` it shows four finished sample calls. Allow, Deny and Retry bubble up from the rows as `ApproveRequested`, `DenyRequested` and `RetryRequested`; see `ToolCall` for what happens when nothing handles them. - 將一個助理回合的所有工具呼叫放在一張安靜的卡片中(Arc 工具呼叫群組,設計參考):標題列包含工具的重疊標記,"Used 4 tools"(代理工作時為帶閃爍效果的 "Using 3 tools",或 "Waiting for approval"),右側為 "1 failed" 或總時長;下方每個呼叫對應一列 `ToolCall`。代理開始呼叫時將其追加到 `Items`(或可觀察的 `ItemsSource`),並原地修改它們:新列從零高度展開,新工具的標記以彈簧動畫加入標記堆疊。展開標題列時各列依序錯開進入。代理工作期間該群組保持展開;每次呼叫完成後 450ms,若使用者未展開或收合該群組或任一列,它會自動摺疊(`AutoCollapse`)。需要決定的呼叫(失敗、需要核准)會展開已摺疊的群組。空群組不佔用空間;其第一個呼叫會使其展開。若既未設定 `Items` 也未設定 `ItemsSource`,則顯示四個已完成的範例呼叫。列中的允許、拒絕和重試會以 `ApproveRequested`、`DenyRequested` 和 `RetryRequested` 冒泡上傳;未被處理時的行為見 `ToolCall`。 + Every tool call of one assistant turn in one quiet card (Arc Tool call group, design reference): a header with the tools' overlapping marks, "Used 4 tools" (or "Using 3 tools" with a shimmer while the agent works, or "Waiting for approval") and, on the right, "1 failed" or the total duration; below it, one `ToolCall` row per call. Append calls to `Items` (or an observable `ItemsSource`) as the agent starts them and change them in place: a new row grows in from zero height, a new tool's mark joins the stack on a spring. Opening the header staggers the rows in. While the agent works the group stays open; 450ms after every call finished it folds itself away (`AutoCollapse`) unless the person opened or closed the group or a row. A call that needs a decision (a failure, an approval) opens a folded group. An empty group takes no space; its first call grows it in. With neither `Items` nor `ItemsSource` it plays its `Simulation` when one is set, and otherwise stays empty. Allow, Deny and Retry bubble up from the rows as `ApproveRequested`, `DenyRequested` and `RetryRequested`; see `ToolCall` for what happens when nothing handles them. + 將一個助理回合的所有工具呼叫放在一張安靜的卡片中(Arc 工具呼叫群組,設計參考):標題列包含工具的重疊標記,"Used 4 tools"(代理工作時為帶閃爍效果的 "Using 3 tools",或 "Waiting for approval"),右側為 "1 failed" 或總時長;下方每個呼叫對應一列 `ToolCall`。代理開始呼叫時將其追加到 `Items`(或可觀察的 `ItemsSource`),並原地修改它們:新列從零高度展開,新工具的標記以彈簧動畫加入標記堆疊。展開標題列時各列依序錯開進入。代理工作期間該群組保持展開;每次呼叫完成後 450ms,若使用者未展開或收合該群組或任一列,它會自動摺疊(`AutoCollapse`)。需要決定的呼叫(失敗、需要核准)會展開已摺疊的群組。空群組不佔用空間;其第一個呼叫會使其展開。若既未設定 `Items` 也未設定 `ItemsSource`,則在設定了 `Simulation` 時播放它,否則保持為空。列中的允許、拒絕和重試會以 `ApproveRequested`、`DenyRequested` 和 `RetryRequested` 冒泡上傳;未被處理時的行為見 `ToolCall`。 Raised when a row's Allow is pressed (it bubbles from the row). @@ -20433,8 +25537,8 @@ 列表是否展開(雙向繫結)。在設定之前,只要有任一呼叫在執行、等待或失敗,該群組就會展開;已完成的紀錄預設摺疊。 - An observable list of calls that wins over `Items`. - 可觀察的呼叫列表,優先於 `Items`。 + An observable list of calls that wins over `Items` and `Simulation`. + 可觀察的呼叫列表,優先於 `Items` 與 `Simulation`。 Every word the group and its rows show or say. @@ -20448,6 +25552,10 @@ Raised when a row's Retry is pressed (it bubbles from the row). 按下某列的重試時引發(從該列冒泡)。 + + A simulated turn to play while the group has no calls of its own (no `ItemsSource` and no `Items`): the group shows its `Items`, says "Using" while it `IsWorking` (unless `IsActive` is set), starts it from the top when it comes on screen and stops it when it leaves or is replaced. Null (the default) leaves a group without calls empty. Give each group its own. + 在群組沒有自己的呼叫時(既無 `ItemsSource` 也無 `Items`)播放的模擬回合:群組顯示其 `Items`,在它 `IsWorking` 時顯示 "Using"(除非設定了 `IsActive`),出現在畫面上時從頭開始執行,離開或被取代時停止。為 null(預設)時,沒有呼叫的群組保持為空。每個群組各用一個。 + The sum of the successful calls' durations (shown once nothing runs or waits). 成功呼叫耗時的總和(在沒有任何呼叫執行或等待後顯示)。 @@ -20529,8 +25637,8 @@ 工具呼叫的回傳內容,在呼叫成功後顯示在"Output"下方:`ToolCallListOutput`、`ToolCallTableOutput`、`ToolCallJsonOutput` 或 `ToolCallTextOutput` 之一。指定新的輸出即可更改該列顯示的內容;列在開啟時才讀取輸出。 - Plays a scripted agent turn into `Items` on a clock: calls are appended one after another, run for their time and settle; a failing call waits for `Retry` and a call that needs approval waits for `Approve` or `Deny`. Bind a `ToolCallGroup`'s `ItemsSource` to `Items` and its `IsActive` to `IsWorking`: Allow, Deny and Retry on the group's rows reach the simulation when nothing else handles them. The default script is `Script`. Time comes from a `TimeProvider`, so tests can step it; callbacks run on the UI thread. - 按時間順序將腳本化的代理回合播放到 `Items` 中:呼叫依序追加,執行其耗時後結束;失敗的呼叫等待 `Retry`,需要核准的呼叫等待 `Approve` 或 `Deny`。將 `ToolCallGroup` 的 `ItemsSource` 繫結到 `Items`,並將其 `IsActive` 繫結到 `IsWorking`:群組各列上的允許、拒絕和重試,若無其他處理方,會傳遞給模擬。預設腳本為 `Script`。時間來自 `TimeProvider`,因此測試可以逐步推進;回呼在 UI 執行緒上執行。 + Plays a scripted agent turn into `Items` on a clock: calls are appended one after another, run for their time and settle; a failing call waits for `Retry` and a call that needs approval waits for `Approve` or `Deny`. Your app writes the `Script`; nothing ships with the package. Give it to `Simulation` (the group shows `Items`, follows `IsWorking`, starts the run when it is on screen and stops it when it leaves), or bind a group's `ItemsSource` to `Items` and its `IsActive` to `IsWorking` yourself. Allow, Deny and Retry on the rows reach the simulation when nothing else handles them. Time comes from a `TimeProvider`, so tests can step it; callbacks run on the UI thread. Create one per group: it keeps the playback state. + 按時間順序將腳本化的代理回合播放到 `Items` 中:呼叫依序追加,執行其耗時後結束;失敗的呼叫等待 `Retry`,需要核准的呼叫等待 `Approve` 或 `Deny`。`Script` 由你的應用程式撰寫,套件不附帶任何腳本。把它交給 `Simulation`(群組顯示 `Items`、跟隨 `IsWorking`,出現在畫面上時啟動執行,離開時停止),或自行將群組的 `ItemsSource` 繫結到 `Items`、將其 `IsActive` 繫結到 `IsWorking`。各列上的允許、拒絕和重試,若無其他處理方,會傳遞給模擬。時間來自 `TimeProvider`,因此測試可以逐步推進;回呼在 UI 執行緒上執行。每個群組各建一個:它保存播放狀態。 Raised on the UI thread at every beat of the run (a call appended, settled, failed, retried, allowed, denied; the start and the end). @@ -20580,6 +25688,154 @@ Tooltip content: a string, or any object (bind it when the text comes from the view model). 提示內容:字串或任意物件(當文字來自檢視模型時進行繫結)。 + + A fine-tuning job card (Arc Training run, design reference): the base model with its mark and the job's name, a live train and validation loss curve drawn across a fixed step axis that doubles as the epoch progress bar, the step, learning rate, elapsed time and ETA, the newest three events (evals, checkpoints, warnings), Pause and Cancel while it runs, and the results with a Deploy button once it completes. Drive it from your job: set `Status`, `TotalSteps` and `Epochs` from the job config so the axis is right from the first sample, then keep `Step`, `LearningRate`, `Elapsed` and `Eta` current and append to `Loss` and `Events` as they stream (no need to throttle: the card eases toward each sample), or hand it updates with `Apply` or `FollowAsync`. The buttons raise events; Deploy calls `DeployHandler`. With no `Status` it plays its `Simulation`, a job you describe, as `Scenario` says, freezing while the card is off screen; with neither it shows a queued job with no data yet. + 微調任務卡片(Arc Training run,設計參考):附標誌的基礎模型和任務名稱、沿著固定步數軸即時繪製的訓練與驗證損失曲線(這條軸同時也是訓練週期進度列)、步數、學習率、已用時間和預估剩餘時間、最新的三則事件(評測、檢查點、警告)、執行時的暫停和取消,以及完成後附 Deploy 按鈕的結果。由你的任務驅動它:依任務設定設置 `Status`、`TotalSteps` 和 `Epochs`,讓座標軸從第一個樣本起就正確;接著持續更新 `Step`、`LearningRate`、`Elapsed` 和 `Eta`,並在資料串流進來時附加到 `Loss` 和 `Events`(不需要節流:卡片會緩動到每個樣本),或以 `Apply` 或 `FollowAsync` 傳入更新。按鈕會引發事件;Deploy 呼叫 `DeployHandler`。沒有 `Status` 時,它會依 `Scenario` 播放它的 `Simulation`(由你描述的任務),卡片離開畫面時暫停;兩者都沒有時,顯示一個排隊中、尚無資料的任務。 + + + Which action buttons may show (all by default; None for a read-only monitor). A button also needs the right status. + 哪些操作按鈕可以顯示(預設全部;唯讀監控設為 None)。按鈕還需要對應的狀態。 + + + A line over the empty chart while queued or starting, such as "Waiting for 8× H200 · 2nd in queue". + 排隊或啟動時顯示在空白圖表上的一行文字,例如 "Waiting for 8× H200 · 2nd in queue"。 + + + The base model and its 24px mark (a monogram when the model has no mark); null shows the simulation's while it plays. + 基礎模型及其 24px 標誌(模型沒有標誌時顯示首字母);為 null 時,播放模擬期間顯示模擬中的模型。 + + + Raised on the second press, on Confirm cancel, within 3 seconds of the first. Bubbles. + 在第一次按下後 3 秒內第二次按下(即 Confirm cancel)時引發。會向上傳遞。 + + + The training data, shown in the footer with its 14px mark and size. + 訓練資料,顯示在頁尾,附 14px 標誌和規模。 + + + Deploys the completed model: called when Deploy is pressed, with a token that is cancelled when the card unloads or the run leaves Completed. Return the endpoint (or null). The button loads while it runs; a thrown exception shows its message and Retry deploy. Deploy shows only when this is set (or while simulating). + 部署已完成的模型:按下 Deploy 時呼叫,傳入的權杖會在卡片卸載或執行離開 Completed 時取消。傳回端點(或 null)。執行期間按鈕顯示載入狀態;擲回的例外會顯示其訊息和 Retry deploy。只有設定了它(或處於模擬時)才顯示 Deploy。 + + + Short facts after the model name, such as "LoRA r16" and "8× H200". + 模型名稱後的簡短事實,例如 "LoRA r16" 和 "8× H200"。 + + + Time spent training so far, excluding pauses. + 到目前為止的訓練時間,不含暫停。 + + + Number of epochs: the axis splits into one band and one progress segment per epoch. + 訓練週期數:座標軸依週期分成一條背景帶和一段進度。 + + + Why the run failed, shown in the footer in place of the dataset. + 執行失敗的原因,顯示在頁尾,取代資料集。 + + + Estimated time left. While queued it reads as "About 18m 40s". + 預估剩餘時間。排隊時顯示為 "About 18m 40s"。 + + + The job or output model name; null shows the simulation's name while it plays, else "Fine-tuning job". + 任務或輸出模型的名稱;為 null 時,播放模擬期間顯示模擬中的名稱,否則顯示 "Fine-tuning job"。 + + + Every word the card shows or says. + 卡片顯示或朗讀的所有文字。 + + + The current learning rate, shown as 1.84e-4. + 目前的學習率,顯示為 1.84e-4。 + + + Up to four results that replace the live stats on completion; null shows the final train and validation loss, the steps and the duration. + 完成時取代即時統計的最多四項結果;為 null 時顯示最終訓練與驗證損失、步數和耗時。 + + + A one-off message for screen readers, such as the result of an action. + 一次性的螢幕閱讀器訊息,例如某個操作的結果。 + + + Raised when Pause is pressed. Bubbles. + 按下 Pause 時引發。會向上傳遞。 + + + Raised when Run again is pressed after the run completed, failed or was cancelled. Bubbles. + 執行完成、失敗或被取消後按下 Run again 時引發。會向上傳遞。 + + + Raised when Resume is pressed. Bubbles. + 按下 Resume 時引發。會向上傳遞。 + + + Raised when Retry from checkpoint is pressed after a failure. Bubbles. + 失敗後按下 Retry from checkpoint 時引發。會向上傳遞。 + + + How the `Simulation` plays while `Status` is null: Healthy, or Oom (fails once at step 940). + `Status` 為 null 時 `Simulation` 的播放方式:Healthy,或 Oom(在第 940 步失敗一次)。 + + + The job the card plays on its own while `Status` is null: its name, base model, details, dataset, hardware and endpoint, played as `Scenario` says. Null shows a queued job with no data instead. + `Status` 為 null 時卡片自行播放的任務:它的名稱、基礎模型、詳細資訊、資料集、硬體與端點,依 `Scenario` 播放。為 null 時改為顯示一個排隊中、尚無資料的任務。 + + + Where the job is. Leave it null to play the `Simulation` (as `Scenario` says), or to show a queued job with no data without one. + 任務所處的階段。保持 null 則播放 `Simulation`(依 `Scenario`);沒有模擬時顯示一個排隊中、尚無資料的任務。 + + + Optimizer steps done. + 已完成的最佳化器步數。 + + + Steps in the whole run. It fixes the x axis so the curve never rescales mid-run. + 整個執行的步數。它固定 x 軸,讓曲線在執行中途不會重新縮放。 + + + The action buttons a `TrainingRun` may offer. A button also needs the right status. + `TrainingRun` 可以提供的操作按鈕。按鈕還需要對應的狀態。 + + + The training data of a `TrainingRun`, shown in the footer. + `TrainingRun` 的訓練資料,顯示在頁尾。 + + + One entry of a `TrainingRun`'s event log. + `TrainingRun` 事件記錄中的一則項目。 + + + What a `TrainingRunEvent` reports; it picks the row's icon and color. + `TrainingRunEvent` 回報的內容;它決定該列的圖示和顏色。 + + + Every word `TrainingRun` shows or says, in English by default. Replace the whole object to localize or reword; counts, steps and times are format functions. + `TrainingRun` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;計數、步數和時間是格式化函式。 + + + One loss sample of a `TrainingRun`: the optimizer step and the train and validation loss at it. + `TrainingRun` 的一個損失樣本:最佳化器步數以及該步的訓練與驗證損失。 + + + One result a completed `TrainingRun` shows in place of the live stats. + 已完成的 `TrainingRun` 取代即時統計時顯示的一項結果。 + + + One update of a running job for `Apply` and `FollowAsync`: every value that is set replaces the card's, a loss sample and an event are appended, and nulls leave the card as it is. + 供 `Apply` 和 `FollowAsync` 使用的一次執行中任務更新:有設定的值會取代卡片上的值,損失樣本和事件會被附加,null 則讓卡片保持不變。 + + + How a `TrainingRun` plays its `Simulation`. + `TrainingRun` 如何播放它的 `Simulation`。 + + + The fine-tuning job a `TrainingRun` plays on its own while the app sets no `Status`: its name, base model, details, dataset, the hardware it queues for and trains on, and the endpoint a simulated deploy returns. The card plays it as `Scenario` says: it queues, starts up and trains over 2,400 steps in three epochs with a procedural loss curve, checkpoints, evals and a done event (or an out of memory failure to retry from the last checkpoint), freezing while the card is off screen. Nothing is sent. Set the data before handing the simulation to the card; to play another job, give the card a new simulation. + 應用程式沒有設定 `Status` 時 `TrainingRun` 自行播放的微調任務:它的名稱、基礎模型、詳細資訊、資料集、它排隊等候與訓練所用的硬體,以及模擬部署傳回的端點。卡片依 `Scenario` 播放它:排隊、啟動,並以程式產生的損失曲線、檢查點、評估與完成事件在三個 epoch 中訓練 2,400 步(或者出現一次記憶體不足的失敗,可從上一個檢查點重試),卡片離開畫面時暫停。不會傳送任何內容。請在把模擬交給卡片之前設定好資料;要播放另一個任務,就給卡片一個新的模擬。 + + + Where a `TrainingRun` job is. + `TrainingRun` 任務所處的階段。 + A table whose rows nest. Rows expand and collapse on a spring with indent guides, parents carry tri-state checkboxes (`IsSelectable`), sorting reorders siblings without breaking the hierarchy, and children can load on first expand (`ChildrenLoader`). The first column shown holds the tree. Keyboard follows the treegrid pattern: ↑/↓ move, → opens or enters, ← closes or goes to the parent, Home/End, PageUp/PageDown move ten rows, `*` opens all siblings, Space selects, Enter toggles a parent or activates a leaf, Ctrl/Cmd+A selects all. Rows come from `ItemsSource` and `ChildrenSelector`; each row is identified by `KeySelector` (the item itself when not set), which is what `ExpandedKeys` and `SelectedKeys` hold. Like Arc, rows are not virtualized: every visible row exists and animates, so keep an expanded tree to a few hundred rows and load deeper levels on demand. 列可以巢狀的表格。列在彈簧上展開和收合並帶有縮排參考線,父列帶有三態核取方塊(`IsSelectable`),排序會重排同層列而不破壞階層,子列可以在第一次展開時載入(`ChildrenLoader`)。顯示出來的第一欄承載樹。鍵盤遵循樹狀格線模式:↑/↓ 移動,→ 展開或進入,← 收合或轉到父層,Home/End,PageUp/PageDown 移動十列,`*` 展開所有同層列,Space 選取,Enter 切換父列或啟動葉節點,Ctrl/Cmd+A 全選。列來自 `ItemsSource` 和 `ChildrenSelector`;每一列由 `KeySelector` 識別(未設定時為列項目本身),`ExpandedKeys` 和 `SelectedKeys` 儲存的就是這些索引鍵。與 Arc 一樣,列不做虛擬化:每個可見列都存在並帶有動畫,因此展開的樹請保持在幾百列以內,更深的層級按需載入。 @@ -20832,6 +26088,226 @@ A node of a `Treemap`: a leaf with a value, or a branch whose value is the sum of its children. `Treemap` 的節點:葉節點帶有一個值,或分支節點的值為其子節點之和。 + + A trial status banner (Arc Trial banner, design reference): a ring counts down the days left, the copy names what the workspace loses, and the upgrade path is one click away. It comes as a slim `Banner` row, a sidebar `Card` with the feature list, or a header `Pill` that opens the card in a popover. All three share one state model: active, the warning tone in the last `WarnAt` days (the copy then names the end day), expired (a lock) and upgraded (a check). Dismiss and snooze fold the banner into the trial pill, which opens it again. Give it `DaysLeft` from your billing provider, or `EndsAt` and let it count the local calendar days with `TimeProvider` (it recounts at local midnight). `AddPaymentHandler` opens your checkout: return once a payment method is on file to show the upgraded state, throw to show Try again. Without it the call is simulated (900ms). With neither `DaysLeft` nor `EndsAt` there is no trial to show: the banner reads "No trial in progress" over an empty ring, without actions. The plan, the workspace, the loss and the features are your app's data and start empty. + 試用狀態橫幅(Arc Trial banner,設計參考):圓環倒數剩餘天數,文案說明工作區將失去什麼,升級只需點擊一次。它可作為細長的 `Banner` 橫幅、附功能清單的側邊欄 `Card`,或在彈出層中開啟卡片的標題列 `Pill`。三者共用一套狀態:進行中、最後 `WarnAt` 天的警告色調(文案會說出結束的那一天)、已結束(鎖頭圖示)和已升級(勾號)。關閉與稍後提醒會把橫幅收合為試用膠囊,點擊膠囊可再次展開。傳入來自計費服務的 `DaysLeft`,或傳入 `EndsAt`,由它藉助 `TimeProvider` 按本地日曆日計數(在本地午夜重新計數)。`AddPaymentHandler` 開啟你的結帳流程:付款方式登錄後傳回即顯示已升級狀態,擲回例外則顯示 Try again。沒有它時該呼叫為模擬(900ms)。`DaysLeft` 與 `EndsAt` 都沒有時沒有可顯示的試用:橫幅在空的圓環上方顯示 "No trial in progress",沒有操作按鈕。方案、工作區、失去的內容與功能都是你的應用程式的資料,預設為空。 + + + Opens checkout or the billing portal. Return once a payment method is on file to show the upgraded state; throw to show the error state with Try again. The token is cancelled when the banner leaves the window. Null simulates it (900ms). + 開啟結帳頁面或計費入口。付款方式登錄後傳回即顯示已升級狀態;擲回例外則顯示附 Try again 的錯誤狀態。橫幅離開視窗時權杖會被取消。為 null 時以模擬方式執行(900ms)。 + + + After a dismiss or snooze: fold into the trial pill, which opens the banner again, or render nothing. + 關閉或稍後提醒之後:收合為試用膠囊(點擊可再次展開橫幅),或什麼也不顯示。 + + + Raised when the person presses Compare plans. Bubbles. Open your plans page and set Handled; unhandled, the banner shows a short simulated note. + 使用者按下「比較方案」時引發。向上傳遞。開啟你的方案頁面並設定 Handled;未處理時,橫幅顯示一則簡短的模擬提示。 + + + Whole days left, from your billing provider; 0 or less shows the expired state. Null counts them from `EndsAt` and today; with neither, the sample shows 9. + 剩餘的整天數,來自你的計費服務;0 或更小顯示已結束狀態。為 null 時根據 `EndsAt` 和今天計數;兩者都沒有時,範例顯示 9。 + + + Raised when the person dismisses the banner. Bubbles. Persist it. + 使用者關閉橫幅時引發。向上傳遞。請儲存此狀態。 + + + When the trial ends, for the date copy (and the days left when `DaysLeft` is null). Null uses a sample date: October 27, 2026 plus the days left. Pass your real end date. + 試用結束的時間,用於文案中的日期(`DaysLeft` 為 null 時也用於計算剩餘天數)。為 null 時使用範例日期:2026 年 10 月 27 日加上剩餘天數。請傳入真實的結束日期。 + + + Whether the banner is folded after a dismiss or snooze (two-way). Set it to false to show the banner again. + 橫幅在關閉或稍後提醒後是否已收合(雙向)。設為 false 可再次顯示橫幅。 + + + Shows the dismiss button. + 顯示關閉按鈕。 + + + Shows the snooze menu. + 顯示稍後提醒選單。 + + + Whether a payment method is on file (two-way). Set by a successful `AddPaymentHandler`. + 是否已登錄付款方式(雙向)。`AddPaymentHandler` 成功後會設定它。 + + + Every word the banner shows or says. + 橫幅顯示或朗讀的所有文字。 + + + One short phrase for the banner copy ("SAML SSO and 12 seats"). Keep it short so the banner stays on one line. Empty by default. + 橫幅文案中的一個簡短片語("SAML SSO and 12 seats")。請保持簡短,讓橫幅保持一行。預設為空。 + + + Raised when the person presses Manage billing in the upgraded state. Bubbles. Set Handled; unhandled, the banner shows a short simulated note. + 已升級狀態下使用者按下「管理帳單」時引發。向上傳遞。請設定 Handled;未處理時,橫幅顯示一則簡短的模擬提示。 + + + The plan being trialed ("Pro"), used in every line of copy. Empty by default. + 正在試用的方案("Pro"),每行文案都會用到。預設為空。 + + + Raised with the chosen snooze and when it ends. Bubbles. Persist `Until` and stop rendering the banner until then. + 附帶所選的稍後提醒選項及其結束時間引發。向上傳遞。請儲存 `Until`,並在此之前不再顯示橫幅。 + + + Where the trial is: active, warning, expired or upgraded. + 試用所處的狀態:進行中、警告、已結束或已升級。 + + + Where "today" comes from when the days are counted from `EndsAt`, and the snooze dates. The system clock by default. + 根據 `EndsAt` 計數天數以及計算稍後提醒日期時,「今天」從何而來。預設為系統時鐘。 + + + The trial's length in days. The ring shows the days left of it. + 試用期的天數。圓環顯示其中剩餘的天數。 + + + Banner (a slim row), Card (a sidebar card with the features) or Pill (a header pill that opens the card). + Banner(細長橫幅)、Card(附功能清單的側邊欄卡片)或 Pill(開啟卡片的標題列膠囊)。 + + + The last N days switch to the warning tone, with copy that names the end day. + 最後 N 天切換為警告色調,文案會說出結束的那一天。 + + + The workspace's name, used in the expired and upgraded copy. Empty (the default) reads "Your workspace". + 工作區名稱,用於已結束和已升級狀態的文案。為空(預設)時寫作 "Your workspace"。 + + + Every word `TrialBanner` shows or says, in English by default. Replace the whole object to localize or reword; lines that carry values are format functions. Dates are local calendar days. + `TrialBanner` 顯示或朗讀的所有文字,預設為英文。替換整個物件即可在地化或改寫;帶有數值的行是格式化函式。日期為本地日曆日。 + + + One thing the workspace loses without paying, listed in the card of a `TrialBanner`. + 工作區不付費就會失去的一項內容,列在 `TrialBanner` 的卡片中。 + + + A short muted detail after the name ("12 seats"), or null. + 名稱後面一段簡短的次要說明("12 seats"),或 null。 + + + The 14px mark before the name (a 24 × 24 stroked glyph); a check when null. + 名稱前的 14px 圖示(24 × 24 描邊圖形);為 null 時顯示勾號。 + + + A stable id for the row. + 該列的穩定 id。 + + + The feature's name ("SAML single sign-on"). + 功能名稱("SAML single sign-on")。 + + + The arguments of `Snoozed`: the chosen option and when the snooze ends. + `Snoozed` 的參數:所選選項及稍後提醒的結束時間。 + + + The upgrade nudge shown the moment someone hits a limit (Arc Upgrade prompt, design reference): a title that says what was used, a usage meter, the next plan's price rolling between monthly and yearly with a billing toggle, the three things the plan unlocks, and Maybe later beside Upgrade. Upgrading runs `UpgradeHandler` and rolls into a success state with the new limits while the meter settles to the new limit; a failure shows its message under the price with Try again; Maybe later shows a calm reminder with See Pro again. Every state shares one cell, so the card never changes size between offer, processing, error, success and reminder and the buttons stay in place. Your app gives the plan and the usage: with no `Plan` the card is empty ("No plan to offer", no meter and no offer, Upgrade and Maybe later off). With no handler it simulates a 1.1s upgrade that charges nothing. + 在使用者用完額度的那一刻顯示的升級提示(Arc Upgrade prompt,設計參考):說明用掉了什麼的標題、用量條、在月繳和年繳之間滾動並附計費切換的下一級方案價格、方案解鎖的三項內容,以及並排的 Maybe later 和 Upgrade。升級會執行 `UpgradeHandler`,並滾入列出新額度的成功狀態,同時用量條落到新的上限;失敗時在價格下方顯示訊息並提供 Try again;Maybe later 顯示一則安靜的提醒和 See Pro again。所有狀態共用一個儲存格,因此卡片在報價、處理中、錯誤、成功和提醒之間從不改變尺寸,按鈕也待在原位。方案與用量由你的應用程式提供:沒有 `Plan` 時卡片為空("No plan to offer",沒有用量條也沒有報價,Upgrade 與 Maybe later 無法使用)。沒有處理常式時,它模擬一次 1.1 秒、不收取任何費用的升級。 + + + Raised by Continue on the success state, usually to close the prompt and retry the blocked action. Set `Handled` to keep the success state; left unhandled, the prompt resets to the offer. Bubbles. + 由成功狀態的 Continue 引發,通常用於關閉提示並重試被擋下的操作。設定 `Handled` 可保留成功狀態;未處理時,提示重設回報價狀態。會冒泡。 + + + Replaces the line under the offer's title. Null writes it from the plan and the reset date. + 取代報價狀態標題下方的那行文字。為 null 時根據方案和重設日期產生。 + + + Raised by Maybe later; record the snooze here. The prompt then shows the reminder. Bubbles. + 由 Maybe later 引發;請在這裡記錄暫緩。提示隨後顯示提醒。會冒泡。 + + + The message of the last failed upgrade while the prompt is in the error state, otherwise null. + 提示處於錯誤狀態時為上一次升級失敗的訊息,否則為 null。 + + + The billing interval (two-way). Yearly by default. + 計費週期(雙向)。預設為 Yearly。 + + + Raised when the person changes the billing toggle. Bubbles. + 使用者變更計費切換時引發。會冒泡。 + + + Every word the prompt shows or announces. + 提示顯示或朗讀的每個詞。 + + + The current plan's limit. At or over it the meter reads Limit reached; below it, how many are left. + 目前方案的上限。達到或超過時用量條顯示 Limit reached;低於時顯示還剩多少。 + + + The plan offered. Null (the default) shows the empty card: nothing to offer. + 提供的方案。為 null(預設)時顯示空卡片:沒有可提供的內容。 + + + When the current limit resets ("Nov 1"). Shown in the description, the meter and the reminder; empty by default. + 目前額度何時重設("Nov 1")。顯示在說明、用量條和提醒中;預設為空。 + + + What the prompt shows: the offer, processing, an error, success or the reminder. + 提示顯示的內容:報價、處理中、錯誤、成功或提醒。 + + + Replaces the limit title of the offer, such as "You've used all 3 seats". Null writes it from the usage. + 取代報價狀態的上限標題,例如 "You've used all 3 seats"。為 null 時根據用量產生。 + + + What is counted, plural ("AI credits", "seats", "generations"). Used in the title, the meter and announcements. + 計數的對象,複數形式("AI credits"、"seats"、"generations")。用於標題、用量條和朗讀。 + + + Raised when the upgrade handler threw, with the exception. Bubbles. + 升級處理常式擲回例外時引發,附帶該例外。會冒泡。 + + + Starts the upgrade with the selection (plan, interval, amount charged today, currency). Complete the task when the plan is active; throw to show the exception's message under the price with Try again. The token is cancelled when the prompt leaves the visual tree. Null simulates a 1.1s upgrade that charges nothing. + 以所選內容(方案、計費週期、今天收取的金額、幣別)開始升級。方案生效後完成工作;擲回例外則在價格下方顯示例外訊息並提供 Try again。提示離開視覺樹時權杖會被取消。為 null 時模擬一次 1.1 秒、不收取任何費用的升級。 + + + Raised once the upgrade succeeded, after the success state shows. Bubbles. + 升級成功、成功狀態顯示之後引發。會冒泡。 + + + How much of the limit is used. + 已用掉上限中的多少。 + + + Arguments of `UpgradeFailed`. + `UpgradeFailed` 的引數。 + + + One unlock an `UpgradePromptPlan` lists in the offer, such as more credits or a better model. + `UpgradePromptPlan` 在報價中列出的一項解鎖內容,例如更多點數或更好的模型。 + + + Arguments of `IntervalChanged`. + `IntervalChanged` 的引數。 + + + Every word an `UpgradePrompt` shows or announces, in English by default. Replace the instance to translate or reword; sentences with values are functions so the word order can change. Counts and amounts arrive already formatted ("2,000", "$192"); `unit` is the prompt's `Unit` and `plan` the plan's name. + `UpgradePrompt` 顯示或朗讀的每個詞,預設為英文。取代該執行個體即可翻譯或改寫;帶值的句子是函式,因此可以調整語序。計數和金額傳入時已格式化("2,000"、"$192");`unit` 是提示的 `Unit`,`plan` 是方案名稱。 + + + One of the new limits an `UpgradePrompt` lists once the upgrade succeeded. + 升級成功後 `UpgradePrompt` 列出的一項新額度。 + + + The plan an `UpgradePrompt` offers: its name, the monthly price and the per month price when billed yearly (the savings badge and the yearly charge derive from them), the new limit, the unlocks and the new limits. + `UpgradePrompt` 提供的方案:名稱、月繳價格和按年繳費時的每月價格(節省徽章和年繳金額由它們算出)、新的上限、解鎖內容和新的額度。 + + + What the person chose: the plan, the interval, the amount charged today and its currency. + 使用者的選擇:方案、計費週期、今天收取的金額及其幣別。 + + + Arguments of `Upgraded`. + `Upgraded` 的引數。 + One day of a service's history, derived from the incidents. 一項服務歷史中的一天,由事故推算而來。 @@ -21700,6 +27176,114 @@ The header value. 標頭的值。 + + One attempt in the recent list: what was sent, and what came back. + 最近清單中的一次嘗試:傳送了什麼,以及回傳了什麼。 + + + An event type the tester can send, with the JSON payload it starts from. + 測試器可以傳送的事件類型,以及它起始的 JSON 負載。 + + + The signed request `WebhookTester` hands to `SendAction`. Send it from your server, not from the browser: the server can re-sign the same body and check that the signature matches. + `WebhookTester` 交給 `SendAction` 的已簽章請求。請從你的伺服器傳送它,而不是從瀏覽器:伺服器可以對同一份本文重新簽章,並核對簽章是否一致。 + + + What an endpoint answered. A `Status` of 0 means no answer arrived: set `Error` (and `TimedOut` when it ran out of time). + 端點的回應。`Status` 為 0 表示沒有收到回應:請設定 `Error`(逾時則同時設定 `TimedOut`)。 + + + A tester for the endpoints of a product that sends webhooks: pick an event, edit its JSON payload and send a signed test event. The request timeline walks from signed to sent to the answer, the response panel shows the body, the headers and the exact request, and a list of recent deliveries keeps the last attempts, failed ones with a Retry button. The block signs in the app (HMAC SHA-256 of `timestamp.body`, sent as `t=…,v1=…` in `SignatureHeader`) but never posts to your customer's endpoint itself: `SendAction` receives the signed `WebhookTestRequest` and passes it to your server, which does the real POST. Without a `SendAction` every delivery is simulated and says so in its announcements. + Webhook 端點測試器:選擇事件、編輯 JSON 負載並傳送帶簽章的測試事件。請求時間軸從已簽章走到已傳送再到回應,回應面板顯示本文、回應標頭和確切的請求,最近投遞清單保留最近的嘗試,失敗的帶有 Retry 按鈕。區塊在應用程式內簽章(對 `timestamp.body` 做 HMAC SHA-256,以 `t=…,v1=…` 放入 `SignatureHeader`),但它自己絕不會向客戶的端點發出請求:`SendAction` 收到已簽章的 `WebhookTestRequest` 並交給你的伺服器,由伺服器真正送出 POST。未設定 `SendAction` 時,每次投遞都是模擬的,並在播報中說明。 + + + Sends one sample event about a second after the block scrolls half into view, unless someone used it first. Only while `SendAction` is null. + 區塊捲動到一半進入視窗約一秒後傳送一個範例事件,除非使用者已經操作過它。僅在 `SendAction` 為 null 時生效。 + + + The widest the card gets (760); the block fills narrower containers. + 卡片的最大寬度(760);更窄的容器中區塊會填滿容器。 + + + Raised after a send or a retry finished, with the new delivery (already at the top of the list). + 傳送或重試完成後觸發,攜帶新的投遞(已位於清單頂端)。 + + + Run with a `WebhookTestDelivery` after a send or a retry finished. + 傳送或重試完成後以 `WebhookTestDelivery` 為參數執行。 + + + The recent deliveries, newest first; a send or a retry inserts at the top. + 最近的投遞,最新的在前;傳送或重試會插入到頂端。 + + + The endpoint test events go to (two-way). https is required, except http for localhost. + 測試事件發往的端點(雙向)。必須是 https,localhost 允許 http。 + + + The events to choose from; while it is empty five sample events are offered. Choosing an event loads its payload. + 可供選擇的事件;為空時提供五個範例事件。選擇事件會載入它的負載。 + + + Draws a bordered card with rounded corners (true); false lays the block flat on the page. + 繪製帶圓角邊框的卡片(true);false 則讓區塊平鋪在頁面上。 + + + Every word the block shows or announces. + 區塊顯示或播報的每一個詞。 + + + The payload as it stands in the editor (two-way), valid JSON or not. + 編輯器中目前的負載(雙向),無論是否為合法 JSON。 + + + Where the timeline stands. + 時間軸目前所處的階段。 + + + Sends a failed delivery again on your server: awaited when Retry is pressed, with the delivery's body under a fresh timestamp and signature. Null uses `SendAction`. + 在你的伺服器上重新傳送失敗的投遞:按下 Retry 時被等待,使用該投遞的本文,並帶上新的時間戳記和簽章。為 null 時使用 `SendAction`。 + + + The signing secret. It is shown masked with its prefix visible; the body is signed with it in the app. + 簽章密鑰。顯示時會被遮罩,前綴可見;區塊在應用程式內用它對本文簽章。 + + + The delivery whose response the panel shows (two-way); a send selects the new delivery. + 面板顯示其回應的投遞(雙向);傳送後會選取新的投遞。 + + + The chosen event (two-way); null selects the first one. + 目前選擇的事件(雙向);為 null 時選取第一個。 + + + Sends the signed request on your server: awaited when the person sends, with a token that leaving the window cancels. Return the endpoint's answer; a status of 0 with `Error` (and `TimedOut`) says no answer arrived, and an exception counts as a failed delivery. Null simulates the endpoint (a 200 after `SimulatedLatency`). + 在你的伺服器上傳送已簽章的請求:使用者傳送時被等待,並帶有離開視窗即取消的權杖。回傳端點的回應;狀態為 0 並帶有 `Error`(以及 `TimedOut`)表示沒有收到回應,擲回例外同樣算作失敗的投遞。為 null 時模擬端點(經過 `SimulatedLatency` 後回傳 200)。 + + + The header that carries the signature (`x-webhook-signature`). + 攜帶簽章的標頭(`x-webhook-signature`)。 + + + How long a simulated delivery takes to answer (0.65 s). + 模擬投遞的回應耗時(0.65 秒)。 + + + The product that sends the events: its name and mark in the header. + 傳送事件的產品:頁首中顯示它的名稱和標誌。 + + + From this width of the block the request and the response sit side by side (680). + 區塊達到這個寬度時,請求與回應並排顯示(680)。 + + + Every word `WebhookTester` shows or announces, in English by default. Replace the instance to translate or reword; texts with values are functions so that word order can change. + `WebhookTester` 顯示或播報的每一個詞,預設為英文。替換該實例即可翻譯或改寫;帶數值的文字是函式,因此語序可以改變。 + + + The product that sends the events: a name for the subtitle and a mark for the header. + 傳送事件的產品:副標題使用的名稱和頁首使用的標誌。 + One slice of an endpoint's traffic chart (Arc shows 48 half hours): how many events were sent in it and how many of those still fail. 端點流量圖表中的一個時間片(Arc 顯示 48 個半小時):該時間片內傳送了多少事件,其中有多少仍然失敗。 @@ -22320,6 +27904,46 @@ The canvas the buttons zoom. 按鈕所縮放的畫布。 + + A small book or magazine that opens on its cover (Arc Page curl, design reference). Tap the right page to go forward and the left page to go back, or drag a page: it folds along a line that follows the pointer, showing its back, a lit curl and a shadow on the page below. A page count with previous and next buttons sits underneath. + 一本從封面開始翻開的小書或小雜誌(Arc Page curl,設計參考)。輕點右頁向前翻,輕點左頁向後翻,或者拖曳書頁:它沿著跟隨指標的摺線翻摺,露出背面、一道被照亮的捲邊,以及落在下一頁上的陰影。下方有頁碼以及上一頁、下一頁按鈕。 + + + The hint under the lower outer corner, shown until the first interaction. Null hides it. + 位於外側下角下方的提示,在第一次互動之前顯示。設為 Null 則隱藏。 + + + The accessible name of the book. + 書的無障礙名稱。 + + + A page's width divided by its height. + 頁寬除以頁高。 + + + The template that builds a page from an item that is not a control. + 為非控制項項目產生頁面的範本。 + + + Whether the page count with previous and next buttons shows under the book. + 是否在書下方顯示頁碼以及上一頁、下一頁按鈕。 + + + The spread on show: 0 is the closed cover, spread `n` shows pages `2n` and `2n + 1`. Two-way. + 目前顯示的跨頁:0 為合上的封面,第 `n` 個跨頁顯示第 `2n` 與 `2n + 1` 頁。雙向。 + + + Raised when a page finishes turning, by a drag, a tap, a key or a button. + 一頁翻完時觸發,無論由拖曳、點按、按鍵還是按鈕引起。 + + + The number of spreads: the closed cover plus one per two pages. + 跨頁的數量:合上的封面加上每兩頁一個。 + + + Whether the cover corner curls once when the book first comes into view, and a little on hover until the first turn. + 書第一次進入視野時封面一角是否捲起一次,以及在第一次翻頁之前懸停時是否輕輕捲起。 + diff --git a/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/en-US.md new file mode 100644 index 00000000..b79856c5 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/en-US.md @@ -0,0 +1,142 @@ +--- +title: Domain verify +summary: A custom domain setup card where customers add a domain and copy the DNS records, then a live checker marks each record valid, not found or misconfigured with a one-line fix and finishes with the domain verified and its SSL certificate issued. +--- + + + +## When to use + +`DomainVerify` is the "Custom domain" step of a product's settings. A customer types a domain, gets the DNS records to +create with a copy button on each, and then watches a live checker work through them: + +- **Letting customers serve an app, docs site, status page or storefront from their own domain.** +- **Verifying a sending domain for email** (SPF, DKIM, DMARC) before turning on delivery. +- **Any setup step where a person must copy values into a third party and wait for propagation.** + +## When not to use + +- **Buying or searching domains.** Use a search field with results instead. +- **Managing many domains at once.** Use a table with one row per domain and open this card for one of them. +- **A one-off copy of a single value.** A copy button next to the value is enough. + +## Usage + +The card derives from Avalonia's `TemplatedControl`. It needs no data: with no action set it plays a simulated flow +(add the domain, check it, find a conflicting record, fix it, verified) once it is on screen, and any click or key press +inside it hands control to the person. The status line announces "Simulated, no DNS was queried" while that is so. + +```xml + + + +``` + +### The flow + +`Phase` tells where the card is, and `PhaseChanged` reports every change: + +1. **Enter**: the domain field and "Add domain". Behind them, dimmed placeholder rows and the line "DNS records appear + here after you add a domain". Protocols, paths, ports and trailing dots are stripped from what was typed before it is + checked. +2. **Adding**: the button shows a spinner while `GetRecordsAction` runs. +3. **Records**: the domain row replaces the field, the records are listed, and the footer offers "Check records". +4. **Checking**: the resolvers are polled every `PollInterval`. Rows that are not valid yet show a spinner; the footer + shimmers and counts the regions that propagated. +5. **Invalid**: a record holds a conflicting value. The polling stops, the row says "Misconfigured" and a hint opens + under it with what was found and the fix. "Check again" starts a new run. +6. **Verified**: every record is valid. The domain row says "Issuing SSL certificate" until `IssueCertificateAction` + returns, then "SSL by Let's Encrypt · renews in 60 days". `Verified` is raised, and "Add another" starts over. + +### Connecting your server + +Three async delegates stand in for your backend. Each receives a cancellation token that removing the domain or the card +leaving the window cancels. + +```csharp +card.GetRecordsAction = async (domain, cancellation) => + await api.AddDomainAsync(projectId, domain, cancellation); // IReadOnlyList + +card.VerifyAction = async (domain, records, context, cancellation) => + await api.CheckDomainAsync(projectId, domain, cancellation); // DomainVerifyResult + +card.IssueCertificateAction = async (domain, cancellation) => + await api.IssueCertificateAsync(projectId, domain, cancellation); // DomainCertificate + +card.Verified += (_, e) => Console.WriteLine($"{e.Domain} is live"); +``` + +`VerifyAction` is one check, not a loop. Return `DomainRecordStatus.Missing` while a record is still propagating, so the +card keeps polling, and `DomainRecordStatus.Invalid` only for a conflicting value, with `Found` and a one-sentence `Fix`. +The card stops on "all valid" or on any invalid record. `PollInterval` is 800 ms while the check is simulated and +5 seconds with a `VerifyAction`. An exception from any delegate is shown in the footer, raises `Failed` and leaves the +card ready for another try. + + + +### Narrow containers + +The card fills its container up to 680 wide. Below 544 the column header leaves and each record stacks: the type, name +and status on the first line, the value with its copy button below, then the purpose. Long names and values truncate with +an ellipsis and keep their full text in a tooltip; the copy button always copies the whole value. + + + +### Sending domains + +For email, return MX, SPF (TXT), DKIM (CNAME or TXT) and DMARC records from `GetRecordsAction`, set `Title` to +"Sending domain" and reword the card with `Labels`: the last step then reads "Turning on delivery" instead of issuing a +certificate. Pass `Provider="{x:Null}"` to hide the "DNS hosted at" line when you do not know the host. + + + +### Texts + +Every word the card shows or announces is a property of `DomainVerifyLabels`; assign a new instance to `Labels` to +translate or reword. Formatted texts, such as the footer's "Checking records · 2 of 6 regions propagated", are functions +so that word order can change. + +## API reference + + + + + + + + + + + + + +## Accessibility + +- The card is named by its title, a level 2 heading. The records are a list named "DNS records"; each row reads its type, + name and status ("CNAME app, Valid"). +- Every status is an icon plus a short word, never a color alone. The conflicting record's hint is part of its row. +- A polite live region announces each step: the domain was added, checking, a record needs a fix, the domain is verified, + the certificate was issued, a failure. While no `VerifyAction` is set it adds "Simulated, no DNS was queried". +- The domain field is labelled "Domain"; a bad domain shows an error under it. Copy buttons are named "Copy TXT value", + "Copy CNAME value" and so on. The dimmed placeholder rows are inert and hidden from screen readers. + +### Keyboard + +- **Enter** in the domain field adds the domain, as "Add domain" does. +- **Tab** moves through the field and button, then the close button, each record's copy button and the footer action. +- **Enter** or **Space** activates a focused copy, close or footer button. +- After a domain is added the focus moves to the footer action; after it is removed it returns to the domain field. + +## Motion + +- **Status**: the status words and icons crossfade (100 ms out, 160 ms in, rising 2 px); a checking row shows a spinner and + the footer a shimmer. +- **Rows**: the records fade in over 160 ms, each 40 ms after the one before. The fix hint opens on the smooth spring. +- Reduced motion keeps every state, drops the stagger and only fades briefly. Off applies every change at once. A simulated flow only plays while the card is on screen. + +## Related + +- `CopyButton`, `Button`, `Input` and `TextShimmer`, the controls the card is made of. +- API keys, Webhooks, Integrations, Env variables and Webhook tester, the other Developer blocks. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/zh-CN.md new file mode 100644 index 00000000..e29bf64b --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/zh-CN.md @@ -0,0 +1,120 @@ +--- +title: Domain verify +summary: 自定义域名设置卡片:客户添加域名并复制 DNS 记录,随后实时检查器把每条记录标为有效、未找到或配置错误并给出一行修复建议,最后完成域名验证并签发 SSL 证书。 +source-hash: ab584178445a +--- + + + +## 何时使用 + +`DomainVerify` 是产品设置里的"自定义域名"一步。客户输入域名后会得到需要创建的 DNS 记录(每条都带复制按钮),然后看着实时检查器逐条处理: + +- **让客户用自己的域名访问应用、文档站、状态页或店铺。** +- **在开启邮件投递之前验证发信域名**(SPF、DKIM、DMARC)。 +- **任何需要用户把值复制到第三方、再等待生效的设置步骤。** + +## 何时不使用 + +- **购买或搜索域名。** 请改用带结果列表的搜索框。 +- **同时管理很多域名。** 请用每个域名一行的表格,再为其中一个打开此卡片。 +- **只需一次性复制单个值。** 在值旁边放一个复制按钮就够了。 + +## 用法 + +该卡片派生自 Avalonia 的 `TemplatedControl`,不需要任何数据:没有设置任何动作时,它出现在屏幕上后会播放一段模拟流程(添加域名、检查、发现冲突记录、修复、验证通过),在其中点击或按键就会把控制权交给用户。此时状态行会播报"Simulated, no DNS was queried"。 + +```xml + + + +``` + +### 流程 + +`Phase` 表示卡片所处的阶段,每次变化都会触发 `PhaseChanged`: + +1. **Enter**:域名输入框和"Add domain"按钮;后面是变暗的占位行和一句"DNS records appear here after you add a domain"。输入的内容在检查之前会先去掉协议、路径、端口和末尾的点。 +2. **Adding**:`GetRecordsAction` 运行期间,按钮显示加载圈。 +3. **Records**:域名行取代输入框,列出各条记录,页脚提供"Check records"。 +4. **Checking**:每隔 `PollInterval` 轮询解析器。尚未生效的行显示加载圈;页脚闪烁并统计已生效的区域数。 +5. **Invalid**:某条记录的值有冲突。轮询停止,该行显示"Misconfigured",下方展开一条提示,说明找到了什么以及如何修复。"Check again"开始新一轮检查。 +6. **Verified**:所有记录均有效。域名行显示"Issuing SSL certificate",直到 `IssueCertificateAction` 返回,随后显示"SSL by Let's Encrypt · renews in 60 days"。此时触发 `Verified`,"Add another"重新开始。 + +### 连接你的服务器 + +三个异步委托代表你的后端。每个委托都会收到取消令牌,移除域名或卡片离开窗口时会取消它。 + +```csharp +card.GetRecordsAction = async (domain, cancellation) => + await api.AddDomainAsync(projectId, domain, cancellation); // IReadOnlyList + +card.VerifyAction = async (domain, records, context, cancellation) => + await api.CheckDomainAsync(projectId, domain, cancellation); // DomainVerifyResult + +card.IssueCertificateAction = async (domain, cancellation) => + await api.IssueCertificateAsync(projectId, domain, cancellation); // DomainCertificate + +card.Verified += (_, e) => Console.WriteLine($"{e.Domain} is live"); +``` + +`VerifyAction` 只做一次检查,不要写成循环。记录仍在生效时返回 `DomainRecordStatus.Missing`,卡片会继续轮询;只有值冲突时才返回 `DomainRecordStatus.Invalid`,并附上 `Found` 和一句话的 `Fix`。卡片在"全部有效"或出现任何无效记录时停止。模拟检查时 `PollInterval` 为 800 毫秒,设置了 `VerifyAction` 时为 5 秒。任一委托抛出的异常会显示在页脚、触发 `Failed`,并让卡片保持可重试状态。 + + + +### 窄容器 + +卡片会填满容器,最宽 680。宽度低于 544 时列标题消失,每条记录改为堆叠:第一行是类型、名称和状态,下面是值和复制按钮,再下面是用途说明。过长的名称和值以省略号截断,完整内容保留在工具提示里;复制按钮始终复制完整的值。 + + + +### 发信域名 + +对于邮件,请从 `GetRecordsAction` 返回 MX、SPF(TXT)、DKIM(CNAME 或 TXT)和 DMARC 记录,把 `Title` 设为"Sending domain",并用 `Labels` 改写文案:这样最后一步显示"Turning on delivery",而不是签发证书。不知道 DNS 托管方时,设置 `Provider="{x:Null}"` 即可隐藏"DNS hosted at"一行。 + + + +### 文案 + +卡片显示或播报的每个词都是 `DomainVerifyLabels` 的属性;给 `Labels` 赋一个新实例即可翻译或改写。带格式的文本(例如页脚的"Checking records · 2 of 6 regions propagated")是函数,因此语序可以调整。 + +## API 参考 + + + + + + + + + + + + + +## 无障碍 + +- 卡片以标题(二级标题)命名。记录是名为"DNS records"的列表;每一行读出类型、名称和状态("CNAME app, Valid")。 +- 每种状态都是图标加一个简短的词,绝不只靠颜色。冲突记录的提示是该行的一部分。 +- 礼貌型实时区域播报每一步:域名已添加、正在检查、某条记录需要修复、域名已验证、证书已签发、失败。未设置 `VerifyAction` 时会附加"Simulated, no DNS was queried"。 +- 域名输入框的标签是"Domain";域名无效时下方显示错误。复制按钮的名称是"Copy TXT value"、"Copy CNAME value"等。变暗的占位行不可交互,并对读屏隐藏。 + +### 键盘 + +- 在域名输入框中按 **Enter** 添加域名,与"Add domain"相同。 +- **Tab** 依次经过输入框和按钮、关闭按钮、每条记录的复制按钮和页脚操作。 +- 在获得焦点的复制、关闭或页脚按钮上按 **Enter** 或 **Space** 即可触发。 +- 添加域名后焦点移到页脚操作;移除后焦点回到域名输入框。 + +## 动效 + +- **状态**:状态文字和图标交叉淡化(100 毫秒淡出、160 毫秒淡入,上升 2 像素);检查中的行显示加载圈,页脚显示闪光。 +- **行**:记录在 160 毫秒内淡入,每一行比前一行晚 40 毫秒。修复提示按平滑弹簧展开。 +- 减少动效时保留所有状态,去掉错开效果,只做短暂淡化。关闭动效时所有变化立即生效。模拟流程只在卡片位于屏幕内时播放。 + +## 相关 + +- `CopyButton`、`Button`、`Input` 和 `TextShimmer`,组成该卡片的控件。 +- API keys、Webhooks、Integrations、Env variables 和 Webhook tester,Developer 分组里的其他区块。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/zh-TW.md new file mode 100644 index 00000000..8ec09975 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Blocks/DomainVerify/zh-TW.md @@ -0,0 +1,120 @@ +--- +title: Domain verify +summary: 自訂網域設定卡片:客戶新增網域並複製 DNS 記錄,接著即時檢查器把每筆記錄標為有效、找不到或設定錯誤並給出一行修正建議,最後完成網域驗證並簽發 SSL 憑證。 +source-hash: ab584178445a +--- + + + +## 何時使用 + +`DomainVerify` 是產品設定中的「自訂網域」一步。客戶輸入網域後會得到需要建立的 DNS 記錄(每筆都附有複製按鈕),然後看著即時檢查器逐筆處理: + +- **讓客戶用自己的網域存取應用程式、文件站、狀態頁或商店。** +- **在開啟郵件投遞之前驗證寄信網域**(SPF、DKIM、DMARC)。 +- **任何需要使用者把值複製到第三方、再等待生效的設定步驟。** + +## 何時不使用 + +- **購買或搜尋網域。** 請改用附結果清單的搜尋框。 +- **同時管理很多網域。** 請用每個網域一列的表格,再為其中一個開啟此卡片。 +- **只需一次性複製單一值。** 在值旁邊放一個複製按鈕就夠了。 + +## 用法 + +此卡片衍生自 Avalonia 的 `TemplatedControl`,不需要任何資料:沒有設定任何動作時,它出現在畫面上後會播放一段模擬流程(新增網域、檢查、發現衝突記錄、修正、驗證通過),在其中點擊或按鍵就會把控制權交給使用者。此時狀態列會播報「Simulated, no DNS was queried」。 + +```xml + + + +``` + +### 流程 + +`Phase` 表示卡片所處的階段,每次變化都會觸發 `PhaseChanged`: + +1. **Enter**:網域輸入框和「Add domain」按鈕;後面是變暗的預留列和一句「DNS records appear here after you add a domain」。輸入的內容在檢查之前會先去掉通訊協定、路徑、連接埠和結尾的點。 +2. **Adding**:`GetRecordsAction` 執行期間,按鈕顯示載入圈。 +3. **Records**:網域列取代輸入框,列出各筆記錄,頁尾提供「Check records」。 +4. **Checking**:每隔 `PollInterval` 輪詢解析器。尚未生效的列顯示載入圈;頁尾閃爍並統計已生效的區域數。 +5. **Invalid**:某筆記錄的值有衝突。輪詢停止,該列顯示「Misconfigured」,下方展開一則提示,說明找到了什麼以及如何修正。「Check again」開始新一輪檢查。 +6. **Verified**:所有記錄均有效。網域列顯示「Issuing SSL certificate」,直到 `IssueCertificateAction` 回傳,隨後顯示「SSL by Let's Encrypt · renews in 60 days」。此時觸發 `Verified`,「Add another」重新開始。 + +### 連接你的伺服器 + +三個非同步委派代表你的後端。每個委派都會收到取消權杖,移除網域或卡片離開視窗時會取消它。 + +```csharp +card.GetRecordsAction = async (domain, cancellation) => + await api.AddDomainAsync(projectId, domain, cancellation); // IReadOnlyList + +card.VerifyAction = async (domain, records, context, cancellation) => + await api.CheckDomainAsync(projectId, domain, cancellation); // DomainVerifyResult + +card.IssueCertificateAction = async (domain, cancellation) => + await api.IssueCertificateAsync(projectId, domain, cancellation); // DomainCertificate + +card.Verified += (_, e) => Console.WriteLine($"{e.Domain} is live"); +``` + +`VerifyAction` 只做一次檢查,不要寫成迴圈。記錄仍在生效時回傳 `DomainRecordStatus.Missing`,卡片會繼續輪詢;只有值衝突時才回傳 `DomainRecordStatus.Invalid`,並附上 `Found` 和一句話的 `Fix`。卡片在「全部有效」或出現任何無效記錄時停止。模擬檢查時 `PollInterval` 為 800 毫秒,設定了 `VerifyAction` 時為 5 秒。任一委派丟出的例外會顯示在頁尾、觸發 `Failed`,並讓卡片保持可重試狀態。 + + + +### 窄容器 + +卡片會填滿容器,最寬 680。寬度低於 544 時欄標題消失,每筆記錄改為堆疊:第一行是類型、名稱和狀態,下面是值和複製按鈕,再下面是用途說明。過長的名稱和值以省略號截斷,完整內容保留在工具提示裡;複製按鈕一律複製完整的值。 + + + +### 寄信網域 + +對於郵件,請從 `GetRecordsAction` 回傳 MX、SPF(TXT)、DKIM(CNAME 或 TXT)和 DMARC 記錄,把 `Title` 設為「Sending domain」,並用 `Labels` 改寫文字:這樣最後一步顯示「Turning on delivery」,而不是簽發憑證。不知道 DNS 代管方時,設定 `Provider="{x:Null}"` 即可隱藏「DNS hosted at」一行。 + + + +### 文字 + +卡片顯示或播報的每個詞都是 `DomainVerifyLabels` 的屬性;給 `Labels` 指派一個新執行個體即可翻譯或改寫。帶格式的文字(例如頁尾的「Checking records · 2 of 6 regions propagated」)是函式,因此語序可以調整。 + +## API 參考 + + + + + + + + + + + + + +## 無障礙 + +- 卡片以標題(二級標題)命名。記錄是名為「DNS records」的清單;每一列讀出類型、名稱和狀態(「CNAME app, Valid」)。 +- 每種狀態都是圖示加一個簡短的詞,絕不只靠顏色。衝突記錄的提示是該列的一部分。 +- 禮貌型即時區域播報每一步:網域已新增、正在檢查、某筆記錄需要修正、網域已驗證、憑證已簽發、失敗。未設定 `VerifyAction` 時會附加「Simulated, no DNS was queried」。 +- 網域輸入框的標籤是「Domain」;網域無效時下方顯示錯誤。複製按鈕的名稱是「Copy TXT value」、「Copy CNAME value」等。變暗的預留列不可互動,並對讀屏隱藏。 + +### 鍵盤 + +- 在網域輸入框中按 **Enter** 新增網域,與「Add domain」相同。 +- **Tab** 依序經過輸入框和按鈕、關閉按鈕、每筆記錄的複製按鈕和頁尾操作。 +- 在取得焦點的複製、關閉或頁尾按鈕上按 **Enter** 或 **Space** 即可觸發。 +- 新增網域後焦點移到頁尾操作;移除後焦點回到網域輸入框。 + +## 動態效果 + +- **狀態**:狀態文字和圖示交叉淡化(100 毫秒淡出、160 毫秒淡入,上升 2 像素);檢查中的列顯示載入圈,頁尾顯示閃光。 +- **列**:記錄在 160 毫秒內淡入,每一列比前一列晚 40 毫秒。修正提示依平滑彈簧展開。 +- 減少動態效果時保留所有狀態,去掉錯開效果,只做短暫淡化。關閉動態效果時所有變化立即生效。模擬流程只在卡片位於畫面內時播放。 + +## 相關 + +- `CopyButton`、`Button`、`Input` 和 `TextShimmer`,組成此卡片的控制項。 +- API keys、Webhooks、Integrations、Env variables 和 Webhook tester,Developer 分組裡的其他區塊。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/en-US.md new file mode 100644 index 00000000..4ac8160d --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/en-US.md @@ -0,0 +1,156 @@ +--- +title: Webhook tester +summary: A webhook tester where you pick an event, edit its JSON payload and send a signed test event, watch it go from signed to sent to the endpoint's answer, read the response and retry failed deliveries from a recent list. +--- + + + +## When to use + +`WebhookTester` is the "Test endpoint" screen of a product that sends webhooks (billing, auth, email, CI). A person picks +an event, adjusts its JSON payload and sends a signed test event; the block shows it going from signed to sent to the +answer, and keeps the last attempts in a list. + +- **Testing before going live.** Customers prove that their handler answers with a 2xx and that it checks the signature. +- **Integration onboarding.** The block signs with the endpoint's secret in the app, so what the handler receives is what + it will receive in production. +- **Replaying failed deliveries.** A failed row has a Retry button that sends the same body again under a fresh timestamp + and signature. + +## When not to use + +- **Managing many endpoints, filters and event subscriptions.** Use the Webhooks block for that and link here to test one + endpoint. +- **A general HTTP client.** The block always sends a signed POST with a JSON body; it has no method or header editor. +- **Event logs with thousands of rows.** The list keeps the last 20 attempts. A paginated log belongs in a data table. + +## Usage + +The block derives from Avalonia's `TemplatedControl`. Give it the endpoint, the events a person can pick (a type and a +default JSON payload each) and the endpoint's signing secret. + +```xml + + + +``` + +```csharp +Tester.Events.Add(new WebhookTestEvent("invoice.paid", """{"id":"evt_1","type":"invoice.paid","amount":4900}""")); +Tester.Events.Add(new WebhookTestEvent("invoice.failed", """{"id":"evt_2","type":"invoice.failed","amount":4900}""")); +``` + +While `Events` is empty the block offers five sample events. Choosing an event loads its payload: valid JSON is pretty +printed, anything else is kept as is. `SelectedEvent` and `Payload` are two-way, so a page can read or preset them. + +### Sending on your server + +The block signs the body in the app (HMAC SHA-256 of `timestamp.body`, sent as `t=…,v1=…` in `SignatureHeader`) but never +posts to your customer's endpoint itself: a browser or a desktop app should not call an arbitrary URL on a customer's +behalf. `SendAction` receives the signed `WebhookTestRequest` and passes it to your server, which does the real POST and +returns a `WebhookTestResponse`. The server can sign the same body again and check that the signatures are identical. + +```csharp +Tester.SendAction = async (request, cancellation) => +{ + var result = await api.PostAsJsonAsync($"endpoints/{endpointId}/test", request, cancellation); + return await result.Content.ReadFromJsonAsync(cancellation) + ?? new WebhookTestResponse { Error = "Empty answer" }; +}; +``` + +A `Status` of 0 with `Error` (and `TimedOut` when the endpoint ran out of time) means no answer arrived; an exception counts +as a failed delivery too. Either way the delivery joins the list as failed, the timeline ends in red and the failure is +announced. `RetryAction` sends a failed delivery again; without it Retry uses `SendAction` with a fresh signature. + + + +Without a `SendAction` every delivery is simulated: a 200 after `SimulatedLatency`, and the announcements say that nothing +was sent. With `AutoPlay` (default) the simulated block sends one event about a second after it scrolls half into view, +unless the person has already used it. Call `SendAsync` and `RetryAsync` to do the same from code. + +### Other products + +`Source` names the product that sends the events (the subtitle reads "Signed test events from Relay Mail") and can supply +its own 16 px mark with `MarkFactory`; without one the block draws a rounded square with the first letter. Set +`SignatureHeader` to the header your product uses. + + + +### The payload editor + +The editor tints JSON as it is typed, shows line numbers, and says in words whether the payload parses and how many bytes +it is ("Valid JSON · 327 bytes"; when it does not parse, the line and the problem). Enter keeps the indentation and indents +after `{` or `[`. **Format** pretty prints valid JSON. A payload that does not parse cannot be sent: the editor takes the +focus and the problem is announced. The endpoint must be https; http is allowed for `localhost` and loopback addresses. + +### Narrow containers + +The block follows its own width. From 680 the request (endpoint, event, secret, payload) and the response (timeline, tabs) +sit side by side and the editor stretches to match the response column; below, they stack. Below 420 the delivery times +leave the list and the paddings tighten from 20 to 16. + + + +### Texts + +Every word the block shows or announces is in `Labels`, a `WebhookTesterLabels` with English defaults. Texts that include a +value are functions, so word order can change: + +```csharp +Tester.Labels = new WebhookTesterLabels +{ + Send = "Send", + FailedCount = count => $"{count} did not arrive", +}; +``` + +## API reference + + + + + + + + + + + + + + + +## Accessibility + +- Every field has a visible label. The payload editor is named by its event ("Payload for order.paid"); while the JSON + does not parse, its help text is the problem and the line, which the footer also shows. +- The signing secret is hidden from assistive technology until it is revealed; the reveal button is a toggle. +- The response tabs are a tab list with a roving tab stop; the panel is focusable and read only. +- Delivery rows are buttons named by event, outcome and time, in a list; the selected row is checked. Status chips pair + the code or word with colour. +- Each send and its outcome are announced politely, with "Simulated, nothing was sent." added while no `SendAction` is set. + +### Keyboard + +- **Ctrl+Enter** (**Command+Enter** on a Mac) sends from anywhere in the block; **Enter** in the endpoint field sends too. +- **Enter** in the payload editor starts a new line with the same indentation. +- **Left** and **Right** arrows, **Home** and **End** move between the Body, Headers and Request tabs. +- **Tab** visits the endpoint, the event list, the reveal button, the copy buttons, the payload, Format, Send, the + selected tab, the response panel and each failed row's Retry button. + +## Motion + +- The timeline's circles change with a short fade; the line between two steps fills from left to right, and while the + request is in flight a dot runs along the last line until the answer arrives. +- A new delivery joins the top of the list and is selected; the response text swaps at once. +- Reduced motion keeps the end states and drops the dot and the fills' travel; Off applies every change at once. + +## Related + +- `Webhooks`, the console for many endpoints, and `ApiKeys`, the other Developer blocks. +- `CopyButton`, `Button`, `Input` and `Select`, the controls the block is made of. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/zh-CN.md new file mode 100644 index 00000000..17742c97 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/zh-CN.md @@ -0,0 +1,131 @@ +--- +title: Webhook tester +summary: Webhook 测试器:选择事件、编辑 JSON 负载并发送带签名的测试事件,观察它从已签名、已发送到端点作出应答的全过程,查看响应,并在最近投递列表中重试失败的投递。 +source-hash: b80e9fc5bad2 +--- + + + +## 何时使用 + +`WebhookTester` 是发送 Webhook 的产品(计费、认证、邮件、CI)中的"测试端点"页面。用户选择一个事件,调整它的 JSON 负载并发送带签名的测试事件;区块展示它从已签名、已发送到收到应答的过程,并在列表中保留最近的几次尝试。 + +- **上线前测试。** 客户确认自己的处理程序会返回 2xx,并且会校验签名。 +- **集成引导。** 区块在应用内用端点的密钥签名,因此处理程序收到的内容与生产环境完全一致。 +- **重放失败的投递。** 失败的行带有 Retry 按钮,会用新的时间戳和签名重新发送同一份内容。 + +## 何时不用 + +- **管理大量端点、过滤器和事件订阅。** 这类场景请使用 Webhooks 区块,并从那里链接到本区块来测试某个端点。 +- **通用 HTTP 客户端。** 本区块始终发送带签名的 POST 与 JSON 正文,没有方法或请求头编辑器。 +- **成千上万行的事件日志。** 列表只保留最近 20 次尝试。分页日志应交给数据表格。 + +## 用法 + +本区块派生自 Avalonia 的 `TemplatedControl`。给它端点、用户可选的事件(每个包含类型和默认 JSON 负载)以及端点的签名密钥。 + +```xml + + + +``` + +```csharp +Tester.Events.Add(new WebhookTestEvent("invoice.paid", """{"id":"evt_1","type":"invoice.paid","amount":4900}""")); +Tester.Events.Add(new WebhookTestEvent("invoice.failed", """{"id":"evt_2","type":"invoice.failed","amount":4900}""")); +``` + +`Events` 为空时,区块提供五个示例事件。选择事件会载入它的负载:合法的 JSON 会被格式化,其他内容原样保留。`SelectedEvent` 与 `Payload` 是双向的,页面可以读取或预设它们。 + +### 在你的服务器上发送 + +区块在应用内对正文签名(对 `timestamp.body` 做 HMAC SHA-256,以 `t=…,v1=…` 放入 `SignatureHeader`),但它自己绝不会向客户的端点发请求:浏览器或桌面应用不应代表客户调用任意网址。`SendAction` 收到已签名的 `WebhookTestRequest` 并交给你的服务器,由服务器真正发出 POST,再返回 `WebhookTestResponse`。服务器可以用同一份正文重新签名,核对两个签名是否一致。 + +```csharp +Tester.SendAction = async (request, cancellation) => +{ + var result = await api.PostAsJsonAsync($"endpoints/{endpointId}/test", request, cancellation); + return await result.Content.ReadFromJsonAsync(cancellation) + ?? new WebhookTestResponse { Error = "Empty answer" }; +}; +``` + +`Status` 为 0 并带有 `Error`(端点超时则同时设 `TimedOut`)表示没有收到应答;抛出异常同样算作失败的投递。无论哪种情况,该投递都会以失败状态进入列表,时间线以红色结束,并播报失败。`RetryAction` 用于重新发送失败的投递;未设置时,Retry 使用 `SendAction` 并带上新的签名。 + + + +未设置 `SendAction` 时,每次投递都是模拟的:经过 `SimulatedLatency` 后返回 200,播报内容会说明没有真正发送任何请求。启用 `AutoPlay`(默认)时,模拟状态的区块在滚动到一半进入视口约一秒后发送一个事件,除非用户已经操作过它。也可以在代码中调用 `SendAsync` 和 `RetryAsync`。 + +### 其他产品 + +`Source` 给出发送事件的产品名称(副标题显示为 "Signed test events from Relay Mail"),并可通过 `MarkFactory` 提供自己的 16 px 标志;没有时,区块会画一个带首字母的圆角方块。把 `SignatureHeader` 设为你的产品所用的请求头。 + + + +### 负载编辑器 + +编辑器在输入时为 JSON 着色,显示行号,并用文字说明负载能否解析以及字节数("Valid JSON · 327 bytes";无法解析时给出行号和原因)。回车保持缩进,并在 `{` 或 `[` 之后加深缩进。**Format** 会格式化合法的 JSON。无法解析的负载不能发送:编辑器获得焦点并播报问题。端点必须是 https;`localhost` 和回环地址允许使用 http。 + +### 窄容器 + +区块跟随自身宽度。宽度达到 680 时,请求(端点、事件、密钥、负载)与响应(时间线、标签页)并排显示,编辑器拉伸到与响应列等高;更窄时上下堆叠。宽度低于 420 时,列表中的投递时间隐藏,内边距从 20 收紧为 16。 + + + +### 文案 + +区块显示或播报的每个词都在 `Labels` 中,它是带英文默认值的 `WebhookTesterLabels`。包含数值的文案是函数,因此语序可以改变: + +```csharp +Tester.Labels = new WebhookTesterLabels +{ + Send = "Send", + FailedCount = count => $"{count} did not arrive", +}; +``` + +## API 参考 + + + + + + + + + + + + + + + +## 无障碍 + +- 每个字段都有可见标签。负载编辑器以事件命名("Payload for order.paid");JSON 无法解析时,它的帮助文本是问题所在及行号,页脚也会显示。 +- 签名密钥在显示之前对辅助技术隐藏;显示按钮是一个开关。 +- 响应标签页是带漫游焦点的标签列表;面板可获得焦点且只读。 +- 投递行是按钮,以事件、结果和时间命名,位于一个列表中;选中的行处于选中状态。状态片把状态码或文字与颜色搭配。 +- 每次发送及其结果都会礼貌地播报;未设置 `SendAction` 时附加 "Simulated, nothing was sent."。 + +### 键盘 + +- **Ctrl+Enter**(Mac 上为 **Command+Enter**)在区块内任何位置发送;端点输入框中按 **Enter** 同样发送。 +- 负载编辑器中按 **Enter** 换行并保持相同缩进。 +- **左**、**右**方向键以及 **Home**、**End** 在 Body、Headers、Request 标签页之间移动。 +- **Tab** 依次经过端点、事件列表、显示按钮、复制按钮、负载、Format、Send、当前标签页、响应面板,以及每个失败行的 Retry 按钮。 + +## 动效 + +- 时间线的圆圈以短促的淡变切换;两步之间的线从左向右填充,请求在途时,最后一段线上有一个圆点循环移动,直到收到应答。 +- 新的投递出现在列表顶部并被选中;响应文本立即替换。 +- 减弱动效保留终态,去掉圆点和填充的行进过程;关闭动效则所有变化立即生效。 + +## 相关 + +- `Webhooks`(管理多个端点的控制台)和 `ApiKeys`,同属 Developer 区块。 +- `CopyButton`、`Button`、`Input` 和 `Select`,组成本区块的控件。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/zh-TW.md new file mode 100644 index 00000000..985f3877 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Blocks/WebhookTester/zh-TW.md @@ -0,0 +1,131 @@ +--- +title: Webhook tester +summary: Webhook 測試器:選擇事件、編輯 JSON 負載並傳送帶簽章的測試事件,觀察它從已簽章、已傳送到端點作出回應的全過程,檢視回應,並在最近投遞清單中重試失敗的投遞。 +source-hash: b80e9fc5bad2 +--- + + + +## 何時使用 + +`WebhookTester` 是傳送 Webhook 的產品(計費、驗證、郵件、CI)中的「測試端點」頁面。使用者選擇一個事件,調整它的 JSON 負載並傳送帶簽章的測試事件;區塊會顯示它從已簽章、已傳送到收到回應的過程,並在清單中保留最近的幾次嘗試。 + +- **上線前測試。** 客戶確認自己的處理程式會回傳 2xx,並且會驗證簽章。 +- **整合引導。** 區塊在應用程式內以端點的密鑰簽章,因此處理程式收到的內容與正式環境完全一致。 +- **重放失敗的投遞。** 失敗的列帶有 Retry 按鈕,會以新的時間戳記和簽章重新傳送同一份內容。 + +## 何時不用 + +- **管理大量端點、篩選器和事件訂閱。** 這類情境請使用 Webhooks 區塊,並從那裡連結到本區塊來測試某個端點。 +- **通用 HTTP 用戶端。** 本區塊一律傳送帶簽章的 POST 與 JSON 本文,沒有方法或標頭編輯器。 +- **成千上萬列的事件記錄。** 清單只保留最近 20 次嘗試。分頁記錄應交給資料表格。 + +## 用法 + +本區塊衍生自 Avalonia 的 `TemplatedControl`。給它端點、使用者可選的事件(每個包含類型和預設 JSON 負載)以及端點的簽章密鑰。 + +```xml + + + +``` + +```csharp +Tester.Events.Add(new WebhookTestEvent("invoice.paid", """{"id":"evt_1","type":"invoice.paid","amount":4900}""")); +Tester.Events.Add(new WebhookTestEvent("invoice.failed", """{"id":"evt_2","type":"invoice.failed","amount":4900}""")); +``` + +`Events` 為空時,區塊會提供五個範例事件。選擇事件會載入它的負載:合法的 JSON 會被格式化,其他內容原樣保留。`SelectedEvent` 與 `Payload` 是雙向的,頁面可以讀取或預設它們。 + +### 在你的伺服器上傳送 + +區塊在應用程式內對本文簽章(對 `timestamp.body` 做 HMAC SHA-256,以 `t=…,v1=…` 放入 `SignatureHeader`),但它自己絕不會向客戶的端點發出請求:瀏覽器或桌面應用程式不應代表客戶呼叫任意網址。`SendAction` 收到已簽章的 `WebhookTestRequest` 並交給你的伺服器,由伺服器真正送出 POST,再回傳 `WebhookTestResponse`。伺服器可以用同一份本文重新簽章,核對兩個簽章是否一致。 + +```csharp +Tester.SendAction = async (request, cancellation) => +{ + var result = await api.PostAsJsonAsync($"endpoints/{endpointId}/test", request, cancellation); + return await result.Content.ReadFromJsonAsync(cancellation) + ?? new WebhookTestResponse { Error = "Empty answer" }; +}; +``` + +`Status` 為 0 並帶有 `Error`(端點逾時則同時設定 `TimedOut`)表示沒有收到回應;擲回例外同樣算作失敗的投遞。無論哪種情況,該投遞都會以失敗狀態進入清單,時間軸以紅色結束,並播報失敗。`RetryAction` 用於重新傳送失敗的投遞;未設定時,Retry 使用 `SendAction` 並帶上新的簽章。 + + + +未設定 `SendAction` 時,每次投遞都是模擬的:經過 `SimulatedLatency` 後回傳 200,播報內容會說明沒有真正傳送任何請求。啟用 `AutoPlay`(預設)時,模擬狀態的區塊在捲動到一半進入視窗約一秒後傳送一個事件,除非使用者已經操作過它。也可以在程式碼中呼叫 `SendAsync` 和 `RetryAsync`。 + +### 其他產品 + +`Source` 給出傳送事件的產品名稱(副標題顯示為 "Signed test events from Relay Mail"),並可透過 `MarkFactory` 提供自己的 16 px 標誌;沒有時,區塊會畫一個帶首字母的圓角方塊。把 `SignatureHeader` 設為你的產品所用的標頭。 + + + +### 負載編輯器 + +編輯器在輸入時為 JSON 上色,顯示行號,並用文字說明負載能否解析以及位元組數("Valid JSON · 327 bytes";無法解析時會給出行號和原因)。Enter 會保持縮排,並在 `{` 或 `[` 之後加深縮排。**Format** 會格式化合法的 JSON。無法解析的負載不能傳送:編輯器會取得焦點並播報問題。端點必須是 https;`localhost` 和迴路位址允許使用 http。 + +### 窄容器 + +區塊會跟隨自身寬度。寬度達到 680 時,請求(端點、事件、密鑰、負載)與回應(時間軸、分頁)並排顯示,編輯器會拉伸到與回應欄等高;更窄時上下堆疊。寬度低於 420 時,清單中的投遞時間會隱藏,內距從 20 收緊為 16。 + + + +### 文案 + +區塊顯示或播報的每個詞都在 `Labels` 中,它是帶英文預設值的 `WebhookTesterLabels`。包含數值的文案是函式,因此語序可以改變: + +```csharp +Tester.Labels = new WebhookTesterLabels +{ + Send = "Send", + FailedCount = count => $"{count} did not arrive", +}; +``` + +## API 參考 + + + + + + + + + + + + + + + +## 無障礙 + +- 每個欄位都有可見標籤。負載編輯器以事件命名("Payload for order.paid");JSON 無法解析時,它的說明文字是問題所在及行號,頁尾也會顯示。 +- 簽章密鑰在顯示之前對輔助技術隱藏;顯示按鈕是一個切換開關。 +- 回應分頁是帶漫遊焦點的分頁清單;面板可取得焦點且唯讀。 +- 投遞列是按鈕,以事件、結果和時間命名,位於一個清單中;選取的列處於選取狀態。狀態標籤把狀態碼或文字與顏色搭配。 +- 每次傳送及其結果都會禮貌地播報;未設定 `SendAction` 時附加 "Simulated, nothing was sent."。 + +### 鍵盤 + +- **Ctrl+Enter**(Mac 上為 **Command+Enter**)在區塊內任何位置傳送;端點輸入框中按 **Enter** 同樣會傳送。 +- 負載編輯器中按 **Enter** 換行並保持相同縮排。 +- **左**、**右**方向鍵以及 **Home**、**End** 在 Body、Headers、Request 分頁之間移動。 +- **Tab** 依序經過端點、事件清單、顯示按鈕、複製按鈕、負載、Format、Send、目前分頁、回應面板,以及每個失敗列的 Retry 按鈕。 + +## 動態效果 + +- 時間軸的圓圈以短促的淡變切換;兩步之間的線從左向右填滿,請求在途時,最後一段線上有一個圓點循環移動,直到收到回應。 +- 新的投遞出現在清單頂端並被選取;回應文字立即替換。 +- 減少動態效果會保留最終狀態,去掉圓點和填滿的行進過程;關閉動態效果則所有變化立即生效。 + +## 相關 + +- `Webhooks`(管理多個端點的主控台)和 `ApiKeys`,同屬 Developer 區塊。 +- `CopyButton`、`Button`、`Input` 和 `Select`,組成本區塊的控制項。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/en-US.md index 07363a59..c42039a5 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/en-US.md @@ -120,6 +120,15 @@ Use `IsLoading` for work that finishes in a few seconds. For a longer job, such progress in the page (for example a progress indicator or a status line) and keep the button for the request that starts the job. A spinner that runs for a minute tells the person nothing about how far the work has come. +`LabelSwapMode` and `LabelSpring` change how the label morphs, for a button that should match quieter copy around it. +`LabelSwapMode="Label"` trades labels with a plain crossfade (100 ms out, 160 ms in with a 2 px rise, no blur), and +`LabelSpring="Quick"` lets the width glide to the new label without the slight bounce of the default `Morph` spring. +Both are opt-in; without them every button keeps the blur swap and the morph spring. + +```xml + +``` + ### Icon-only button An icon-only button has no visible text, so give it an accessible name. A button that is 48 px wide or narrower is treated diff --git a/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-CN.md index ec012b92..612ae4b4 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-CN.md @@ -1,7 +1,7 @@ --- title: 按钮 summary: 清晰、响应迅速的操作按钮,次要状态保持低调。 -source-hash: 2e7052f3cb01 +source-hash: a6c5d133a709 --- @@ -93,6 +93,12 @@ source-hash: 2e7052f3cb01 `IsLoading` 适用于几秒内完成的工作。对于耗时更长的任务,例如导出数千行数据,应在页面中显示进度(例如进度指示器或状态行),让按钮只负责发起任务的请求。转了一分钟的加载圆环无法告诉人们工作进展到了哪里。 +`LabelSwapMode` 和 `LabelSpring` 改变标签变形的方式,适合需要与周围较安静的文案保持一致的按钮。`LabelSwapMode="Label"` 以普通交叉淡化替换标签(100 ms 淡出,160 ms 淡入并上升 2 px,没有模糊),`LabelSpring="Quick"` 让宽度滑向新标签,不带默认 `Morph` 弹簧的轻微回弹。两者都需要显式设置;不设置时,每个按钮仍使用模糊替换和变形弹簧。 + +```xml + +``` + ### 纯图标按钮 纯图标按钮没有可见文字,因此要为它提供无障碍名称。宽度为 48 px 或更窄的按钮被视为图标按钮:按下时会比文字按钮更深,压至其尺寸的 96%。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-TW.md index df861431..ed599fbb 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Buttons/Button/zh-TW.md @@ -1,7 +1,7 @@ --- title: 按鈕 summary: 清楚、反應迅速的操作按鈕,次要狀態保持低調。 -source-hash: 2e7052f3cb01 +source-hash: a6c5d133a709 --- @@ -93,6 +93,12 @@ source-hash: 2e7052f3cb01 `IsLoading` 適用於幾秒內完成的工作。對於耗時更長的任務,例如匯出數千列資料,應在頁面中顯示進度(例如進度指示器或狀態列),讓按鈕只負責發起任務的請求。轉了一分鐘的載入圓環無法告訴人們工作進展到了哪裡。 +`LabelSwapMode` 和 `LabelSpring` 改變標籤變形的方式,適合需要與周圍較安靜的文案保持一致的按鈕。`LabelSwapMode="Label"` 以一般交叉淡化替換標籤(100 ms 淡出,160 ms 淡入並上升 2 px,沒有模糊),`LabelSpring="Quick"` 讓寬度滑向新標籤,不帶預設 `Morph` 彈簧的輕微回彈。兩者都需要明確設定;不設定時,每個按鈕仍使用模糊替換和變形彈簧。 + +```xml + +``` + ### 純圖示按鈕 純圖示按鈕沒有可見文字,因此要為它提供無障礙名稱。寬度為 48 px 或更窄的按鈕被視為圖示按鈕:按下時會比文字按鈕更深,壓至其尺寸的 96%。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/en-US.md index 8449f160..5798b202 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/en-US.md @@ -51,6 +51,11 @@ from the Arc core, which does the digit work. The counter adds the label, the af its theme sets the font, size, weight and foreground of the number, so you rarely need to set those yourself. Because it is a template, you can restyle the counter with a `ControlTheme` in the same way as any other Arc control. +`RollingDigits` also works on its own, for a number inside a layout of your own. Its wheels turn on the `Smooth` spring; +`WheelSpring` picks another Arc preset, and `WheelMotion` takes any spring or timed ease as a `MotionSpec`, such as +`MotionSpec.Spring(SpringSpec.FromVisual(0.28, 0))` or `MotionSpec.Tween(TimeSpan.FromMilliseconds(240), easing)`. +Reduced and Off motion still change the digits at once. + `Value` takes the raw number, not a formatted string. The counter formats it with `Decimals`, `CultureName`, `Prefix` and `Suffix`, so the same value can show "$48,250" with en-US or "48.250" with de-DE, without changing the data. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-CN.md index ebb44890..a62d3223 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-CN.md @@ -1,7 +1,7 @@ --- title: 动画计数器 summary: 让变化的总数具有清晰的动感。 -source-hash: 87248781031b +source-hash: bf322a01198b --- @@ -37,6 +37,8 @@ source-hash: 87248781031b `AnimatedCounter` 派生自 Avalonia 的 `TemplatedControl`。其模板由一个标签和位于其上的 `RollingDigits` 组成,后者是 Arc 核心中负责数位滚动的基础件。计数器本身加入标签、前后缀以及首次查看时的显现效果;它的主题会设置数字的字体、字号、字重和前景色,因此你很少需要自行设置这些属性。由于它使用模板,你可以像对待其他 Arc 控件一样,用 `ControlTheme` 重新设计计数器的外观。 +`RollingDigits` 也可以单独使用,放在你自己的布局里显示数字。它的数位滚轮默认按 `Smooth` 弹簧转动;`WheelSpring` 可选其他 Arc 预设,`WheelMotion` 接受任意弹簧或定时缓动的 `MotionSpec`,例如 `MotionSpec.Spring(SpringSpec.FromVisual(0.28, 0))` 或 `MotionSpec.Tween(TimeSpan.FromMilliseconds(240), easing)`。减弱动效和关闭动效时数位仍立即变化。 + `Value` 接收原始数字,而不是格式化后的字符串。计数器会用 `Decimals`、`CultureName`、`Prefix` 和 `Suffix` 对它进行格式化,因此同一个值在 en-US 下可显示为“$48,250”,在 de-DE 下可显示为“48.250”,而数据本身无需改变。 ### 结构 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-TW.md index 339e41e2..0ad10f51 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/Charts/AnimatedCounter/zh-TW.md @@ -1,7 +1,7 @@ --- title: 動畫計數器 summary: 讓變動的總數具有清楚的動感。 -source-hash: 87248781031b +source-hash: bf322a01198b --- @@ -37,6 +37,8 @@ source-hash: 87248781031b `AnimatedCounter` 衍生自 Avalonia 的 `TemplatedControl`。其範本由一個標籤和位於其上的 `RollingDigits` 組成,後者是 Arc 核心中負責數位捲動的基礎件。計數器本身加入標籤、前後綴以及首次檢視時的顯現效果;它的佈景主題會設定數字的字型、字級、字重和前景色,因此你很少需要自行設定這些屬性。由於它使用範本,你可以像對待其他 Arc 控制項一樣,用 `ControlTheme` 重新設計計數器的外觀。 +`RollingDigits` 也可以單獨使用,放在你自己的版面裡顯示數字。它的數位滾輪預設按 `Smooth` 彈簧轉動;`WheelSpring` 可選其他 Arc 預設,`WheelMotion` 接受任意彈簧或定時緩動的 `MotionSpec`,例如 `MotionSpec.Spring(SpringSpec.FromVisual(0.28, 0))` 或 `MotionSpec.Tween(TimeSpan.FromMilliseconds(240), easing)`。減少動態效果和關閉動態效果時數位仍立即變化。 + `Value` 接收原始數字,而不是格式化後的字串。計數器會用 `Decimals`、`CultureName`、`Prefix` 和 `Suffix` 對它進行格式化,因此同一個值在 en-US 下可顯示為「$48,250」,在 de-DE 下可顯示為「48.250」,而資料本身無需改變。 ### 結構 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/en-US.md new file mode 100644 index 00000000..af83aa1e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/en-US.md @@ -0,0 +1,158 @@ +--- +title: Orbit logos +summary: An integrations orbit: your product mark in the centre, logos on two slowly turning rings, and a card that morphs out of the logo you pick. +--- + + + +## When to use + +`OrbitLogos` shows everything your product plugs into as logos on two rings around your own mark. The rings turn +slowly and hold still whenever the pointer or keyboard focus is inside. Picking a logo glides it to a fixed spot, draws +a line to it from the centre and opens a card with what the integration does, its status and a Connect action. + +- **An integrations page or a hero**: a lively picture of the ecosystem that still lets people look closely. +- **An integrations screen in your app**: browse by category, read about a tool and connect it in one place. +- **A launch page**: everything the product works with, in one glance. + +## When not to use + +- **Logos that are only social proof**: if nobody will open them, a quiet row of marks says it with less motion. +- **A big catalogue**: two rings get crowded past a couple of dozen marks, and people want to search a big list; use + a grid or a table with a search field. +- **When the order or a search matters**: the rings turn, so nothing stays in a fixed place. + +## Usage + +```xml + + + + + + +``` + +`OrbitLogos` is a templated control in the Special package. Register the package with `UseArcSpecial()` next to +`UseArcControls()`. The logos are its content: each `OrbitLogo` has a `Name`, an optional `Id` (selections use the `Id`, +or the name when there is none), an `Icon`, a `Category`, a one-line `Description`, a starting `IsConnected` and an +`Href` for the Docs link. The orbit fills the width up to 460px; from 760px wide it sits beside the detail panel, below +that the card sits under the orbit. + +### Marks + +`Icon` takes SVG path data on a 24 by 24 grid (the Simple Icons format), an image address (a `.png`, `.jpg` or `.webp` +file, or an http(s), `avares`, file or data URL), an `ImageSource` or any control. Path data follows the text color, so a +black-and-white brand needs no color; set `Color` on the logo to paint it in the brand color. Images keep their own +colors. A control as `Icon` shows on the ring only; the card then falls back to the first letter of the name. + +Brand marks are sample content. Show only integrations that exist and follow each brand's trademark rules. + +### Categories + +Give every logo a `Category` and the chips appear above the orbit; `AllLabel` renames the first chip. A chip turns the +matching logos toward the detail side and dims the rest, and the arrow keys then step only through that category. A +category that does not contain the open logo closes its card. + +### Opening, connecting and the centre + +`DefaultSelected` opens a logo on first display; `SelectedId` is the open logo's id or name, two-way, and `null` closes the +card. `SelectionChanged` reports `Logo` and `Index` when a person opens, steps or closes, with no logo and index -1 when +the card closes. + +```csharp +orbit.ConnectHandler = async logo => await api.ConnectAsync(logo.Id ?? logo.Name); +``` + +The Connect button calls `ConnectHandler` (or a `ConnectRequested` handler that sets `Completion`). The card shows +Connecting while the task runs, marks the logo connected when it completes and shows Try again if it faults. Without a +handler the card simulates a short connection. + + + +`Center` is the content of the centre disc, such as your product mark; it defaults to a simple mark in the accent color. +`Speed` multiplies the rotation (at 1 the inner ring turns once every 64 seconds and the outer ring, the other way, +every 88 seconds; 0 holds the orbit still) and `IsPaused` holds it for a page-level motion control. `Label` is the +accessible name of the logo list and the title of the idle panel. + + + +### Best practices + +**Do** + +- Put your product mark in the centre and give every logo a category and a one-line description. +- Wire `ConnectHandler` to your real flow and return its task. +- Keep to about a dozen logos so the rings stay readable. + +**Avoid** + +- Logos without a name: the name is the accessible name and the tag people see. +- Using the orbit as the only way to reach an integration; keep the settings screen it belongs to. + +### Common questions + +**Why does the orbit stop when I move over it?** It eases to rest while the pointer or focus is inside, a category is +chosen or a card is open, so nothing moves while you aim. + +**Do name tags show on touch screens?** No. Tags appear on hover and keyboard focus only. + +## API reference + + + + + +## Accessibility + +- The logos are a labelled list of toggle buttons; each name includes the category and whether it is connected. +- Only one logo is in the tab order, so the orbit is a single stop between the chips and the card. +- The card is a labelled group with real buttons and a Docs link. +- Connected status is shown as text and an icon in the card, not by the dot on the chip alone. +- Opening, stepping, filtering and connecting are announced politely. +- Pointer focus draws no ring; keyboard focus shows the shared focus ring, the lifted chip and its name tag. + +### Keyboard + +| Key | Action | +|---|---| +| Tab | Moves into the category chips, then to one logo in the orbit, then into the card. The orbit holds still while focus is inside. | +| Arrow keys | Step to the previous or next logo in the current category. With a card open, the card and the ring follow. | +| Home, End | Jump to the first or last logo in the current category. | +| Enter, Space | Opens the focused logo, or closes it if it is already open. | +| Escape | Closes the card and returns focus to its logo. | + +## Motion + +- One loop moves the chips with render transforms and sleeps when everything is at rest, the orbit is out of view or the + window is hidden. +- The turn eases to rest on a critically damped spring whenever the pointer or focus enters, a category is picked or a + card is open. +- Opening a logo springs its ring the short way, with a touch of overshoot, until the logo is at the fixed spot: on the + right of the orbit when it sits beside the detail panel, at the bottom otherwise. When an outer logo parks, the inner + ring turns a gap to that side so the connector stays clear. +- An accent connector grows from the centre to the chosen logo. +- The card grows out of the chip: it starts as a circle at the chip's box and springs to its own box, and then its + content fades in. Previous and next slide the content in the direction of travel. +- The category highlight is one pill that glides between the chips on the shape spring. + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Rotation | Turns slowly, eases to rest | None | None | +| Rings and connector | Spring into place | Placed at once | Placed at once | +| Card | Morphs out of the chip | Appears | Appears | +| Previous and next | Slide | Swap | Swap | + +## Related + +- `HoverCard` previews a person or link on hover without leaving the page. +- `Carousel` browses a row of slides instead of a ring of logos. diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/zh-CN.md new file mode 100644 index 00000000..0601ed39 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/zh-CN.md @@ -0,0 +1,134 @@ +--- +title: Orbit logos +summary: 集成轨道:你的产品标志在中央,标志排在两条缓慢转动的环上,选中的标志会长出一张详情卡片。 +source-hash: 98316a6d1296 +--- + + + +## 何时使用 + +`OrbitLogos` 把产品能接入的一切以标志的形式排在两条环上,环绕着你自己的标志。环缓慢转动,指针或键盘焦点进入时就停下。选中一个标志会让它滑到固定位置,从中央向它画一条线,并打开一张卡片,说明这个集成做什么、当前状态,以及连接操作。 + +- **集成页面或首屏**:生动呈现生态,又允许人们仔细查看。 +- **应用内的集成页面**:在同一处按类别浏览、了解工具并连接。 +- **发布页**:一眼看到产品能配合的所有工具。 + +## 何时不用 + +- **只用来背书的标志**:如果没人会打开它们,一排安静的标志就够了,动得更少。 +- **很大的目录**:两条环放到二三十个标志就会拥挤,而且大列表需要搜索;请用带搜索框的网格或表格。 +- **顺序或搜索很重要时**:环在转动,没有东西停留在固定位置。 + +## 用法 + +```xml + + + + + + +``` + +`OrbitLogos` 是 Special 包中的模板化控件。在 `UseArcControls()` 旁边调用 `UseArcSpecial()` 注册该包。标志就是它的内容:每个 `OrbitLogo` 有 `Name`、可选的 `Id`(选择以 `Id` 为准,没有时用名称)、`Icon`、`Category`、一行 `Description`、起始的 `IsConnected`,以及用于 Docs 链接的 `Href`。轨道占满宽度,最宽 460px;宽度达到 760px 时与详情面板并排,更窄时卡片位于轨道下方。 + +### 标志图形 + +`Icon` 可以是 24 × 24 网格上的 SVG 路径数据(Simple Icons 格式)、图片地址(`.png`、`.jpg`、`.webp` 文件,或 http(s)、`avares`、file、data URL)、`ImageSource` 或任意控件。路径数据跟随文本颜色,所以黑白品牌无需设置颜色;在标志上设置 `Color` 则用品牌色绘制。图片保留自己的颜色。把控件作为 `Icon` 时只显示在环上,卡片里改为显示名称的首字母。 + +品牌标志只是示例内容。请只展示确实存在的集成,并遵守各品牌的商标规则。 + +### 类别 + +给每个标志设置 `Category`,轨道上方就会出现类别胶囊;`AllLabel` 用于改名第一个胶囊。选中一个胶囊会把匹配的标志转向详情一侧并让其余标志变暗,此后方向键只在该类别内移动。如果打开的卡片不属于所选类别,卡片会关闭。 + +### 打开、连接与中央 + +`DefaultSelected` 在首次显示时打开一个标志;`SelectedId` 是打开的标志的 id 或名称,双向绑定,`null` 会关闭卡片。当用户打开、切换或关闭时,`SelectionChanged` 报告 `Logo` 与 `Index`,卡片关闭时没有标志且索引为 -1。 + +```csharp +orbit.ConnectHandler = async logo => await api.ConnectAsync(logo.Id ?? logo.Name); +``` + +连接按钮调用 `ConnectHandler`(或设置了 `Completion` 的 `ConnectRequested` 处理程序)。任务运行期间卡片显示 Connecting,完成后把标志标为已连接,失败则显示 Try again。没有处理程序时,卡片会模拟一次短暂的连接。 + + + +`Center` 是中央圆盘的内容,例如你的产品标志;默认是强调色的简单图形。`Speed` 用于缩放转速(为 1 时内环每 64 秒转一圈,外环反向每 88 秒转一圈;0 让轨道静止),`IsPaused` 则用于页面级的动效控制。`Label` 是标志列表的无障碍名称,也是空闲面板的标题。 + + + +### 最佳实践 + +**建议** + +- 把产品标志放在中央,给每个标志设置类别和一行说明。 +- 把 `ConnectHandler` 接到真实流程并返回它的任务。 +- 保持在十二个左右,让环保持清晰易读。 + +**避免** + +- 没有名称的标志:名称既是无障碍名称,也是人们看到的标签。 +- 把轨道作为到达集成的唯一入口;请保留它所属的设置页面。 + +### 常见问题 + +**为什么我移到轨道上它就停了?** 指针或焦点在其中、选中了类别或打开了卡片时,轨道会缓动到静止,这样你瞄准时没有东西在动。 + +**触屏上会显示名称标签吗?** 不会。标签只在悬停和键盘焦点时出现。 + +## API 参考 + + + + + +## 无障碍 + +- 标志是带标签的开关按钮列表;每个名称都包含类别以及是否已连接。 +- 只有一个标志在 Tab 顺序中,所以轨道在类别胶囊与卡片之间只占一个停靠点。 +- 卡片是带标签的分组,包含真正的按钮和 Docs 链接。 +- 连接状态在卡片中以文字和图标显示,而不只依靠标志上的圆点。 +- 打开、切换、筛选和连接都会以礼貌的方式播报。 +- 指针聚焦不绘制焦点环;键盘聚焦显示共用的焦点环、抬起的标志及其名称标签。 + +### 键盘 + +| 按键 | 操作 | +|---|---| +| Tab | 先进入类别胶囊,再到轨道中的一个标志,然后进入卡片。焦点在内时轨道保持静止。 | +| 方向键 | 移到当前类别中的上一个或下一个标志。卡片打开时,卡片和环随之移动。 | +| Home、End | 跳到当前类别的第一个或最后一个标志。 | +| Enter、Space | 打开获得焦点的标志;若已打开则关闭。 | +| Escape | 关闭卡片,并把焦点还给它的标志。 | + +## 动效 + +- 一个循环用渲染变换移动标志,所有东西静止、轨道不在视野内或窗口隐藏时就休眠。 +- 指针或焦点进入、选中类别或打开卡片时,转动在临界阻尼弹簧上缓动到静止。 +- 打开一个标志时,它所在的环沿近路弹到位,带一点点过冲,直到标志停在固定位置:与详情面板并排时在轨道右侧,否则在底部。外环标志停靠时,内环会转开一个间隙,让连线保持畅通。 +- 强调色连线从中央长到所选标志。 +- 卡片从标志中长出:先是标志所在位置的一个圆,再弹簧般展开成自己的区域,然后内容淡入。上一个和下一个按行进方向滑入内容。 +- 类别高亮是一块在胶囊之间沿形变弹簧滑动的药丸。 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 旋转 | 缓慢转动,缓动到静止 | 无 | 无 | +| 环与连线 | 弹簧到位 | 立即到位 | 立即到位 | +| 卡片 | 从标志中长出 | 直接出现 | 直接出现 | +| 上一个和下一个 | 滑动 | 直接切换 | 直接切换 | + +## 相关组件 + +- `HoverCard` 在悬停时预览人员或链接,不离开当前页面。 +- `Carousel` 浏览一排幻灯片,而不是一圈标志。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/zh-TW.md new file mode 100644 index 00000000..5eb83399 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/OrbitLogos/zh-TW.md @@ -0,0 +1,134 @@ +--- +title: Orbit logos +summary: 整合軌道:你的產品標誌在中央,標誌排在兩條緩慢轉動的環上,選中的標誌會長出一張詳情卡片。 +source-hash: 98316a6d1296 +--- + + + +## 何時使用 + +`OrbitLogos` 把產品能接入的一切以標誌的形式排在兩條環上,環繞著你自己的標誌。環緩慢轉動,指標或鍵盤焦點進入時就停下。選中一個標誌會讓它滑到固定位置,從中央向它畫一條線,並開啟一張卡片,說明這個整合做什麼、目前狀態,以及連線操作。 + +- **整合頁面或首屏**:生動呈現生態,又允許人們仔細查看。 +- **應用程式內的整合頁面**:在同一處依類別瀏覽、了解工具並連線。 +- **發佈頁**:一眼看到產品能配合的所有工具。 + +## 何時不用 + +- **只用來背書的標誌**:如果沒人會開啟它們,一排安靜的標誌就夠了,動得更少。 +- **很大的目錄**:兩條環放到二三十個標誌就會擁擠,而且大清單需要搜尋;請用帶搜尋框的格狀或表格。 +- **順序或搜尋很重要時**:環在轉動,沒有東西停留在固定位置。 + +## 用法 + +```xml + + + + + + +``` + +`OrbitLogos` 是 Special 套件中的範本化控制項。在 `UseArcControls()` 旁邊呼叫 `UseArcSpecial()` 註冊該套件。標誌就是它的內容:每個 `OrbitLogo` 有 `Name`、可選的 `Id`(選擇以 `Id` 為準,沒有時用名稱)、`Icon`、`Category`、一行 `Description`、起始的 `IsConnected`,以及用於 Docs 連結的 `Href`。軌道佔滿寬度,最寬 460px;寬度達到 760px 時與詳情面板並排,更窄時卡片位於軌道下方。 + +### 標誌圖形 + +`Icon` 可以是 24 × 24 網格上的 SVG 路徑資料(Simple Icons 格式)、圖片位址(`.png`、`.jpg`、`.webp` 檔案,或 http(s)、`avares`、file、data URL)、`ImageSource` 或任意控制項。路徑資料跟隨文字顏色,所以黑白品牌無需設定顏色;在標誌上設定 `Color` 則用品牌色繪製。圖片保留自己的顏色。把控制項作為 `Icon` 時只顯示在環上,卡片裡改為顯示名稱的首字母。 + +品牌標誌只是範例內容。請只展示確實存在的整合,並遵守各品牌的商標規則。 + +### 類別 + +給每個標誌設定 `Category`,軌道上方就會出現類別膠囊;`AllLabel` 用於改名第一個膠囊。選中一個膠囊會把相符的標誌轉向詳情一側並讓其餘標誌變暗,此後方向鍵只在該類別內移動。如果開啟的卡片不屬於所選類別,卡片會關閉。 + +### 開啟、連線與中央 + +`DefaultSelected` 在首次顯示時開啟一個標誌;`SelectedId` 是開啟的標誌的 id 或名稱,雙向繫結,`null` 會關閉卡片。當使用者開啟、切換或關閉時,`SelectionChanged` 回報 `Logo` 與 `Index`,卡片關閉時沒有標誌且索引為 -1。 + +```csharp +orbit.ConnectHandler = async logo => await api.ConnectAsync(logo.Id ?? logo.Name); +``` + +連線按鈕呼叫 `ConnectHandler`(或設定了 `Completion` 的 `ConnectRequested` 處理常式)。工作執行期間卡片顯示 Connecting,完成後把標誌標為已連線,失敗則顯示 Try again。沒有處理常式時,卡片會模擬一次短暫的連線。 + + + +`Center` 是中央圓盤的內容,例如你的產品標誌;預設是強調色的簡單圖形。`Speed` 用於縮放轉速(為 1 時內環每 64 秒轉一圈,外環反向每 88 秒轉一圈;0 讓軌道靜止),`IsPaused` 則用於頁面層級的動態控制。`Label` 是標誌清單的無障礙名稱,也是閒置面板的標題。 + + + +### 最佳實務 + +**建議** + +- 把產品標誌放在中央,給每個標誌設定類別和一行說明。 +- 把 `ConnectHandler` 接到真實流程並傳回它的工作。 +- 保持在十二個左右,讓環保持清晰易讀。 + +**避免** + +- 沒有名稱的標誌:名稱既是無障礙名稱,也是人們看到的標籤。 +- 把軌道作為到達整合的唯一入口;請保留它所屬的設定頁面。 + +### 常見問題 + +**為什麼我移到軌道上它就停了?** 指標或焦點在其中、選中了類別或開啟了卡片時,軌道會緩動到靜止,這樣你瞄準時沒有東西在動。 + +**觸控螢幕上會顯示名稱標籤嗎?** 不會。標籤只在懸停和鍵盤焦點時出現。 + +## API 參考 + + + + + +## 無障礙 + +- 標誌是帶標籤的切換按鈕清單;每個名稱都包含類別以及是否已連線。 +- 只有一個標誌在 Tab 順序中,所以軌道在類別膠囊與卡片之間只佔一個停靠點。 +- 卡片是帶標籤的群組,包含真正的按鈕和 Docs 連結。 +- 連線狀態在卡片中以文字和圖示顯示,而不只依靠標誌上的圓點。 +- 開啟、切換、篩選和連線都會以禮貌的方式播報。 +- 指標聚焦不繪製焦點環;鍵盤聚焦顯示共用的焦點環、抬起的標誌及其名稱標籤。 + +### 鍵盤 + +| 按鍵 | 操作 | +|---|---| +| Tab | 先進入類別膠囊,再到軌道中的一個標誌,然後進入卡片。焦點在內時軌道保持靜止。 | +| 方向鍵 | 移到目前類別中的上一個或下一個標誌。卡片開啟時,卡片和環隨之移動。 | +| Home、End | 跳到目前類別的第一個或最後一個標誌。 | +| Enter、Space | 開啟取得焦點的標誌;若已開啟則關閉。 | +| Escape | 關閉卡片,並把焦點還給它的標誌。 | + +## 動態 + +- 一個迴圈用轉換矩陣移動標誌,所有東西靜止、軌道不在視野內或視窗隱藏時就休眠。 +- 指標或焦點進入、選中類別或開啟卡片時,轉動在臨界阻尼彈簧上緩動到靜止。 +- 開啟一個標誌時,它所在的環沿近路彈到位,帶一點點過衝,直到標誌停在固定位置:與詳情面板並排時在軌道右側,否則在底部。外環標誌停靠時,內環會轉開一個間隙,讓連線保持暢通。 +- 強調色連線從中央長到所選標誌。 +- 卡片從標誌中長出:先是標誌所在位置的一個圓,再彈簧般展開成自己的區域,然後內容淡入。上一個和下一個依行進方向滑入內容。 +- 類別高亮是一塊在膠囊之間沿形變彈簧滑動的藥丸。 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 旋轉 | 緩慢轉動,緩動到靜止 | 無 | 無 | +| 環與連線 | 彈簧到位 | 立即到位 | 立即到位 | +| 卡片 | 從標誌中長出 | 直接出現 | 直接出現 | +| 上一個和下一個 | 滑動 | 直接切換 | 直接切換 | + +## 相關元件 + +- `HoverCard` 在懸停時預覽人員或連結,不離開目前頁面。 +- `Carousel` 瀏覽一排投影片,而不是一圈標誌。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/en-US.md new file mode 100644 index 00000000..46f9b3dc --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/en-US.md @@ -0,0 +1,147 @@ +--- +title: Page curl +summary: A small book that opens on its cover, with pages that fold along a line that follows the pointer. +--- + + + +## When to use + +`PageCurl` is a small book or magazine. It opens on its cover; tap the right page to go forward and the left page to go +back, or drag a corner and watch the page fold over along a line that follows your hand, showing its back, a lit curl +and a shadow on the page below. A page count with previous and next buttons sits underneath. + +- **A printed piece brought online**: an issue, a brochure or a programme that people expect to leaf through. +- **A few spreads, each worth a look**: a seasonal menu, a short catalogue, a class book. +- **Pictures in a fixed order**: a portfolio or an annual report told in eight to twenty pages. + +## When not to use + +- **Anything long or searchable**: turning forty pages one at a time is tiring; use a scrolling page or `Carousel`. +- **Small print people must read closely**: pages are scaled into slots and cannot be zoomed or selected. +- **A shelf of books on one screen**: one book draws the eye; many compete for it. + +## Usage + +```xml + + + + + + + +``` + +`PageCurl` is a templated control in the Special package. Register the package with `UseArcSpecial()` next to +`UseArcControls()`. The pages are the children, in reading order. Page one is the cover and sits alone on the right; +spread `n` shows pages `2n` and `2n + 1`. A book with an even number of pages ends on a back cover alone on the left, +and the closed book slides to the middle on its cover and its back cover. + +### Sizing + +Page size derives from the control's width: the width holds two pages side by side, so a 288px wide book still shows +two readable pages. `PageAspect` is a page's width divided by its height (0.72 by default). Each page renders into a +slot of that size, so make pages simple and fixed in size. A `Viewbox` around a page of a fixed design size makes it +scale with the book: + +```xml + + + +``` + +### Pages from data + +Items that are not controls are built with `PageTemplate`. A control is used as the page itself, so it can be on one +page only. + + + +`Spread` is the spread on show (0 is the closed cover) and binds two-way. Setting it from code turns one step with the +curl when the new spread is next to the current one, and jumps otherwise. `SpreadChanged` is raised when a page +finishes turning by a tap, a drag, a key or a button, with the new spread. `NextSpread()` and `PreviousSpread()` turn +from code. + +### Tease, hint and controls + +On first view the cover corner curls up and settles next to a "Drag the corner" hint. `Tease` turns the curl off, and +`Hint` replaces the text or hides it when it is `null`; the hint fades out for good after the first drag, tap, key or +button press. Until the first turn, moving the mouse into the book repeats the tease at half the size, at most every +two and a half seconds. `ShowControls` hides the page count row. + + + +### Best practices + +**Do** + +- Make page one a real cover with a title, so the closed book reads as something to open. +- Keep pages simple and fixed in size, with a few spreads. +- Give the book a name with `Label`. + +**Avoid** + +- Pages that need scrolling or text selection: the page is a drag target. +- Interactive controls on pages: taps and drags turn the page. + +### Common questions + +**Why does a drag on a phone not scroll the page?** A vertical swipe does scroll the page behind the book; only a +horizontal drag turns a page. + +**Can a page be the same control twice?** No. A control has one parent, so a page shown in two places needs a +`PageTemplate` and a data item. + +## API reference + + + +## Accessibility + +- The book is a focusable group named by `Label`, announced as a book. A polite status reads the visible pages, such as + "Page 2 and 3 of 8". +- Only the resting pages are in the accessibility tree; the pages that are turning are hidden from it. +- Previous and next are real buttons with labels, and they disable at the covers. The visible count and the hint are + decorative duplicates of the status. +- The keyboard focus ring shows only for keyboard focus. + +### Keyboard + +| Key | Action | +|---|---| +| Right arrow, Page Down | Turn to the next spread | +| Left arrow, Page Up | Turn to the previous spread | +| Tab | Move from the book to the previous and next page buttons | + +## Motion + +- The fold line is the perpendicular bisector between the corner and the pointer, so the page follows the finger from + where it was grabbed. The corner is held within a page width of the spine, like paper bound on one side. +- The flap is the sheet mirrored across the fold, showing the back of the page with a lit curl at the crease; the page + below gets a soft shadow at the fold and the flap casts a light shadow that always falls downward. +- On release, past the middle or a flick turns the page on a spring that keeps the release velocity; otherwise it lays + back down. Grabbing a page that is still settling catches it where it is. +- The first time the book is mostly in view, the cover corner lifts about a quarter of a page on a soft spring, holds, + and lays back down. Hovering a corner lifts it a little, and moving to another corner lays the first one flat before + lifting the next. Hovering a page shows a chevron at its outer edge for the direction a tap turns. +- Taps, arrow keys and the buttons turn along a lifted arc. +- Only the pages involved in a turn are drawn, and a turn moves them with transforms and clips: nothing is laid out + per frame. + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Turning | Curls and follows the pointer | Swaps spreads | Swaps spreads | +| Tease and hover lift | Run | Skipped | Skipped | + +## Related + +- `Carousel` browses a row of slides by dragging or arrowing through them. +- `CardStack` fans a deck of cards you can flick away. + +| Component | Choose it when | +|---|---| +| `PageCurl` | A short sequence should feel like paper | +| `Carousel` | You have a row of slides of any length | +| `CardStack` | Items are a deck rather than a sequence | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/zh-CN.md new file mode 100644 index 00000000..ea67836e --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/zh-CN.md @@ -0,0 +1,123 @@ +--- +title: Page curl +summary: 一本从封面开始翻开的小书,书页沿着一条跟随指针的折线翻折。 +source-hash: 6626c257167d +--- + + + +## 何时使用 + +`PageCurl` 是一本小书或小杂志。它从封面打开:轻点右页向前翻,轻点左页向后翻,或者拖动一角,看书页沿着跟随你手指的折线翻过去,露出背面、一道被照亮的卷边,以及落在下一页上的阴影。下方有页码,以及上一页、下一页按钮。 + +- **把印刷品搬到线上**:一期刊物、一本小册子或一份节目单,人们本就想一页页翻看。 +- **少量值得细看的跨页**:季节菜单、简短的产品目录、班级纪念册。 +- **按固定顺序看的图片**:用八到二十页讲完的作品集或年度报告。 + +## 何时不使用 + +- **很长或需要搜索的内容**:一页页翻四十页很累;请用可滚动页面或 `Carousel`。 +- **需要细读的小字**:页面按槽位缩放,不能放大,也不能选中文字。 +- **一屏放一排书**:一本书能吸引目光,多本会互相争抢。 + +## 用法 + +```xml + + + + + + + +``` + +`PageCurl` 是 Special 包里的模板化控件。请在 `UseArcControls()` 旁边用 `UseArcSpecial()` 注册该包。子元素就是按阅读顺序排列的书页。第 1 页是封面,单独位于右侧;第 `n` 个跨页显示第 `2n` 与 `2n + 1` 页。页数为偶数时,书以单独位于左侧的封底结束;合上的书会在封面与封底时滑到中间。 + +### 尺寸 + +页面大小由控件宽度决定:宽度容纳并排的两页,所以即使书只有 288px 宽,两页也依然清晰可读。`PageAspect` 是页宽除以页高(默认 0.72)。每一页都渲染到这个尺寸的槽位里,因此页面要简单、尺寸固定。在固定设计尺寸的页面外包一层 `Viewbox`,就能随书缩放: + +```xml + + + +``` + +### 由数据生成页面 + +不是控件的条目用 `PageTemplate` 生成。控件则直接作为页面使用,所以只能出现在一页上。 + + + +`Spread` 是当前显示的跨页(0 为合上的封面),支持双向绑定。从代码设置它时,如果新跨页与当前相邻,就带着翻页效果翻一步,否则直接跳转。点按、拖动、按键或按钮让一页翻完后,会触发 `SpreadChanged` 并带上新的跨页。`NextSpread()` 与 `PreviousSpread()` 可从代码翻页。 + +### 逗引、提示与控件 + +首次出现时,封面的一角会卷起并落回,旁边有一句“Drag the corner”的提示。`Tease` 可关闭这个卷角,`Hint` 可替换文字,设为 `null` 则隐藏;第一次拖动、点按、按键或按钮之后,提示会永久淡出。在第一次翻页之前,鼠标移入书时会以一半大小重复逗引,最多每 2.5 秒一次。`ShowControls` 可隐藏页码一行。 + + + +### 最佳实践 + +**建议** + +- 让第 1 页成为带标题的真正封面,这样合上的书看起来就是值得打开的东西。 +- 页面保持简单、尺寸固定,跨页不要太多。 +- 用 `Label` 给书起个名字。 + +**避免** + +- 需要滚动或选择文字的页面:页面是拖动目标。 +- 在页面上放可交互的控件:点按与拖动会翻页。 + +### 常见问题 + +**在手机上拖动为什么不会滚动页面?** 垂直滑动会滚动书后面的页面,只有水平拖动才会翻页。 + +**同一个控件能当两页吗?** 不能。一个控件只有一个父级,需要出现在两处的页面请用 `PageTemplate` 和数据项。 + +## API 参考 + + + +## 无障碍 + +- 书是可聚焦的分组,名称取自 `Label`,读作“book”。礼貌级状态会读出当前可见的页码,例如“Page 2 and 3 of 8”。 +- 只有静止的页面在无障碍树中;正在翻动的页面对它隐藏。 +- 上一页、下一页是带标签的真实按钮,在封面与封底处禁用。可见的页码与提示是状态的装饰性重复。 +- 键盘焦点环只在键盘聚焦时显示。 + +### 键盘 + +| 按键 | 作用 | +|---|---| +| 右方向键、Page Down | 翻到下一个跨页 | +| 左方向键、Page Up | 翻到上一个跨页 | +| Tab | 从书移到上一页、下一页按钮 | + +## 动效 + +- 折线是指针与书角连线的垂直平分线,所以书页会从被抓住的位置开始跟随手指。书角被限制在离书脊一个页宽以内,就像一侧装订的纸。 +- 翻起的部分是纸张沿折线的镜像,显示页面背面,并在折痕处有一道被照亮的卷边;折线处下方的页面有柔和阴影,翻起的部分还会投下始终朝下的淡淡阴影。 +- 松手时,过了中线或快速一甩,书页会用保留松手速度的弹簧翻过去;否则落回原位。抓住仍在回落的书页,会把它就地接住。 +- 书第一次大部分进入视野时,封面的一角用柔和的弹簧抬起约四分之一页,停一下,再落回。悬停书角会让它轻轻抬起,移到另一个角时先让前一个放平再抬起下一个。悬停在书页上会在其外缘显示一个箭头,指示点按所翻的方向。 +- 点按、方向键与按钮会沿着一道抬起的弧线翻页。 +- 翻页时只绘制涉及的书页,并用变换与裁剪移动它们:不会逐帧布局。 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 翻页 | 卷起并跟随指针 | 直接切换跨页 | 直接切换跨页 | +| 逗引与悬停抬角 | 运行 | 跳过 | 跳过 | + +## 相关 + +- `Carousel` 通过拖动或方向键浏览一排幻灯片。 +- `CardStack` 把一叠卡片扇开,可以一甩而去。 + +| 组件 | 何时选择 | +|---|---| +| `PageCurl` | 一小段序列应该有纸张的感觉 | +| `Carousel` | 有一排任意长度的幻灯片 | +| `CardStack` | 条目是一叠牌而不是一段序列 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/zh-TW.md new file mode 100644 index 00000000..9580808c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/PageCurl/zh-TW.md @@ -0,0 +1,123 @@ +--- +title: Page curl +summary: 一本從封面開始翻開的小書,書頁沿著一條跟隨指標的摺線翻摺。 +source-hash: 6626c257167d +--- + + + +## 何時使用 + +`PageCurl` 是一本小書或小雜誌。它從封面打開:輕點右頁向前翻,輕點左頁向後翻,或者拖曳一角,看書頁沿著跟隨你手指的摺線翻過去,露出背面、一道被照亮的捲邊,以及落在下一頁上的陰影。下方有頁碼,以及上一頁、下一頁按鈕。 + +- **把印刷品搬到線上**:一期刊物、一本小冊子或一份節目單,人們本就想一頁頁翻看。 +- **少量值得細看的跨頁**:季節菜單、簡短的產品型錄、班級紀念冊。 +- **按固定順序看的圖片**:用八到二十頁講完的作品集或年度報告。 + +## 何時不使用 + +- **很長或需要搜尋的內容**:一頁頁翻四十頁很累;請用可捲動頁面或 `Carousel`。 +- **需要細讀的小字**:頁面按槽位縮放,不能放大,也不能選取文字。 +- **一個畫面放一排書**:一本書能吸引目光,多本會互相爭搶。 + +## 用法 + +```xml + + + + + + + +``` + +`PageCurl` 是 Special 套件裡的範本化控制項。請在 `UseArcControls()` 旁邊用 `UseArcSpecial()` 註冊該套件。子元素就是依閱讀順序排列的書頁。第 1 頁是封面,單獨位於右側;第 `n` 個跨頁顯示第 `2n` 與 `2n + 1` 頁。頁數為偶數時,書以單獨位於左側的封底結束;合上的書會在封面與封底時滑到中間。 + +### 尺寸 + +頁面大小由控制項寬度決定:寬度容納並排的兩頁,所以即使書只有 288px 寬,兩頁也依然清晰可讀。`PageAspect` 是頁寬除以頁高(預設 0.72)。每一頁都會繪製到這個尺寸的槽位裡,因此頁面要簡單、尺寸固定。在固定設計尺寸的頁面外包一層 `Viewbox`,就能隨書縮放: + +```xml + + + +``` + +### 由資料產生頁面 + +不是控制項的項目用 `PageTemplate` 產生。控制項則直接作為頁面使用,所以只能出現在一頁上。 + + + +`Spread` 是目前顯示的跨頁(0 為合上的封面),支援雙向繫結。從程式碼設定它時,如果新跨頁與目前相鄰,就帶著翻頁效果翻一步,否則直接跳轉。點按、拖曳、按鍵或按鈕讓一頁翻完後,會觸發 `SpreadChanged` 並帶上新的跨頁。`NextSpread()` 與 `PreviousSpread()` 可從程式碼翻頁。 + +### 逗引、提示與控制項 + +首次出現時,封面的一角會捲起並落回,旁邊有一句「Drag the corner」的提示。`Tease` 可關閉這個捲角,`Hint` 可替換文字,設為 `null` 則隱藏;第一次拖曳、點按、按鍵或按鈕之後,提示會永久淡出。在第一次翻頁之前,滑鼠移入書時會以一半大小重複逗引,最多每 2.5 秒一次。`ShowControls` 可隱藏頁碼一行。 + + + +### 最佳實踐 + +**建議** + +- 讓第 1 頁成為帶標題的真正封面,這樣合上的書看起來就是值得打開的東西。 +- 頁面保持簡單、尺寸固定,跨頁不要太多。 +- 用 `Label` 替書取個名字。 + +**避免** + +- 需要捲動或選取文字的頁面:頁面是拖曳目標。 +- 在頁面上放可互動的控制項:點按與拖曳會翻頁。 + +### 常見問題 + +**在手機上拖曳為什麼不會捲動頁面?** 垂直滑動會捲動書後面的頁面,只有水平拖曳才會翻頁。 + +**同一個控制項能當兩頁嗎?** 不能。一個控制項只有一個父層,需要出現在兩處的頁面請用 `PageTemplate` 和資料項。 + +## API 參考 + + + +## 無障礙 + +- 書是可聚焦的群組,名稱取自 `Label`,讀作「book」。禮貌級狀態會讀出目前可見的頁碼,例如「Page 2 and 3 of 8」。 +- 只有靜止的頁面在無障礙樹中;正在翻動的頁面對它隱藏。 +- 上一頁、下一頁是帶標籤的真實按鈕,在封面與封底處停用。可見的頁碼與提示是狀態的裝飾性重複。 +- 鍵盤焦點環只在鍵盤聚焦時顯示。 + +### 鍵盤 + +| 按鍵 | 作用 | +|---|---| +| 右方向鍵、Page Down | 翻到下一個跨頁 | +| 左方向鍵、Page Up | 翻到上一個跨頁 | +| Tab | 從書移到上一頁、下一頁按鈕 | + +## 動態效果 + +- 摺線是指標與書角連線的垂直平分線,所以書頁會從被抓住的位置開始跟隨手指。書角被限制在離書脊一個頁寬以內,就像一側裝訂的紙。 +- 翻起的部分是紙張沿摺線的鏡像,顯示頁面背面,並在摺痕處有一道被照亮的捲邊;摺線處下方的頁面有柔和陰影,翻起的部分還會投下始終朝下的淡淡陰影。 +- 放開時,過了中線或快速一甩,書頁會用保留放開速度的彈簧翻過去;否則落回原位。抓住仍在回落的書頁,會把它就地接住。 +- 書第一次大部分進入視野時,封面的一角用柔和的彈簧抬起約四分之一頁,停一下,再落回。懸停書角會讓它輕輕抬起,移到另一個角時先讓前一個放平再抬起下一個。懸停在書頁上會在其外緣顯示一個箭頭,指示點按所翻的方向。 +- 點按、方向鍵與按鈕會沿著一道抬起的弧線翻頁。 +- 翻頁時只繪製涉及的書頁,並用變換與裁剪移動它們:不會逐影格版面配置。 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 翻頁 | 捲起並跟隨指標 | 直接切換跨頁 | 直接切換跨頁 | +| 逗引與懸停抬角 | 執行 | 略過 | 略過 | + +## 相關 + +- `Carousel` 透過拖曳或方向鍵瀏覽一排投影片。 +- `CardStack` 把一疊卡片攤開,可以一甩而去。 + +| 元件 | 何時選擇 | +|---|---| +| `PageCurl` | 一小段序列應該有紙張的感覺 | +| `Carousel` | 有一排任意長度的投影片 | +| `CardStack` | 項目是一疊牌而不是一段序列 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/en-US.md new file mode 100644 index 00000000..8bb42442 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/en-US.md @@ -0,0 +1,158 @@ +--- +title: Sticker peel +summary: A surface of stickers with a real peel. Hovering lifts a corner, pulling folds the sticker back with its paper backing showing, and letting go sticks it down where you dropped it. +--- + + + +## When to use + +`StickerPeel` is a surface you can decorate. Each sticker has a white die cut border; hovering lifts its nearest +corner, pulling folds it back along a real fold, and pulling far enough peels it off so you can carry it somewhere +else. It works with the keyboard and with touch, and it tells you the layout so you can save it. + +- **Profiles and team walls**: a corner of the product people decorate themselves, with badges they earned or picked. +- **A launch or a campaign page**: a playful object that rewards anyone who reaches out and touches it. +- **Collections**: an achievement shelf or a sticker book that fills up over time. + +## When not to use + +- **Ranking or ordering things**: position here is decoration, not data; use a reorderable list. +- **Text people have to read**: a sticker holds a mark or a word, not a paragraph. +- **Busy tools and forms**: it invites play, which is the wrong mood where people are trying to finish a task. + +## Usage + +```xml + + + + + + + + + + + +``` + +`StickerPeel` is a `ContentControl` in the Special package. Register the package with `UseArcSpecial()` next to +`UseArcControls()`. Give it a height; it is at least 280px high. Its `Content` is drawn on the surface under the +stickers, and the stickers are clipped to the surface's rounded box. + +### Stickers + +Each `StickerPeelItem` has a stable `Id`, a `Label` for assistive technology, the printed `Content`, a starting centre +(`X` and `Y`, each from 0 to 1 across the surface), an optional `Rotate` in degrees and a `Shape`. Give the content a +fixed size: the white die cut border and the outline are added for you. Keep it under about a dozen stickers so the +surface stays readable. + +| Shape | Outline | Where it peels from | +|---|---|---| +| `Circle` | The smaller side is the diameter | The point of the rim nearest the pointer | +| `Rounded` | A rectangle with rounded corners | The nearest corner, along its diagonal | +| `Pill` | A rectangle with fully rounded ends | The nearest corner, along its diagonal | + + + +### Saving the arrangement + +Placements are fractions of the surface, so the arrangement scales with its container and survives resizes. +`PositionsChanged` is raised once a sticker settles somewhere new, with the placement of every sticker. Keep them to +save the layout, and pass them back as `Positions`. Changing `Positions` from outside, such as a reset, glides each +sticker to its new spot. `Reset()` puts every sticker back where its item starts. + + + +```xml + +``` + +### Using the keyboard + +Tab moves between stickers. Enter or Space picks the focused sticker up, and again sticks it down where it is. While +carrying, the arrow keys move it by 10px, or 40px with Shift; Escape puts it back where it was picked up. Leaving a +sticker you are carrying with Tab sticks it down. + +### Best practices + +**Do** + +- Choose it for one playful, tactile moment: a profile page, a launch page, a team wall or a badge shelf. +- Give every sticker a fixed-size face and a meaningful `Label`. +- Store `PositionsChanged` output to persist the arrangement. + +**Avoid** + +- Using it where precise placement or order matters. +- Putting text people must read on a sticker's back: a folded corner only shows plain paper. + +### Common questions + +**Can I peel it on a touch screen?** Yes: a drag on a sticker peels it the same way. Swipes that start outside a +sticker still scroll the page. + +**Why does the sticker just follow my pointer?** Reduced motion skips the peel and the hover lift: pressing picks the +sticker up directly and it follows the pointer 1:1. + +## API reference + + + +## Accessibility + +- The surface is a group named by `Label`; each sticker is a button with the role description "sticker", its own + label, and a toggled state while carried. +- Each sticker's help text explains the keyboard path, and a polite status region announces peeled off, picked up, + stuck down and put back. +- The artwork carries no meaning of its own; the label does. +- Pointer focus draws no ring; keyboard focus shows the shared focus ring. The lift and shadow of a carried sticker + show which one is active. + +### Keyboard interactions + +| Key | Action | +|---|---| +| Tab | Moves between stickers. Leaving a sticker you are carrying sticks it down. | +| Enter or Space | Picks the focused sticker up, or sticks it down where it is. | +| Arrow keys | Moves a sticker you are carrying by 10px, or 40px with Shift. | +| Escape | Puts a carried sticker back where it was picked up. | + +## Motion + +- The peel is a real fold: the pulled part is mirrored across the fold line, showing a plain paper backing with one + soft shade at the bend and a light shadow that always falls downward. The fold may lean at most 35 degrees, so it is + always a short, clean corner. +- Circles peel from the point of the rim nearest the pointer; rounded shapes and pills peel from a corner along its + diagonal. +- Hovering with a mouse lifts the nearest corner a few pixels. Moving to another corner lays the first one flat + before lifting the next. +- All springs are critically damped: the paper trails the finger slightly and settles without overshoot, bounce or + swing. +- A sticker comes off once the fold passes the middle, sooner on a quick tug, or once the hand has clearly carried it + away. It then rises 3% with a soft shadow and follows the hand at its own angle. +- Dropping lands it exactly where it is on screen and the shadow fades in 200ms. Grabbing it again while it settles + catches it in place. +- Carried stickers stretch against the surface edges with growing resistance and are clamped inside on drop, + accounting for their angle. +- Frames run only while a sticker moves, and pause while the surface is out of view. Each frame writes the pose of a + sticker (a transform) and the geometry of its fold; nothing is laid out. + +| Change | Full | Reduced | Off | +|---|---|---|---| +| Hover lift | Lifts the nearest corner | None | None | +| Peel | Folds the corner back | None; pressing picks the sticker up | None; pressing picks the sticker up | +| Carry | Rises and casts a shadow, trailing the hand | Follows the pointer 1:1 | Follows the pointer 1:1 | + +## Related + +- `DotGrid` and `GravityWell` are other playful pointer surfaces. +- `ReorderableList` is the choice when order carries meaning. + +| Component | Choose it when | +|---|---| +| `StickerPeel` | People arrange marks for fun and the layout is theirs | +| `ReorderableList` | The order of the items matters | +| `GravityWell` | A grid of pictures should react to the pointer | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/zh-CN.md new file mode 100644 index 00000000..70e0d77c --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/zh-CN.md @@ -0,0 +1,133 @@ +--- +title: Sticker peel +summary: 一面贴满贴纸的表面,贴纸能真正地被揭起。悬停会翘起一角,拉动会把贴纸折回并露出纸质背面,松手后贴纸就贴在放下的位置。 +source-hash: 478907c2abd0 +--- + + + +## 何时使用 + +`StickerPeel` 是一面可以随手装饰的表面。每张贴纸都带一圈白色模切边;悬停会翘起离指针最近的一角,拉动会沿真实的折线把它折回,拉得足够远就会整张揭下,可以拿到别处。它支持键盘和触摸,并会告诉你当前的布局,方便保存。 + +- **个人资料与团队墙**:产品里一块由用户自己装饰的角落,贴上赢得或挑选的徽章。 +- **发布或活动页面**:一个好玩的物件,奖励每一个伸手去碰它的人。 +- **收藏**:随时间慢慢填满的成就架或贴纸册。 + +## 何时不用 + +- **排名或排序**:这里的位置只是装饰,不是数据;请用可重排列表。 +- **需要阅读的文字**:贴纸上放一个图形或一个词,不放一段话。 +- **繁忙的工具与表单**:它邀人玩耍,而人们想完成任务时这种情绪不合适。 + +## 用法 + +```xml + + + + + + + + + + + +``` + +`StickerPeel` 是 Special 包里的 `ContentControl`。请在 `UseArcControls()` 旁边用 `UseArcSpecial()` 注册该包。给它设一个高度;它至少 280px 高。它的 `Content` 画在表面上、贴纸之下,贴纸会被裁剪到表面的圆角框内。 + +### 贴纸 + +每个 `StickerPeelItem` 有稳定的 `Id`、供辅助技术使用的 `Label`、印在上面的 `Content`、起始中心(`X` 和 `Y`,在表面上各取 0 到 1)、可选的 `Rotate`(角度)和 `Shape`。请给内容一个固定尺寸:白色模切边和轮廓由控件添加。贴纸最好不超过十来张,这样表面才不会显得杂乱。 + +| 形状 | 轮廓 | 从哪里揭起 | +|---|---|---| +| `Circle` | 较短的边是直径 | 圆边上离指针最近的点 | +| `Rounded` | 圆角矩形 | 最近的角,沿对角线 | +| `Pill` | 两端完全圆润的矩形 | 最近的角,沿对角线 | + + + +### 保存布局 + +位置是表面的比例,所以布局会随容器缩放,并且在改变大小后依然成立。贴纸在新位置落定后会触发一次 `PositionsChanged`,带上每张贴纸的位置。保存下来,再通过 `Positions` 传回即可。从外部改变 `Positions`(比如重置)时,每张贴纸会滑向新的位置。`Reset()` 会让每张贴纸回到它的项所设的起始位置。 + + + +```xml + +``` + +### 使用键盘 + +Tab 在贴纸之间移动。Enter 或 Space 拿起当前贴纸,再按一次就把它贴在原地。拿着贴纸时,方向键每次移动 10px,按住 Shift 为 40px;Escape 把它放回拿起时的位置。用 Tab 离开正拿着的贴纸会把它贴下。 + +### 最佳实践 + +**建议** + +- 把它用在一个好玩、有触感的时刻:个人主页、发布页、团队墙或徽章架。 +- 给每张贴纸一个固定尺寸的画面和有意义的 `Label`。 +- 保存 `PositionsChanged` 给出的结果,以持久化布局。 + +**避免** + +- 用在需要精确摆放或顺序重要的地方。 +- 把人们必须阅读的文字放在贴纸背面:翘起的角只会显示素白的纸。 + +### 常见问题 + +**触摸屏上能揭贴纸吗?** 可以:在贴纸上拖动,同样会揭起。从贴纸之外开始的滑动仍会滚动页面。 + +**为什么贴纸只是跟着指针走?** 减弱动效会跳过揭起和悬停翘角:按下就直接拿起贴纸,它与指针一比一同步。 + +## API 参考 + + + +## 无障碍 + +- 表面是一个以 `Label` 命名的分组;每张贴纸是一个按钮,角色描述为“sticker”,带有自己的标签,拿着时处于已切换状态。 +- 每张贴纸的帮助文本说明了键盘操作方式,一个礼貌(polite)的状态区域会播报揭下、拿起、贴下和放回。 +- 贴纸的图案本身不承载含义,含义由标签承担。 +- 指针聚焦不绘制焦点环;键盘聚焦显示共用的焦点环。拿着的贴纸的抬升和阴影表明哪一张处于活动状态。 + +### 键盘交互 + +| 按键 | 作用 | +|---|---| +| Tab | 在贴纸之间移动。离开正拿着的贴纸会把它贴下。 | +| Enter 或 Space | 拿起当前贴纸,或把它贴在原地。 | +| 方向键 | 把拿着的贴纸移动 10px,按住 Shift 为 40px。 | +| Escape | 把拿着的贴纸放回拿起时的位置。 | + +## 动效 + +- 揭起是真实的折叠:被拉起的部分沿折线镜像,露出素色纸背,折痕处有一片柔和的阴影,另有一层始终向下落的轻阴影。折线最多偏斜 35 度,所以它始终是一个短而干净的折角。 +- 圆形从圆边上离指针最近的点揭起;圆角矩形和胶囊形从一个角沿对角线揭起。 +- 鼠标悬停会把最近的一角翘起几个像素。移到另一个角时,先让第一个角平放,再翘起下一个。 +- 所有弹簧都是临界阻尼的:纸张略微落后于手指,落定时没有过冲、回弹或摆动。 +- 折线越过中点、快速一拽,或手明显把它带走时,贴纸就会揭下。随后它抬升 3%,带着柔和的阴影,按自己的角度跟着手移动。 +- 放下时,它正好落在屏幕上当前的位置,阴影在 200ms 内淡入。在它落定的过程中再次抓住它,它会就地被接住。 +- 被拿着的贴纸碰到表面边缘时会带着越来越大的阻力被拉伸,放下时按其角度夹回表面内。 +- 只有贴纸在动时才会产生帧,表面滚出视野时暂停。每一帧只写贴纸的姿态(一个变换)和折叠的几何,不触发布局。 + +| 变化 | 完整 | 减弱 | 关闭 | +|---|---|---|---| +| 悬停翘角 | 翘起最近的一角 | 无 | 无 | +| 揭起 | 把角折回 | 无;按下就拿起贴纸 | 无;按下就拿起贴纸 | +| 拿着 | 抬升并投下阴影,略落后于手 | 与指针一比一同步 | 与指针一比一同步 | + +## 相关组件 + +- `DotGrid` 和 `GravityWell` 是另外两个好玩的指针表面。 +- 顺序有含义时,请选择 `ReorderableList`。 + +| 组件 | 何时选择 | +|---|---| +| `StickerPeel` | 人们为了好玩自己摆放标志,布局归他们所有 | +| `ReorderableList` | 项目的顺序很重要 | +| `GravityWell` | 一个图片网格要对指针作出反应 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/zh-TW.md new file mode 100644 index 00000000..bf5505d4 --- /dev/null +++ b/gallery/TKCode.Arc.Gallery/Docs/Playful/StickerPeel/zh-TW.md @@ -0,0 +1,133 @@ +--- +title: Sticker peel +summary: 一面貼滿貼紙的表面,貼紙能真正地被撕起。懸停會翹起一角,拉動會把貼紙折回並露出紙質背面,放手後貼紙就貼在放下的位置。 +source-hash: 478907c2abd0 +--- + + + +## 何時使用 + +`StickerPeel` 是一面可以隨手裝飾的表面。每張貼紙都帶一圈白色模切邊;懸停會翹起離指標最近的一角,拉動會沿真實的折線把它折回,拉得夠遠就會整張撕下,可以拿到別處。它支援鍵盤和觸控,並會告訴你目前的版面,方便儲存。 + +- **個人檔案與團隊牆**:產品裡一塊由使用者自己裝飾的角落,貼上贏得或挑選的徽章。 +- **發布或活動頁面**:一個好玩的物件,獎勵每一個伸手去碰它的人。 +- **收藏**:隨時間慢慢填滿的成就架或貼紙冊。 + +## 何時不用 + +- **排名或排序**:這裡的位置只是裝飾,不是資料;請用可重排清單。 +- **需要閱讀的文字**:貼紙上放一個圖形或一個詞,不放一段話。 +- **繁忙的工具與表單**:它邀人玩耍,而人們想完成任務時這種情緒不合適。 + +## 用法 + +```xml + + + + + + + + + + + +``` + +`StickerPeel` 是 Special 套件裡的 `ContentControl`。請在 `UseArcControls()` 旁邊用 `UseArcSpecial()` 註冊該套件。給它設一個高度;它至少 280px 高。它的 `Content` 畫在表面上、貼紙之下,貼紙會被裁切到表面的圓角框內。 + +### 貼紙 + +每個 `StickerPeelItem` 有穩定的 `Id`、供輔助技術使用的 `Label`、印在上面的 `Content`、起始中心(`X` 和 `Y`,在表面上各取 0 到 1)、可選的 `Rotate`(角度)和 `Shape`。請給內容一個固定尺寸:白色模切邊和輪廓由控制項加上。貼紙最好不超過十來張,這樣表面才不會顯得雜亂。 + +| 形狀 | 輪廓 | 從哪裡撕起 | +|---|---|---| +| `Circle` | 較短的邊是直徑 | 圓邊上離指標最近的點 | +| `Rounded` | 圓角矩形 | 最近的角,沿對角線 | +| `Pill` | 兩端完全圓潤的矩形 | 最近的角,沿對角線 | + + + +### 儲存版面 + +位置是表面的比例,所以版面會隨容器縮放,並且在改變大小後依然成立。貼紙在新位置落定後會觸發一次 `PositionsChanged`,帶上每張貼紙的位置。存下來,再透過 `Positions` 傳回即可。從外部改變 `Positions`(例如重設)時,每張貼紙會滑向新的位置。`Reset()` 會讓每張貼紙回到它的項目所設的起始位置。 + + + +```xml + +``` + +### 使用鍵盤 + +Tab 在貼紙之間移動。Enter 或 Space 拿起目前的貼紙,再按一次就把它貼在原地。拿著貼紙時,方向鍵每次移動 10px,按住 Shift 為 40px;Escape 把它放回拿起時的位置。用 Tab 離開正拿著的貼紙會把它貼下。 + +### 最佳實務 + +**建議** + +- 把它用在一個好玩、有觸感的時刻:個人首頁、發布頁、團隊牆或徽章架。 +- 給每張貼紙一個固定尺寸的畫面和有意義的 `Label`。 +- 儲存 `PositionsChanged` 給出的結果,以保存版面。 + +**避免** + +- 用在需要精確擺放或順序重要的地方。 +- 把人們必須閱讀的文字放在貼紙背面:翹起的角只會顯示素白的紙。 + +### 常見問題 + +**觸控螢幕上能撕貼紙嗎?** 可以:在貼紙上拖動,同樣會撕起。從貼紙之外開始的滑動仍會捲動頁面。 + +**為什麼貼紙只是跟著指標走?** 減弱動態會跳過撕起和懸停翹角:按下就直接拿起貼紙,它與指標一比一同步。 + +## API 參考 + + + +## 無障礙 + +- 表面是一個以 `Label` 命名的群組;每張貼紙是一個按鈕,角色描述為「sticker」,帶有自己的標籤,拿著時處於已切換狀態。 +- 每張貼紙的說明文字講明了鍵盤操作方式,一個禮貌(polite)的狀態區域會播報撕下、拿起、貼下和放回。 +- 貼紙的圖案本身不承載意義,意義由標籤承擔。 +- 指標聚焦不繪製焦點環;鍵盤聚焦顯示共用的焦點環。拿著的貼紙的抬升和陰影表明哪一張處於作用中。 + +### 鍵盤互動 + +| 按鍵 | 作用 | +|---|---| +| Tab | 在貼紙之間移動。離開正拿著的貼紙會把它貼下。 | +| Enter 或 Space | 拿起目前的貼紙,或把它貼在原地。 | +| 方向鍵 | 把拿著的貼紙移動 10px,按住 Shift 為 40px。 | +| Escape | 把拿著的貼紙放回拿起時的位置。 | + +## 動態 + +- 撕起是真實的摺疊:被拉起的部分沿摺線鏡射,露出素色紙背,摺痕處有一片柔和的陰影,另有一層始終向下落的輕陰影。摺線最多偏斜 35 度,所以它始終是一個短而乾淨的摺角。 +- 圓形從圓邊上離指標最近的點撕起;圓角矩形和膠囊形從一個角沿對角線撕起。 +- 滑鼠懸停會把最近的一角翹起幾個像素。移到另一個角時,先讓第一個角平放,再翹起下一個。 +- 所有彈簧都是臨界阻尼的:紙張略微落後於手指,落定時沒有過衝、回彈或擺動。 +- 摺線越過中點、快速一扯,或手明顯把它帶走時,貼紙就會撕下。隨後它抬升 3%,帶著柔和的陰影,依自己的角度跟著手移動。 +- 放下時,它正好落在螢幕上目前的位置,陰影在 200ms 內淡入。在它落定的過程中再次抓住它,它會就地被接住。 +- 被拿著的貼紙碰到表面邊緣時會帶著越來越大的阻力被拉伸,放下時依其角度夾回表面內。 +- 只有貼紙在動時才會產生影格,表面捲出視野時暫停。每一影格只寫貼紙的姿態(一個變換)和摺疊的幾何,不觸發版面配置。 + +| 變化 | 完整 | 減弱 | 關閉 | +|---|---|---|---| +| 懸停翹角 | 翹起最近的一角 | 無 | 無 | +| 撕起 | 把角折回 | 無;按下就拿起貼紙 | 無;按下就拿起貼紙 | +| 拿著 | 抬升並投下陰影,略落後於手 | 與指標一比一同步 | 與指標一比一同步 | + +## 相關元件 + +- `DotGrid` 和 `GravityWell` 是另外兩個好玩的指標表面。 +- 順序有意義時,請選擇 `ReorderableList`。 + +| 元件 | 何時選擇 | +|---|---| +| `StickerPeel` | 人們為了好玩自己擺放標誌,版面歸他們所有 | +| `ReorderableList` | 項目的順序很重要 | +| `GravityWell` | 一個圖片網格要對指標作出反應 | diff --git a/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/en-US.md b/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/en-US.md index 36697ef8..d3e18dc5 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/en-US.md +++ b/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/en-US.md @@ -83,6 +83,13 @@ grip writes the chosen height in pixels, and any value below the 110 px minimum geometry. The field then scrolls its text. - **A field that must keep the size the person chose**: bind `FrameHeight` with `Mode=TwoWay` and store the value in the view model or in user settings. `FrameHeight` is one-way by default. +- **A short box inside a compact card** (an optional note under a rating, a one-line prompt): lower the minimum with + `FrameMinHeight`, the frame's height with its border and padding. Two lines of text at the default size need 66 px, + three need 85 px. The grip then never drags below the new minimum. + +```xml + +``` ### Binding to a view model @@ -286,7 +293,8 @@ description in the same words everywhere it appears, so a reply shows "character move. The text scrolls inside the frame. Let the person drag the grip, or set `FrameHeight` to the height the task needs. **Why does `FrameHeight` of 60 still show a 110 px field?** Any height below the 110 px minimum is raised to the minimum. -The minimum applies at every `Size`, so `Small` does not make the field shorter. +The minimum applies at every `Size`, so `Small` does not make the field shorter; lower `FrameMinHeight` for a +shorter field. **Does the field insert a tab character when the person presses Tab?** No. `AcceptsTab` is false, so Tab moves focus to the next control. Set `AcceptsTab="True"` only when the field is a code or table editor and a tab character is the value. diff --git a/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-CN.md b/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-CN.md index ce67773d..fde22ea8 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-CN.md +++ b/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-CN.md @@ -1,7 +1,7 @@ --- title: 多行文本框 summary: 用于备注、描述和较长文本的多行输入字段。 -source-hash: 7e1f98021e7c +source-hash: 500366a42911 --- @@ -60,6 +60,11 @@ source-hash: 7e1f98021e7c - **多段落的回复和摘要**:将 `FrameHeight` 设为约 200,增加初始高度,使第一屏就能显示整份草稿。 - **固定布局,例如仪表板卡片或侧边面板**:设置 `IsResizable="False"`,使卡片保持其几何尺寸。此时字段会滚动其文字。 - **必须保留用户所选尺寸的字段**:以 `Mode=TwoWay` 绑定 `FrameHeight`,并将该值保存在视图模型或用户设置中。`FrameHeight` 默认为单向。 +- **紧凑卡片中的矮输入框**(评分下方的可选备注、一行提示词):用 `FrameMinHeight` 降低最小值,它是外框连同边框和内边距的高度。默认尺寸下两行文字需要 66 px,三行需要 85 px。之后手柄也不会拖到新的最小值以下。 + +```xml + +``` ### 绑定到视图模型 @@ -240,7 +245,7 @@ notes.FrameHeight = 200; **为什么我输入更多行时字段高度保持不变?** 它保持高度,以免表单布局移动。文字在外框内滚动。让用户拖动手柄,或将 `FrameHeight` 设为任务所需的高度。 -**为什么 `FrameHeight` 为 60 时字段仍是 110 px?** 低于 110 px 最小值的任何高度都会被提升到最小值。最小值适用于每个 `Size`,因此 `Small` 不会使字段变矮。 +**为什么 `FrameHeight` 为 60 时字段仍是 110 px?** 低于 110 px 最小值的任何高度都会被提升到最小值。最小值适用于每个 `Size`,因此 `Small` 不会使字段变矮;需要更矮的字段时降低 `FrameMinHeight`。 **用户按 Tab 时字段会插入制表符吗?** 不会。`AcceptsTab` 为 false,因此 Tab 会把焦点移到下一个控件。只有当字段是代码或表格编辑器、且制表符本身就是值时,才设置 `AcceptsTab="True"`。 diff --git a/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-TW.md b/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-TW.md index 8250e627..3936524c 100644 --- a/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-TW.md +++ b/gallery/TKCode.Arc.Gallery/Docs/TextFields/Textarea/zh-TW.md @@ -1,7 +1,7 @@ --- title: 多行文字框 summary: 用於備註、描述和較長文字的多行輸入欄位。 -source-hash: 7e1f98021e7c +source-hash: 500366a42911 --- @@ -60,6 +60,11 @@ source-hash: 7e1f98021e7c - **多段落的回覆和摘要**:將 `FrameHeight` 設為約 200,增加初始高度,使第一屏就能顯示整份草稿。 - **固定版面,例如儀表板卡片或側邊面板**:設定 `IsResizable="False"`,使卡片保持其幾何尺寸。此時欄位會捲動其文字。 - **必須保留使用者所選尺寸的欄位**:以 `Mode=TwoWay` 繫結 `FrameHeight`,並將該值保存在檢視模型或使用者設定中。`FrameHeight` 預設為單向。 +- **緊湊卡片中的矮輸入框**(評分下方的選用備註、一行提示詞):用 `FrameMinHeight` 降低最小值,它是外框連同邊框和內距的高度。預設尺寸下兩行文字需要 66 px,三行需要 85 px。之後把手也不會拖到新的最小值以下。 + +```xml + +``` ### 繫結到檢視模型 @@ -240,7 +245,7 @@ notes.FrameHeight = 200; **為什麼我輸入更多行時欄位高度保持不變?** 它保持高度,以免表單版面移動。文字在外框內捲動。讓使用者拖動把手,或將 `FrameHeight` 設為任務所需的高度。 -**為什麼 `FrameHeight` 為 60 時欄位仍是 110 px?** 低於 110 px 最小值的任何高度都會被提升到最小值。最小值適用於每個 `Size`,因此 `Small` 不會使欄位變矮。 +**為什麼 `FrameHeight` 為 60 時欄位仍是 110 px?** 低於 110 px 最小值的任何高度都會被提升到最小值。最小值適用於每個 `Size`,因此 `Small` 不會使欄位變矮;需要更矮的欄位時降低 `FrameMinHeight`。 **使用者按 Tab 時欄位會插入定位字元嗎?** 不會。`AcceptsTab` 為 false,因此 Tab 會把焦點移到下一個控制項。只有當欄位是程式碼或表格編輯器、且定位字元本身就是值時,才設定 `AcceptsTab="True"`。 diff --git a/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.Chrome.cs b/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.Chrome.cs index b2b3b1e9..7cff5d62 100644 --- a/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.Chrome.cs +++ b/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.Chrome.cs @@ -405,8 +405,10 @@ void Render() if (e.Key == Key.Escape) { e.Handled = true; - popup.IsOpen = false; + // Before the popup closes: closing it with focus inside puts focus back by itself, without the keyboard + // ring, and focusing the already focused anchor then changes nothing. anchor.Focus(NavigationMethod.Tab); + popup.IsOpen = false; } }, RoutingStrategies.Bubble); ((ISetLogicalParent)popup).SetParent(this); @@ -418,7 +420,8 @@ void Render() } else { - list.Children.OfType().FirstOrDefault()?.Focus(NavigationMethod.Tab); + // Opened with a click: the first value takes focus without the keyboard ring. + list.Children.OfType().FirstOrDefault()?.Focus(DataGridFocusModality.For(anchor)); } } diff --git a/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.cs b/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.cs index 132bc94d..1adeba59 100644 --- a/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.cs +++ b/src/TKCode.Arc.AdvancedCharts/DataGrid/DataGrid.cs @@ -565,6 +565,7 @@ internal void RaiseCellInvoked(object row, object? key, DataGridColumn column, o static DataGrid() { FocusableProperty.OverrideDefaultValue(false); + DataGridFocusModality.Watch(); } public DataGrid() diff --git a/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridColumnCells.cs b/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridColumnCells.cs index 4da80812..ca8e7f25 100644 --- a/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridColumnCells.cs +++ b/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridColumnCells.cs @@ -499,7 +499,8 @@ public void FocusInput() { if (Column?.Type is DataGridColumnType.Select or DataGridColumnType.Boolean) { - _select?.Focus(NavigationMethod.Tab); + // Reached from the column menu's Filter: no keyboard ring after a click. + _select?.Focus(DataGridFocusModality.For(this)); } else { diff --git a/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridEditorLayer.cs b/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridEditorLayer.cs index f2a2cee9..1b4dc48c 100644 --- a/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridEditorLayer.cs +++ b/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridEditorLayer.cs @@ -160,7 +160,10 @@ public void OpenTemplate(DataGridEditState edit, Avalonia.Controls.Templates.IDa InvalidateArrange(); UpdateLayout(); var first = host.GetVisualDescendants().OfType().FirstOrDefault(c => c.Focusable && c.IsEffectivelyVisible && c.IsEffectivelyEnabled); - (first ?? host as InputElement)?.Focus(NavigationMethod.Tab); + // Editing started with a double-click: a non-text editor takes focus without the keyboard ring (a text box keeps + // Tab, which selects its text). + var target = first ?? host as InputElement; + target?.Focus(target is TextBox ? NavigationMethod.Tab : DataGridFocusModality.For(this)); } private void OnHostKeyDown(object? sender, KeyEventArgs e) diff --git a/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridFocusModality.cs b/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridFocusModality.cs new file mode 100644 index 00000000..4df80eee --- /dev/null +++ b/src/TKCode.Arc.AdvancedCharts/DataGrid/Parts/DataGridFocusModality.cs @@ -0,0 +1,83 @@ +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace TKCode.Arc; + +/// +/// The kind of the last input each window saw: a pointer press or a key. Code that moves focus because of an action (a +/// pick closes a menu and focus goes back to its trigger, a button leaves and focus goes to its neighbour) takes its +/// navigation method from here, so the keyboard focus ring (:focus-visible) shows after keyboard use only, as +/// browsers do. Input in a popup counts for the window that hosts it. +/// +/// +/// The data grid's copy of Core's internal FocusModality, which this package cannot see; keep the two alike. +/// A control that calls calls when it is created, so the input that leads to its +/// first focus move is already seen. Before any input a window counts as keyboard (the method stays what it was). +/// +internal static class DataGridFocusModality +{ + private static readonly ConditionalWeakTable> s_pointer = new(); + + static DataGridFocusModality() + { + InputElement.PointerPressedEvent.AddClassHandler(static (root, _) => Set(root, pointer: true), + RoutingStrategies.Tunnel, handledEventsToo: true); + InputElement.KeyDownEvent.AddClassHandler(static (root, _) => Set(root, pointer: false), + RoutingStrategies.Tunnel, handledEventsToo: true); + } + + /// Starts watching input (the first call does; later ones do nothing). + public static void Watch() + { + } + + /// + /// Whether the last press or key in the window of was a pointer press; null when the window + /// has seen neither yet. + /// + public static bool? LastWasPointer(Visual? visual) => + WindowOf(visual is null ? null : TopLevel.GetTopLevel(visual)) is { } window && s_pointer.TryGetValue(window, out var slot) + ? slot.Value + : null; + + /// + /// after a pointer press in the window of , else + /// . + /// + public static NavigationMethod For(Visual? visual, NavigationMethod keyboard = NavigationMethod.Tab) => + LastWasPointer(visual) == true ? NavigationMethod.Pointer : keyboard; + + private static void Set(TopLevel root, bool pointer) + { + if (WindowOf(root) is { } window) + { + s_pointer.GetValue(window, static _ => new StrongBox()).Value = pointer; + } + } + + /// A native popup root stands for the window of its popup (the root's logical parent). + private static TopLevel? WindowOf(TopLevel? root) + { + for (var depth = 0; depth < 8 && root is PopupRoot popupRoot; depth++) + { + TopLevel? owner = null; + for (var element = popupRoot.Parent; element is not null && owner is null; element = element.Parent) + { + if (element is Visual visual && TopLevel.GetTopLevel(visual) is { } top && !ReferenceEquals(top, root)) + { + owner = top; + } + } + if (owner is null) + { + break; + } + root = owner; + } + return root; + } +} diff --git a/src/TKCode.Arc.AdvancedCharts/DataGrid/Themes/DataGridTheme.axaml b/src/TKCode.Arc.AdvancedCharts/DataGrid/Themes/DataGridTheme.axaml index 94efe1e2..677bdfea 100644 --- a/src/TKCode.Arc.AdvancedCharts/DataGrid/Themes/DataGridTheme.axaml +++ b/src/TKCode.Arc.AdvancedCharts/DataGrid/Themes/DataGridTheme.axaml @@ -542,7 +542,8 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" - Padding="{TemplateBinding Padding}"> + Padding="{TemplateBinding Padding}" + ClipToBounds="True"> + diff --git a/src/TKCode.Arc.Blocks/BlockCatalog.cs b/src/TKCode.Arc.Blocks/BlockCatalog.cs index 88cf2261..bb4618af 100644 --- a/src/TKCode.Arc.Blocks/BlockCatalog.cs +++ b/src/TKCode.Arc.Blocks/BlockCatalog.cs @@ -240,6 +240,12 @@ public static class BlockCatalog new("EnvVariables", "Env variables", "env-variables", "Developer", "A project's environment variables as a matrix of switches per environment, with masked values, inline editing, .env import, a fingerprint comparison and a redeploy banner.", IsArcPort: false), + new("DomainVerify", "Domain verify", "domain-verify", "Developer", + "A custom domain setup card where customers add a domain and copy the DNS records, then a live checker marks each record valid, not found or misconfigured with a one-line fix and finishes with the domain verified and its SSL certificate issued.", + IsArcPort: false), + new("WebhookTester", "Webhook tester", "webhook-tester", "Developer", + "A webhook tester where you pick an event, edit its JSON payload and send a signed test event, watch it go from signed to sent to the endpoint's answer, read the response and retry failed deliveries from a recent list.", + IsArcPort: false), new("FileUpload", "File upload", "file-upload", "Media", "A complete file selection flow with constraints, progress, and error feedback.", IsArcPort: true), diff --git a/src/TKCode.Arc.Blocks/BlockFocusModality.cs b/src/TKCode.Arc.Blocks/BlockFocusModality.cs new file mode 100644 index 00000000..c0ab21be --- /dev/null +++ b/src/TKCode.Arc.Blocks/BlockFocusModality.cs @@ -0,0 +1,83 @@ +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace TKCode.Arc; + +/// +/// The kind of the last input each window saw: a pointer press or a key. Code that moves focus because of an action (a +/// pick closes a menu and focus goes back to its trigger, a button leaves and focus goes to its neighbour) takes its +/// navigation method from here, so the keyboard focus ring (:focus-visible) shows after keyboard use only, as +/// browsers do. Input in a popup counts for the window that hosts it. +/// +/// +/// The blocks' copy of Core's internal FocusModality, which this package cannot see; keep the two alike. +/// A control that calls calls when it is created, so the input that leads to its +/// first focus move is already seen. Before any input a window counts as keyboard (the method stays what it was). +/// +internal static class BlockFocusModality +{ + private static readonly ConditionalWeakTable> s_pointer = new(); + + static BlockFocusModality() + { + InputElement.PointerPressedEvent.AddClassHandler(static (root, _) => Set(root, pointer: true), + RoutingStrategies.Tunnel, handledEventsToo: true); + InputElement.KeyDownEvent.AddClassHandler(static (root, _) => Set(root, pointer: false), + RoutingStrategies.Tunnel, handledEventsToo: true); + } + + /// Starts watching input (the first call does; later ones do nothing). + public static void Watch() + { + } + + /// + /// Whether the last press or key in the window of was a pointer press; null when the window + /// has seen neither yet. + /// + public static bool? LastWasPointer(Visual? visual) => + WindowOf(visual is null ? null : TopLevel.GetTopLevel(visual)) is { } window && s_pointer.TryGetValue(window, out var slot) + ? slot.Value + : null; + + /// + /// after a pointer press in the window of , else + /// . + /// + public static NavigationMethod For(Visual? visual, NavigationMethod keyboard = NavigationMethod.Tab) => + LastWasPointer(visual) == true ? NavigationMethod.Pointer : keyboard; + + private static void Set(TopLevel root, bool pointer) + { + if (WindowOf(root) is { } window) + { + s_pointer.GetValue(window, static _ => new StrongBox()).Value = pointer; + } + } + + /// A native popup root stands for the window of its popup (the root's logical parent). + private static TopLevel? WindowOf(TopLevel? root) + { + for (var depth = 0; depth < 8 && root is PopupRoot popupRoot; depth++) + { + TopLevel? owner = null; + for (var element = popupRoot.Parent; element is not null && owner is null; element = element.Parent) + { + if (element is Visual visual && TopLevel.GetTopLevel(visual) is { } top && !ReferenceEquals(top, root)) + { + owner = top; + } + } + if (owner is null) + { + break; + } + root = owner; + } + return root; + } +} diff --git a/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/AgentMemory.Template.cs b/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/AgentMemory.Template.cs index e2674099..3f4074d2 100644 --- a/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/AgentMemory.Template.cs +++ b/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/AgentMemory.Template.cs @@ -533,7 +533,7 @@ private void FocusFilters(NavigationMethod method) _search.FocusInput(method); return; } - _filters?.FocusSelected(); + _filters?.FocusSelected(method); } #endregion diff --git a/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/Themes/AgentMemoryTheme.axaml b/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/Themes/AgentMemoryTheme.axaml index 219b76f0..139bf49d 100644 --- a/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/Themes/AgentMemoryTheme.axaml +++ b/src/TKCode.Arc.Blocks/Blocks/Ai/AgentMemory/Themes/AgentMemoryTheme.axaml @@ -192,6 +192,7 @@ --> + + diff --git a/src/TKCode.Arc.Blocks/Blocks/Ai/_Shared/PillFilterBar/PillFilterBar.cs b/src/TKCode.Arc.Blocks/Blocks/Ai/_Shared/PillFilterBar/PillFilterBar.cs index a484f930..e4784bf8 100644 --- a/src/TKCode.Arc.Blocks/Blocks/Ai/_Shared/PillFilterBar/PillFilterBar.cs +++ b/src/TKCode.Arc.Blocks/Blocks/Ai/_Shared/PillFilterBar/PillFilterBar.cs @@ -159,16 +159,17 @@ public event EventHandler? SelectionChanged internal ScrollViewer? Scroller => _scrollViewer; /// - /// Focuses the selected pill as the keyboard does (with its focus ring) and scrolls it into view. Returns false when - /// there is no selected pill or it cannot take focus (concealed, disabled, not shown). + /// Focuses the selected pill as the keyboard does (with its focus ring; Pointer leaves the + /// ring off) and scrolls it into view. Returns false when there is no selected pill or it cannot take focus (concealed, + /// disabled, not shown). /// - public bool FocusSelected() + public bool FocusSelected(NavigationMethod method = NavigationMethod.Tab) { if (_selected is not { } pill || IsConcealed || !pill.IsEffectivelyVisible || !pill.IsEffectivelyEnabled) { return false; } - if (!pill.Focus(NavigationMethod.Tab)) + if (!pill.Focus(method)) { return false; } diff --git a/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorer.Behavior.cs b/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorer.Behavior.cs index 187c5a5d..ebf3e287 100644 --- a/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorer.Behavior.cs +++ b/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorer.Behavior.cs @@ -109,6 +109,7 @@ public partial class MetricExplorer static MetricExplorer() { + BlockFocusModality.Watch(); FocusableProperty.OverrideDefaultValue(false); } @@ -431,7 +432,9 @@ private void OnIsOpenChanged(bool open) _announcer?.Announce(string.Format(CultureInfo.CurrentCulture, ClosedAnnouncementFormat, _series.Length)); if (_closedByUser) { - Dispatcher.UIThread.Post(() => _stage?.FocusCard(index), DispatcherPriority.Loaded); + // Closed with a click: the card takes focus back without the keyboard ring. + var method = BlockFocusModality.For(this); + Dispatcher.UIThread.Post(() => _stage?.FocusCard(index, method), DispatcherPriority.Loaded); } } _openedByKeyboard = false; diff --git a/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorerStage.cs b/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorerStage.cs index 765c196e..d841b779 100644 --- a/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorerStage.cs +++ b/src/TKCode.Arc.Blocks/Blocks/Analytics/MetricExplorer/MetricExplorerStage.cs @@ -108,12 +108,12 @@ public MetricExplorerGeometry Geometry } } - /// Moves keyboard focus to card . - public void FocusCard(int index) + /// Moves focus to card (with the keyboard ring unless is Pointer). + public void FocusCard(int index, NavigationMethod method = NavigationMethod.Tab) { if ((uint)index < (uint)_cards.Count) { - _cards[index].Focus(NavigationMethod.Tab); + _cards[index].Focus(method); } } diff --git a/src/TKCode.Arc.Blocks/Blocks/AppShell/PageHeader/Themes/PageHeaderTheme.axaml b/src/TKCode.Arc.Blocks/Blocks/AppShell/PageHeader/Themes/PageHeaderTheme.axaml index 5719dc75..4107dfc5 100644 --- a/src/TKCode.Arc.Blocks/Blocks/AppShell/PageHeader/Themes/PageHeaderTheme.axaml +++ b/src/TKCode.Arc.Blocks/Blocks/AppShell/PageHeader/Themes/PageHeaderTheme.axaml @@ -46,6 +46,7 @@ + diff --git a/src/TKCode.Arc.Blocks/Blocks/AppShell/SearchResults/Themes/SearchResultsTheme.axaml b/src/TKCode.Arc.Blocks/Blocks/AppShell/SearchResults/Themes/SearchResultsTheme.axaml index e806805c..2ecff52e 100644 --- a/src/TKCode.Arc.Blocks/Blocks/AppShell/SearchResults/Themes/SearchResultsTheme.axaml +++ b/src/TKCode.Arc.Blocks/Blocks/AppShell/SearchResults/Themes/SearchResultsTheme.axaml @@ -31,6 +31,7 @@ + @@ -80,6 +81,7 @@ + diff --git a/src/TKCode.Arc.Blocks/Blocks/Auth/MultiStepForm/Themes/MultiStepFormTheme.axaml b/src/TKCode.Arc.Blocks/Blocks/Auth/MultiStepForm/Themes/MultiStepFormTheme.axaml index 80f76c95..3aab02e0 100644 --- a/src/TKCode.Arc.Blocks/Blocks/Auth/MultiStepForm/Themes/MultiStepFormTheme.axaml +++ b/src/TKCode.Arc.Blocks/Blocks/Auth/MultiStepForm/Themes/MultiStepFormTheme.axaml @@ -213,6 +213,7 @@ + diff --git a/src/TKCode.Arc.Blocks/Blocks/Developer/DomainVerify/DomainVerify.Logic.cs b/src/TKCode.Arc.Blocks/Blocks/Developer/DomainVerify/DomainVerify.Logic.cs new file mode 100644 index 00000000..1ab37ca1 --- /dev/null +++ b/src/TKCode.Arc.Blocks/Blocks/Developer/DomainVerify/DomainVerify.Logic.cs @@ -0,0 +1,1105 @@ +using Avalonia; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using TKCode.Arc.DesignTokens; +using TKCode.Arc.Motion; +using TKCode.Arc.Primitives; + +namespace TKCode.Arc; + +[TemplatePart(TitlePart, typeof(TextBlock))] +[TemplatePart(DescriptionPart, typeof(TextBlock))] +[TemplatePart(AddAreaPart, typeof(Control))] +[TemplatePart(AddHeadingPart, typeof(TextBlock))] +[TemplatePart(AddHintPart, typeof(TextBlock))] +[TemplatePart(DomainLabelPart, typeof(TextBlock))] +[TemplatePart(DomainInputPart, typeof(Input))] +[TemplatePart(AddButtonPart, typeof(Button))] +[TemplatePart(DomainAreaPart, typeof(Control))] +[TemplatePart(DomainIconPart, typeof(Border))] +[TemplatePart(DomainTextPart, typeof(TextBlock))] +[TemplatePart(DomainDetailPart, typeof(ArcContentSwap))] +[TemplatePart(DomainStatusPart, typeof(ArcContentSwap))] +[TemplatePart(RemoveButtonPart, typeof(Button))] +[TemplatePart(ColumnHeaderPart, typeof(Grid))] +[TemplatePart(RecordsPart, typeof(Panel))] +[TemplatePart(PlaceholdersPart, typeof(Panel))] +[TemplatePart(EmptyPart, typeof(Control))] +[TemplatePart(EmptyTextPart, typeof(TextBlock))] +[TemplatePart(FooterPart, typeof(Control))] +[TemplatePart(FooterStatusPart, typeof(ArcContentSwap))] +[TemplatePart(FooterActionPart, typeof(Button))] +[TemplatePart(AnnouncerPart, typeof(DeveloperAnnouncer))] +[PseudoClasses(FramedPseudoClass, CompactPseudoClass, EnterPseudoClass, AddingPseudoClass, RecordsPseudoClass, + CheckingPseudoClass, InvalidPseudoClass, VerifiedPseudoClass)] +public partial class DomainVerify +{ + internal const string TitlePart = "PART_Title"; + internal const string DescriptionPart = "PART_Description"; + internal const string AddAreaPart = "PART_AddArea"; + internal const string AddHeadingPart = "PART_AddHeading"; + internal const string AddHintPart = "PART_AddHint"; + internal const string DomainLabelPart = "PART_DomainLabel"; + internal const string DomainInputPart = "PART_Domain"; + internal const string AddButtonPart = "PART_Add"; + internal const string DomainAreaPart = "PART_DomainArea"; + internal const string DomainIconPart = "PART_DomainIcon"; + internal const string DomainTextPart = "PART_DomainText"; + internal const string DomainDetailPart = "PART_DomainDetail"; + internal const string DomainStatusPart = "PART_DomainStatus"; + internal const string RemoveButtonPart = "PART_Remove"; + internal const string ColumnHeaderPart = "PART_ColumnHeader"; + internal const string RecordsPart = "PART_Records"; + internal const string PlaceholdersPart = "PART_Placeholders"; + internal const string EmptyPart = "PART_Empty"; + internal const string EmptyTextPart = "PART_EmptyText"; + internal const string FooterPart = "PART_Footer"; + internal const string FooterStatusPart = "PART_FooterStatus"; + internal const string FooterActionPart = "PART_FooterAction"; + internal const string AnnouncerPart = "PART_Announcer"; + + /// The pseudo-class while the card is a bordered card. + public const string FramedPseudoClass = ":framed"; + + /// The pseudo-class below . + public const string CompactPseudoClass = ":compact"; + + /// The pseudo-class of . + public const string EnterPseudoClass = ":enter"; + + /// The pseudo-class of . + public const string AddingPseudoClass = ":adding"; + + /// The pseudo-class of . + public const string RecordsPseudoClass = ":records"; + + /// The pseudo-class of . + public const string CheckingPseudoClass = ":checking"; + + /// The pseudo-class of . + public const string InvalidPseudoClass = ":invalid"; + + /// The pseudo-class of . + public const string VerifiedPseudoClass = ":verified"; + + private TextBlock? _title; + private TextBlock? _description; + private Control? _addArea; + private TextBlock? _addHeading; + private TextBlock? _addHint; + private TextBlock? _domainLabel; + private Input? _input; + private Button? _add; + private Control? _domainArea; + private Border? _domainIcon; + private TextBlock? _domainText; + private ArcContentSwap? _domainDetail; + private ArcContentSwap? _domainStatus; + private Button? _remove; + private Grid? _columnHeader; + private Panel? _recordsHost; + private Panel? _placeholders; + private Control? _empty; + private TextBlock? _emptyText; + private Control? _footer; + private ArcContentSwap? _footerStatus; + private Button? _footerAction; + private DeveloperAnnouncer? _announcer; + + private readonly List _records = new(); + private readonly List _rows = new(); + private CancellationTokenSource? _operation; + private CancellationTokenSource? _autoPlay; + private ViewportVisibility? _viewport; + private DomainRegions _regions; + private string? _footerError; + private string? _detailKey; + private string? _footerKey; + private int _run; + private bool _compact; + private bool _inputInitialized; + private bool _autoPlayStarted; + private bool _interacted; + private string _lastDefault = string.Empty; + private string? _errorFor; + + /// Scales the delays of the simulated server and of the autoplay script; 0 removes them (tests). + internal double SimulationScale { get; set; } = 1; + + internal IReadOnlyList Rows => _rows; + + internal IReadOnlyList RecordList => _records; + + internal Input? DomainInput => _input; + + internal Button? AddButton => _add; + + internal Button? RemoveButton => _remove; + + internal Button? FooterAction => _footerAction; + + internal ArcContentSwap? FooterStatus => _footerStatus; + + internal ArcContentSwap? DomainStatus => _domainStatus; + + internal ArcContentSwap? DomainDetail => _domainDetail; + + internal Control? EmptyState => _empty; + + internal Grid? ColumnHeader => _columnHeader; + + internal DeveloperAnnouncer? Announcer => _announcer; + + /// True while the card is in its stacked layout (narrower than ). + public bool IsCompact => _compact; + + /// The footer text now (or the message of the last failure). + public string? FooterText => _footerError ?? FooterTextFor(Phase); + + static DomainVerify() + { + FocusableProperty.OverrideDefaultValue(false); + } + + /// Creates the card. + public DomainVerify() + { + PseudoClasses.Set(FramedPseudoClass, true); + PseudoClasses.Set(EnterPseudoClass, true); + AddHandler(PointerPressedEvent, OnUserInput, RoutingStrategies.Tunnel, handledEventsToo: true); + AddHandler(KeyDownEvent, OnUserInput, RoutingStrategies.Tunnel, handledEventsToo: true); + AutomationProperties.SetAccessibilityView(this, AccessibilityView.Content); + } + + #region Public methods + + /// + /// Adds a domain as the "Add domain" button does: (or the text in the field when null) is + /// reduced to a host name and checked, then is awaited and the records are listed. + /// Returns true when the records are listed. + /// + public async Task AddDomainAsync(string? domain = null) + { + if (Phase != DomainVerifyPhase.Enter) + { + return false; + } + var host = DomainVerifySimulation.Normalize(domain ?? _input?.Text); + if (!DomainVerifySimulation.IsValid(host)) + { + SetInputError(Labels.DomainInvalid); + Announce(Labels.DomainInvalid); + return false; + } + SetInputError(null); + if (_input is not null && domain is not null) + { + _input.Text = host; + } + _footerError = null; + var cts = NewOperation(); + SetPhase(DomainVerifyPhase.Adding); + try + { + var records = GetRecordsAction is { } get + ? await get(host, cts.Token) + : await SimulateRecordsAsync(host, cts.Token); + cts.Token.ThrowIfCancellationRequested(); + if (records is null || records.Count == 0) + { + throw new InvalidOperationException(Labels.AddFailed); + } + _records.Clear(); + _records.AddRange(records); + Domain = host; + BuildRows(); + var focusInside = IsKeyboardFocusWithin; + SetPhase(DomainVerifyPhase.Records); + Announce(Labels.AddedAnnouncement(host)); + if (focusInside) + { + _footerAction?.Focus(); + } + return true; + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + if (ReferenceEquals(_operation, cts) && Phase == DomainVerifyPhase.Adding) + { + SetPhase(DomainVerifyPhase.Enter); + } + return false; + } + catch (Exception exception) + { + RaiseEvent(new DomainVerifyFailedEventArgs(FailedEvent, this, host, exception)); + var message = string.IsNullOrWhiteSpace(exception.Message) ? Labels.AddFailed : exception.Message; + SetPhase(DomainVerifyPhase.Enter); + SetInputError(message); + Announce(message); + return false; + } + } + + /// + /// Runs a check as the footer button does: polls every until every + /// record is valid (then the certificate is issued) or one is conflicting. Returns when the run has ended; the phase + /// tells how (, , or back at + /// after a failure). + /// + public async Task CheckAsync() + { + if (Phase is not (DomainVerifyPhase.Records or DomainVerifyPhase.Invalid) || _domain is not { } host) + { + return; + } + var cts = NewOperation(); + var ct = cts.Token; + var run = ++_run; + _footerError = null; + _regions = default; + foreach (var row in _rows) + { + row.SetState(DomainRowState.Checking, null); + } + SetPhase(DomainVerifyPhase.Checking); + Announce(Labels.CheckingAnnouncement); + try + { + for (var attempt = 1; ; attempt++) + { + var context = new DomainVerifyContext(attempt, run); + DomainVerifyResult result; + if (VerifyAction is { } verify) + { + result = await verify(host, _records, context, ct); + } + else + { + await Delay(TimeSpan.FromMilliseconds(600), ct); + result = DomainVerifySimulation.Check(_records, context); + } + ct.ThrowIfCancellationRequested(); + + var invalid = Apply(result); + if (invalid is not null) + { + SetPhase(DomainVerifyPhase.Invalid); + Announce(Labels.NeedsFixAnnouncement(invalid.Record.Type)); + return; + } + if (_rows.All(r => r.State == DomainRowState.Valid)) + { + await IssueAsync(host, cts); + return; + } + await (PollInterval is null && VerifyAction is null ? SimulatedDelay(800, ct) : Delay(EffectivePollInterval, ct)); + foreach (var row in _rows.Where(r => r.State == DomainRowState.NotFound)) + { + row.SetState(DomainRowState.Checking, null); + } + } + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + if (ReferenceEquals(_operation, cts) && Phase == DomainVerifyPhase.Checking) + { + ResetRows(); + SetPhase(DomainVerifyPhase.Records); + } + } + catch (Exception exception) + { + Fail(host, exception); + } + } + + /// + /// Removes the domain as the close button and "Add another" do: cancels any running step, clears the records, raises + /// and returns to the empty field. + /// + public void Remove() + { + var old = _domain; + var focusInside = IsKeyboardFocusWithin; + _operation?.Cancel(); + _operation = null; + _records.Clear(); + ClearRows(); + _footerError = null; + _regions = default; + Domain = null; + Certificate = null; + SetPhase(DomainVerifyPhase.Enter); + if (_input is not null) + { + _input.Text = string.Empty; + SetInputError(null); + } + if (focusInside) + { + _input?.Focus(); + } + Announce(Labels.RemovedAnnouncement); + if (old is not null) + { + var args = new DomainRemovedEventArgs(RemovedEvent, this, old); + RaiseEvent(args); + if (RemovedCommand is { } command && command.CanExecute(args)) + { + command.Execute(args); + } + } + } + + /// Stops the simulated flow, as a click in the card does; whatever step is running finishes. + public void StopAutoPlay() + { + _interacted = true; + _autoPlay?.Cancel(); + } + + #endregion + + #region Template and lifetime + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + if (_input is not null) + { + _input.RemoveHandler(KeyDownEvent, OnInputKeyDown); + _input.TextChanged -= OnInputTextChanged; + } + if (_add is not null) + { + _add.Click -= OnAddClick; + } + if (_remove is not null) + { + _remove.Click -= OnRemoveClick; + } + if (_footerAction is not null) + { + _footerAction.Click -= OnFooterActionClick; + } + + _title = e.NameScope.Find(TitlePart); + _description = e.NameScope.Find(DescriptionPart); + _addArea = e.NameScope.Find(AddAreaPart); + _addHeading = e.NameScope.Find(AddHeadingPart); + _addHint = e.NameScope.Find(AddHintPart); + _domainLabel = e.NameScope.Find(DomainLabelPart); + _input = e.NameScope.Find(DomainInputPart); + _add = e.NameScope.Find