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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ComponentDocumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ This component has no public parameters.
| OnMessageReceivedEvent | EventCallback<ChatMessage> | An event callback that is invoked when a message is received. |
| OnAutoRestored | EventCallback | An event callback that is invoked when the chat window is automatically restored. |

**Toast notifications:** PDChat can double as a toast surface. When a message arrives while the chat is closed (minimized, or fully hidden when `MinimizedButtonPosition` is `None`), it animates into view and, if auto-dismiss is enabled, animates out after a configurable time. Toasts stack (oldest at the top) and each runs its own independent dismiss timer. Defaults are set on `IChatService` via the `Toast*` members (`ToastEnabled`, `ToastEntryAnimation`, `ToastExitAnimation`, `ToastAnimationDurationMs`, `ToastAutoDismiss`, `ToastDisplayDurationSeconds`, `ToastShowTitle`, `ToastMinWidth`/`ToastMaxWidth`/`ToastMinHeight`/`ToastMaxHeight`, `ToastMaxVisible`, `ToastAnchor`) and can be overridden per message via `ChatMessage.ToastOptions`. The legacy `ShowLastMessage` / `ShowLastMessageDurationSeconds` members are obsolete and map onto `ToastEnabled` / `ToastDisplayDurationSeconds`.

---

## PDChatContainer
Expand Down
42 changes: 42 additions & 0 deletions PanoramicData.Blazor.Demo/Pages/PDChatDocumentation.razor
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,46 @@
</tbody>
</table>
</section>
<section id="toasts">
<h2 class="doc-section">Toast notifications</h2>
<p>
<code>PDChat</code> can double as a toast surface. When a message arrives while the chat is
<em>closed</em> &mdash; minimized to its button, or fully hidden when
<code>MinimizedButtonPosition</code> is <code>None</code> (a headless toast) &mdash; the message
animates into view. It stays for a configurable time and then, if auto-dismiss is enabled,
animates out again.
</p>
<p>
Multiple toasts <strong>stack</strong> (oldest at the top, newest at the bottom) and each runs
its own independent dismiss timer, so a longer-lived toast can outlast a later, shorter one.
While more than one toast is present, a dismissing toast leaves via a fixed de-stack animation;
the last remaining toast uses its own configured exit animation. Clicking a toast opens the
chat; hovering pauses its dismiss timer.
</p>
<p>
Defaults are configured on <code>IChatService</code> via the <code>Toast*</code> members and can
be overridden per message with <code>ChatMessage.ToastOptions</code> (any unset override falls
back to the service default).
</p>
<table class="table table-sm doc-props-table">
<thead><tr><th>IChatService member</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>ToastEnabled</code></td><td><code>bool</code></td><td><code>true</code></td><td>Master switch (supersedes <code>ShowLastMessage</code>).</td></tr>
<tr><td><code>ToastEntryAnimation</code></td><td><code>PDChatToastAnimation</code></td><td><code>Grow</code></td><td>Animation used when a toast appears.</td></tr>
<tr><td><code>ToastExitAnimation</code></td><td><code>PDChatToastAnimation</code></td><td><code>Shrink</code></td><td>Animation used when a toast is dismissed.</td></tr>
<tr><td><code>ToastAnimationDurationMs</code></td><td><code>double</code></td><td><code>250</code></td><td>Entry / exit transition time in milliseconds.</td></tr>
<tr><td><code>ToastAutoDismiss</code></td><td><code>bool</code></td><td><code>true</code></td><td>Whether toasts auto-dismiss after the display duration.</td></tr>
<tr><td><code>ToastDisplayDurationSeconds</code></td><td><code>double</code></td><td><code>5</code></td><td>Dwell time before auto-dismiss (supersedes <code>ShowLastMessageDurationSeconds</code>).</td></tr>
<tr><td><code>ToastShowTitle</code></td><td><code>bool</code></td><td><code>true</code></td><td>Whether the message title is shown in the toast.</td></tr>
<tr><td><code>ToastMinWidth</code> / <code>ToastMaxWidth</code></td><td><code>string</code></td><td><code>200px</code> / <code>300px</code></td><td>Toast width constraints (any CSS length).</td></tr>
<tr><td><code>ToastMinHeight</code> / <code>ToastMaxHeight</code></td><td><code>string</code></td><td>(auto)</td><td>Toast height constraints (any CSS length).</td></tr>
<tr><td><code>ToastMaxVisible</code></td><td><code>int</code></td><td><code>5</code></td><td>Max toasts shown at once; the oldest is dismissed to make room.</td></tr>
<tr><td><code>ToastAnchor</code></td><td><code>PDChatButtonPosition</code></td><td><code>BottomRight</code></td><td>Corner the stack anchors to when the button is hidden (otherwise it follows the button).</td></tr>
</tbody>
</table>
<p class="text-muted small">
<code>ShowLastMessage</code> and <code>ShowLastMessageDurationSeconds</code> are obsolete and map onto
<code>ToastEnabled</code> / <code>ToastDisplayDurationSeconds</code> for backward compatibility.
</p>
</section>
</div>
125 changes: 114 additions & 11 deletions PanoramicData.Blazor.Demo/Pages/PDChatPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,99 @@
<div class="form-text">Examples: HH:mm:ss, yyyy-MM-dd HH:mm, dd/MM/yyyy HH:mm:ss</div>
</div>

