Message.svelte 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <script lang="ts">
  2. import dayjs from 'dayjs';
  3. import relativeTime from 'dayjs/plugin/relativeTime';
  4. import isToday from 'dayjs/plugin/isToday';
  5. import isYesterday from 'dayjs/plugin/isYesterday';
  6. dayjs.extend(relativeTime);
  7. dayjs.extend(isToday);
  8. dayjs.extend(isYesterday);
  9. import { getContext } from 'svelte';
  10. const i18n = getContext<Writable<i18nType>>('i18n');
  11. import { settings, user } from '$lib/stores';
  12. import { WEBUI_BASE_URL } from '$lib/constants';
  13. import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
  14. import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
  15. import Name from '$lib/components/chat/Messages/Name.svelte';
  16. import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
  17. import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
  18. import Pencil from '$lib/components/icons/Pencil.svelte';
  19. import Tooltip from '$lib/components/common/Tooltip.svelte';
  20. import Textarea from '$lib/components/common/Textarea.svelte';
  21. import Image from '$lib/components/common/Image.svelte';
  22. import FileItem from '$lib/components/common/FileItem.svelte';
  23. import ProfilePreview from './Message/ProfilePreview.svelte';
  24. export let message;
  25. export let showUserProfile = true;
  26. export let onDelete: Function = () => {};
  27. export let onEdit: Function = () => {};
  28. let edit = false;
  29. let editedContent = null;
  30. let showDeleteConfirmDialog = false;
  31. const formatDate = (inputDate) => {
  32. const date = dayjs(inputDate);
  33. const now = dayjs();
  34. if (date.isToday()) {
  35. return `Today at ${date.format('HH:mm')}`;
  36. } else if (date.isYesterday()) {
  37. return `Yesterday at ${date.format('HH:mm')}`;
  38. } else {
  39. return `${date.format('DD/MM/YYYY')} at ${date.format('HH:mm')}`;
  40. }
  41. };
  42. </script>
  43. <ConfirmDialog
  44. bind:show={showDeleteConfirmDialog}
  45. title={$i18n.t('Delete Message')}
  46. message={$i18n.t('Are you sure you want to delete this message?')}
  47. onConfirm={async () => {
  48. await onDelete();
  49. }}
  50. />
  51. {#if message}
  52. <div
  53. class="flex flex-col justify-between px-5 {showUserProfile
  54. ? 'pt-1.5 pb-0.5'
  55. : ''} w-full {($settings?.widescreenMode ?? null)
  56. ? 'max-w-full'
  57. : 'max-w-5xl'} mx-auto group hover:bg-gray-500/5 transition relative"
  58. >
  59. {#if (message.user_id === $user.id || $user.role === 'admin') && !edit}
  60. <div class=" absolute invisible group-hover:visible right-1 -top-2 z-30">
  61. <div
  62. 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"
  63. >
  64. <button
  65. class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
  66. on:click={() => {
  67. edit = true;
  68. editedContent = message.content;
  69. }}
  70. >
  71. <Pencil />
  72. </button>
  73. <button
  74. class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
  75. on:click={() => (showDeleteConfirmDialog = true)}
  76. >
  77. <GarbageBin />
  78. </button>
  79. </div>
  80. </div>
  81. {/if}
  82. <div
  83. class=" flex w-full message-{message.id}"
  84. id="message-{message.id}"
  85. dir={$settings.chatDirection}
  86. >
  87. <div
  88. class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
  89. >
  90. {#if showUserProfile}
  91. <ProfilePreview user={message.user}>
  92. <ProfileImage
  93. src={message.user?.profile_image_url ??
  94. ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
  95. className={'size-8 translate-y-1 ml-0.5'}
  96. />
  97. </ProfilePreview>
  98. {:else}
  99. <!-- <div class="w-7 h-7 rounded-full bg-transparent" /> -->
  100. {#if message.created_at}
  101. <div
  102. 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"
  103. >
  104. <Tooltip
  105. content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
  106. >
  107. {dayjs(message.created_at / 1000000).format('HH:mm')}
  108. </Tooltip>
  109. </div>
  110. {/if}
  111. {/if}
  112. </div>
  113. <div class="flex-auto w-0 pl-1">
  114. {#if showUserProfile}
  115. <Name>
  116. <div class=" self-end text-base font-medium">
  117. {message?.user?.name}
  118. </div>
  119. {#if message.created_at}
  120. <div
  121. class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]"
  122. >
  123. <Tooltip
  124. content={dayjs(message.created_at / 1000000).format('dddd, DD MMMM YYYY HH:mm')}
  125. >
  126. {formatDate(message.created_at / 1000000)}
  127. </Tooltip>
  128. </div>
  129. {/if}
  130. </Name>
  131. {/if}
  132. {#if (message?.data?.files ?? []).length > 0}
  133. <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
  134. {#each message?.data?.files as file}
  135. <div>
  136. {#if file.type === 'image'}
  137. <Image src={file.url} alt={file.name} imageClassName=" max-h-96 rounded-lg" />
  138. {:else}
  139. <FileItem
  140. item={file}
  141. url={file.url}
  142. name={file.name}
  143. type={file.type}
  144. size={file?.size}
  145. colorClassName="bg-white dark:bg-gray-850 "
  146. />
  147. {/if}
  148. </div>
  149. {/each}
  150. </div>
  151. {/if}
  152. {#if edit}
  153. <div class="py-2">
  154. <Textarea
  155. className=" bg-transparent outline-none w-full resize-none"
  156. bind:value={editedContent}
  157. onKeydown={(e) => {
  158. if (e.key === 'Escape') {
  159. document.getElementById('close-edit-message-button')?.click();
  160. }
  161. const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
  162. const isEnterPressed = e.key === 'Enter';
  163. if (isCmdOrCtrlPressed && isEnterPressed) {
  164. document.getElementById('confirm-edit-message-button')?.click();
  165. }
  166. }}
  167. />
  168. <div class=" mt-2 mb-1 flex justify-end text-sm font-medium">
  169. <div class="flex space-x-1.5">
  170. <button
  171. id="close-edit-message-button"
  172. 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"
  173. on:click={() => {
  174. edit = false;
  175. editedContent = null;
  176. }}
  177. >
  178. {$i18n.t('Cancel')}
  179. </button>
  180. <button
  181. id="confirm-edit-message-button"
  182. 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"
  183. on:click={async () => {
  184. onEdit(editedContent);
  185. edit = false;
  186. editedContent = null;
  187. }}
  188. >
  189. {$i18n.t('Save')}
  190. </button>
  191. </div>
  192. </div>
  193. </div>
  194. {:else}
  195. <div class=" min-w-full markdown-prose">
  196. <Markdown
  197. id={message.id}
  198. content={message.content}
  199. />{#if message.created_at !== message.updated_at}<span class="text-gray-500 text-[10px]"
  200. >(edited)</span
  201. >{/if}
  202. </div>
  203. {/if}
  204. </div>
  205. </div>
  206. </div>
  207. {/if}