Skip to content

Commit 073f4ae

Browse files
committed
feat: add queue component
1 parent f1fc8a8 commit 073f4ae

26 files changed

+1805
-0
lines changed

apps/test/app/examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { default as MessageMarkdown } from './message-markdown.vue'
99
export { default as Message } from './message.vue'
1010
export { default as OpenInChat } from './open-in-chat.vue'
1111
export { default as PromptInput } from './prompt-input.vue'
12+
export { default as Queue } from './queue.vue'
1213
export { default as Response } from './response.vue'
1314
export { default as Shimmer } from './shimmer.vue'
1415
export { default as Task } from './task.vue'

apps/test/app/examples/queue.vue

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<script setup lang="ts">
2+
import {
3+
Queue,
4+
QueueItem,
5+
QueueItemAction,
6+
QueueItemActions,
7+
QueueItemAttachment,
8+
QueueItemContent,
9+
QueueItemDescription,
10+
QueueItemFile,
11+
QueueItemImage,
12+
QueueItemIndicator,
13+
QueueList,
14+
QueueSection,
15+
QueueSectionContent,
16+
QueueSectionLabel,
17+
QueueSectionTrigger,
18+
} from '@repo/elements/queue'
19+
import { ArrowUp, Trash2 } from 'lucide-vue-next'
20+
import { computed, ref } from 'vue'
21+
22+
export interface QueueMessagePart {
23+
type: string
24+
text?: string
25+
url?: string
26+
filename?: string
27+
mediaType?: string
28+
}
29+
30+
export interface QueueMessage {
31+
id: string
32+
parts: QueueMessagePart[]
33+
}
34+
35+
export interface QueueTodo {
36+
id: string
37+
title: string
38+
description?: string
39+
status?: 'pending' | 'completed'
40+
}
41+
42+
const sampleMessages: QueueMessage[] = [
43+
{ id: 'msg-1', parts: [{ type: 'text', text: 'How do I set up the project?' }] },
44+
{ id: 'msg-2', parts: [{ type: 'text', text: 'What is the roadmap for Q4?' }] },
45+
{ id: 'msg-3', parts: [{ type: 'text', text: 'Can you review my PR?' }] },
46+
{ id: 'msg-4', parts: [{ type: 'text', text: 'Please generate a changelog.' }] },
47+
{ id: 'msg-5', parts: [{ type: 'text', text: 'Add dark mode support.' }] },
48+
{ id: 'msg-6', parts: [{ type: 'text', text: 'Optimize database queries.' }] },
49+
{ id: 'msg-7', parts: [{ type: 'text', text: 'Set up CI/CD pipeline.' }] },
50+
]
51+
52+
const sampleTodos: QueueTodo[] = [
53+
{ id: 'todo-1', title: 'Write project documentation', description: 'Complete the README and API docs', status: 'completed' },
54+
{ id: 'todo-2', title: 'Implement authentication', status: 'pending' },
55+
{ id: 'todo-3', title: 'Fix bug #42', description: 'Resolve crash on settings page', status: 'pending' },
56+
{ id: 'todo-4', title: 'Refactor queue logic', description: 'Unify queue and todo state management', status: 'pending' },
57+
{ id: 'todo-5', title: 'Add unit tests', description: 'Increase test coverage for hooks', status: 'pending' },
58+
]
59+
60+
const messages = ref<QueueMessage[]>(sampleMessages)
61+
const todos = ref<QueueTodo[]>(sampleTodos)
62+
63+
function handleRemoveMessage(id: string) {
64+
messages.value = messages.value.filter(msg => msg.id !== id)
65+
}
66+
67+
function handleRemoveTodo(id: string) {
68+
todos.value = todos.value.filter(todo => todo.id !== id)
69+
}
70+
71+
function handleSendNow(id: string) {
72+
// eslint-disable-next-line no-console
73+
console.log('Send now:', id)
74+
handleRemoveMessage(id)
75+
}
76+
77+
const hasQueue = computed(() => messages.value.length > 0 || todos.value.length > 0)
78+
</script>
79+
80+
<template>
81+
<Queue v-if="hasQueue">
82+
<!-- Queued Messages Section -->
83+
<QueueSection v-if="messages.length > 0">
84+
<QueueSectionTrigger>
85+
<QueueSectionLabel :count="messages.length" label="Queued" />
86+
</QueueSectionTrigger>
87+
88+
<QueueSectionContent>
89+
<QueueList>
90+
<QueueItem
91+
v-for="message in messages"
92+
:key="message.id"
93+
>
94+
<div class="flex items-center gap-2">
95+
<QueueItemIndicator />
96+
<QueueItemContent>
97+
{{
98+
message.parts
99+
.filter((p) => p.type === 'text')
100+
.map((p) => p.text)
101+
.join(' ')
102+
.trim() || '(queued message)'
103+
}}
104+
</QueueItemContent>
105+
106+
<QueueItemActions>
107+
<QueueItemAction
108+
aria-label="Remove from queue"
109+
title="Remove from queue"
110+
@click.stop="handleRemoveMessage(message.id)"
111+
>
112+
<Trash2 :size="12" />
113+
</QueueItemAction>
114+
115+
<QueueItemAction
116+
aria-label="Send now"
117+
@click.stop="handleSendNow(message.id)"
118+
>
119+
<ArrowUp :size="14" />
120+
</QueueItemAction>
121+
</QueueItemActions>
122+
</div>
123+
124+
<QueueItemAttachment v-if="message.parts.some((p) => p.type === 'file' && p.url)">
125+
<template
126+
v-for="file in message.parts.filter((p) => p.type === 'file' && p.url)"
127+
:key="file.url"
128+
>
129+
<QueueItemImage
130+
v-if="file.mediaType?.startsWith('image/')"
131+
:src="file.url"
132+
:alt="file.filename || 'attachment'"
133+
/>
134+
<QueueItemFile v-else>
135+
{{ file.filename || 'file' }}
136+
</QueueItemFile>
137+
</template>
138+
</QueueItemAttachment>
139+
</QueueItem>
140+
</QueueList>
141+
</QueueSectionContent>
142+
</QueueSection>
143+
144+
<!-- Todos Section -->
145+
<QueueSection v-if="todos.length > 0">
146+
<QueueSectionTrigger>
147+
<QueueSectionLabel :count="todos.length" label="Todo" />
148+
</QueueSectionTrigger>
149+
150+
<QueueSectionContent>
151+
<QueueList>
152+
<QueueItem
153+
v-for="todo in todos"
154+
:key="todo.id"
155+
>
156+
<div class="flex items-center gap-2">
157+
<QueueItemIndicator :completed="todo.status === 'completed'" />
158+
<QueueItemContent :completed="todo.status === 'completed'">
159+
{{ todo.title }}
160+
</QueueItemContent>
161+
162+
<QueueItemActions>
163+
<QueueItemAction
164+
aria-label="Remove todo"
165+
@click="handleRemoveTodo(todo.id)"
166+
>
167+
<Trash2 :size="12" />
168+
</QueueItemAction>
169+
</QueueItemActions>
170+
</div>
171+
172+
<QueueItemDescription
173+
v-if="todo.description"
174+
:completed="todo.status === 'completed'"
175+
>
176+
{{ todo.description }}
177+
</QueueItemDescription>
178+
</QueueItem>
179+
</QueueList>
180+
</QueueSectionContent>
181+
</QueueSection>
182+
</Queue>
183+
</template>

apps/test/app/pages/index.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import MessageMarkdown from '~/examples/message-markdown.vue'
1111
import Message from '~/examples/message.vue'
1212
import OpenInChat from '~/examples/open-in-chat.vue'
1313
import PromptInput from '~/examples/prompt-input.vue'
14+
import Queue from '~/examples/queue.vue'
1415
import Response from '~/examples/response.vue'
1516
import Shimmer from '~/examples/shimmer.vue'
1617
import Sources from '~/examples/sources.vue'
@@ -34,6 +35,7 @@ const components = [
3435
{ name: 'OpenInChat', Component: OpenInChat },
3536
{ name: 'Loader', Component: Loader },
3637
{ name: 'Sources', Component: Sources },
38+
{ name: 'Queue', Component: Queue },
3739
]
3840
</script>
3941

0 commit comments

Comments
 (0)