<div class="mb-3">
<label class="form-label small">Show Last Message Duration (seconds):</label>
<input type="number" @bind="ChatService.ShowLastMessageDurationSeconds" min="1" max="30" step="0.5" class="form-control form-control-sm" placeholder="5">
<div class="form-text">How long the message preview shows when the chat is minimized (1-30 seconds)</div>
<hr />
<h6 class="small text-uppercase text-muted mb-2"><i class="fas fa-bell me-1"></i>Toast Notifications</h6>
<div class="form-text mb-2">PDChat can double as a toast surface: messages arriving while the chat is closed animate in (and, when the chat button is hidden, appear headless). Minimize the chat, then use the buttons on the right.</div>

<div class="row">
<div class="col-6 mb-3">
<label class="form-label small">Entry Animation:</label>
<select @bind="ChatService.ToastEntryAnimation" class="form-select form-select-sm">
<option value="@PDChatToastAnimation.Grow">Grow</option>
<option value="@PDChatToastAnimation.Shrink">Shrink</option>
<option value="@PDChatToastAnimation.Fade">Fade</option>
<option value="@PDChatToastAnimation.Slide">Slide</option>
<option value="@PDChatToastAnimation.None">None</option>
</select>
</div>
<div class="col-6 mb-3">
<label class="form-label small">Exit Animation:</label>
<select @bind="ChatService.ToastExitAnimation" class="form-select form-select-sm">
<option value="@PDChatToastAnimation.Shrink">Shrink</option>
<option value="@PDChatToastAnimation.Grow">Grow</option>
<option value="@PDChatToastAnimation.Fade">Fade</option>
<option value="@PDChatToastAnimation.Slide">Slide</option>
<option value="@PDChatToastAnimation.None">None</option>
</select>
</div>
</div>

<div class="row">
<div class="col-6 mb-3">
<label class="form-label small">Animation Time (ms):</label>
<input type="number" @bind="ChatService.ToastAnimationDurationMs" min="0" max="2000" step="50" class="form-control form-control-sm">
</div>
<div class="col-6 mb-3">
<label class="form-label small">Display Duration (seconds):</label>
<input type="number" @bind="ChatService.ToastDisplayDurationSeconds" min="0.5" max="60" step="0.5" class="form-control form-control-sm">
</div>
</div>

<div class="row">
<div class="col-6 mb-3">
<label class="form-label small">Min / Max Width:</label>
<div class="input-group input-group-sm">
<input type="text" @bind="ChatService.ToastMinWidth" class="form-control" placeholder="200px">
<input type="text" @bind="ChatService.ToastMaxWidth" class="form-control" placeholder="300px">
</div>
</div>
<div class="col-6 mb-3">
<label class="form-label small">Min / Max Height:</label>
<div class="input-group input-group-sm">
<input type="text" @bind="ChatService.ToastMinHeight" class="form-control" placeholder="(auto)">
<input type="text" @bind="ChatService.ToastMaxHeight" class="form-control" placeholder="(auto)">
</div>
</div>
</div>

<div class="row">
<div class="col-6 mb-3">
<label class="form-label small">Max Visible Toasts:</label>
<input type="number" @bind="ChatService.ToastMaxVisible" min="1" max="10" step="1" class="form-control form-control-sm">
</div>
<div class="col-6 mb-3">
<label class="form-label small">Headless Anchor (button = None):</label>
<select @bind="ChatService.ToastAnchor" class="form-select form-select-sm">
<option value="@PDChatButtonPosition.BottomRight">Bottom Right</option>
<option value="@PDChatButtonPosition.BottomLeft">Bottom Left</option>
<option value="@PDChatButtonPosition.TopRight">Top Right</option>
<option value="@PDChatButtonPosition.TopLeft">Top Left</option>
</select>
</div>
</div>

<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="ChatService.ToastEnabled" id="toastEnabledCheck">
<label class="form-check-label small" for="toastEnabledCheck">Enable Toasts</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="ChatService.ToastAutoDismiss" id="toastAutoDismissCheck">
<label class="form-check-label small" for="toastAutoDismissCheck">Auto-Dismiss</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="ChatService.ToastShowTitle" id="toastShowTitleCheck">
<label class="form-check-label small" for="toastShowTitleCheck">Show Title in Toast</label>
</div>
</div>
</div>
<hr />

