Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions packages/stream_chat_flutter_ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@
- A pie's screen-reader summary lists its slice names.
- `ChartSemantics.isEmpty` is `true` for a pie with no positive slice.
- The README documents the new components — agent steps, reasoning, tool calls, approvals, the client tool runner, `AILocalModel` and chart legends — and its `SpeechToTextButton` snippet now uses `SpeechToTextConfig`.
- The `AIConversationView`, `AIComponentBuilders.userMessage`, `assistantMessage` and `replyActions`
docs and the README say that the conversation caches its rows, so a builder must read state that
changes, such as a reply's rating, through a `Listenable`, and the builders should be created once.
The README's `replyActions` example now does that; before, its 👍 never filled.

🐞 Fixed

Expand Down
32 changes: 24 additions & 8 deletions packages/stream_chat_flutter_ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -915,22 +915,38 @@ AIConversationView(
The rows are `AIUserMessageView` and `AIAssistantMessageView`, with attachments as
`AIMessageAttachmentView`. Under each finished reply there is room for actions, empty until you
set `AIComponentBuilders.replyActions`, typically to an `AIReplyActionsView` (copy, share,
regenerate, rate):
regenerate, rate).

The conversation caches its rows: a row is built again only when what its message says changes, or
the builders do, so a streaming reply doesn't rebuild the rest. Two consequences for builders that
show state of your own, such as a reply's rating:

- Read that state through a `Listenable` (a `ListenableBuilder` or `ValueListenableBuilder` in what
the builder returns). A value captured directly isn't read again when it changes, so a tapped 👍
wouldn't fill.
- Create the builders once, such as in a `late final` field, not in `build`: new builders rebuild
every row whenever your widget rebuilds, such as on every chunk of a streaming reply.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Optional: the same applies to onAttachmentTap and approver. _Shown compares both, so an onAttachmentTap: (a) => ... closure written in build rebuilds every row on each host rebuild, just like inline builders. I checked this with a test. The AIConversationView class doc could say the same.

Suggested change
- Create the builders once, such as in a `late final` field, not in `build`: new builders rebuild
every row whenever your widget rebuilds, such as on every chunk of a streaming reply.
- Create the builders once, such as in a `late final` field, not in `build`: new builders rebuild
every row whenever your widget rebuilds, such as on every chunk of a streaming reply. The same goes
for the `onAttachmentTap` and `approver` you give `AIConversationView`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3b7168e, in the README and the AIConversationView class doc.


```dart
AIComponentFactory(
builders: AIComponentBuilders(
replyActions: (context, props) => AIReplyActionsView(
// In your State:
final ratings = ValueNotifier<Map<String, AIMessageRating>>({});

late final builders = AIComponentBuilders(
replyActions: (context, props) => ValueListenableBuilder(
valueListenable: ratings,
builder: (context, rated, _) => AIReplyActionsView(
text: props.text,
actions: AIReplyActions(
rating: ratings[props.message.id],
onRate: (rating) => rate(props.message, rating),
rating: rated[props.message.id],
onRate: (rating) => rate(props.message, rating), // updates `ratings`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Optional: show how rate updates ratings. A reader who writes ratings.value[id] = rating changes the map in place, ValueNotifier doesn't notify, and the thumbs-up still doesn't fill, which is the bug this section is warning about.

Suggested change
onRate: (rating) => rate(props.message, rating), // updates `ratings`
onRate: (rating) => rate(props.message, rating), // sets `ratings.value` to a new map

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3b7168e.

onShare: (text, origin) => SharePlus.instance.share(ShareParams(text: text, sharePositionOrigin: origin)),
),
),
),
child: conversation,
)
);

// In build:
AIComponentFactory(builders: builders, child: conversation)
```

