Skip to content

Commit 3f45173

Browse files
committed
feat: add loader component
1 parent 3051a8e commit 3f45173

File tree

22 files changed

+452
-0
lines changed

22 files changed

+452
-0
lines changed

apps/test/app/examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { default as Branch } from './branch.vue'
44
export { default as CodeBlock } from './code-block.vue'
55
export { default as Conversation } from './conversation.vue'
66
export { default as Image } from './image.vue'
7+
export { default as Loader } from './loader.vue'
78
export { default as MessageMarkdown } from './message-markdown.vue'
89
export { default as Message } from './message.vue'
910
export { default as PromptInput } from './prompt-input.vue'

apps/test/app/examples/loader.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script setup lang="ts">
2+
import { Loader } from '@repo/elements/loader'
3+
</script>
4+
5+
<template>
6+
<div class="flex items-center justify-center p-8">
7+
<Loader />
8+
</div>
9+
</template>

apps/test/app/pages/index.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Branch from '~/examples/branch.vue'
66
import CodeBlock from '~/examples/code-block.vue'
77
import Conversation from '~/examples/conversation.vue'
88
import Image from '~/examples/image.vue'
9+
import Loader from '~/examples/loader.vue'
910
import MessageMarkdown from '~/examples/message-markdown.vue'
1011
import Message from '~/examples/message.vue'
1112
import PromptInput from '~/examples/prompt-input.vue'
@@ -24,6 +25,7 @@ const components = [
2425
{ name: 'CodeBlock', Component: CodeBlock },
2526
{ name: 'Image', Component: Image },
2627
{ name: 'Shimmer', Component: Shimmer },
28+
{ name: 'Loader', Component: Loader },
2729
]
2830
</script>
2931

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
title: Loader
3+
description:
4+
icon: lucide:loader
5+
---
6+
7+
The `Loader` component provides a spinning animation to indicate loading states in your AI applications. It includes both a customizable wrapper component and the underlying icon for flexible usage.
8+
9+
:::ComponentLoader{label="Preview" componentName="Loader"}
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 loader
18+
```
19+
::
20+
::div{label="shadcn-vue"}
21+
22+
```sh
23+
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/loader.json
24+
```
25+
::
26+
:::
27+
28+
## Install Manually
29+
30+
Copy and paste the following files into the same folder.
31+
32+
:::code-group
33+
```vue [Loader.vue]
34+
<script setup lang="ts">
35+
import { cn } from '@repo/shadcn-vue/lib/utils'
36+
import LoaderIcon from './LoaderIcon.vue'
37+
38+
interface Props {
39+
size?: number
40+
class?: string
41+
}
42+
43+
const props = withDefaults(defineProps<Props>(), {
44+
size: 16,
45+
})
46+
</script>
47+
48+
<template>
49+
<div
50+
:class="cn('inline-flex animate-spin items-center justify-center', props.class)"
51+
v-bind="$attrs"
52+
>
53+
<LoaderIcon :size="props.size" />
54+
</div>
55+
</template>
56+
```
57+
58+
```vue [LoaderIcon.vue]
59+
<script setup lang="ts">
60+
interface Props {
61+
size?: number
62+
}
63+
64+
withDefaults(defineProps<Props>(), {
65+
size: 16,
66+
})
67+
</script>
68+
69+
<template>
70+
<svg
71+
:height="size"
72+
stroke-linejoin="round"
73+
:style="{ color: 'currentcolor' }"
74+
viewBox="0 0 16 16"
75+
:width="size"
76+
>
77+
<title>Loader</title>
78+
<g clip-path="url(#clip0_2393_1490)">
79+
<path d="M8 0V4" stroke="currentColor" stroke-width="1.5" />
80+
<path d="M8 16V12" opacity="0.5" stroke="currentColor" stroke-width="1.5" />
81+
<path d="M3.29773 1.52783L5.64887 4.7639" opacity="0.9" stroke="currentColor" stroke-width="1.5" />
82+
<path d="M12.7023 1.52783L10.3511 4.7639" opacity="0.1" stroke="currentColor" stroke-width="1.5" />
83+
<path d="M12.7023 14.472L10.3511 11.236" opacity="0.4" stroke="currentColor" stroke-width="1.5" />
84+
<path d="M3.29773 14.472L5.64887 11.236" opacity="0.6" stroke="currentColor" stroke-width="1.5" />
85+
<path d="M15.6085 5.52783L11.8043 6.7639" opacity="0.2" stroke="currentColor" stroke-width="1.5" />
86+
<path d="M0.391602 10.472L4.19583 9.23598" opacity="0.7" stroke="currentColor" stroke-width="1.5" />
87+
<path d="M15.6085 10.4722L11.8043 9.2361" opacity="0.3" stroke="currentColor" stroke-width="1.5" />
88+
<path d="M0.391602 5.52783L4.19583 6.7639" opacity="0.8" stroke="currentColor" stroke-width="1.5" />
89+
</g>
90+
<defs>
91+
<clipPath id="clip0_2393_1490">
92+
<rect fill="white" height="16" width="16" />
93+
</clipPath>
94+
</defs>
95+
</svg>
96+
</template>
97+
```
98+
99+
```ts [index.ts]
100+
export { default as Loader } from './Loader.vue'
101+
```
102+
:::
103+
104+
## Usage
105+
106+
```vue
107+
<script setup lang="ts">
108+
import { Loader } from '@/components/ai-elements/loader'
109+
</script>
110+
111+
<template>
112+
<Loader />
113+
</template>
114+
```
115+
116+
## Usage with AI SDK
117+
118+
Build a simple chat app that displays a loader before it the response streans by using `status === "submitted"`.
119+
120+
Add the following component to your frontend:
121+
122+
```vue [pages/index.vue]
123+
<script lang="ts" setup>
124+
import { useChat } from '@ai-sdk/vue'
125+
import { cn } from '@repo/shadcn-vue/lib/utils'
126+
import { ref } from 'vue'
127+
import { Conversation, ConversationContent, ConversationScrollButton } from '@/components/ai-elements/conversation'
128+
import { Loader } from '@/components/ai-elements/loader'
129+
import { Message, MessageContent } from '@/components/ai-elements/message'
130+
import { Input, PromptInputSubmit, PromptInputTextarea } from '@/components/ai-elements/prompt-input'
131+
132+
const input = ref('')
133+
const { messages, sendMessage, status } = useChat()
134+
135+
function handleSubmit(e: Event) {
136+
if (input.value.trim()) {
137+
sendMessage({ text: input.value })
138+
input.value = ''
139+
}
140+
}
141+
</script>
142+
143+
<template>
144+
<div
145+
class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
146+
>
147+
<div class="flex flex-col h-full">
148+
<Conversation>
149+
<ConversationContent>
150+
<Message v-for="message in messages" :key="message.id" :from="message.role">
151+
<MessageContent>
152+
<template v-for="(part, i) in message.parts" :key="`${message.id}-${i}`">
153+
<div v-if="part.type === 'text'">
154+
{{ part.text }}
155+
</div>
156+
</template>
157+
</MessageContent>
158+
</Message>
159+
<Loader v-if="status === 'submitted'" />
160+
</ConversationContent>
161+
<ConversationScrollButton />
162+
</Conversation>
163+
164+
<Input
165+
class="mt-4 w-full max-w-2xl mx-auto relative"
166+
@submit.prevent="handleSubmit"
167+
>
168+
<PromptInputTextarea
169+
v-model="input"
170+
placeholder="Say something..."
171+
class="pr-12"
172+
/>
173+
<PromptInputSubmit
174+
:status="status === 'streaming' ? 'streaming' : 'ready'"
175+
:disabled="!input.trim()"
176+
class="absolute bottom-1 right-1"
177+
/>
178+
</Input>
179+
</div>
180+
</div>
181+
</template>
182+
```
183+
184+
Add the following route to your backend:
185+
186+
```ts [api/chat/route.ts]
187+
import { convertToModelMessages, streamText, UIMessage } from 'ai'
188+
189+
// Allow streaming responses up to 30 seconds
190+
export const maxDuration = 30
191+
192+
export async function POST(req: Request) {
193+
const { model, messages }: { messages: UIMessage[], model: string } = await req.json()
194+
195+
const result = streamText({
196+
model: 'openai/gpt-4o',
197+
messages: convertToModelMessages(messages),
198+
})
199+
200+
return result.toUIMessageStreamResponse()
201+
}
202+
```
203+
204+
## Features
205+
206+
- Clean, modern spinning animation using CSS animations
207+
- Configurable size with the `size` prop
208+
- Customizable styling with CSS classes
209+
- Built-in `animate-spin` animation with proper centering
210+
- Exports both `AILoader` wrapper and `LoaderIcon` for flexible usage
211+
- Supports all standard HTML div attributes
212+
- TypeScript support with proper type definitions
213+
- Optimized SVG icon with multiple opacity levels for smooth animation
214+
- Uses `currentColor` for proper theme integration
215+
- Responsive and accessible design
216+
217+
## Examples
218+
219+
### Different Sizes
220+
221+
:::ComponentLoader{label="Preview" componentName="LoaderSizes"}
222+
:::
223+
224+
### Custom Styling
225+
226+
:::ComponentLoader{label="Preview" componentName="LoaderCustomStyling"}
227+
:::
228+
229+
## Props
230+
231+
### `<Loader />`
232+
233+
:::field-group
234+
::field{name="size" type="number" optional defaultValue="16"}
235+
The size (width and height) of the loader in pixels. Defaults to 16.
236+
::
237+
238+
::field{name="class" type="string" optional defaultValue="''"}
239+
Additional CSS classes applied to the component.
240+
::
241+
:::
File renamed without changes.

0 commit comments

Comments
 (0)