Skip to content

Commit 77f0ac6

Browse files
committed
feat: update message component
1 parent 8417043 commit 77f0ac6

24 files changed

+980
-114
lines changed

apps/www/assets/css/tailwind.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,29 @@
127127
line-height: 0 !important;
128128
}
129129
}
130+
131+
@layer base {
132+
::after,
133+
::before,
134+
::backdrop,
135+
::file-selector-button {
136+
@apply border-border;
137+
}
138+
* {
139+
@apply min-w-0;
140+
}
141+
html {
142+
text-rendering: optimizelegibility;
143+
}
144+
body {
145+
@apply min-h-dvh;
146+
}
147+
input::placeholder,
148+
textarea::placeholder {
149+
@apply text-muted-foreground;
150+
}
151+
button:not(:disabled),
152+
[role="button"]:not(:disabled) {
153+
@apply cursor-pointer;
154+
}
155+
}
Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
<script setup lang="ts">
2+
import type { UIMessage } from 'ai'
3+
import type { HTMLAttributes } from 'vue'
24
import { cn } from '@repo/shadcn-vue/lib/utils'
3-
import { computed } from 'vue'
45
56
interface Props {
6-
from: 'user' | 'assistant'
7-
class?: string
7+
from: UIMessage['role']
8+
class?: HTMLAttributes['class']
89
}
910
1011
const props = defineProps<Props>()
11-
12-
const classes = computed(() => cn(
13-
'group w-full py-4',
14-
props.from === 'user'
15-
? 'is-user flex items-end justify-end gap-2'
16-
: 'is-assistant flex flex-row-reverse justify-end gap-2',
17-
props.class,
18-
))
1912
</script>
2013

