|
1 | 1 | <script lang="ts" setup>
|
2 | 2 | import type { ChatMessage } from '~/types/chat'
|
3 |
| -import { defineAsyncComponent } from 'vue' |
4 | 3 |
|
5 | 4 | const props = defineProps<{
|
6 |
| - message: ChatMessage |
7 |
| - sending: boolean |
8 |
| - showToggleButton?: boolean |
9 |
| - isPreviewing?: boolean |
| 5 | + message: ChatMessage |
| 6 | + sending: boolean |
| 7 | + showToggleButton?: boolean |
| 8 | + isPreviewing?: boolean |
10 | 9 | }>()
|
11 | 10 |
|
12 | 11 | const emits = defineEmits<{
|
13 |
| - resend: [message: ChatMessage] |
14 |
| - remove: [message: ChatMessage] |
15 |
| - preview: [content: string] |
| 12 | + resend: [message: ChatMessage] |
| 13 | + remove: [message: ChatMessage] |
| 14 | + preview: [content: string] |
16 | 15 | }>()
|
17 | 16 |
|
18 | 17 | const markdown = useMarkdown()
|
19 | 18 |
|
20 | 19 | const opened = ref(props.showToggleButton === true ? false : true)
|
21 | 20 | const isModelMessage = computed(() => props.message.role === 'assistant')
|
22 | 21 | const contentClass = computed(() => {
|
23 |
| - return [ |
24 |
| - isModelMessage.value ? 'max-w-[calc(100%-2rem)]' : 'max-w-full', |
25 |
| - props.message.type === 'error' |
26 |
| - ? 'bg-red-50 dark:bg-red-800/60' |
27 |
| - : (isModelMessage.value ? 'bg-gray-50 dark:bg-gray-800' : 'bg-primary-50 dark:bg-primary-400/60'), |
28 |
| - ] |
| 22 | + return [ |
| 23 | + isModelMessage.value ? 'max-w-[calc(100%-2rem)]' : 'max-w-full', |
| 24 | + props.message.type === 'error' |
| 25 | + ? 'bg-red-50 dark:bg-red-800/60' |
| 26 | + : (isModelMessage.value ? 'bg-gray-50 dark:bg-gray-800' : 'bg-primary-50 dark:bg-primary-400/60'), |
| 27 | + ] |
29 | 28 | })
|
30 | 29 |
|
31 | 30 | const timeUsed = computed(() => {
|
32 |
| - const endTime = props.message.type === 'loading' ? Date.now() : props.message.endTime |
33 |
| - return Number(((endTime - props.message.startTime) / 1000).toFixed(1)) |
| 31 | + const endTime = props.message.type === 'loading' ? Date.now() : props.message.endTime |
| 32 | + return Number(((endTime - props.message.startTime) / 1000).toFixed(1)) |
34 | 33 | })
|
35 | 34 |
|
36 | 35 | const modelName = computed(() => {
|
37 |
| - return parseModelValue(props.message.model) |
| 36 | + return parseModelValue(props.message.model) |
38 | 37 | })
|
39 | 38 |
|
40 | 39 | watch(() => props.showToggleButton, (value) => {
|
41 |
| - opened.value = value === true ? false : true |
| 40 | + opened.value = value === true ? false : true |
42 | 41 | })
|
43 | 42 |
|
44 | 43 | const showPreview = ref(false)
|
45 | 44 |
|
46 |
| -const previewComponent = ref<any>(null) |
47 |
| -const isVueComponent = ref(false) |
48 |
| -
|
49 | 45 | const togglePreview = () => {
|
50 |
| - if (!props.message.content) return |
51 |
| -
|
52 |
| - const vueMatch = props.message.content.match(/```vue\n([\s\S]*?)```/) |
53 |
| - if (vueMatch) { |
54 |
| - emits('preview', vueMatch[1]) |
55 |
| - } |
56 |
| -} |
57 |
| -
|
58 |
| -const extractTemplate = (code: string) => { |
59 |
| - const templateMatch = code.match(/<template>([\s\S]*)<\/template>/) |
60 |
| - return templateMatch ? templateMatch[1] : '' |
61 |
| -} |
62 |
| -
|
63 |
| -const extractScript = (code: string) => { |
64 |
| - const scriptMatch = code.match(/<script.*>([\s\S]*)<\/script>/) |
65 |
| - return scriptMatch ? scriptMatch[1] : '' |
66 |
| -} |
67 |
| -
|
68 |
| -const extractStyles = (code: string) => { |
69 |
| - const styleMatch = code.match(/<style.*>([\s\S]*)<\/style>/) |
70 |
| - return styleMatch ? styleMatch[1] : '' |
| 46 | + emits('preview', props.message.content) |
71 | 47 | }
|
72 | 48 |
|
73 | 49 | const contentDisplay = computed(() => {
|
74 |
| - if (props.isPreviewing && isModelMessage.value) { |
75 |
| - return isVueComponent.value ? 'component-preview' : 'preview-mode' |
76 |
| - } |
77 |
| - return props.message.type === 'loading' ? 'loading' : 'normal' |
| 50 | + if (props.isPreviewing && isModelMessage.value) { |
| 51 | + return 'preview-mode' |
| 52 | + } |
| 53 | + return props.message.type === 'loading' ? 'loading' : 'normal' |
78 | 54 | })
|
79 | 55 | </script>
|
80 | 56 |
|
81 | 57 | <template>
|
82 |
| - <div class="flex flex-col my-2" |
83 |
| - :class="{ 'items-end': message.role === 'user' }"> |
84 |
| - <div class="text-gray-500 dark:text-gray-400 p-1"> |
85 |
| - <Icon v-if="message.role === 'user'" name="i-material-symbols-account-circle" class="text-lg" /> |
86 |
| - <div v-else class="text-sm flex items-center"> |
87 |
| - <UTooltip :text="modelName.family" :popper="{ placement: 'top' }"> |
88 |
| - <span class="text-primary/80">{{ modelName.name }}</span> |
89 |
| - </UTooltip> |
90 |
| - <template v-if="timeUsed > 0"> |
91 |
| - <span class="mx-2 text-muted/20 text-xs">|</span> |
92 |
| - <span class="text-gray-400 dark:text-gray-500 text-xs">{{ timeUsed }}s</span> |
93 |
| - </template> |
94 |
| - </div> |
95 |
| - </div> |
96 |
| - <div class="leading-6 text-sm flex items-center max-w-full message-content" |
97 |
| - :class="{ 'text-gray-400 dark:text-gray-500': message.type === 'canceled', 'flex-row-reverse': !isModelMessage }"> |
98 |
| - <div class="flex border border-primary/20 rounded-lg overflow-hidden box-border" |
99 |
| - :class="contentClass"> |
100 |
| - <div v-if="contentDisplay === 'loading'" class="text-xl text-primary p-3"> |
101 |
| - <span class="block i-svg-spinners-3-dots-scale"></span> |
102 |
| - </div> |
103 |
| - <div v-else-if="contentDisplay === 'preview-mode'" class="p-3 flex items-center text-gray-500"> |
104 |
| - <UIcon name="i-heroicons-document-text" class="mr-2" /> |
105 |
| - <span>Content in preview</span> |
| 58 | + <div class="flex flex-col my-2" |
| 59 | + :class="{ 'items-end': message.role === 'user' }"> |
| 60 | + <div class="text-gray-500 dark:text-gray-400 p-1"> |
| 61 | + <Icon v-if="message.role === 'user'" name="i-material-symbols-account-circle" class="text-lg" /> |
| 62 | + <div v-else class="text-sm flex items-center"> |
| 63 | + <UTooltip :text="modelName.family" :popper="{ placement: 'top' }"> |
| 64 | + <span class="text-primary/80">{{ modelName.name }}</span> |
| 65 | + </UTooltip> |
| 66 | + <template v-if="timeUsed > 0"> |
| 67 | + <span class="mx-2 text-muted/20 text-xs">|</span> |
| 68 | + <span class="text-gray-400 dark:text-gray-500 text-xs">{{ timeUsed }}s</span> |
| 69 | + </template> |
| 70 | + </div> |
106 | 71 | </div>
|
107 |
| - <div v-else-if="contentDisplay === 'component-preview'" class="p-3 w-full"> |
108 |
| - <div class="preview-container"> |
109 |
| - <component :is="previewComponent" v-if="previewComponent" /> |
110 |
| - </div> |
| 72 | + <div class="leading-6 text-sm flex items-center max-w-full message-content" |
| 73 | + :class="{ 'text-gray-400 dark:text-gray-500': message.type === 'canceled', 'flex-row-reverse': !isModelMessage }"> |
| 74 | + <div class="flex border border-primary/20 rounded-lg overflow-hidden box-border" |
| 75 | + :class="contentClass"> |
| 76 | + <div v-if="contentDisplay === 'loading'" class="text-xl text-primary p-3"> |
| 77 | + <span class="block i-svg-spinners-3-dots-scale"></span> |
| 78 | + </div> |
| 79 | + <div v-else-if="contentDisplay === 'preview-mode'" class="p-3 flex items-center text-gray-500"> |
| 80 | + <UIcon name="i-heroicons-document-text" class="mr-2" /> |
| 81 | + <span>Content in preview</span> |
| 82 | + </div> |
| 83 | + <template v-else-if="isModelMessage"> |
| 84 | + <div class="p-3 overflow-hidden"> |
| 85 | + <div v-html="markdown.render(message.content || '')" class="md-body" :class="{ 'line-clamp-3 max-h-[5rem]': !opened }" /> |
| 86 | + <Sources v-show="opened" :relevant_documents="message?.relevantDocs || []" /> |
| 87 | + </div> |
| 88 | + <div class="flex flex-col"> |
| 89 | + <MessageToggleCollapseButton v-if="showToggleButton" :opened="opened" @click="opened = !opened" /> |
| 90 | + <UButton v-if="message.content" |
| 91 | + icon="i-heroicons-eye-20-solid" |
| 92 | + color="gray" |
| 93 | + variant="ghost" |
| 94 | + size="xs" |
| 95 | + class="mt-1 preview-btn" |
| 96 | + :class="{ 'text-primary-500': isPreviewing }" |
| 97 | + @click="togglePreview" /> |
| 98 | + </div> |
| 99 | + </template> |
| 100 | + <pre v-else v-text="message.content" class="p-3 whitespace-break-spaces" /> |
| 101 | + </div> |
| 102 | + <ChatMessageActionMore :message="message" |
| 103 | + :disabled="sending" |
| 104 | + @resend="emits('resend', message)" |
| 105 | + @remove="emits('remove', message)"> |
| 106 | + <UButton :class="{ invisible: sending }" icon="i-material-symbols-more-vert" color="gray" |
| 107 | + :variant="'link'" |
| 108 | + class="action-more"> |
| 109 | + </UButton> |
| 110 | + </ChatMessageActionMore> |
111 | 111 | </div>
|
112 |
| - <template v-else-if="isModelMessage"> |
113 |
| - <div class="p-3 overflow-hidden"> |
114 |
| - <div v-html="markdown.render(message.content || '')" class="md-body" :class="{ 'line-clamp-3 max-h-[5rem]': !opened }" /> |
115 |
| - <Sources v-show="opened" :relevant_documents="message?.relevantDocs || []" /> |
116 |
| - </div> |
117 |
| - <div class="flex flex-col"> |
118 |
| - <MessageToggleCollapseButton v-if="showToggleButton" :opened="opened" @click="opened = !opened" /> |
119 |
| - <UButton v-if="message.content" |
120 |
| - icon="i-heroicons-eye-20-solid" |
121 |
| - color="gray" |
122 |
| - variant="ghost" |
123 |
| - size="xs" |
124 |
| - class="mt-1 preview-btn" |
125 |
| - :class="{ 'text-primary-500': isPreviewing }" |
126 |
| - @click="togglePreview" /> |
127 |
| - </div> |
128 |
| - </template> |
129 |
| - <pre v-else v-text="message.content" class="p-3 whitespace-break-spaces" /> |
130 |
| - </div> |
131 |
| - <ChatMessageActionMore :message="message" |
132 |
| - :disabled="sending" |
133 |
| - @resend="emits('resend', message)" |
134 |
| - @remove="emits('remove', message)"> |
135 |
| - <UButton :class="{ invisible: sending }" icon="i-material-symbols-more-vert" color="gray" |
136 |
| - :variant="'link'" |
137 |
| - class="action-more"> |
138 |
| - </UButton> |
139 |
| - </ChatMessageActionMore> |
140 | 112 | </div>
|
141 |
| - </div> |
142 | 113 | </template>
|
143 | 114 |
|
144 | 115 | <style scoped lang="scss">
|
145 | 116 | .message-content {
|
146 |
| - .action-more { |
147 |
| - transform-origin: center center; |
148 |
| - transition: all 0.3s; |
149 |
| - transform: scale(0); |
150 |
| - opacity: 0; |
151 |
| - } |
152 |
| -
|
153 |
| - &:hover { |
154 | 117 | .action-more {
|
155 |
| - transform: scale(1); |
156 |
| - opacity: 1; |
| 118 | + transform-origin: center center; |
| 119 | + transition: all 0.3s; |
| 120 | + transform: scale(0); |
| 121 | + opacity: 0; |
157 | 122 | }
|
158 |
| - } |
159 | 123 |
|
160 |
| - .preview-btn { |
161 |
| - opacity: 0; |
162 |
| - transition: opacity 0.3s; |
163 |
| - } |
| 124 | + &:hover { |
| 125 | + .action-more { |
| 126 | + transform: scale(1); |
| 127 | + opacity: 1; |
| 128 | + } |
| 129 | + } |
164 | 130 |
|
165 |
| - &:hover { |
166 | 131 | .preview-btn {
|
167 |
| - opacity: 1; |
| 132 | + opacity: 0; |
| 133 | + transition: opacity 0.3s; |
168 | 134 | }
|
169 |
| - } |
170 |
| -} |
171 |
| -
|
172 |
| -.preview-container { |
173 |
| - border: 1px solid var(--color-gray-200); |
174 |
| - border-radius: 0.5rem; |
175 |
| - padding: 1rem; |
176 |
| - background: var(--color-gray-50); |
177 |
| - min-height: 100px; |
178 | 135 |
|
179 |
| - :deep() { |
180 |
| - * { |
181 |
| - margin: initial; |
182 |
| - padding: initial; |
| 136 | + &:hover { |
| 137 | + .preview-btn { |
| 138 | + opacity: 1; |
| 139 | + } |
183 | 140 | }
|
184 |
| - } |
185 |
| -} |
186 |
| -
|
187 |
| -.dark { |
188 |
| - .preview-container { |
189 |
| - border-color: var(--color-gray-700); |
190 |
| - background: var(--color-gray-800); |
191 |
| - } |
192 | 141 | }
|
193 | 142 | </style>
|
0 commit comments