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
580 changes: 580 additions & 0 deletions apps/www/content/3.components/1.chatbot/tool.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions apps/www/plugins/ai-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ import {
Suggestion,
SuggestionAiInput,
Task,
Tool,
ToolInputAvailable,
ToolInputStreaming,
ToolOutputAvailable,
ToolOutputError,
Workflow,
} from '@repo/examples'

Expand Down Expand Up @@ -80,6 +85,11 @@ export default defineNuxtPlugin((nuxtApp) => {
vueApp.component('CodeBlockDark', CodeBlockDark)
vueApp.component('Checkpoint', Checkpoint)
vueApp.component('Workflow', Workflow)
vueApp.component('Tool', Tool)
vueApp.component('ToolInputStreaming', ToolInputStreaming)
vueApp.component('ToolInputAvailable', ToolInputAvailable)
vueApp.component('ToolOutputAvailable', ToolOutputAvailable)
vueApp.component('ToolOutputError', ToolOutputError)
vueApp.component('ModelSelector', ModelSelector)
vueApp.component('Context', Context)
vueApp.component('Confirmation', Confirmation)
Expand Down
1 change: 1 addition & 0 deletions packages/elements/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './shimmer'
export * from './sources'
export * from './suggestion'
export * from './task'
export * from './tool'
18 changes: 18 additions & 0 deletions packages/elements/src/tool/Tool.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'

const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

<template>
<Collapsible
:class="cn('not-prose mb-4 w-full rounded-md border', props.class)"
v-bind="$attrs"
>
<slot />
</Collapsible>
</template>
23 changes: 23 additions & 0 deletions packages/elements/src/tool/ToolContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { CollapsibleContent } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'

const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

<template>
<CollapsibleContent
:class="
cn(
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
props.class,
)
"
v-bind="$attrs"
>
<slot />
</CollapsibleContent>
</template>
38 changes: 38 additions & 0 deletions packages/elements/src/tool/ToolHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import type { HTMLAttributes } from 'vue'
import { CollapsibleTrigger } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { ChevronDownIcon, WrenchIcon } from 'lucide-vue-next'
import StatusBadge from './ToolStatusBadge.vue'

const props = defineProps<{
title?: string
type: ToolUIPart['type']
state: ToolUIPart['state']
class?: HTMLAttributes['class']
}>()
</script>

<template>
<CollapsibleTrigger
:class="
cn(
'flex w-full items-center justify-between gap-4 p-3',
props.class,
)
"
v-bind="$attrs"
>
<div class="flex items-center gap-2">
<WrenchIcon class="size-4 text-muted-foreground" />
<span class="font-medium text-sm">
{{ props.title ?? props.type.split('-').slice(1).join(' ') }}
</span>
<StatusBadge :state="props.state" />
</div>
<ChevronDownIcon
class="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
/>
</CollapsibleTrigger>
</template>
32 changes: 32 additions & 0 deletions packages/elements/src/tool/ToolInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { computed } from 'vue'
import { CodeBlock } from '../code-block'

const props = defineProps<{
input: ToolUIPart['input']
class?: HTMLAttributes['class']
}>()

const formattedInput = computed(() => {
return JSON.stringify(props.input, null, 2)
})
</script>

<template>
<div
:class="cn('space-y-2 overflow-hidden p-4', props.class)"
v-bind="$attrs"
>
<h4
class="font-medium text-muted-foreground text-xs uppercase tracking-wide"
>
Parameters
</h4>
<div class="rounded-md bg-muted/50">
<CodeBlock :code="formattedInput" language="json" />
</div>
</div>
</template>
69 changes: 69 additions & 0 deletions packages/elements/src/tool/ToolOutput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { computed } from 'vue'
import { CodeBlock } from '../code-block'

const props = defineProps<{
output: ToolUIPart['output']
errorText: ToolUIPart['errorText']
class?: HTMLAttributes['class']
}>()

const showOutput = computed(() => props.output || props.errorText)

const isObjectOutput = computed(
() => typeof props.output === 'object' && props.output !== null,
)
const isStringOutput = computed(() => typeof props.output === 'string')

const formattedOutput = computed(() => {
if (isObjectOutput.value) {
return JSON.stringify(props.output, null, 2)
}
return props.output as string
})
</script>

<template>
<div
v-if="showOutput"
:class="cn('space-y-2 p-4', props.class)"
v-bind="$attrs"
>
<h4
class="font-medium text-muted-foreground text-xs uppercase tracking-wide"
>
{{ props.errorText ? "Error" : "Result" }}
</h4>
<div
:class="
cn(
'overflow-x-auto rounded-md text-xs [&_table]:w-full',
props.errorText
? 'bg-destructive/10 text-destructive'
: 'bg-muted/50 text-foreground',
)
"
>
<div v-if="errorText" class="p-3">
{{ props.errorText }}
</div>

<CodeBlock
v-else-if="isObjectOutput"
:code="formattedOutput"
language="json"
/>
<CodeBlock
v-else-if="isStringOutput"
:code="formattedOutput"
language="json"
/>
<div v-else class="p-3">
{{ props.output }}
</div>
</div>
</div>
</template>
63 changes: 63 additions & 0 deletions packages/elements/src/tool/ToolStatusBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!-- StatusBadge.vue -->
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import type { Component } from 'vue'
import { Badge } from '@repo/shadcn-vue/components/ui/badge'
import {
CheckCircleIcon,
CircleIcon,
ClockIcon,
XCircleIcon,
} from 'lucide-vue-next'
import { computed } from 'vue'

const props = defineProps<{
state: ToolUIPart['state']
}>()

const label = computed(() => {
const labels: Record<ToolUIPart['state'], string> = {
'input-streaming': 'Pending',
'input-available': 'Running',
'approval-requested': 'Awaiting Approval',
'approval-responded': 'Responded',
'output-available': 'Completed',
'output-error': 'Error',
'output-denied': 'Denied',
}
return labels[props.state]
})

const icon = computed<Component>(() => {
const icons: Record<ToolUIPart['state'], Component> = {
'input-streaming': CircleIcon,
'input-available': ClockIcon,
'approval-requested': ClockIcon,
'approval-responded': CheckCircleIcon,
'output-available': CheckCircleIcon,
'output-error': XCircleIcon,
'output-denied': XCircleIcon,
}
return icons[props.state]
})

const iconClass = computed(() => {
const classes: Record<string, boolean> = {
'size-4': true,
'animate-pulse': props.state === 'input-available',
'text-yellow-600': props.state === 'approval-requested',
'text-blue-600': props.state === 'approval-responded',
'text-green-600': props.state === 'output-available',
'text-red-600': props.state === 'output-error',
'text-orange-600': props.state === 'output-denied',
}
return classes
})
</script>

<template>
<Badge class="gap-1.5 rounded-full text-xs" variant="secondary">
<component :is="icon" :class="iconClass" />
<span>{{ label }}</span>
</Badge>
</template>
5 changes: 5 additions & 0 deletions packages/elements/src/tool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as Tool } from './Tool.vue'
export { default as ToolContent } from './ToolContent.vue'
export { default as ToolHeader } from './ToolHeader.vue'
export { default as ToolInput } from './ToolInput.vue'
export { default as ToolOutput } from './ToolOutput.vue'
5 changes: 5 additions & 0 deletions packages/examples/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ export { default as Sources } from './sources.vue'
export { default as SuggestionAiInput } from './suggestion-ai-input.vue'
export { default as Suggestion } from './suggestion.vue'
export { default as Task } from './task.vue'
export { default as ToolInputAvailable } from './tool-input-available.vue'
export { default as ToolInputStreaming } from './tool-input-streaming.vue'
export { default as ToolOutputAvailable } from './tool-output-available.vue'
export { default as ToolOutputError } from './tool-output-error.vue'
export { default as Tool } from './tool.vue'
export { default as Workflow } from './workflow.vue'
29 changes: 29 additions & 0 deletions packages/examples/src/tool-input-available.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { Tool, ToolContent, ToolHeader, ToolInput } from '@repo/elements/tool'
import { nanoid } from 'nanoid'

const toolCall = {
type: 'tool-image_generation' as const,
toolCallId: nanoid(),
state: 'input-available' as const,
input: {
prompt: 'A futuristic cityscape at sunset with flying cars',
style: 'digital_art',
resolution: '1024x1024',
quality: 'high',
},
output: undefined,
errorText: undefined,
}
</script>

<template>
<div style="height: 500px">
<Tool>
<ToolHeader :state="toolCall.state" :type="toolCall.type" />
<ToolContent>
<ToolInput :input="toolCall.input" />
</ToolContent>
</Tool>
</div>
</template>
28 changes: 28 additions & 0 deletions packages/examples/src/tool-input-streaming.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { Tool, ToolContent, ToolHeader, ToolInput } from '@repo/elements/tool'
import { nanoid } from 'nanoid'

const toolCall = {
type: 'tool-web_search' as const,
toolCallId: nanoid(),
state: 'input-streaming' as const,
input: {
query: 'latest AI market trends 2024',
max_results: 10,
include_snippets: true,
},
output: undefined,
errorText: undefined,
}
</script>

<template>
<div style="height: 500px">
<Tool>
<ToolHeader :state="toolCall.state" :type="toolCall.type" />
<ToolContent>
<ToolInput :input="toolCall.input" />
</ToolContent>
</Tool>
</div>
</template>
35 changes: 35 additions & 0 deletions packages/examples/src/tool-output-available.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from '@repo/elements/tool'
import { nanoid } from 'nanoid'

const toolCall = {
type: 'tool-database_query' as const,
toolCallId: nanoid(),
state: 'output-available' as const,
input: {
query: 'SELECT COUNT(*) FROM users WHERE created_at >= ?',
params: ['2024-01-01'],
database: 'analytics',
},
output: [
{ 'User ID': 1, 'Name': 'John Doe', 'Email': 'john@example.com', 'Created At': '2024-01-15' },
{ 'User ID': 2, 'Name': 'Jane Smith', 'Email': 'jane@example.com', 'Created At': '2024-01-20' },
{ 'User ID': 3, 'Name': 'Bob Wilson', 'Email': 'bob@example.com', 'Created At': '2024-02-01' },
{ 'User ID': 4, 'Name': 'Alice Brown', 'Email': 'alice@example.com', 'Created At': '2024-02-10' },
{ 'User ID': 5, 'Name': 'Charlie Davis', 'Email': 'charlie@example.com', 'Created At': '2024-02-15' },
],
errorText: undefined,
}
</script>

<template>
<div style="height: 500px">
<Tool>
<ToolHeader :state="toolCall.state" :type="toolCall.type" />
<ToolContent>
<ToolInput :input="toolCall.input" />
<ToolOutput v-if="toolCall.state === 'output-available'" :error-text="toolCall.errorText" :output="toolCall.output" />
</ToolContent>
</Tool>
</div>
</template>
Loading