Skip to content

Commit 8b0ed9e

Browse files
authored
Merge pull request #9 from peoray/feature/suggestion
feat: add suggestion component
2 parents dbe2385 + de9ebb7 commit 8b0ed9e

File tree

11 files changed

+467
-0
lines changed

11 files changed

+467
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script setup lang="ts">
2+
import { Suggestion, Suggestions } from '@repo/elements/suggestion'
3+
4+
const suggestions = [
5+
'What are the latest trends in AI?',
6+
'How does machine learning work?',
7+
'Explain quantum computing',
8+
'Best practices for React development',
9+
'Tell me about TypeScript benefits',
10+
'How to optimize database queries?',
11+
'What is the difference between SQL and NoSQL?',
12+
'Explain cloud computing basics',
13+
]
14+
15+
function handleSuggestionClick(suggestion: string) {
16+
// eslint-disable-next-line no-console
17+
console.log('Selected suggestion:', suggestion)
18+
}
19+
</script>
20+
21+
<template>
22+
<Suggestions>
23+
<Suggestion
24+
v-for="suggestion in suggestions"
25+
:key="suggestion"
26+
:suggestion="suggestion"
27+
@click="handleSuggestionClick"
28+
/>
29+
</Suggestions>
30+
</template>

apps/test/app/pages/index.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import OpenInChat from '~/examples/open-in-chat.vue'
1313
import PromptInput from '~/examples/prompt-input.vue'
1414
import Response from '~/examples/response.vue'
1515
import Shimmer from '~/examples/shimmer.vue'
16+
import Suggestion from '~/examples/suggestion.vue'
1617
1718
const components = [
1819
{ name: 'Message', Component: Message },
@@ -26,6 +27,7 @@ const components = [
2627
{ name: 'CodeBlock', Component: CodeBlock },
2728
{ name: 'Image', Component: Image },
2829
{ name: 'Shimmer', Component: Shimmer },
30+
{ name: 'Suggestion', Component: Suggestion },
2931
{ name: 'OpenInChat', Component: OpenInChat },
3032
{ name: 'Loader', Component: Loader },
3133
]
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
title: Suggestion
3+
description:
4+
icon: lucide:lightbulb
5+
---
6+
7+
The `Suggestion` component displays a horizontal row of clickable suggestions for user interaction.
8+
9+
:::ComponentLoader{label="Preview" componentName="Suggestion"}
10+
:::
11+
12+
## Install using CLI
13+
14+
::tabs{variant="card"}
15+
::div{label="ai-elements-vue"}
16+
```sh
17+
npx ai-elements-vue@latest add suggestion
18+
```
19+
::
20+
::div{label="shadcn-vue"}
21+
22+
```sh
23+
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/suggestion.json
24+
```
25+
::
26+
::
27+
28+
## Install Manually
29+
30+
Copy and paste the following code in the same folder.
31+
32+
:::code-group
33+
```vue [Suggestions.vue]
34+
<script setup lang="ts">
35+
import type { HTMLAttributes } from 'vue'
36+
import { ScrollArea, ScrollBar } from '@repo/shadcn-vue/components/ui/scroll-area'
37+
import { cn } from '@repo/shadcn-vue/lib/utils'
38+
39+
interface SuggestionsProps {
40+
class?: HTMLAttributes['class']
41+
}
42+
43+
const props = defineProps<SuggestionsProps>()
44+
</script>
45+
46+
<template>
47+
<ScrollArea class="w-full overflow-x-auto whitespace-nowrap" v-bind="$attrs">
48+
<div :class="cn('flex w-max flex-nowrap items-center gap-2', props.class)">
49+
<slot />
50+
</div>
51+
<ScrollBar class="hidden" orientation="horizontal" />
52+
</ScrollArea>
53+
</template>
54+
```
55+
56+
```vue [Suggestions.vue]
57+
<script setup lang="ts">
58+
import type { HTMLAttributes } from 'vue'
59+
import { Button } from '@repo/shadcn-vue/components/ui/button'
60+
import { cn } from '@repo/shadcn-vue/lib/utils'
61+
62+
interface SuggestionProps {
63+
suggestion: string
64+
class?: HTMLAttributes['class']
65+
variant?: 'outline' | 'default' | 'destructive' | 'secondary' | 'ghost' | 'link'
66+
size?: 'default' | 'sm' | 'lg' | 'icon'
67+
}
68+
69+
const props = withDefaults(defineProps<SuggestionProps>(), {
70+
variant: 'outline',
71+
size: 'sm',
72+
})
73+
74+
const emit = defineEmits<{
75+
(e: 'click', suggestion: string): void
76+
}>()
77+
78+
function handleClick() {
79+
emit('click', props.suggestion)
80+
}
81+
</script>
82+
83+
<template>
84+
<Button
85+
:class="cn('cursor-pointer rounded-full px-4', props.class)"
86+
:size="props.size"
87+
type="button"
88+
:variant="props.variant"
89+
v-bind="$attrs"
90+
@click="handleClick"
91+
>
92+
<slot>{{ props.suggestion }}</slot>
93+
</Button>
94+
</template>
95+
```
96+
97+
```ts [index.ts]
98+
export { default as Suggestion } from './Suggestion.vue'
99+
export { default as Suggestions } from './Suggestions.vue'
100+
```
101+
:::
102+
103+
## Usage
104+
105+
```ts
106+
import { Suggestion, Suggestions } from '@/components/ai-elements/suggestion'
107+
```
108+
109+
```vue
110+
<Suggestions>
111+
<Suggestion suggestion="What are the latest trends in AI?" />
112+
</Suggestions>
113+
```
114+
115+
## Usage with AI SDK
116+
117+
Build a simple input with suggestions users can click to send a message to the LLM.
118+
119+
Add the following component to your frontend:
120+
121+
```vue [pages/index.vue]
122+
<script setup lang="ts">
123+
import { useChat } from '@ai-sdk/vue'
124+
import { ref } from 'vue'
125+
import {
126+
Conversation,
127+
ConversationContent,
128+
ConversationScrollButton,
129+
} from '@/components/ai-elements/conversation'
130+
import { Message, MessageContent } from '@/components/ai-elements/message'
131+
import { Response } from '@/components/ai-elements/response'
132+
133+
const input = ref('')
134+
const { sendMessage, status } = useChat()
135+
136+
function handleSubmit() {
137+
if (input.value.trim()) {
138+
sendMessage({ text: input.value })
139+
setInput('')
140+
}
141+
}
142+
143+
function handleSuggestionClick(suggestion: string) {
144+
sendMessage({ text: suggestion })
145+
}
146+
</script>
147+
148+
<template>
149+
<div class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
150+
<div class="flex flex-col h-full">
151+
<div class="flex flex-col gap-4">
152+
<Suggestions>
153+
<Suggestion
154+
v-for="suggestion in suggestions"
155+
:key="suggestion"
156+
:suggestion="suggestion"
157+
@click="handleSuggestionClick"
158+
/>
159+
</Suggestions>
160+
161+
<Input class="mt-4 w-full max-w-2xl mx-auto relative" @submit.prevent="handleSubmit">
162+
<PromptInputTextarea
163+
v-model="input"
164+
placeholder="Say something..."
165+
class="pr-12"
166+
/>
167+
<PromptInputSubmit
168+
:status="status === 'streaming' ? 'streaming' : 'ready'"
169+
:disabled="!input.trim()"
170+
class="absolute bottom-1 right-1"
171+
/>
172+
</Input>
173+
</div>
174+
</div>
175+
</div>
176+
</template>
177+
```
178+
179+
## Features
180+
181+
- Horizontal row of clickable suggestion buttons
182+
- Customizable styling with variant and size options
183+
- Flexible layout that wraps suggestions on smaller screens
184+
- click event that emits the selected suggestion string
185+
- Support for both individual suggestions and suggestion lists
186+
- Clean, modern styling with hover effects
187+
- Responsive design with mobile-friendly touch targets
188+
- TypeScript support with proper type definitions
189+
190+
## Examples
191+
192+
### Usage with AI Input
193+
194+
:::ComponentLoader{label="Preview" componentName="SuggestionAiInput"}
195+
:::
196+
197+
## Props
198+
199+
### `<Suggestion />`
200+
201+
:::field-group
202+
::field{name="click" type="event: Event" defaultValue="''"}
203+
The suggestion string to display and emit on click.
204+
::
205+
:::
206+
207+
## Emits
208+
209+
### `<Suggestion />`
210+
211+
:::field-group
212+
::field{name="click" type="string" defaultValue="''"}
213+
The click event that's emitted.
214+
::
215+
:::

apps/www/plugins/ai-elements.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
Shimmer,
1616
ShimmerCustomElements,
1717
ShimmerDurations,
18+
Suggestion,
19+
SuggestionAiInput,
1820
} from '@repo/examples'
1921

