All right, we now have multiple suggestions being given to us. We can ask
What is the capital of Germany?
and it will give us an answer as well as stream in a bunch of suggested questions. Let's ask another follow-up.
What are some popular tourist attractions?
And wow, we get a super detailed answer and actually way too many suggestions.
It seems like the suggestions are scaling with the length of the answer. That's quite funny. Either way, we should see a lot of detail inside our network tab. We get all of the text deltas of this huge answer. And then we see the data-suggestions parts being streamed in below.
So just like before, we have these two distinct phases. In our code, we've changed MyMessage to handle suggestions:
export type MyMessage = UIMessage<never,{suggestions: string[]}>
and we're doing some different logic down the bottom with streamObject. We've given it a Zod schema to say suggestions are an array of strings:
const followupSuggestionsResult = streamObject({model: google("gemini-2.0-flash"),schema: z.object({suggestions: z.array(z.string()),}),messages: [...modelMessages,{role: "assistant",
Then in our prompt below, we are asking the question and saying,
return an array of suggested questions, return two to three suggestions.
That should at least prevent it scaling the number of suggestions with the length of the answer.
We are then creating a data part ID down here:
const dataPartId = crypto.randomUUID()
And then inside the partialObjectStream, we are writing to the UI message stream, using our consistent ID to make sure it overwrites and passing in the suggestions, filtering out the ones that are undefined:
for await (const chunk of followupSuggestionsResult.partialObjectStream) {writer.write({id: dataPartId,type: "data-suggestions",data:chunk.suggestions?.filter((suggestion) => suggestion !== undefined) ?? [],})}
This appears to be necessary because of I think a TypeScript bug with the way that partialObjectStream is typed. So this might not be needed in future, but it seems to be needed now.
In the front end then, we are now grabbing latestSuggestions by checking for the data-suggestions part:
Very similar to the previous logic. And all we're doing now is the default messages now include two messages instead of just one:
This combination of streamObject and custom data parts is incredibly powerful because we can take an object that we're generating it on the fly and stream it to the front end. This ends up being useful in so many applications. Streaming a to-do list to the front end, generating Google searches and queries that you also stream to the front end.
In building my experiments for this course, I've honestly found this combination to be so, so powerful. And let me know in the Discord what ideas you come up with when working with this combo.
Nice work, and I will see you in the next one.
const latestSuggestions = messages[messages.length - 1]?.parts.find((part) => part.type === "data-suggestions")?.data
suggestions={messages.length === 0? ['What is the capital of France?','What is the capital of Germany?',]: latestSuggestions}