| <script setup lang='ts'> |
| import { computed, ref } from 'vue' |
| import { NDropdown, useMessage } from 'naive-ui' |
| import AvatarComponent from './Avatar.vue' |
| import TextComponent from './Text.vue' |
| import { SvgIcon } from '@/components/common' |
| import { useIconRender } from '@/hooks/useIconRender' |
| import { t } from '@/locales' |
| import { useBasicLayout } from '@/hooks/useBasicLayout' |
| import { copyToClip } from '@/utils/copy' |
|
|
| interface Props { |
| dateTime?: string |
| text?: string |
| inversion?: boolean |
| error?: boolean |
| loading?: boolean |
| } |
|
|
| interface Emit { |
| (ev: 'regenerate'): void |
| (ev: 'delete'): void |
| } |
|
|
| const props = defineProps<Props>() |
|
|
| const emit = defineEmits<Emit>() |
|
|
| const { isMobile } = useBasicLayout() |
|
|
| const { iconRender } = useIconRender() |
|
|
| const message = useMessage() |
|
|
| const textRef = ref<HTMLElement>() |
|
|
| const asRawText = ref(props.inversion) |
|
|
| const messageRef = ref<HTMLElement>() |
|
|
| const options = computed(() => { |
| const common = [ |
| { |
| label: t('chat.copy'), |
| key: 'copyText', |
| icon: iconRender({ icon: 'ri:file-copy-2-line' }), |
| }, |
| { |
| label: t('common.delete'), |
| key: 'delete', |
| icon: iconRender({ icon: 'ri:delete-bin-line' }), |
| }, |
| ] |
|
|
| if (!props.inversion) { |
| common.unshift({ |
| label: asRawText.value ? t('chat.preview') : t('chat.showRawText'), |
| key: 'toggleRenderType', |
| icon: iconRender({ icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code' }), |
| }) |
| } |
|
|
| return common |
| }) |
|
|
| function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') { |
| switch (key) { |
| case 'copyText': |
| handleCopy() |
| return |
| case 'toggleRenderType': |
| asRawText.value = !asRawText.value |
| return |
| case 'delete': |
| emit('delete') |
| } |
| } |
|
|
| function handleRegenerate() { |
| messageRef.value?.scrollIntoView() |
| emit('regenerate') |
| } |
|
|
| async function handleCopy() { |
| try { |
| await copyToClip(props.text || '') |
| message.success(t('chat.copied')) |
| } |
| catch { |
| message.error(t('chat.copyFailed')) |
| } |
| } |
| </script> |
|
|
| <template> |
| <div |
| ref="messageRef" |
| class="flex w-full mb-6 overflow-hidden" |
| :class="[{ 'flex-row-reverse': inversion }]" |
| > |
| <div |
| class="flex items-center justify-center flex-shrink-0 h-8 overflow-hidden rounded-full basis-8" |
| :class="[inversion ? 'ml-2' : 'mr-2']" |
| > |
| <AvatarComponent :image="inversion" /> |
| </div> |
| <div class="overflow-hidden text-sm " :class="[inversion ? 'items-end' : 'items-start']"> |
| <p class="text-xs text-[ |
| {{ dateTime }} |
| </p> |
| <div |
| class="flex items-end gap-1 mt-2" |
| :class="[inversion ? 'flex-row-reverse' : 'flex-row']" |
| > |
| <TextComponent |
| ref="textRef" |
| :inversion="inversion" |
| :error="error" |
| :text="text" |
| :loading="loading" |
| :as-raw-text="asRawText" |
| /> |
| <div class="flex flex-col"> |
| <button |
| v-if="!inversion" |
| class="mb-2 transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-300" |
| @click="handleRegenerate" |
| > |
| <SvgIcon icon="ri:restart-line" /> |
| </button> |
| <NDropdown |
| :trigger="isMobile ? 'click' : 'hover'" |
| :placement="!inversion ? 'right' : 'left'" |
| :options="options" |
| @select="handleSelect" |
| > |
| <button class="transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200"> |
| <SvgIcon icon="ri:more-2-fill" /> |
| </button> |
| </NDropdown> |
| </div> |
| </div> |
| </div> |
| </div> |
| </template> |
| |