123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- <script lang="ts">
- import dayjs from 'dayjs';
- import relativeTime from 'dayjs/plugin/relativeTime';
- import isToday from 'dayjs/plugin/isToday';
- import isYesterday from 'dayjs/plugin/isYesterday';
- import localizedFormat from 'dayjs/plugin/localizedFormat';
- dayjs.extend(relativeTime);
- dayjs.extend(isToday);
- dayjs.extend(isYesterday);
- dayjs.extend(localizedFormat);
- import { getContext, onMount } from 'svelte';
- const i18n = getContext<Writable<i18nType>>('i18n');
- import { settings, user, shortCodesToEmojis } from '$lib/stores';
- import { WEBUI_BASE_URL } from '$lib/constants';
- import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
- import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
- import Name from '$lib/components/chat/Messages/Name.svelte';
- import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
- import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
- import Pencil from '$lib/components/icons/Pencil.svelte';
- import Tooltip from '$lib/components/common/Tooltip.svelte';
- import Textarea from '$lib/components/common/Textarea.svelte';
- import Image from '$lib/components/common/Image.svelte';
- import FileItem from '$lib/components/common/FileItem.svelte';
- import ProfilePreview from './Message/ProfilePreview.svelte';
- import ChatBubbleOvalEllipsis from '$lib/components/icons/ChatBubbleOvalEllipsis.svelte';
- import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
- import ReactionPicker from './Message/ReactionPicker.svelte';
- import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
- import { formatDate } from '$lib/utils';
- export let message;
- export let showUserProfile = true;
- export let thread = false;
- export let onDelete: Function = () => {};
- export let onEdit: Function = () => {};
- export let onThread: Function = () => {};
- export let onReaction: Function = () => {};
- let showButtons = false;
- let edit = false;
- let editedContent = null;
- let showDeleteConfirmDialog = false;
- </script>
- <ConfirmDialog
- bind:show={showDeleteConfirmDialog}
- title={$i18n.t('Delete Message')}
- message={$i18n.t('Are you sure you want to delete this message?')}
- onConfirm={async () => {
- await onDelete();
- }}
- />
- {#if message}
- <div
- class="flex flex-col justify-between px-5 {showUserProfile
- ? 'pt-1.5 pb-0.5'
- : ''} w-full {($settings?.widescreenMode ?? null)
- ? 'max-w-full'
- : 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
- >
- {#if !edit}
- <div
- class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
- >
- <div
- class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800"
- >
- <ReactionPicker
- onClose={() => (showButtons = false)}
- onSubmit={(name) => {
- showButtons = false;
- onReaction(name);
- }}
- >
- <Tooltip content={$i18n.t('Add Reaction')}>
- <button
- class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
- on:click={() => {
- showButtons = true;
- }}
- >
- <FaceSmile />
- </button>
- </Tooltip>
- </ReactionPicker>
- {#if !thread}
- <Tooltip content={$i18n.t('Reply in Thread')}>
- <button
- class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
- on:click={() => {
- onThread(message.id);
- }}
- >
- <ChatBubbleOvalEllipsis />
- </button>
- </Tooltip>
- {/if}
- {#if message.user_id === $user.id || $user.role === 'admin'}
- <Tooltip content={$i18n.t('Edit')}>
- <button
- class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
- on:click={() => {
- edit = true;
- editedContent = message.content;
- }}
- >
- <Pencil />
- </button>
- </Tooltip>
- <Tooltip content={$i18n.t('Delete')}>
- <button
- class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
- on:click={() => (showDeleteConfirmDialog = true)}
- >
- <GarbageBin />
- </button>
- </Tooltip>
- {/if}
- </div>
- </div>
- {/if}
- <div
- class=" flex w-full message-{message.id}"
- id="message-{message.id}"
- dir={$settings.chatDirection}
- >
- <div
- class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
- >
- {#if showUserProfile}
- <ProfilePreview user={message.user}>
- <ProfileImage
- src={message.user?.profile_image_url ??
- ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
- className={'size-8 translate-y-1 ml-0.5'}
- />
- </ProfilePreview>
- {:else}
- <!-- <div class="w-7 h-7 rounded-full bg-transparent" /> -->
- {#if message.created_at}
- <div
- class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
- >
- <Tooltip
- content={dayjs(message.created_at / 1000000).format('LLLL')}
- >
- {dayjs(message.created_at / 1000000).format('LT')}
- </Tooltip>
- </div>
- {/if}
- {/if}
- </div>
- <div class="flex-auto w-0 pl-1">
- {#if showUserProfile}
- <Name>
- <div class=" self-end text-base shrink-0 font-medium truncate">
- {message?.user?.name}
- </div>
- {#if message.created_at}
- <div
- class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
- >
- <Tooltip
- content={dayjs(message.created_at / 1000000).format('LLLL')}
- >
- <span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span>
- </Tooltip>
- </div>
- {/if}
- </Name>
- {/if}
- {#if (message?.data?.files ?? []).length > 0}
- <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
- {#each message?.data?.files as file}
- <div>
- {#if file.type === 'image'}
- <Image src={file.url} alt={file.name} imageClassName=" max-h-96 rounded-lg" />
- {:else}
- <FileItem
- item={file}
- url={file.url}
- name={file.name}
- type={file.type}
- size={file?.size}
- colorClassName="bg-white dark:bg-gray-850 "
- />
- {/if}
- </div>
- {/each}
- </div>
- {/if}
- {#if edit}
- <div class="py-2">
- <Textarea
- className=" bg-transparent outline-none w-full resize-none"
- bind:value={editedContent}
- onKeydown={(e) => {
- if (e.key === 'Escape') {
- document.getElementById('close-edit-message-button')?.click();
- }
- const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
- const isEnterPressed = e.key === 'Enter';
- if (isCmdOrCtrlPressed && isEnterPressed) {
- document.getElementById('confirm-edit-message-button')?.click();
- }
- }}
- />
- <div class=" mt-2 mb-1 flex justify-end text-sm font-medium">
- <div class="flex space-x-1.5">
- <button
- id="close-edit-message-button"
- class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
- on:click={() => {
- edit = false;
- editedContent = null;
- }}
- >
- {$i18n.t('Cancel')}
- </button>
- <button
- id="confirm-edit-message-button"
- class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
- on:click={async () => {
- onEdit(editedContent);
- edit = false;
- editedContent = null;
- }}
- >
- {$i18n.t('Save')}
- </button>
- </div>
- </div>
- </div>
- {:else}
- <div class=" min-w-full markdown-prose">
- <Markdown
- id={message.id}
- content={message.content}
- />{#if message.created_at !== message.updated_at}<span class="text-gray-500 text-[10px]"
- >(edited)</span
- >{/if}
- </div>
- {#if (message?.reactions ?? []).length > 0}
- <div>
- <div class="flex items-center flex-wrap gap-y-1.5 gap-1 mt-1 mb-2">
- {#each message.reactions as reaction}
- <Tooltip content={`:${reaction.name}:`}>
- <button
- class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes(
- $user.id
- )
- ? ' bg-blue-300/10 outline outline-blue-500/50 outline-1'
- : 'bg-gray-300/10 dark:bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
- on:click={() => {
- onReaction(reaction.name);
- }}
- >
- {#if $shortCodesToEmojis[reaction.name]}
- <img
- src="/assets/emojis/{$shortCodesToEmojis[
- reaction.name
- ].toLowerCase()}.svg"
- alt={reaction.name}
- class=" size-4"
- loading="lazy"
- />
- {:else}
- <div>
- {reaction.name}
- </div>
- {/if}
- {#if reaction.user_ids.length > 0}
- <div class="text-xs font-medium text-gray-500 dark:text-gray-400">
- {reaction.user_ids?.length}
- </div>
- {/if}
- </button>
- </Tooltip>
- {/each}
- <ReactionPicker
- onSubmit={(name) => {
- onReaction(name);
- }}
- >
- <Tooltip content={$i18n.t('Add Reaction')}>
- <div
- class="flex items-center gap-1.5 bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1 transition rounded-xl px-1 py-1 cursor-pointer text-gray-500 dark:text-gray-400"
- >
- <FaceSmile />
- </div>
- </Tooltip>
- </ReactionPicker>
- </div>
- </div>
- {/if}
- {#if !thread && message.reply_count > 0}
- <div class="flex items-center gap-1.5 -mt-0.5 mb-1.5">
- <button
- class="flex items-center text-xs py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"
- on:click={() => {
- onThread(message.id);
- }}
- >
- <span class="font-medium mr-1">
- {$i18n.t('{{COUNT}} Replies', { COUNT: message.reply_count })}</span
- ><span>
- {' - '}{$i18n.t('Last reply')}
- {dayjs.unix(message.latest_reply_at / 1000000000).fromNow()}</span
- >
- <span class="ml-1">
- <ChevronRight className="size-2.5" strokeWidth="3" />
- </span>
- <!-- {$i18n.t('View Replies')} -->
- </button>
- </div>
- {/if}
- {/if}
- </div>
- </div>
- </div>
- {/if}
|