2114
<template>
22-
<div :class="classes">
15+
<div
16+
:class="
17+
cn(
18+
'group flex w-full max-w-[80%] gap-2',
19+
props.from === 'user' ? 'is-user ml-auto justify-end' : 'is-assistant',
20+
props.class as string,
21+
)
22+
"
23+
v-bind="$attrs"
24+
>
2325
<slot />
2426
</div>
2527
</template>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import type { ButtonVariants } from '@repo/shadcn-vue/components/ui/button'
3+
import { Button } from '@repo/shadcn-vue/components/ui/button'
4+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@repo/shadcn-vue/components/ui/tooltip'
5+
6+
interface Props {
7+
tooltip?: string
8+
label?: string
9+
variant?: ButtonVariants['variant']
10+
size?: ButtonVariants['size']
11+
}
12+
13+
const props = withDefaults(defineProps<Props>(), {
14+
variant: 'ghost',
15+
size: 'icon-sm',
16+
})
17+
18+
const buttonProps = {
19+
variant: props.variant,
20+
size: props.size,
21+
type: 'button' as const,
22+
}
23+
</script>
24+
25+
<template>
26+
<TooltipProvider v-if="props.tooltip">
27+
<Tooltip>
28+
<TooltipTrigger as-child>
29+
<Button v-bind="{ ...buttonProps, ...$attrs }">
30+
<slot />
31+
<span class="sr-only">
32+
{{ props.label || props.tooltip }}</span>
33+
</Button>
34+
</TooltipTrigger>
35+
<TooltipContent>
36+
<p>{{ props.tooltip }}</p>
37+
</TooltipContent>
38+
</Tooltip>
39+
</TooltipProvider>
40+
41+
<Button v-else v-bind="{ ...buttonProps, ...$attrs }">
42+
<slot />
43+
<span class="sr-only">{{ props.label || props.tooltip }}</span>
44+
</Button>
45+
</template>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
5+
interface Props {
6+
class?: HTMLAttributes['class']
7+
}
8+
9+
const props = defineProps<Props>()
10+
</script>
11+
12+
<template>
13+
<div
14+
:class="cn('flex items-center gap-1', props.class)"
15+
v-bind="$attrs"
16+
>
17+
<slot />
18+
</div>
19+
</template>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script setup lang="ts">
2+
import type { FileUIPart } from 'ai'
3+
import type { HTMLAttributes } from 'vue'
4+
import { Button } from '@repo/shadcn-vue/components/ui/button'
5+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@repo/shadcn-vue/components/ui/tooltip'
6+
import { cn } from '@repo/shadcn-vue/lib/utils'
7+
import { PaperclipIcon, XIcon } from 'lucide-vue-next'
8+
import { computed } from 'vue'
9+
10+
interface Props {
11+
data: FileUIPart
12+
class?: HTMLAttributes['class']
13+
}
14+
const props = defineProps<Props>()
15+
16+
const emits = defineEmits<{
17+
(e: 'remove'): void
18+
}>()
19+
20+
const filename = computed(() => props.data.filename || '')
21+
const mediaType = computed(() =>
22+
props.data.mediaType?.startsWith('image/') && props.data.url ? 'image' : 'file',
23+
)
24+
const isImage = computed(() => mediaType.value === 'image')
25+
const attachmentLabel = computed(() =>
26+
filename.value || (isImage.value ? 'Image' : 'Attachment'),
27+
)
28+
</script>
29+
30+
<template>
31+
<div
32+
:class="
33+
cn(
34+
'group relative size-24 overflow-hidden rounded-lg',
35+
props.class,
36+
)
37+
"
38+
v-bind="$attrs"
39+
>
40+
<template v-if="isImage">
41+
<img
42+
:src="props.data.url"
43+
:alt="filename || 'attachment'"
44+
class="size-full object-cover"
45+
height="100"
46+
width="100"
47+
>
48+
<Button
49+
aria-label="Remove attachment"
50+
class="absolute top-2 right-2 size-6 rounded-full bg-background/80 p-0 opacity-0 backdrop-blur-sm transition-opacity hover:bg-background group-hover:opacity-100 [&>svg]:size-3"
51+
type="button"
52+
variant="ghost"
53+
@click.stop="emits('remove')"
54+
>
55+
<XIcon />
56+
<span class="sr-only">Remove</span>
57+
</Button>
58+
</template>
59+
60+
<template v-else>
61+
<TooltipProvider>
62+
<Tooltip>
63+
<TooltipTrigger as-child>
64+
<div
65+
class="flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground"
66+
>
67+
<PaperclipIcon class="size-4" />
68+
</div>
69+
</TooltipTrigger>
70+
<TooltipContent>
71+
<p>{{ attachmentLabel }}</p>
72+
</TooltipContent>
73+
</Tooltip>
74+
</TooltipProvider>
75+
<Button
76+
aria-label="Remove attachment"
77+
class="size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100 [&>svg]:size-3"
78+
type="button"
79+
variant="ghost"
80+
@click.stop="emits('remove')"
81+
>
82+
<XIcon />
83+
<span class="sr-only">Remove</span>
84+
</Button>
85+
</template>
86+
</div>
87+
</template>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
import { useSlots } from 'vue'
5+
6+
interface Props {
7+
class?: HTMLAttributes['class']
8+
}
9+
10+
const props = defineProps<Props>()
11+
12+
const slots = useSlots()
13+
</script>
14+
15+
<template>
16+
<div
17+
v-if="slots.default"
18+
:class="
19+
cn(
20+
'ml-auto flex w-fit flex-wrap items-start gap-2',
21+
props.class,
22+
)
23+
"
24+
v-bind="$attrs"
25+
>
26+
<slot />
27+
</div>
28+
</template>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes, VNode } from 'vue'
3+
import type { MessageBranchContextType } from './context'
4+
import { cn } from '@repo/shadcn-vue/lib/utils'
5+
import { provide, readonly, ref } from 'vue'
6+
import { MessageBranchKey } from './context'
7+
8+
interface Props {
9+
defaultBranch?: number
10+
class?: HTMLAttributes['class']
11+
}
12+
const props = withDefaults(defineProps<Props>(), {
13+
defaultBranch: 0,
14+
})
15+
16+
const emits = defineEmits<{
17+
(e: 'branchChange', branchIndex: number): void
18+
}>()
19+
20+
const currentBranch = ref<number>(props.defaultBranch)
21+
const branches = ref<VNode[]>([])
22+
const totalBranches = ref<number>(0)
23+
24+
function handleBranchChange(index: number) {
25+
currentBranch.value = index
26+
emits('branchChange', index)
27+
}
28+
29+
function goToPrevious() {
30+
if (totalBranches.value === 0)
31+
return
32+
const next = currentBranch.value > 0 ? currentBranch.value - 1 : totalBranches.value - 1
33+
handleBranchChange(next)
34+
}
35+
36+
function goToNext() {
37+
if (totalBranches.value === 0)
38+
return
39+
const next = currentBranch.value < totalBranches.value - 1 ? currentBranch.value + 1 : 0
40+
handleBranchChange(next)
41+
}
42+
43+
function setBranches(count: number) {
44+
totalBranches.value = count
45+
}
46+
47+
const contextValue: MessageBranchContextType = {
48+
currentBranch: readonly(currentBranch),
49+
totalBranches: readonly(totalBranches),
50+
goToPrevious,
51+
goToNext,
52+
branches,
53+
setBranches,
54+
}
55+
56+
provide(MessageBranchKey, contextValue)
57+
</script>
58+
59+
<template>
60+
<div
61+
:class="cn('grid w-full gap-2 [&>div]:pb-0', props.class)"
62+
v-bind="$attrs"
63+
>
64+
<slot />
65+
</div>
66+
</template>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from 'vue'
3+
import { cn } from '@repo/shadcn-vue/lib/utils'
4+
import { computed, Fragment, isVNode, onMounted, useAttrs, useSlots, watch } from 'vue'
5+
import { useMessageBranchContext } from './context'
6+
7+
interface Props {
8+
class?: HTMLAttributes['class']
9+
}
10+
11+
const props = defineProps<Props>()
12+
const attrs = useAttrs()
13+
const slots = useSlots()
14+
15+
const { currentBranch, setBranches } = useMessageBranchContext()
16+
17+
const branchVNodes = computed(() => {
18+
const nodes = slots.default?.() ?? []
19+
20+
const extractChildren = (node: any): any[] => {
21+
if (isVNode(node) && node.type === Fragment) {
22+
return Array.isArray(node.children) ? node.children : []
23+
}
24+
return [node]
25+
}
26+
27+
const allNodes = nodes.flatMap(extractChildren)
28+
29+
return allNodes.filter((node) => {
30+
if (!isVNode(node))
31+
return false
32+
return node.type && typeof node.type === 'object'
33+
})
34+
})
35+
36+
const sync = () => setBranches(branchVNodes.value.length)
37+
onMounted(sync)
38+
watch(branchVNodes, sync)
39+
40+
const baseClasses = computed(() => cn('grid gap-2 overflow-hidden [&>div]:pb-0', props.class))
41+
</script>
42+
43+
<template>
44+
<template v-for="(node, index) in branchVNodes" :key="(node.key as any) ?? index">
45+
<div
46+
:class="cn(baseClasses, index === currentBranch ? 'block' : 'hidden')"
47+
v-bind="attrs"
48+
>
49+
<component :is="node" />
50+
</div>
51+
</template>
52+
</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 { Button } from '@repo/shadcn-vue/components/ui/button'
3+
import { ChevronRightIcon } from 'lucide-vue-next'
4+
import { useMessageBranchContext } from './context'
5+
6+
const { goToNext, totalBranches } = useMessageBranchContext()
7+
</script>
8+
9+
<template>
10+
<Button
11+
aria-label="Next branch"
12+
:disabled="totalBranches <= 1"
13+
size="icon-sm"
14+
type="button"
15+
variant="ghost"
16+
v-bind="$attrs"
17+
@click="goToNext"
18+
>
19+
<slot>
20+
<ChevronRightIcon :size="14" />
21+
</slot>
22+
</Button>
23+
</template>

0 commit comments

Comments
 (0)