# AiConversationList
**Also known as:** ThreadList
## Description
BitAiConversationList is the sidebar list of conversations, grouped by recency the way people think about them: today, yesterday, the previous seven days, the previous thirty days, then by month. It lists the BitAiConversationSummary records it is given, marks the open one, and raises OnSelect, OnNew and OnDelete - where the conversations are kept, and what switching does, is the app's.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Classes | `BitAiConversationListClassStyles?` | null | Custom CSS classes for different parts of the conversation list. |
| Conversations | `IReadOnlyList<BitAiConversationSummary>?` | null | The conversations to list, in any order: they are sorted by UpdatedAt, newest first, and grouped into today, yesterday, the previous seven days, the previous thirty days, then by month - counted in the TimeZone, else in the TimeZone of the BitAiUiOptions in force, else in the browser's zone. |
| OnDelete | `EventCallback<BitAiConversationSummary>` | | The callback for when the delete button of a conversation is pressed. The delete buttons are shown only while it is set; removing the conversation (and handing the list the new set) is the app's. |
| OnNew | `EventCallback` | | The callback for when the new-conversation button is pressed. |
| OnSelect | `EventCallback<BitAiConversationSummary>` | | The callback for when a conversation is chosen. Opening it - loading it, pointing the chat at its session - is the app's. |
| SelectedId | `string?` | null | The ConversationId of the conversation currently open, which is marked with aria-current and set apart. |
| ShowNewButton | `bool` | true | Shows the new-conversation button at the top of the list. |
| Styles | `BitAiConversationListClassStyles?` | null | Custom CSS styles for different parts of the conversation list. |
| TimeZone | `TimeZoneInfo?` | null | The time zone the conversations are grouped by day in - what counts as today, yesterday, the previous seven and thirty days, and the month of an older one. While it is null, the TimeZone of the BitAiUiOptions in force, else the browser's: under Blazor Server the list is drawn in the zone of the server until the browser has said which zone it runs in. |
| AriaLabel | `string?` | null | Gets or sets the accessible label for the component, used by assistive technologies. |
| Class | `string?` | null | Gets or sets the CSS class name(s) to apply to the rendered element. |
| Dir | `BitDir?` | null | Gets or sets the text directionality for the component's content. |
| Disabled | `bool` | false | Gets or sets a value indicating whether the component is disabled and cannot respond to user interaction. |
| ForceAnimation | `bool` | false | Gets or sets a value indicating whether the component's animations play at their full duration even when reduced motion is requested. |
| HtmlAttributes | `Dictionary<string, object>` | new Dictionary<string, object>() | Captures additional HTML attributes to be applied to the rendered element, in addition to the component's parameters. An attribute written on the component wins over an entry of the same name. |
| Id | `string?` | null | Gets or sets the unique identifier for the component's root element. |
| Style | `string?` | null | Gets or sets the CSS style string to apply to the rendered element. |
| TabIndex | `string?` | null | Gets or sets the tab order index for the component when navigating with the keyboard. |
| Visibility | `BitVisibility` | BitVisibility.Visible | Gets or sets the visibility state (visible, hidden, or collapsed) of the component. |
## Public Members
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| UniqueId | `Guid` | Guid.NewGuid() | Gets the readonly unique identifier for the component's root element, assigned when the component instance is constructed. |
| RootElement | `ElementReference` | | Gets the reference to the root HTML element associated with this component. |
## Enums
### BitVisibility Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Visible | 0 | The content of the component is visible. |
| Hidden | 1 | The content of the component is hidden, but the space it takes on the page remains (visibility:hidden). |
| Collapsed | 2 | The component is hidden (display:none). |
### BitDir Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Ltr | 0 | Ltr (left to right) is to be used for languages that are written from the left to the right (like English). |
| Rtl | 1 | Rtl (right to left) is to be used for languages that are written from the right to the left (like Arabic). |
| Auto | 2 | Auto lets the user agent decide. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then applies that directionality to the whole element. |
## Sub Classes
### BitAiConversationListClassStyles Properties
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Root | `string?` | null | Custom CSS classes/styles for the root element of the BitAiConversationList. |
| NewButton | `BitActionButtonClassStyles?` | null | Custom CSS classes/styles for the new-conversation button of the BitAiConversationList. |
| GroupHeader | `string?` | null | Custom CSS classes/styles for the heading of each group of the BitAiConversationList. |
| List | `string?` | null | Custom CSS classes/styles for the list of each group of the BitAiConversationList. |
| Item | `string?` | null | Custom CSS classes/styles for each item of the BitAiConversationList. |
| ItemButton | `string?` | null | Custom CSS classes/styles for the button each conversation of the BitAiConversationList is chosen with. |
| SelectedItemButton | `string?` | null | Custom CSS classes/styles for the button of the conversation currently open in the BitAiConversationList, added to those of ItemButton. |
| ItemTitle | `string?` | null | Custom CSS classes/styles for the title of each conversation of the BitAiConversationList. |
| DeleteButton | `BitButtonClassStyles?` | null | Custom CSS classes/styles for the delete button of each conversation of the BitAiConversationList. |
### BitAiConversationSummary Properties
The lightweight projection of a conversation a list renders without loading its messages: a positional record, new BitAiConversationSummary(conversationId, title, updatedAt, preview?, messageCount?). BitAiConversation.ToSummary() and IBitAiConversationStore.ListAsync() return them.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ConversationId | `string` | | The identifier of the conversation, which SelectedId is compared with. |
| Title | `string?` | | The display title. A conversation without one is listed under the Untitled string. |
| UpdatedAt | `DateTimeOffset` | | When the conversation last changed, which sorts the list and picks the group it is listed in. |
| Preview | `string?` | null | A snippet of the last message, shown as the tooltip of the conversation. |
| MessageCount | `int` | 0 | How many messages the conversation holds. |
## CSS Variables
Public CSS custom properties read off the component's root. Set one on `:root` to restyle every instance, on any ancestor to restyle the ones inside it, or on the `Style` of one instance to restyle it and every instance nested inside it, which inherits the value too - a parameter changes one alone, as does the real CSS property on its `Style` / `Styles` when that property does not inherit (`opacity`); an inherited one (`color`, `font-family`) reaches the nested instances that do not set it themselves.
| Name | Default Value | Description |
| :--- | :------------ | :---------- |
| `--bit-AiConversationList-gap` | spacing(0.25) | Room between two conversations, and between the parts of the list. |
| `--bit-AiConversationList-padding` | spacing(1) | Inset of the list. |
| `--bit-AiConversationList-group-color` | var(--bit-clr-fg-ter) | Text of the heading of a group. |
| `--bit-AiConversationList-item-radius` | var(--bit-shp-radius-control) | Corners of a conversation. |
| `--bit-AiConversationList-item-color` | var(--bit-clr-fg-pri) | Text of a conversation. |
| `--bit-AiConversationList-item-background` | transparent | Background of a conversation. |
| `--bit-AiConversationList-item-hover-background` | var(--bit-clr-bg-pri-hover) | Background of a conversation under the pointer. |
| `--bit-AiConversationList-selected-color` | var(--bit-clr-pri-fg) | Text of the conversation that is open. |
| `--bit-AiConversationList-selected-background` | var(--bit-clr-pri-tint) | Background of the conversation that is open. |
| `--bit-AiConversationList-selected-hover-background` | var(--bit-clr-bg-pri-hover) | Background of the conversation that is open, under the pointer. |
## Examples
\n**Basic**:
```razor
summaries = CreateSummaries();
private void HandleSelect(BitAiConversationSummary summary)
{
selectedId = summary.ConversationId;
}
private static IReadOnlyList CreateSummaries()
{
var now = DateTimeOffset.Now;
return
[
new("c1", "Plan a weekend in Lisbon", now.AddMinutes(-20), "Three days, mostly on foot, with one day trip to Sintra.", 6),
new("c2", "Draft the release notes", now.AddHours(-2), "Grouped by package, breaking changes first.", 4),
new("c3", "Why does my circuit drop?", now.AddDays(-1), "Most likely an idle timeout on the proxy.", 8),
new("c4", "Compare the pricing tiers", now.AddDays(-4), "The middle tier covers most teams.", 10),
new("c5", "Explain render modes", now.AddDays(-12), "Server, WebAssembly and Auto, per component.", 2),
new("c6", "Name the new component", now.AddDays(-45), "BitAiChat won the vote.", 12),
];
}
```
\n**New & delete**:
```razor
```
```csharp
private int newCount;
private string? editableSelectedId = "c1";
private IReadOnlyList editableSummaries = CreateSummaries(); // CreateSummaries() of the Basic example
private void HandleEditableSelect(BitAiConversationSummary summary)
{
editableSelectedId = summary.ConversationId;
}
private void HandleNew()
{
var summary = new BitAiConversationSummary($"new-{++newCount}", $"New conversation {newCount}", DateTimeOffset.Now);
editableSummaries = [summary, .. editableSummaries];
editableSelectedId = summary.ConversationId;
}
private void HandleDelete(BitAiConversationSummary summary)
{
editableSummaries = editableSummaries.Where(s => s.ConversationId != summary.ConversationId).ToList();
if (editableSelectedId == summary.ConversationId) editableSelectedId = editableSummaries.FirstOrDefault()?.ConversationId;
}
```
\n**With a chat**:
```razor
@implements IAsyncDisposable
@if (chatSession is not null)
{
}
```
```csharp
// One live session per thread, so a reply keeps streaming in a thread that was switched away from.
private readonly Dictionary chatSessions = new(StringComparer.Ordinal);
private IReadOnlyList chatSummaries = [];
private BitAiChatSession? chatSession;
[Inject] private BitAiChatSessionFactory SessionFactory { get; set; } = default!;
protected override void OnInitialized() => OpenChat(new BitAiConversation());
private void OpenChat(BitAiConversation conversation)
{
if (chatSessions.TryGetValue(conversation.ConversationId, out var session) is false)
{
session = SessionFactory.Create(conversation);
conversation.MessageAdded += OnChatMessageAdded;
conversation.Changed += OnChatChanged;
chatSessions[conversation.ConversationId] = session;
}
chatSession = session;
}
private void NewChat()
{
// An untouched thread is reused rather than stacking empty ones.
if (chatSession?.Conversation.Messages.Count == 0) return;
OpenChat(new BitAiConversation());
}
private void SelectChat(BitAiConversationSummary summary)
{
if (chatSessions.TryGetValue(summary.ConversationId, out var session)) OpenChat(session.Conversation);
}
private async Task DeleteChat(BitAiConversationSummary summary)
{
if (chatSessions.Remove(summary.ConversationId, out var removed) is false) return;
removed.Conversation.MessageAdded -= OnChatMessageAdded;
removed.Conversation.Changed -= OnChatChanged;
await removed.DisposeAsync();
RefreshChatSummaries();
if (ReferenceEquals(chatSession, removed) is false) return;
var next = chatSessions.Values.FirstOrDefault(s => s.Conversation.Messages.Count > 0);
OpenChat(next?.Conversation ?? new BitAiConversation());
}
// Only the threads with a message in them are listed, newest first.
private void RefreshChatSummaries()
{
chatSummaries = chatSessions.Values.Select(s => s.Conversation)
.Where(c => c.Messages.Count > 0)
.Select(c => c.ToSummary())
.ToList();
}
// Raised from whatever thread the stream is on, so the list is refreshed on the renderer's.
private void OnChatMessageAdded(BitAiConversation conversation, BitAiMessage message) => _ = InvokeAsync(() =>
{
RefreshChatSummaries();
StateHasChanged();
});
private void OnChatChanged(BitAiConversation conversation) => _ = InvokeAsync(() =>
{
RefreshChatSummaries();
StateHasChanged();
});
public async ValueTask DisposeAsync()
{
foreach (var session in chatSessions.Values)
{
session.Conversation.MessageAdded -= OnChatMessageAdded;
session.Conversation.Changed -= OnChatChanged;
await session.DisposeAsync();
}
chatSessions.Clear();
}
```
\n**TimeZone**:
```razor
```
```csharp
// Two zones twelve hours apart and with no daylight saving time, so a new day has always begun in one of them and not
// yet in the other for twelve hours, whatever the time is.
private static readonly TimeZoneInfo tokyoZone = TimeZoneInfo.CreateCustomTimeZone("Tokyo", TimeSpan.FromHours(9), "Tokyo (UTC+9)", "Tokyo (UTC+9)");
private static readonly TimeZoneInfo saoPauloZone = TimeZoneInfo.CreateCustomTimeZone("Sao Paulo", TimeSpan.FromHours(-3), "São Paulo (UTC-3)", "São Paulo (UTC-3)");
private readonly IReadOnlyList zoneSummaries = CreateZoneSummaries();
private static IReadOnlyList CreateZoneSummaries()
{
var now = DateTimeOffset.UtcNow;
// Halfway between the midnights the two cities passed last: after the later one in one city, before it in the other.
var sinceTokyoMidnight = TimeZoneInfo.ConvertTime(now, tokyoZone).TimeOfDay;
var sinceSaoPauloMidnight = TimeZoneInfo.ConvertTime(now, saoPauloZone).TimeOfDay;
var ago = (sinceTokyoMidnight + sinceSaoPauloMidnight) / 2;
return
[
new("z1", "Check in for the trip", now.AddMinutes(-10), "Seats 14A and 14B.", 2),
new("z2", "Book the flights", now - ago, "The direct one on Friday morning.", 6),
new("z3", "Pack for the weather", now.AddDays(-3), "Light layers and an umbrella.", 4),
];
}
```
\n**Disabled**:
```razor
```
```csharp
// The summaries of the Basic example. None of the handlers is called while the list is disabled.
private string? selectedId = "c2";
private readonly IReadOnlyList summaries = CreateSummaries();
private void HandleSelect(BitAiConversationSummary summary)
{
selectedId = summary.ConversationId;
}
private void HandleDisabledNew() { }
private void HandleDisabledDelete(BitAiConversationSummary summary) { }
private static IReadOnlyList CreateSummaries()
{
var now = DateTimeOffset.Now;
return
[
new("c1", "Plan a weekend in Lisbon", now.AddMinutes(-20), "Three days, mostly on foot, with one day trip to Sintra.", 6),
new("c2", "Draft the release notes", now.AddHours(-2), "Grouped by package, breaking changes first.", 4),
new("c3", "Why does my circuit drop?", now.AddDays(-1), "Most likely an idle timeout on the proxy.", 8),
new("c4", "Compare the pricing tiers", now.AddDays(-4), "The middle tier covers most teams.", 10),
new("c5", "Explain render modes", now.AddDays(-12), "Server, WebAssembly and Auto, per component.", 2),
new("c6", "Name the new component", now.AddDays(-45), "BitAiChat won the vote.", 12),
];
}
```
\n**Cascading parameters**:
```razor
```
```csharp
private readonly BitAiConversationListParams[] conversationListParams =
[
new()
{
ShowNewButton = false,
TimeZone = TimeZoneInfo.Utc,
Styles = new() { GroupHeader = "color:var(--bit-clr-pri)", Root = "border:1px solid var(--bit-clr-brd-sec);border-radius:0.5rem" },
}
];
// The summaries and the handler of the Basic example (CreateSummaries() included there).
private string? selectedId = "c2";
private readonly IReadOnlyList summaries = CreateSummaries();
private void HandleSelect(BitAiConversationSummary summary)
{
selectedId = summary.ConversationId;
}
```
\n**Style & Class**:
```razor
```
```csharp
// The summaries and the handler of the Basic example (CreateSummaries() included there).
private string? selectedId = "c2";
private readonly IReadOnlyList summaries = CreateSummaries();
private void HandleSelect(BitAiConversationSummary summary)
{
selectedId = summary.ConversationId;
}
```
\n**RTL**:
```razor
```
```csharp
private string? rtlSelectedId = "r1";
private readonly IReadOnlyList rtlSummaries =
[
new("r1", "برنامهی سفر آخر هفته", DateTimeOffset.Now.AddHours(-1), "سه روز، بیشتر پیاده.", 6),
new("r2", "یادداشتهای انتشار", DateTimeOffset.Now.AddDays(-1), "دستهبندی بر اساس بسته.", 4),
new("r3", "مقایسهی طرحهای قیمت", DateTimeOffset.Now.AddDays(-5), "طرح میانی برای بیشتر تیمها کافی است.", 10),
];
private void HandleRtlSelect(BitAiConversationSummary summary)
{
rtlSelectedId = summary.ConversationId;
}
```
Open: @selectedId
```
```csharp
private string? selectedId = "c2";
private readonly IReadOnlyList
Tokyo
São Paulo