Skip to content

Commit 60c658f

Browse files
peoraycwandev
andauthored
feat: add tool component (#34)
* feat: add tool component * fix: update tool example component * fix: remove unnecessary styles from component loader and tool example --------- Co-authored-by: Charlie Wang <18888351756@163.com>
1 parent d43b6c3 commit 60c658f

File tree

16 files changed

+1185
-0
lines changed

16 files changed

+1185
-0
lines changed

apps/www/content/3.components/1.chatbot/tool.md

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.

apps/www/plugins/ai-elements.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ import {
3636
Suggestion,
3737
SuggestionAiInput,
3838
Task,
39+
Tool,
40+
ToolInputAvailable,
41+
ToolInputStreaming,
42+
ToolOutputAvailable,
43+
ToolOutputError,
3944
Workflow,
4045
} from '@repo/examples'
4146

@@ -80,6 +85,11 @@ export default defineNuxtPlugin((nuxtApp) => {
8085
vueApp.component('CodeBlockDark', CodeBlockDark)
8186
vueApp.component('Checkpoint', Checkpoint)
8287
vueApp.component('Workflow', Workflow)
88+
vueApp.component('Tool', Tool)
89+
vueApp.component('ToolInputStreaming', ToolInputStreaming)
90+
vueApp.component('ToolInputAvailable', ToolInputAvailable)
91+
vueApp.component('ToolOutputAvailable', ToolOutputAvailable)
92+
vueApp.component('ToolOutputError', ToolOutputError)
8393
vueApp.component('ModelSelector', ModelSelector)
8494
vueApp.component('Context', Context)
8595
vueApp.component('Confirmation', Confirmation)

packages/elements/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export * from './shimmer'
2121
export * from './sources'
2222
export * from './suggestion'
2323
export * from './task'
24+
export * from './tool'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
6+
const props = defineProps<{
7+
class?: HTMLAttributes['class']
8+
}>()
9+
</script>
10+
11+
<template>
12+
<Collapsible
13+
:class="cn('not-prose mb-4 w-full rounded-md border', props.class)"
14+
v-bind="$attrs"
15+
>
16+
<slot />
17+
</Collapsible>
18+
</template>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { CollapsibleContent } from '@repo/shadcn-vue/components/ui/collapsible'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
6+
const props = defineProps<{
7+
class?: HTMLAttributes['class']
8+
}>()
9+
</script>
10+
11+
<template>
12+
<CollapsibleContent
13+
:class="
14+
cn(
15+
'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',
16+
props.class,
17+
)
18+
"
19+
v-bind="$attrs"
20+
>
21+
<slot />
22+
</CollapsibleContent>
23+
</template>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import type { ToolUIPart } from 'ai'
3+
import type { HTMLAttributes } from 'vue'
4+
import { CollapsibleTrigger } from '@repo/shadcn-vue/components/ui/collapsible'
5+
import { cn } from '@repo/shadcn-vue/lib/utils'
6+
import { ChevronDownIcon, WrenchIcon } from 'lucide-vue-next'
7+
import StatusBadge from './ToolStatusBadge.vue'
8+
9+
const props = defineProps<{
10+
title?: string
11+
type: ToolUIPart['type']
12+
state: ToolUIPart['state']
13+
class?: HTMLAttributes['class']
14+
}>()
15+
</script>
16+
17+
<template>
18+
<CollapsibleTrigger
19+
:class="
20+
cn(
21+
'flex w-full items-center justify-between gap-4 p-3',
22+
props.class,
23+
)
24+
"
25+
v-bind="$attrs"
26+
>
27+
<div class="flex items-center gap-2">
28+
<WrenchIcon class="size-4 text-muted-foreground" />
29+
<span class="font-medium text-sm">
30+
{{ props.title ?? props.type.split('-').slice(1).join(' ') }}
31+
</span>
32+
<StatusBadge :state="props.state" />
33+
</div>
34+
<ChevronDownIcon
35+
class="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
36+
/>
37+
</CollapsibleTrigger>
38+
</template>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script setup lang="ts">
2+
import type { ToolUIPart } from 'ai'
3+
import type { HTMLAttributes } from 'vue'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
import { computed } from 'vue'
6+
import { CodeBlock } from '../code-block'
7+
8+
const props = defineProps<{
9+
input: ToolUIPart['input']
10+
class?: HTMLAttributes['class']
11+
}>()
12+
13+
const formattedInput = computed(() => {
14+
return JSON.stringify(props.input, null, 2)
15+
})
16+
</script>
17+
18+
<template>
19+
<div
20+
:class="cn('space-y-2 overflow-hidden p-4', props.class)"
21+
v-bind="$attrs"
22+
>
23+
<h4
24+
class="font-medium text-muted-foreground text-xs uppercase tracking-wide"
25+
>
26+
Parameters
27+
</h4>
28+
<div class="rounded-md bg-muted/50">
29+
<CodeBlock :code="formattedInput" language="json" />
30+
</div>
31+
</div>
32+
</template>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
import type { ToolUIPart } from 'ai'
3+
import type { HTMLAttributes } from 'vue'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
import { computed } from 'vue'
6+
import { CodeBlock } from '../code-block'
7+
8+
const props = defineProps<{
9+
output: ToolUIPart['output']
10+
errorText: ToolUIPart['errorText']
11+
class?: HTMLAttributes['class']
12+
}>()
13+
14+
const showOutput = computed(() => props.output || props.errorText)
15+
16+
const isObjectOutput = computed(
17+
() => typeof props.output === 'object' && props.output !== null,
18+
)
19+
const isStringOutput = computed(() => typeof props.output === 'string')
20+
21+
const formattedOutput = computed(() => {
22+
if (isObjectOutput.value) {
23+
return JSON.stringify(props.output, null, 2)
24+
}
25+
return props.output as string
26+
})
27+
</script>
28+
29+
<template>
30+
<div
31+
v-if="showOutput"
32+
:class="cn('space-y-2 p-4', props.class)"
33+
v-bind="$attrs"
34+
>
35+
<h4
36+
class="font-medium text-muted-foreground text-xs uppercase tracking-wide"
37+
>
38+
{{ props.errorText ? "Error" : "Result" }}
39+
</h4>
40+
<div
41+
:class="
42+
cn(
43+
'overflow-x-auto rounded-md text-xs [&_table]:w-full',
44+
props.errorText
45+
? 'bg-destructive/10 text-destructive'
46+
: 'bg-muted/50 text-foreground',
47+
)
48+
"
49+
>
50+
<div v-if="errorText" class="p-3">
51+
{{ props.errorText }}
52+
</div>
53+
54+
<CodeBlock
55+
v-else-if="isObjectOutput"
56+
:code="formattedOutput"
57+
language="json"
58+
/>
59+
<CodeBlock
60+
v-else-if="isStringOutput"
61+
:code="formattedOutput"
62+
language="json"
63+
/>
64+
<div v-else class="p-3">
65+
{{ props.output }}
66+
</div>
67+
</div>
68+
</div>
69+
</template>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!-- StatusBadge.vue -->
2+
<script setup lang="ts">
3+
import type { ToolUIPart } from 'ai'
4+
import type { Component } from 'vue'
5+
import { Badge } from '@repo/shadcn-vue/components/ui/badge'
6+
import {
7+
CheckCircleIcon,
8+
CircleIcon,
9+
ClockIcon,
10+
XCircleIcon,
11+
} from 'lucide-vue-next'
12+
import { computed } from 'vue'
13+
14+
const props = defineProps<{
15+
state: ToolUIPart['state']
16+
}>()
17+
18+
const label = computed(() => {
19+
const labels: Record<ToolUIPart['state'], string> = {
20+
'input-streaming': 'Pending',
21+
'input-available': 'Running',
22+
'approval-requested': 'Awaiting Approval',
23+
'approval-responded': 'Responded',
24+
'output-available': 'Completed',
25+
'output-error': 'Error',
26+
'output-denied': 'Denied',
27+
}
28+
return labels[props.state]
29+
})
30+
31+
const icon = computed<Component>(() => {
32+
const icons: Record<ToolUIPart['state'], Component> = {
33+
'input-streaming': CircleIcon,
34+
'input-available': ClockIcon,
35+
'approval-requested': ClockIcon,
36+
'approval-responded': CheckCircleIcon,
37+
'output-available': CheckCircleIcon,
38+
'output-error': XCircleIcon,
39+
'output-denied': XCircleIcon,
40+
}
41+
return icons[props.state]
42+
})
43+
44+
const iconClass = computed(() => {
45+
const classes: Record<string, boolean> = {
46+
'size-4': true,
47+
'animate-pulse': props.state === 'input-available',
48+
'text-yellow-600': props.state === 'approval-requested',
49+
'text-blue-600': props.state === 'approval-responded',
50+
'text-green-600': props.state === 'output-available',
51+
'text-red-600': props.state === 'output-error',
52+
'text-orange-600': props.state === 'output-denied',
53+
}
54+
return classes
55+
})
56+
</script>
57+
58+
<template>
59+
<Badge class="gap-1.5 rounded-full text-xs" variant="secondary">
60+
<component :is="icon" :class="iconClass" />
61+
<span>{{ label }}</span>
62+
</Badge>
63+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as Tool } from './Tool.vue'
2+
export { default as ToolContent } from './ToolContent.vue'
3+
export { default as ToolHeader } from './ToolHeader.vue'
4+
export { default as ToolInput } from './ToolInput.vue'
5+
export { default as ToolOutput } from './ToolOutput.vue'

0 commit comments

Comments
 (0)