<div class="row">
<div class="col-6">
<div class="form-check">
Expand All @@ -160,20 +247,20 @@
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="ChatService.AutoRestoreOnNewMessage" @bind:after="OnAutoRestoreChanged" id="autoRestoreCheck">
<label class="form-check-label small" for="autoRestoreCheck">Auto-Restore</label>
<input class="form-check-input" type="checkbox" @bind="ChatService.IsInputPermitted" id="inputPermittedCheck">
<label class="form-check-label small" for="inputPermittedCheck">Enable Message Input</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="ChatService.UseFullWidthMessages" id="fullWidthCheck">
<label class="form-check-label small" for="fullWidthCheck">Full Width Messages</label>
<input class="form-check-input" type="checkbox" @bind="ChatService.AutoRestoreOnNewMessage" @bind:after="OnAutoRestoreChanged" id="autoRestoreCheck">
<label class="form-check-label small" for="autoRestoreCheck">Auto-Restore</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="ChatService.ShowLastMessage" id="showLastMessageCheck">
<label class="form-check-label small" for="showLastMessageCheck">Show Last Message Preview</label>
<input class="form-check-input" type="checkbox" @bind="ChatService.UseFullWidthMessages" id="fullWidthCheck">
<label class="form-check-label small" for="fullWidthCheck">Full Width Messages</label>
</div>
</div>
<div class="col-6">
Expand All @@ -195,6 +282,12 @@
</div>
</div>
</div>

<div class="mb-3 mt-2">
<label class="form-label small">Input Disabled Message (shown at the bottom when input is off):</label>
<input type="text" @bind="ChatService.InputDisabledMessage" @bind:event="oninput" class="form-control form-control-sm" placeholder="(blank = notifications-only, nothing shown)">
<div class="form-text">Uncheck "Enable Message Input" above to see this. Leave blank for pure toast-only mode (the input simply disappears with no message).</div>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -250,7 +343,17 @@
</div>
<div class="col-12">
<button class="btn btn-outline-primary btn-sm w-100 mb-2" @onclick="TestMessagePreview">
<i class="fas fa-eye me-1"></i>Test Message Preview
<i class="fas fa-eye me-1"></i>Test Toast (single)
</button>
</div>
<div class="col-12">
<button class="btn btn-outline-info btn-sm w-100 mb-2" @onclick="TestToastStack">
<i class="fas fa-layer-group me-1"></i>Test Toast Stack (mixed durations)
</button>
</div>
<div class="col-12">
<button class="btn btn-outline-warning btn-sm w-100 mb-2" @onclick="TestToastOverride">
<i class="fas fa-sliders-h me-1"></i>Test Per-Message Override (slide, 12s)
</button>
</div>
<div class="col-12">
Expand Down
59 changes: 57 additions & 2 deletions PanoramicData.Blazor.Demo/Pages/PDChatPage.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,73 @@ private async Task TestMessagePreview()
// Force chat to minimized state first
ChatService.PreferredDockMode = PDChatDockMode.Minimized;
await Task.Delay(100);

var message = new ChatMessage
{
Id = Guid.NewGuid(),
Message = "👀 This message demonstrates the message preview feature! If enabled, you should see this message slide out from the chat button for a few seconds.",
Message = "👀 This message demonstrates the toast feature! With the chat closed, it animates in using the configured entry animation and auto-dismisses after the display duration.",
Sender = Bot,
Type = MessageType.Normal,
Timestamp = DateTime.UtcNow
};
ChatService.SendMessage(message);
}

// Sends three toasts in quick succession with different display durations to demonstrate the
// stacking behaviour: oldest at the top, each dismissing on its own independent timer.
private async Task TestToastStack()
{
ChatService.PreferredDockMode = PDChatDockMode.Minimized;
await Task.Delay(100);

var specs = new (string Text, MessageType Type, double Seconds)[]
{
("🥇 First toast — stays 12 seconds (oldest, at the top).", MessageType.Normal, 12),
("🥈 Second toast — stays 6 seconds.", MessageType.Warning, 6),
("🥉 Third toast — stays 2 seconds (dismisses first, de-stacking the others).", MessageType.Success, 2),
};

foreach (var spec in specs)
{
ChatService.SendMessage(new ChatMessage
{
Id = Guid.NewGuid(),
Message = spec.Text,
Sender = Bot,
Type = spec.Type,
Timestamp = DateTime.UtcNow,
ToastOptions = new ChatToastOptions { DisplayDurationSeconds = spec.Seconds }
});

await Task.Delay(300);
}
}

// Sends a single toast that overrides the service defaults on a per-message basis.
private async Task TestToastOverride()
{
ChatService.PreferredDockMode = PDChatDockMode.Minimized;
await Task.Delay(100);

ChatService.SendMessage(new ChatMessage
{
Id = Guid.NewGuid(),
Title = "Per-message override",
Message = "This toast overrides the defaults: it slides in and out, stays for 12 seconds, and uses a wider max-width — regardless of the service-level toast settings.",
Sender = Bot,
Type = MessageType.Normal,
Timestamp = DateTime.UtcNow,
ToastOptions = new ChatToastOptions
{
EntryAnimation = PDChatToastAnimation.Slide,
ExitAnimation = PDChatToastAnimation.Slide,
DisplayDurationSeconds = 12,
AnimationDurationMs = 350,
MaxWidth = "360px"
}
});
}

private void SendHtmlMessage()
{
var message = new ChatMessage
Expand Down
Loading
Loading