2022
import ComponentLoader from '@/components/ComponentLoader.vue'
@@ -38,6 +40,8 @@ export default defineNuxtPlugin((nuxtApp) => {
3840
vueApp.component('Shimmer', Shimmer)
3941
vueApp.component('ShimmerCustomElements', ShimmerCustomElements)
4042
vueApp.component('ShimmerDurations', ShimmerDurations)
43+
vueApp.component('Suggestion', Suggestion)
44+
vueApp.component('SuggestionAiInput', SuggestionAiInput)
4145
vueApp.component('OpenInChat', OpenInChat)
4246
vueApp.component('Loader', Loader)
4347
vueApp.component('LoaderCustomStyling', LoaderCustomStyling)

packages/elements/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './open-in-chat'
99
export * from './prompt-input'
1010
export * from './response'
1111
export * from './shimmer'
12+
export * from './suggestion'
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 { HTMLAttributes } from 'vue'
3+
import { Button } from '@repo/shadcn-vue/components/ui/button'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
6+
interface SuggestionProps {
7+
suggestion: string
8+
class?: HTMLAttributes['class']
9+
variant?: 'outline' | 'default' | 'destructive' | 'secondary' | 'ghost' | 'link'
10+
size?: 'default' | 'sm' | 'lg' | 'icon'
11+
}
12+
13+
const props = withDefaults(defineProps<SuggestionProps>(), {
14+
variant: 'outline',
15+
size: 'sm',
16+
})
17+
18+
const emit = defineEmits<{
19+
(e: 'click', suggestion: string): void
20+
}>()
21+
22+
function handleClick() {
23+
emit('click', props.suggestion)
24+
}
25+
</script>
26+
27+
<template>
28+
<Button
29+
:class="cn('cursor-pointer rounded-full px-4', props.class)"
30+
:size="props.size"
31+
type="button"
32+
:variant="props.variant"
33+
v-bind="$attrs"
34+
@click="handleClick"
35+
>
36+
<slot>{{ props.suggestion }}</slot>
37+
</Button>
38+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { ScrollArea, ScrollBar } from '@repo/shadcn-vue/components/ui/scroll-area'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
6+
interface SuggestionsProps {
7+
class?: HTMLAttributes['class']
8+
}
9+
10+
const props = defineProps<SuggestionsProps>()
11+
</script>
12+
13+
<template>
14+
<ScrollArea class="w-full overflow-x-auto whitespace-nowrap" v-bind="$attrs">
15+
<div :class="cn('flex w-max flex-nowrap items-center gap-2', props.class)">
16+
<slot />
17+
</div>
18+
<ScrollBar class="hidden" orientation="horizontal" />
19+
</ScrollArea>
20+
</template>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Suggestion } from './Suggestion.vue'
2+
export { default as Suggestions } from './Suggestions.vue'

packages/examples/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export { default as Response } from './response.vue'
1414
export { default as ShimmerCustomElements } from './shimmer-custom-elements.vue'
1515
export { default as ShimmerDurations } from './shimmer-durations.vue'
1616
export { default as Shimmer } from './shimmer.vue'
17+
export { default as SuggestionAiInput } from './suggestion-ai-input.vue'
18+
export { default as Suggestion } from './suggestion.vue'

0 commit comments

Comments
 (0)