ChatsModal.svelte 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { getContext } from 'svelte';
  4. import dayjs from 'dayjs';
  5. import localizedFormat from 'dayjs/plugin/localizedFormat';
  6. dayjs.extend(localizedFormat);
  7. import { deleteChatById } from '$lib/apis/chats';
  8. import Modal from '$lib/components/common/Modal.svelte';
  9. import Tooltip from '$lib/components/common/Tooltip.svelte';
  10. import Spinner from '../common/Spinner.svelte';
  11. import Loader from '../common/Loader.svelte';
  12. import XMark from '../icons/XMark.svelte';
  13. const i18n = getContext('i18n');
  14. export let show = false;
  15. export let title = 'Chats';
  16. export let emptyPlaceholder = '';
  17. export let query = '';
  18. export let chatList = null;
  19. export let allChatsLoaded = false;
  20. export let chatListLoading = false;
  21. let selectedIdx = 0;
  22. export let onUpdate = () => {};
  23. export let loadHandler: null | Function = null;
  24. export let unarchiveHandler: null | Function = null;
  25. const deleteHandler = async (chatId) => {
  26. const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
  27. toast.error(`${error}`);
  28. });
  29. onUpdate();
  30. };
  31. </script>
  32. <Modal size="lg" bind:show>
  33. <div>
  34. <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
  35. <div class=" text-lg font-medium self-center">{title}</div>
  36. <button
  37. class="self-center"
  38. on:click={() => {
  39. show = false;
  40. }}
  41. >
  42. <svg
  43. xmlns="http://www.w3.org/2000/svg"
  44. viewBox="0 0 20 20"
  45. fill="currentColor"
  46. class="w-5 h-5"
  47. >
  48. <path
  49. fill-rule="evenodd"
  50. d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
  51. clip-rule="evenodd"
  52. />
  53. </svg>
  54. </button>
  55. </div>
  56. <div class="flex flex-col w-full px-5 pb-4 dark:text-gray-200">
  57. <div class=" flex w-full space-x-2">
  58. <div class="flex flex-1">
  59. <div class=" self-center ml-1 mr-3">
  60. <svg
  61. xmlns="http://www.w3.org/2000/svg"
  62. viewBox="0 0 20 20"
  63. fill="currentColor"
  64. class="w-4 h-4"
  65. >
  66. <path
  67. fill-rule="evenodd"
  68. d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
  69. clip-rule="evenodd"
  70. />
  71. </svg>
  72. </div>
  73. <input
  74. class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
  75. bind:value={query}
  76. placeholder={$i18n.t('Search Chats')}
  77. />
  78. {#if query}
  79. <div class="self-center pl-1.5 pr-1 translate-y-[0.5px] rounded-l-xl bg-transparent">
  80. <button
  81. class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
  82. on:click={() => {
  83. query = '';
  84. selectedIdx = 0;
  85. }}
  86. >
  87. <XMark className="size-3" strokeWidth="2" />
  88. </button>
  89. </div>
  90. {/if}
  91. </div>
  92. </div>
  93. <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
  94. {#if chatList}
  95. <div class="w-full">
  96. <div class="text-left text-sm w-full mb-3 max-h-[22rem] overflow-y-scroll">
  97. {#if chatList.length === 0}
  98. <div
  99. class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 min-h-20 w-full h-full flex justify-center items-center"
  100. >
  101. {$i18n.t('No results found')}
  102. </div>
  103. {/if}
  104. {#each chatList as chat, idx (chat.id)}
  105. {#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
  106. <div
  107. class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
  108. ? ''
  109. : 'pt-5'} pb-2 px-2"
  110. >
  111. {$i18n.t(chat.time_range)}
  112. <!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
  113. {$i18n.t('Today')}
  114. {$i18n.t('Yesterday')}
  115. {$i18n.t('Previous 7 days')}
  116. {$i18n.t('Previous 30 days')}
  117. {$i18n.t('January')}
  118. {$i18n.t('February')}
  119. {$i18n.t('March')}
  120. {$i18n.t('April')}
  121. {$i18n.t('May')}
  122. {$i18n.t('June')}
  123. {$i18n.t('July')}
  124. {$i18n.t('August')}
  125. {$i18n.t('September')}
  126. {$i18n.t('October')}
  127. {$i18n.t('November')}
  128. {$i18n.t('December')}
  129. -->
  130. </div>
  131. {/if}
  132. <a
  133. class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
  134. idx
  135. ? 'bg-gray-50 dark:bg-gray-850'
  136. : ''}"
  137. href="/c/{chat.id}"
  138. draggable="false"
  139. data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
  140. on:mouseenter={() => {
  141. selectedIdx = idx;
  142. }}
  143. on:click={() => {
  144. show = false;
  145. }}
  146. >
  147. <div class=" flex-1">
  148. <div class="text-ellipsis line-clamp-1 w-full">
  149. {chat?.title}
  150. </div>
  151. </div>
  152. <div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
  153. {dayjs(chat?.updated_at * 1000).calendar()}
  154. </div>
  155. </a>
  156. {/each}
  157. {#if !allChatsLoaded && loadHandler}
  158. <Loader
  159. on:visible={(e) => {
  160. if (!chatListLoading) {
  161. loadHandler();
  162. }
  163. }}
  164. >
  165. <div
  166. class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
  167. >
  168. <Spinner className=" size-4" />
  169. <div class=" ">Loading...</div>
  170. </div>
  171. </Loader>
  172. {/if}
  173. </div>
  174. {#if query === ''}
  175. <slot name="footer"></slot>
  176. {/if}
  177. </div>
  178. {:else}
  179. <div class="w-full h-full flex justify-center items-center min-h-20">
  180. <Spinner />
  181. </div>
  182. {/if}
  183. <!-- {#if chats !== null}
  184. {#if chats.length > 0}
  185. <div class="w-full">
  186. <div class="text-left text-sm w-full mb-3 max-h-[22rem] overflow-y-scroll">
  187. <div class="relative overflow-x-auto">
  188. <table
  189. class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"
  190. >
  191. <thead
  192. class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-1 border-gray-50 dark:border-gray-850"
  193. >
  194. <tr>
  195. <th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
  196. <th scope="col" class="px-3 py-2 hidden md:flex">
  197. {$i18n.t('Created At')}
  198. </th>
  199. <th scope="col" class="px-3 py-2 text-right" />
  200. </tr>
  201. </thead>
  202. <tbody>
  203. {#each chats as chat, idx}
  204. <tr
  205. class="bg-transparent {idx !== chats.length - 1 &&
  206. 'border-b'} dark:bg-gray-900 border-gray-50 dark:border-gray-850 text-xs"
  207. >
  208. <td class="px-3 py-1 w-2/3">
  209. <a href="/c/{chat.id}" target="_blank">
  210. <div class=" hover:underline line-clamp-1">
  211. {chat.title}
  212. </div>
  213. </a>
  214. </td>
  215. <td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
  216. <div class="my-auto">
  217. {dayjs(chat.created_at * 1000).format('LLL')}
  218. </div>
  219. </td>
  220. <td class="px-3 py-1 text-right">
  221. <div class="flex justify-end w-full">
  222. {#if unarchiveHandler}
  223. <Tooltip content={$i18n.t('Unarchive Chat')}>
  224. <button
  225. class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
  226. on:click={async () => {
  227. unarchiveHandler(chat.id);
  228. }}
  229. >
  230. <svg
  231. xmlns="http://www.w3.org/2000/svg"
  232. fill="none"
  233. viewBox="0 0 24 24"
  234. stroke-width="1.5"
  235. stroke="currentColor"
  236. class="size-4"
  237. >
  238. <path
  239. stroke-linecap="round"
  240. stroke-linejoin="round"
  241. d="M9 8.25H7.5a2.25 2.25 0 0 0-2.25 2.25v9a2.25 2.25 0 0 0 2.25 2.25h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25H15m0-3-3-3m0 0-3 3m3-3V15"
  242. />
  243. </svg>
  244. </button>
  245. </Tooltip>
  246. {/if}
  247. <Tooltip content={$i18n.t('Delete Chat')}>
  248. <button
  249. class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
  250. on:click={async () => {
  251. deleteHandler(chat.id);
  252. }}
  253. >
  254. <svg
  255. xmlns="http://www.w3.org/2000/svg"
  256. fill="none"
  257. viewBox="0 0 24 24"
  258. stroke-width="1.5"
  259. stroke="currentColor"
  260. class="w-4 h-4"
  261. >
  262. <path
  263. stroke-linecap="round"
  264. stroke-linejoin="round"
  265. d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
  266. />
  267. </svg>
  268. </button>
  269. </Tooltip>
  270. </div>
  271. </td>
  272. </tr>
  273. {/each}
  274. </tbody>
  275. </table>
  276. </div>
  277. </div>
  278. <slot name="footer"></slot>
  279. </div>
  280. {:else}
  281. <div class="text-left text-sm w-full mb-8">
  282. {emptyPlaceholder || $i18n.t('No chats found.')}
  283. </div>
  284. {/if}
  285. {:else}
  286. <div class="w-full h-full">
  287. <Spinner />
  288. </div>
  289. {/if} -->
  290. </div>
  291. </div>
  292. </div>
  293. </Modal>