@meistrari/tela-build 1.57.4 → 1.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tela/chat/chat-body.vue +92 -0
- package/components/tela/chat/chat-empty-state.vue +40 -0
- package/components/tela/chat/chat-model-selector.vue +64 -0
- package/components/tela/chat/chat-powered-by-tela.vue +8 -0
- package/components/tela/chat/chat-prompt-submit-button.vue +48 -0
- package/components/tela/chat/chat-reasoning-step.vue +109 -0
- package/components/tela/chat/chat-suggestion.vue +15 -0
- package/components/tela/chat/chat-suggestions.vue +5 -0
- package/components/tela/chat/chat-text-shimmer.vue +48 -0
- package/components/tela/chat/chat-widget-card.vue +62 -0
- package/components/tela/chat/chat-widget-header.vue +82 -0
- package/components/tela/chat/chat-widget-history-list.vue +63 -0
- package/components/tela/chat/chat-widget-source.vue +46 -0
- package/components/tela/chat/chat-widget-sources.vue +5 -0
- package/components/tela/chat/chat.mdx +1237 -182
- package/components/tela/chat/conversation/conversation-content.vue +5 -0
- package/components/tela/chat/conversation/conversation.vue +105 -0
- package/components/tela/chat/floating/floating-window-trigger.vue +21 -0
- package/components/tela/chat/floating/floating-window.vue +117 -0
- package/components/tela/chat/icons/anthropic.vue +5 -0
- package/components/tela/chat/icons/gemini.vue +3 -0
- package/components/tela/chat/icons/openai.vue +3 -0
- package/components/tela/chat/icons/tela-wordmark.vue +9 -0
- package/components/tela/chat/message/message-avatar.vue +15 -0
- package/components/tela/chat/message/message-content.vue +25 -0
- package/components/tela/chat/message/message-feedback.vue +275 -0
- package/components/tela/chat/message/message.vue +15 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu-content.vue +15 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu-item.vue +21 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu-sub-content.vue +5 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu-sub-trigger.vue +21 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu-sub.vue +5 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu-trigger.vue +24 -0
- package/components/tela/chat/prompt-input/prompt-input-action-menu.vue +9 -0
- package/components/tela/chat/prompt-input/prompt-input-actions.vue +5 -0
- package/components/tela/chat/prompt-input/prompt-input-attachment.vue +55 -0
- package/components/tela/chat/prompt-input/prompt-input-attachments.vue +27 -0
- package/components/tela/chat/prompt-input/prompt-input-hints.vue +53 -0
- package/components/tela/chat/prompt-input/prompt-input-textarea.vue +149 -0
- package/components/tela/chat/prompt-input/prompt-input-toolbar.vue +5 -0
- package/components/tela/chat/prompt-input/prompt-input-tools.vue +5 -0
- package/components/tela/chat/prompt-input/prompt-input.vue +59 -0
- package/components/tela/chat/pure-text-input/chat-text-input.vue +1 -2
- package/components/tela/chat/sidepanel/sidepanel-window.vue +7 -0
- package/components/tela/chat/text-input/index.vue +1 -2
- package/components/tela/chat/text-message/index.vue +2 -3
- package/components/tela/combobox/combobox-list.vue +1 -33
- package/components/tela/combobox/combobox.vue +2 -1
- package/components/tela/date-range-picker.vue +2 -1
- package/components/tela/dropdown-menu/DropdownMenuContent.vue +1 -32
- package/components/tela/icon-button/icon-button.vue +1 -0
- package/components/tela/menubar/menubar-content.vue +1 -32
- package/components/tela/menubar/menubar-sub-trigger.vue +1 -1
- package/components/tela/menubar/menubar.vue +1 -1
- package/components/tela/multiple-select/multiple-select.vue +1 -36
- package/components/tela/scroll-area/scroll-area.vue +3 -1
- package/components/tela/select-menu/select-menu-content.vue +2 -32
- package/components/tela/skeleton/skeleton.vue +20 -29
- package/components/tela/tabs/tabs-trigger.vue +1 -1
- package/components/tela/tags/tags-select.mdx +2 -3
- package/components/tela/tags/tags-select.vue +3 -34
- package/composables/scroll-fog.ts +48 -0
- package/css/animations.css +36 -0
- package/lib/doc-generator.ts +8 -1
- package/nuxt.config.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{
|
|
3
|
+
/** Spacing scale: `widget` for floating/sidepanel shells, `page` for full-page chats. */
|
|
4
|
+
variant?: 'widget' | 'page'
|
|
5
|
+
}>(), {
|
|
6
|
+
variant: 'widget',
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const rootRef = ref<HTMLElement | null>(null)
|
|
10
|
+
let inputObserver: ResizeObserver | null = null
|
|
11
|
+
let slotObserver: MutationObserver | null = null
|
|
12
|
+
let observedInput: HTMLElement | null = null
|
|
13
|
+
|
|
14
|
+
function bindInputObserver() {
|
|
15
|
+
const input = rootRef.value?.querySelector<HTMLElement>('[data-chat-prompt-input]') ?? null
|
|
16
|
+
|
|
17
|
+
if (input === observedInput)
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
if (observedInput)
|
|
21
|
+
inputObserver?.unobserve(observedInput)
|
|
22
|
+
|
|
23
|
+
observedInput = input
|
|
24
|
+
|
|
25
|
+
if (!input) {
|
|
26
|
+
rootRef.value?.style.removeProperty('--chat-input-height')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
inputObserver ??= new ResizeObserver(() => {
|
|
31
|
+
if (observedInput)
|
|
32
|
+
rootRef.value?.style.setProperty('--chat-input-height', `${observedInput.offsetHeight}px`)
|
|
33
|
+
})
|
|
34
|
+
inputObserver.observe(input)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onMounted(() => {
|
|
38
|
+
if (!rootRef.value || typeof ResizeObserver === 'undefined')
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
bindInputObserver()
|
|
42
|
+
|
|
43
|
+
slotObserver = new MutationObserver(bindInputObserver)
|
|
44
|
+
slotObserver.observe(rootRef.value, { childList: true, subtree: true })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
onBeforeUnmount(() => {
|
|
48
|
+
inputObserver?.disconnect()
|
|
49
|
+
inputObserver = null
|
|
50
|
+
slotObserver?.disconnect()
|
|
51
|
+
slotObserver = null
|
|
52
|
+
observedInput = null
|
|
53
|
+
})
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div ref="rootRef" data-chat-body :data-variant="variant" class="relative flex-1 min-h-0 flex flex-col">
|
|
58
|
+
<slot />
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
[data-chat-body] [data-chat-conversation] {
|
|
64
|
+
--at-apply: 'pl-18px pr-12px py-16px';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[data-chat-body]:has([data-chat-prompt-input]) [data-chat-conversation] {
|
|
68
|
+
padding-bottom: calc(var(--chat-input-height, 64px) + 24px);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[data-chat-body] [data-chat-prompt-input] {
|
|
72
|
+
--at-apply: 'absolute bottom-0 left-0 right-0 z-2 mx-12px mb-12px';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[data-chat-body]:has([data-chat-prompt-input]) .chat-conversation-scrollbar {
|
|
76
|
+
top: 8px !important;
|
|
77
|
+
bottom: calc(var(--chat-input-height, 64px) + 16px) !important;
|
|
78
|
+
height: auto !important;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[data-chat-body][data-variant='page'] [data-chat-conversation] {
|
|
82
|
+
--at-apply: 'pl-24px pr-16px py-16px';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
[data-chat-body][data-variant='page']:has([data-chat-prompt-input]) [data-chat-conversation] {
|
|
86
|
+
padding-bottom: calc(var(--chat-input-height, 64px) + 32px);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[data-chat-body][data-variant='page'] [data-chat-prompt-input] {
|
|
90
|
+
--at-apply: 'mx-24px mb-16px';
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
/** Tighter spacing/typography for widget shells (floating/sidepanel). */
|
|
4
|
+
compact?: boolean
|
|
5
|
+
/** Greeting headline, e.g. "Hey, user". */
|
|
6
|
+
title?: string
|
|
7
|
+
/** Supporting line under the title. */
|
|
8
|
+
description?: string
|
|
9
|
+
}>()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div
|
|
14
|
+
flex-1 flex="~ col" items-center justify-center
|
|
15
|
+
:class="compact ? 'gap-24px px-12px pt-16px' : 'gap-40px px-24px'"
|
|
16
|
+
>
|
|
17
|
+
<div flex="~ col" gap-32px :class="compact ? 'mt-auto' : ''">
|
|
18
|
+
<div v-if="title || description" flex="~ col" items-center gap-2px text-center>
|
|
19
|
+
<component
|
|
20
|
+
:is="compact ? 'h5' : 'h1'"
|
|
21
|
+
v-if="title"
|
|
22
|
+
text-primary
|
|
23
|
+
:class="compact ? 'heading-h4-semibold' : 'text-24px font-semibold leading-28px tracking-[-.8px]'"
|
|
24
|
+
>
|
|
25
|
+
{{ title }}
|
|
26
|
+
</component>
|
|
27
|
+
<p v-if="description" text-secondary :class="compact ? 'body-14-regular' : 'text-16px leading-20px tracking-[-.2px]'">
|
|
28
|
+
{{ description }}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Suggestions, hints, or any other content under the greeting. -->
|
|
33
|
+
<slot />
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div w-full max-w-700px mx-auto :class="compact ? 'mt-auto pb-12px' : ''">
|
|
37
|
+
<slot name="input" />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface ModelOption {
|
|
5
|
+
value: string
|
|
6
|
+
label: string
|
|
7
|
+
description?: string
|
|
8
|
+
group?: string
|
|
9
|
+
icon?: string | Component
|
|
10
|
+
cost?: number
|
|
11
|
+
maxInputTokens?: number
|
|
12
|
+
maxOutputTokens?: number
|
|
13
|
+
isMultiModal?: boolean
|
|
14
|
+
tabs?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<{
|
|
18
|
+
/** Model options, already resolved by the host (labels, provider icons, tabs...). */
|
|
19
|
+
options: ModelOption[]
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
placeholder?: string
|
|
22
|
+
inputPlaceholder?: string
|
|
23
|
+
/** Maps tab keys (e.g. `advanced`) to display labels. */
|
|
24
|
+
tabLabels?: Record<string, string>
|
|
25
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
26
|
+
align?: 'start' | 'center' | 'end'
|
|
27
|
+
}>(), {
|
|
28
|
+
placeholder: 'Auto',
|
|
29
|
+
inputPlaceholder: 'Search models...',
|
|
30
|
+
side: 'top',
|
|
31
|
+
align: 'end',
|
|
32
|
+
tabLabels: () => ({
|
|
33
|
+
all: 'All',
|
|
34
|
+
advanced: 'Advanced',
|
|
35
|
+
cheap: 'Economical',
|
|
36
|
+
multimodal: 'Multimodal',
|
|
37
|
+
}),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const model = defineModel<string | null>({ default: null })
|
|
41
|
+
|
|
42
|
+
function getTabLabel(tab: string) {
|
|
43
|
+
return props.tabLabels[tab] ?? tab
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<TelaCombobox
|
|
49
|
+
:model-value="model ?? undefined"
|
|
50
|
+
:options="options"
|
|
51
|
+
:read-only="disabled"
|
|
52
|
+
:class="disabled && 'opacity-50 pointer-events-none'"
|
|
53
|
+
:placeholder="placeholder"
|
|
54
|
+
:side="side"
|
|
55
|
+
:align="align"
|
|
56
|
+
:label-icon-visible="false"
|
|
57
|
+
:input-placeholder="inputPlaceholder"
|
|
58
|
+
:get-tab-label="getTabLabel"
|
|
59
|
+
icon-class="scale-90"
|
|
60
|
+
content-class="w-[min(440px,calc(100vw-32px))]"
|
|
61
|
+
trigger-class="rounded-8px! gap-6px! bg-transparent! border-none! hover:bg-muted! shadow-none! duration-80 pl-10px! pr-8px! data-[state=open]:bg-muted!"
|
|
62
|
+
@update:model-value="model = $event ?? null"
|
|
63
|
+
/>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
status?: 'ready' | 'uploading' | 'streaming'
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
sendLabel?: string
|
|
6
|
+
uploadingLabel?: string
|
|
7
|
+
stopLabel?: string
|
|
8
|
+
}>(), {
|
|
9
|
+
status: 'ready',
|
|
10
|
+
sendLabel: 'Send message',
|
|
11
|
+
uploadingLabel: 'Uploading attachments',
|
|
12
|
+
stopLabel: 'Stop generating',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(e: 'send'): void
|
|
17
|
+
(e: 'stop'): void
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const isDisabled = computed(() => props.disabled || props.status === 'uploading')
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<TelaIconButton
|
|
25
|
+
v-if="status === 'streaming'"
|
|
26
|
+
size="sm"
|
|
27
|
+
icon="i-ph-stop-fill"
|
|
28
|
+
:aria-label="stopLabel"
|
|
29
|
+
@click="emit('stop')"
|
|
30
|
+
/>
|
|
31
|
+
<TelaIconButton
|
|
32
|
+
v-else
|
|
33
|
+
:icon="status === 'uploading' ? 'i-ph-circle-notch' : 'i-ph-arrow-up-bold'"
|
|
34
|
+
size="sm"
|
|
35
|
+
:color="!isDisabled ? 'primary' : 'secondary'"
|
|
36
|
+
class="disabled:bg-muted!"
|
|
37
|
+
:class="cn(
|
|
38
|
+
isDisabled && 'bg-subtle! hover:bg-muted!',
|
|
39
|
+
)"
|
|
40
|
+
:icon-class="cn(
|
|
41
|
+
!isDisabled ? 'text-icon-reverse' : 'text-icon-tertiary',
|
|
42
|
+
status === 'uploading' && 'animate-spin',
|
|
43
|
+
)"
|
|
44
|
+
:disabled="isDisabled"
|
|
45
|
+
:aria-label="status === 'uploading' ? uploadingLabel : sendLabel"
|
|
46
|
+
@click="emit('send')"
|
|
47
|
+
/>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string
|
|
4
|
+
icon?: string
|
|
5
|
+
isLast?: boolean
|
|
6
|
+
isFirst?: boolean
|
|
7
|
+
defaultExpanded?: boolean
|
|
8
|
+
hasResult?: boolean
|
|
9
|
+
isError?: boolean
|
|
10
|
+
isTextStep?: boolean
|
|
11
|
+
hideExpand?: boolean
|
|
12
|
+
isActive?: boolean
|
|
13
|
+
completed?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
icon: 'i-ph-info',
|
|
18
|
+
isLast: false,
|
|
19
|
+
isFirst: false,
|
|
20
|
+
defaultExpanded: false,
|
|
21
|
+
hasResult: false,
|
|
22
|
+
isError: false,
|
|
23
|
+
isTextStep: false,
|
|
24
|
+
hideExpand: false,
|
|
25
|
+
isActive: false,
|
|
26
|
+
completed: false,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Check if waiting for next step (last step with result but reasoning not completed)
|
|
30
|
+
const isWaitingForNext = computed(() => {
|
|
31
|
+
return props.isLast && props.hasResult && !props.completed
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const isExpanded = ref(props.defaultExpanded)
|
|
35
|
+
|
|
36
|
+
watch(() => props.defaultExpanded, (newVal) => {
|
|
37
|
+
isExpanded.value = newVal
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const statusIconClass = computed(() => {
|
|
41
|
+
if (props.completed)
|
|
42
|
+
return props.isError ? 'i-ph-x-circle' : props.icon
|
|
43
|
+
|
|
44
|
+
if (props.isActive || isWaitingForNext.value || (!props.isTextStep && !props.hasResult))
|
|
45
|
+
return 'i-ph-circle-notch animate-spin'
|
|
46
|
+
|
|
47
|
+
if (props.isTextStep)
|
|
48
|
+
return 'i-ph-check-circle'
|
|
49
|
+
|
|
50
|
+
return props.isError ? 'i-ph-x-circle' : 'i-ph-check-circle'
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<!-- Non-expandable row (when content is same as title) -->
|
|
56
|
+
<div
|
|
57
|
+
v-if="hideExpand"
|
|
58
|
+
relative flex items-center gap-10px h-28px
|
|
59
|
+
class="group/step"
|
|
60
|
+
>
|
|
61
|
+
<div class="flex items-center justify-center flex-shrink-0">
|
|
62
|
+
<div class="text-14px text-tertiary" :class="statusIconClass" />
|
|
63
|
+
</div>
|
|
64
|
+
<div class="flex-1 min-w-0 h-24px flex items-center">
|
|
65
|
+
<span class="text-14px font-medium text-tertiary leading-18px">
|
|
66
|
+
{{ title }}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<TelaCollapsible
|
|
72
|
+
v-else
|
|
73
|
+
:open="isExpanded"
|
|
74
|
+
@update:open="isExpanded = $event"
|
|
75
|
+
>
|
|
76
|
+
<TelaCollapsibleTrigger
|
|
77
|
+
as="button"
|
|
78
|
+
type="button"
|
|
79
|
+
relative flex items-center gap-10px h-32px w-full
|
|
80
|
+
class="group/step"
|
|
81
|
+
>
|
|
82
|
+
<div class="relative flex items-center justify-center flex-shrink-0">
|
|
83
|
+
<TelaIcon
|
|
84
|
+
size="16px"
|
|
85
|
+
color="icon-tertiary"
|
|
86
|
+
class="transition-opacity duration-80 group-hover/step:opacity-0"
|
|
87
|
+
:name="statusIconClass"
|
|
88
|
+
:class="[{ 'opacity-0': isExpanded }]"
|
|
89
|
+
/>
|
|
90
|
+
<TelaIcon
|
|
91
|
+
name="i-ph-caret-right"
|
|
92
|
+
size="16px"
|
|
93
|
+
color="icon-tertiary"
|
|
94
|
+
class="absolute inset-0 m-auto opacity-0 group-hover/step:opacity-100 duration-80 [transition-property:transform,opacity]"
|
|
95
|
+
:class="{ 'rotate-90 opacity-100': isExpanded }"
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
<span body-14-medium :class="cn(isExpanded ? 'text-primary' : 'text-secondary group-hover/step:text-primary')">
|
|
99
|
+
{{ title }}
|
|
100
|
+
</span>
|
|
101
|
+
</TelaCollapsibleTrigger>
|
|
102
|
+
|
|
103
|
+
<TelaCollapsibleContent>
|
|
104
|
+
<div pl-26px pb-4px>
|
|
105
|
+
<slot />
|
|
106
|
+
</div>
|
|
107
|
+
</TelaCollapsibleContent>
|
|
108
|
+
</TelaCollapsible>
|
|
109
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
suggestion: string
|
|
4
|
+
}>()
|
|
5
|
+
|
|
6
|
+
const emit = defineEmits<{
|
|
7
|
+
(e: 'click', suggestion: string): void
|
|
8
|
+
}>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<TelaButton type="button" variant="secondary" @click="emit('click', suggestion)">
|
|
13
|
+
<slot>{{ suggestion }}</slot>
|
|
14
|
+
</TelaButton>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
class?: HTMLAttributes['class']
|
|
6
|
+
duration?: number
|
|
7
|
+
}>(), {
|
|
8
|
+
duration: 2.8,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const baseColor = DT.colors.text.secondary
|
|
12
|
+
const shineColor = DT.colors.text.subtle
|
|
13
|
+
const spread = 20
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<span
|
|
18
|
+
data-shimmer
|
|
19
|
+
:class="cn(
|
|
20
|
+
'relative inline-block bg-[length:200%,auto] bg-clip-text text-transparent',
|
|
21
|
+
props.class,
|
|
22
|
+
)"
|
|
23
|
+
:style="{
|
|
24
|
+
'--base-color': baseColor,
|
|
25
|
+
'--base-gradient-color': shineColor,
|
|
26
|
+
'backgroundImage': `linear-gradient(to right, var(--base-color) ${50 - spread}%, var(--base-gradient-color) 50%, var(--base-color) ${50 + spread}%)`,
|
|
27
|
+
'animationDuration': `${props.duration}s`,
|
|
28
|
+
}"
|
|
29
|
+
>
|
|
30
|
+
<slot />
|
|
31
|
+
</span>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
[data-shimmer] {
|
|
36
|
+
animation: shimmer 4s infinite linear;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes shimmer {
|
|
40
|
+
0% {
|
|
41
|
+
background-position: 200%;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
100% {
|
|
45
|
+
background-position: -200%;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VNode } from 'vue'
|
|
3
|
+
import type { TelaStatusVariant } from '@meistrari/tela-build/types'
|
|
4
|
+
import { Comment, Fragment, Text, useSlots } from 'vue'
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
title: string
|
|
8
|
+
statusVariant: TelaStatusVariant
|
|
9
|
+
statusLabel: string
|
|
10
|
+
icon?: string
|
|
11
|
+
isIconAnimated?: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const slots = useSlots()
|
|
15
|
+
|
|
16
|
+
function isRenderedNode(node: VNode): boolean {
|
|
17
|
+
if (node.type === Comment)
|
|
18
|
+
return false
|
|
19
|
+
|
|
20
|
+
if (node.type === Text)
|
|
21
|
+
return typeof node.children === 'string' && node.children.trim().length > 0
|
|
22
|
+
|
|
23
|
+
if (node.type === Fragment)
|
|
24
|
+
return Array.isArray(node.children) && (node.children as VNode[]).some(isRenderedNode)
|
|
25
|
+
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasContent(): boolean {
|
|
30
|
+
return (slots.default?.() ?? []).some(isRenderedNode)
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div border-0.5px border p-12px rounded-12px>
|
|
36
|
+
<!-- Header -->
|
|
37
|
+
<div flex items-center justify-between>
|
|
38
|
+
<div flex items-center gap-8px flex-1 min-w-0>
|
|
39
|
+
<div v-if="icon" size-24px rounded-7px flex items-center justify-center bg-lowered>
|
|
40
|
+
<TelaIcon :name="icon" size="16px" color="icon-secondary" />
|
|
41
|
+
</div>
|
|
42
|
+
<div v-if="$slots.subtitle" class="min-w-0">
|
|
43
|
+
<p body-14-medium text-primary>
|
|
44
|
+
{{ title }}
|
|
45
|
+
</p>
|
|
46
|
+
<p body-12-regular text-secondary>
|
|
47
|
+
<slot name="subtitle" />
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<h3 v-else body-14-medium text-primary truncate>
|
|
51
|
+
{{ title }}
|
|
52
|
+
</h3>
|
|
53
|
+
</div>
|
|
54
|
+
<TelaStatus :variant="statusVariant" :label="statusLabel" :is-icon-animated="isIconAnimated" />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Content -->
|
|
58
|
+
<div v-if="hasContent()" flex="~ col" gap-16px mt-12px>
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{
|
|
3
|
+
title?: string
|
|
4
|
+
newConversationLabel?: string
|
|
5
|
+
historyLabel?: string
|
|
6
|
+
}>(), {
|
|
7
|
+
newConversationLabel: 'New conversation',
|
|
8
|
+
historyLabel: 'Conversation history',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
(e: 'newConversation'): void
|
|
13
|
+
(e: 'selectConversation', id: string): void
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const historyOpen = ref(false)
|
|
17
|
+
const historySelection = ref('')
|
|
18
|
+
|
|
19
|
+
function selectConversation(id: unknown) {
|
|
20
|
+
if (typeof id !== 'string' || !id)
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
historyOpen.value = false
|
|
24
|
+
emit('selectConversation', id)
|
|
25
|
+
|
|
26
|
+
nextTick(() => {
|
|
27
|
+
historySelection.value = ''
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div
|
|
34
|
+
data-widget-header
|
|
35
|
+
flex items-center justify-between gap-8px
|
|
36
|
+
h-48px pl-18px pr-10px flex-shrink-0
|
|
37
|
+
bg border-b-0.5px border
|
|
38
|
+
>
|
|
39
|
+
<h5 v-if="title" heading-h5-semibold text-primary truncate>
|
|
40
|
+
{{ title }}
|
|
41
|
+
</h5>
|
|
42
|
+
|
|
43
|
+
<div flex items-center gap-4px ml-auto>
|
|
44
|
+
<TelaIconButton
|
|
45
|
+
icon="i-ph-plus"
|
|
46
|
+
size="sm"
|
|
47
|
+
color="secondary"
|
|
48
|
+
:aria-label="newConversationLabel"
|
|
49
|
+
@click.stop.prevent="emit('newConversation')"
|
|
50
|
+
/>
|
|
51
|
+
<TelaComboboxRoot
|
|
52
|
+
v-if="$slots.history"
|
|
53
|
+
v-model="historySelection"
|
|
54
|
+
v-model:open="historyOpen"
|
|
55
|
+
reset-search-term-on-select
|
|
56
|
+
@update:model-value="selectConversation"
|
|
57
|
+
>
|
|
58
|
+
<TelaComboboxAnchor>
|
|
59
|
+
<TelaComboboxTrigger as-child class="group">
|
|
60
|
+
<TelaIconButton
|
|
61
|
+
icon="i-ph-clock"
|
|
62
|
+
size="sm"
|
|
63
|
+
color="secondary"
|
|
64
|
+
icon-class="group-data-[state=open]:text-icon"
|
|
65
|
+
:aria-label="historyLabel"
|
|
66
|
+
:class="historyOpen && 'bg-muted!'"
|
|
67
|
+
/>
|
|
68
|
+
</TelaComboboxTrigger>
|
|
69
|
+
</TelaComboboxAnchor>
|
|
70
|
+
<TelaComboboxList
|
|
71
|
+
side="bottom"
|
|
72
|
+
align="end"
|
|
73
|
+
:side-offset="4"
|
|
74
|
+
class="w-300px no-scrollbar"
|
|
75
|
+
>
|
|
76
|
+
<!-- Host supplies the history content (e.g. ChatWidgetHistoryList). -->
|
|
77
|
+
<slot v-if="historyOpen" name="history" />
|
|
78
|
+
</TelaComboboxList>
|
|
79
|
+
</TelaComboboxRoot>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
/** Conversations to list. Selection is emitted by the surrounding `TelaComboboxRoot`. */
|
|
4
|
+
items: { id: string, title: string }[]
|
|
5
|
+
loading?: boolean
|
|
6
|
+
searchPlaceholder?: string
|
|
7
|
+
emptyMessage?: string
|
|
8
|
+
emptySearchMessage?: string
|
|
9
|
+
}>(), {
|
|
10
|
+
searchPlaceholder: 'Search conversations',
|
|
11
|
+
emptyMessage: 'No conversations yet',
|
|
12
|
+
emptySearchMessage: 'No conversations found',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const skeletonRows = [
|
|
16
|
+
{ id: 'row-1', width: '72%' },
|
|
17
|
+
{ id: 'row-2', width: '54%' },
|
|
18
|
+
{ id: 'row-3', width: '64%' },
|
|
19
|
+
{ id: 'row-4', width: '46%' },
|
|
20
|
+
{ id: 'row-5', width: '78%' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const searchQuery = ref('')
|
|
24
|
+
|
|
25
|
+
const showSkeleton = computed(() => props.loading && props.items.length === 0)
|
|
26
|
+
const resolvedEmptyMessage = computed(() =>
|
|
27
|
+
searchQuery.value.trim() ? props.emptySearchMessage : props.emptyMessage,
|
|
28
|
+
)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<TelaComboboxInput
|
|
33
|
+
v-model="searchQuery"
|
|
34
|
+
auto-focus
|
|
35
|
+
:placeholder="searchPlaceholder"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<div flex flex-col gap-2px p-4px max-h-280px overflow-y-auto no-scrollbar>
|
|
39
|
+
<template v-if="showSkeleton">
|
|
40
|
+
<div
|
|
41
|
+
v-for="row in skeletonRows"
|
|
42
|
+
:key="row.id"
|
|
43
|
+
flex items-center h-32px px-8px
|
|
44
|
+
>
|
|
45
|
+
<TelaSkeleton h-14px rounded-4px :style="{ width: row.width }" />
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<template v-else>
|
|
50
|
+
<TelaComboboxEmpty class="body-14-regular! text-center text-tertiary py-8px">
|
|
51
|
+
{{ resolvedEmptyMessage }}
|
|
52
|
+
</TelaComboboxEmpty>
|
|
53
|
+
<TelaComboboxItem
|
|
54
|
+
v-for="item in items"
|
|
55
|
+
:key="item.id"
|
|
56
|
+
:value="item.id"
|
|
57
|
+
:text-value="item.title"
|
|
58
|
+
>
|
|
59
|
+
<span truncate>{{ item.title }}</span>
|
|
60
|
+
</TelaComboboxItem>
|
|
61
|
+
</template>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|