Skip to content

Generative UI ​

The model answers with components as well as text. When it calls a tool, <MessageParts> renders the component you mapped to that tool. The component receives the arguments while they stream, then the result. It can also ask the user to decide before the conversation goes on.

The demo uses a scripted model so it runs instantly and without a network. The code below is the same as with a real provider.

1. Define tools ​

ts
import { defineTool } from 'vue-llm'

export const getWeather = defineTool<{ city: string; days: number }, Weather>({
  name: 'get_weather',
  description: 'Current weather and forecast for a city',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string' }, days: { type: 'number' } },
    required: ['city'],
  },
  execute: ({ city, days }, { signal }) =>
    fetch(`/api/weather?city=${city}&days=${days}`, { signal }).then(r => r.json()),
})

// No execute: the call waits until the user answers in the UI
export const confirmBooking = defineTool<Booking, Booking & { confirmed: boolean }>({
  name: 'confirm_booking',
  description: 'Ask the user to confirm a restaurant booking',
  parameters: { /* … */ },
})

2. Write a component per tool ​

Tool components receive ToolComponentProps<Args, Result>:

PropDescription
argsArguments, partial while state is 'streaming'
statestreaming → pending → running → result / error
resultThe tool's return value
errorError message if the tool threw
addResult(result)Resolve the call from the UI
toolCallId, toolNameIdentifiers
vue
<!-- WeatherCard.vue -->
<script setup lang="ts">
import type { ToolComponentProps } from 'vue-llm'

const props = defineProps<ToolComponentProps<{ city: string; days: number }, Weather>>()
</script>

<template>
  <div class="card">
    <h3>{{ props.args?.city ?? '…' }}</h3>
    <Forecast v-if="props.result" :weather="props.result" />
    <Skeleton v-else />
  </div>
</template>
vue
<!-- BookingCard.vue: human in the loop -->
<script setup lang="ts">
import type { ToolComponentProps } from 'vue-llm'

const props = defineProps<ToolComponentProps<Booking, Booking & { confirmed: boolean }>>()
const decide = (confirmed: boolean) => props.addResult({ ...props.args as Booking, confirmed })
</script>

<template>
  <div class="card">
    {{ props.args?.restaurant }} at {{ props.args?.time }}
    <template v-if="props.state === 'pending'">
      <button @click="decide(true)">Confirm</button>
      <button @click="decide(false)">Cancel</button>
    </template>
  </div>
</template>

3. Render messages ​

vue
<script setup lang="ts">
import { useChat } from 'vue-llm'
import { MessageParts } from 'vue-llm/components'
import 'vue-llm/style.css'

const { messages, input, handleSubmit, addToolResult } = useChat({
  provider: ai,
  model: 'gpt-4o',
  tools: [getWeather, confirmBooking],
})

const components = { get_weather: WeatherCard, confirm_booking: BookingCard }
</script>

<template>
  <MessageParts
    v-for="message in messages"
    :key="message.id"
    :message="message"
    :components="components"
    @tool-result="addToolResult"
  />
</template>

That's the whole setup. get_weather runs on its own, and the model continues once the result arrives. confirm_booking pauses the chat in pending. When the user clicks Confirm, addToolResult records the answer and the chat continues. See Human in the loop.

Without MessageParts ​

MessageParts is optional. Tool calls are ordinary message parts, so you can render them yourself:

vue
<template v-for="part in message.parts">
  <WeatherCard
    v-if="part.type === 'tool-call' && part.toolName === 'get_weather'"
    v-bind="part"
    :add-result="(result) => addToolResult({ toolCallId: part.toolCallId, result })"
  />
</template>

This also works on Vue 2.7, where vue-llm/components is not available.

Streaming arguments ​

Arguments arrive token by token. vue-llm parses the incomplete JSON on every chunk with parsePartialJSON, which is also exported, so args.city appears before the model has finished writing the call. Fields are added as they complete. Strings grow in place, and incomplete keys are dropped until they finish.

ts
import { parsePartialJSON } from 'vue-llm'

parsePartialJSON('{"city":"Os')           // { city: 'Os' }
parsePartialJSON('{"items":[{"n":"a"},{') // { items: [{ n: 'a' }, {}] }

Server-side tools ​

Tools that need secrets belong on the server (createLLMHandler tools). Their calls still stream to the client as tool-call parts with state: 'result', so the same components render them.

Released under the MIT License.