Attachments are `AIMessageAttachment`s. `AIMessageAttachmentView` shows images as thumbnails and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,25 +126,42 @@ class AIComponentBuilders with Diagnosticable {
/// A message the person sent, in [AIConversationView]. Defaults to
/// [AIUserMessageView].
///
/// The conversation builds a row again only when what [AIMessage] says about
/// it changes, so read anything else that changes, such as a send status,
/// from your own listenable rather than from the message.
/// The conversation caches its rows: it builds one again only when what
/// [AIMessage] says about it changes, or the builders do. Read anything else
/// that changes, such as a send status, through a [Listenable] of your own
/// (a [ListenableBuilder] or [ValueListenableBuilder] in what you return),
/// and create the builders once, not in `build`.
final AIComponentBuilder<AIUserMessageProps>? userMessage;

/// A reply of the agent, in [AIConversationView]. Defaults to
/// [AIAssistantMessageView]. Like [userMessage], it is built again only when
/// what [AIMessage] says about the reply changes.
/// what [AIMessage] says about the reply changes, or the builders do, so read
/// state of your own through a [Listenable].
final AIComponentBuilder<AIAssistantMessageProps>? assistantMessage;

/// What the person can do with a finished reply, shown under it in
/// [AIConversationView] once its answer is typed out. Nothing by default; an
/// [AIReplyActionsView] is what to return:
/// [AIReplyActionsView] is what to return.
///
/// Like [userMessage], it is built again only when the reply changes, or the
/// builders do. So read the rating, which changes without the reply
/// changing, through a [Listenable], and create the builders once:
///
/// ```dart
/// replyActions: (context, props) => AIReplyActionsView(
/// text: props.text,
/// actions: AIReplyActions(onRegenerate: props.isLatestReply ? chat.regenerate : null),
/// ),
/// // In your State, next to a `ValueNotifier<Map<String, AIMessageRating>> ratings`.
/// late final builders = AIComponentBuilders(
/// replyActions: (context, props) => ValueListenableBuilder(
/// valueListenable: ratings,
/// builder: (context, rated, _) => AIReplyActionsView(
/// text: props.text,
/// actions: AIReplyActions(
/// rating: rated[props.message.id],
/// onRate: (rating) => rate(props.message, rating), // updates `ratings`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Optional: same as in the README. Assigning a new map is what makes the ValueListenableBuilder rebuild.

Suggested change
/// onRate: (rating) => rate(props.message, rating), // updates `ratings`
/// onRate: (rating) => rate(props.message, rating), // sets `ratings.value` to a new map

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3b7168e.

/// onRegenerate: props.isLatestReply ? chat.regenerate : null,
/// ),
/// ),
/// ),
/// );
/// ```
final AIComponentBuilder<AIReplyActionsProps>? replyActions;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@ import 'package:stream_chat_flutter_ai/src/theme/components/conversation_theme.d
/// The rows can be replaced through [AIComponentBuilders.userMessage] and
/// [AIComponentBuilders.assistantMessage], and under each finished reply
/// [AIComponentBuilders.replyActions] shows the person's actions, nothing until
/// you set it. A row is only built again when what it shows changes, so a
/// reply streaming in does not rebuild the rest of the conversation.
/// you set it. A row is only built again when what its message says changes,
/// or the builders do, so a reply streaming in does not rebuild the rest of the
/// conversation. A builder that shows state of your own, such as a reply's
/// rating, must therefore read it through a [Listenable] (a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: "must" says more than the code does. The row clears its cache in didChangeDependencies, so a builder that looks up an InheritedWidget through the context it gets (Theme.of, provider's context.watch) also rebuilds when that widget changes. I checked this with a test. "should" keeps the advice without telling Provider users their setup is wrong.

Suggested change
/// rating, must therefore read it through a [Listenable] (a
/// rating, should therefore read it through a [Listenable] (a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 3b7168e.

/// [ListenableBuilder] or [ValueListenableBuilder] in what it returns), and
/// the builders should be created once rather than in `build`, since new ones
/// rebuild every row.
///
/// Place the composer below the list rather than over it, so the list knows how
/// much of it is visible.
Expand Down
Loading