1
0

InputMenu.svelte 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. <script lang="ts">
  2. import { DropdownMenu } from 'bits-ui';
  3. import { getContext, onMount, tick } from 'svelte';
  4. import { fly } from 'svelte/transition';
  5. import { flyAndScale } from '$lib/utils/transitions';
  6. import { config, user, tools as _tools, mobile, knowledge, chats } from '$lib/stores';
  7. import { getKnowledgeBases } from '$lib/apis/knowledge';
  8. import { createPicker } from '$lib/utils/google-drive-picker';
  9. import Dropdown from '$lib/components/common/Dropdown.svelte';
  10. import Tooltip from '$lib/components/common/Tooltip.svelte';
  11. import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
  12. import Camera from '$lib/components/icons/Camera.svelte';
  13. import Note from '$lib/components/icons/Note.svelte';
  14. import Clip from '$lib/components/icons/Clip.svelte';
  15. import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
  16. import Refresh from '$lib/components/icons/Refresh.svelte';
  17. import Agile from '$lib/components/icons/Agile.svelte';
  18. import ClockRotateRight from '$lib/components/icons/ClockRotateRight.svelte';
  19. import Database from '$lib/components/icons/Database.svelte';
  20. import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
  21. import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
  22. import PageEdit from '$lib/components/icons/PageEdit.svelte';
  23. import Chats from './InputMenu/Chats.svelte';
  24. import Notes from './InputMenu/Notes.svelte';
  25. import Knowledge from './InputMenu/Knowledge.svelte';
  26. import AttachWebpageModal from './AttachWebpageModal.svelte';
  27. import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
  28. const i18n = getContext('i18n');
  29. export let files = [];
  30. export let selectedModels: string[] = [];
  31. export let fileUploadCapableModels: string[] = [];
  32. export let screenCaptureHandler: Function;
  33. export let uploadFilesHandler: Function;
  34. export let inputFilesHandler: Function;
  35. export let uploadGoogleDriveHandler: Function;
  36. export let uploadOneDriveHandler: Function;
  37. export let onUpload: Function;
  38. export let onClose: Function;
  39. let show = false;
  40. let tab = '';
  41. let showAttachWebpageModal = false;
  42. let fileUploadEnabled = true;
  43. $: fileUploadEnabled =
  44. fileUploadCapableModels.length === selectedModels.length &&
  45. ($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
  46. const detectMobile = () => {
  47. const userAgent = navigator.userAgent || navigator.vendor || window.opera;
  48. return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
  49. };
  50. const handleFileChange = (event) => {
  51. const inputFiles = Array.from(event.target?.files);
  52. if (inputFiles && inputFiles.length > 0) {
  53. console.log(inputFiles);
  54. inputFilesHandler(inputFiles);
  55. }
  56. };
  57. const init = async () => {
  58. if ($knowledge === null) {
  59. await knowledge.set(await getKnowledgeBases(localStorage.token));
  60. }
  61. };
  62. $: if (show) {
  63. init();
  64. }
  65. const onSelect = (item) => {
  66. if (files.find((f) => f.id === item.id)) {
  67. return;
  68. }
  69. files = [
  70. ...files,
  71. {
  72. ...item,
  73. status: 'processed'
  74. }
  75. ];
  76. show = false;
  77. };
  78. </script>
  79. <AttachWebpageModal
  80. bind:show={showAttachWebpageModal}
  81. onSubmit={(e) => {
  82. onUpload(e);
  83. }}
  84. />
  85. <!-- Hidden file input used to open the camera on mobile -->
  86. <input
  87. id="camera-input"
  88. type="file"
  89. accept="image/*"
  90. capture="environment"
  91. on:change={handleFileChange}
  92. style="display: none;"
  93. />
  94. <Dropdown
  95. bind:show
  96. on:change={(e) => {
  97. if (e.detail === false) {
  98. onClose();
  99. }
  100. }}
  101. >
  102. <Tooltip content={$i18n.t('More')}>
  103. <slot />
  104. </Tooltip>
  105. <div slot="content">
  106. <DropdownMenu.Content
  107. class="w-full max-w-70 rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin transition"
  108. sideOffset={4}
  109. alignOffset={-6}
  110. side="bottom"
  111. align="start"
  112. transition={flyAndScale}
  113. >
  114. {#if tab === ''}
  115. <div in:fly={{ x: -20, duration: 150 }}>
  116. <Tooltip
  117. content={fileUploadCapableModels.length !== selectedModels.length
  118. ? $i18n.t('Model(s) do not support file upload')
  119. : !fileUploadEnabled
  120. ? $i18n.t('You do not have permission to upload files.')
  121. : ''}
  122. className="w-full"
  123. >
  124. <DropdownMenu.Item
  125. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
  126. ? 'opacity-50'
  127. : ''}"
  128. on:click={() => {
  129. if (fileUploadEnabled) {
  130. uploadFilesHandler();
  131. }
  132. }}
  133. >
  134. <Clip />
  135. <div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
  136. </DropdownMenu.Item>
  137. </Tooltip>
  138. <Tooltip
  139. content={fileUploadCapableModels.length !== selectedModels.length
  140. ? $i18n.t('Model(s) do not support file upload')
  141. : !fileUploadEnabled
  142. ? $i18n.t('You do not have permission to upload files.')
  143. : ''}
  144. className="w-full"
  145. >
  146. <DropdownMenu.Item
  147. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
  148. ? 'opacity-50'
  149. : ''}"
  150. on:click={() => {
  151. if (fileUploadEnabled) {
  152. if (!detectMobile()) {
  153. screenCaptureHandler();
  154. } else {
  155. const cameraInputElement = document.getElementById('camera-input');
  156. if (cameraInputElement) {
  157. cameraInputElement.click();
  158. }
  159. }
  160. }
  161. }}
  162. >
  163. <Camera />
  164. <div class=" line-clamp-1">{$i18n.t('Capture')}</div>
  165. </DropdownMenu.Item>
  166. </Tooltip>
  167. <Tooltip
  168. content={fileUploadCapableModels.length !== selectedModels.length
  169. ? $i18n.t('Model(s) do not support file upload')
  170. : !fileUploadEnabled
  171. ? $i18n.t('You do not have permission to upload files.')
  172. : ''}
  173. className="w-full"
  174. >
  175. <DropdownMenu.Item
  176. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
  177. on:click={() => {
  178. if (fileUploadEnabled) {
  179. showAttachWebpageModal = true;
  180. }
  181. }}
  182. >
  183. <GlobeAlt />
  184. <div class="line-clamp-1">{$i18n.t('Attach Webpage')}</div>
  185. </DropdownMenu.Item>
  186. </Tooltip>
  187. {#if $config?.features?.enable_notes ?? false}
  188. <Tooltip
  189. content={fileUploadCapableModels.length !== selectedModels.length
  190. ? $i18n.t('Model(s) do not support file upload')
  191. : !fileUploadEnabled
  192. ? $i18n.t('You do not have permission to upload files.')
  193. : ''}
  194. className="w-full"
  195. >
  196. <button
  197. class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
  198. ? 'opacity-50'
  199. : ''}"
  200. on:click={() => {
  201. tab = 'notes';
  202. }}
  203. >
  204. <PageEdit />
  205. <div class="flex items-center w-full justify-between">
  206. <div class=" line-clamp-1">
  207. {$i18n.t('Attach Notes')}
  208. </div>
  209. <div class="text-gray-500">
  210. <ChevronRight />
  211. </div>
  212. </div>
  213. </button>
  214. </Tooltip>
  215. {/if}
  216. {#if ($knowledge ?? []).length > 0}
  217. <Tooltip
  218. content={fileUploadCapableModels.length !== selectedModels.length
  219. ? $i18n.t('Model(s) do not support file upload')
  220. : !fileUploadEnabled
  221. ? $i18n.t('You do not have permission to upload files.')
  222. : ''}
  223. className="w-full"
  224. >
  225. <button
  226. class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
  227. ? 'opacity-50'
  228. : ''}"
  229. on:click={() => {
  230. tab = 'knowledge';
  231. }}
  232. >
  233. <Database />
  234. <div class="flex items-center w-full justify-between">
  235. <div class=" line-clamp-1">
  236. {$i18n.t('Attach Knowledge')}
  237. </div>
  238. <div class="text-gray-500">
  239. <ChevronRight />
  240. </div>
  241. </div>
  242. </button>
  243. </Tooltip>
  244. {/if}
  245. {#if ($chats ?? []).length > 0}
  246. <Tooltip
  247. content={fileUploadCapableModels.length !== selectedModels.length
  248. ? $i18n.t('Model(s) do not support file upload')
  249. : !fileUploadEnabled
  250. ? $i18n.t('You do not have permission to upload files.')
  251. : ''}
  252. className="w-full"
  253. >
  254. <button
  255. class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
  256. ? 'opacity-50'
  257. : ''}"
  258. on:click={() => {
  259. tab = 'chats';
  260. }}
  261. >
  262. <ClockRotateRight />
  263. <div class="flex items-center w-full justify-between">
  264. <div class=" line-clamp-1">
  265. {$i18n.t('Reference Chats')}
  266. </div>
  267. <div class="text-gray-500">
  268. <ChevronRight />
  269. </div>
  270. </div>
  271. </button>
  272. </Tooltip>
  273. {/if}
  274. {#if fileUploadEnabled}
  275. {#if $config?.features?.enable_google_drive_integration}
  276. <DropdownMenu.Item
  277. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
  278. on:click={() => {
  279. uploadGoogleDriveHandler();
  280. }}
  281. >
  282. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
  283. <path
  284. d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
  285. fill="#0066da"
  286. />
  287. <path
  288. d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
  289. fill="#00ac47"
  290. />
  291. <path
  292. d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
  293. fill="#ea4335"
  294. />
  295. <path
  296. d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
  297. fill="#00832d"
  298. />
  299. <path
  300. d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
  301. fill="#2684fc"
  302. />
  303. <path
  304. d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
  305. fill="#ffba00"
  306. />
  307. </svg>
  308. <div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
  309. </DropdownMenu.Item>
  310. {/if}
  311. {#if $config?.features?.enable_onedrive_integration && ($config?.features?.enable_onedrive_personal || $config?.features?.enable_onedrive_business)}
  312. <DropdownMenu.Sub>
  313. <DropdownMenu.SubTrigger
  314. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl w-full"
  315. >
  316. <svg
  317. xmlns="http://www.w3.org/2000/svg"
  318. viewBox="0 0 32 32"
  319. class="w-5 h-5"
  320. fill="none"
  321. >
  322. <mask
  323. id="mask0_87_7796"
  324. style="mask-type:alpha"
  325. maskUnits="userSpaceOnUse"
  326. x="0"
  327. y="6"
  328. width="32"
  329. height="20"
  330. >
  331. <path
  332. d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
  333. fill="#C4C4C4"
  334. />
  335. </mask>
  336. <g mask="url(#mask0_87_7796)">
  337. <path
  338. d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
  339. fill="url(#paint0_linear_87_7796)"
  340. />
  341. <path
  342. d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
  343. fill="url(#paint1_linear_87_7796)"
  344. />
  345. <path
  346. d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
  347. fill="url(#paint2_linear_87_7796)"
  348. />
  349. <path
  350. d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
  351. fill="url(#paint3_linear_87_7796)"
  352. />
  353. </g>
  354. <defs>
  355. <linearGradient
  356. id="paint0_linear_87_7796"
  357. x1="4.42591"
  358. y1="24.6668"
  359. x2="27.2309"
  360. y2="23.2764"
  361. gradientUnits="userSpaceOnUse"
  362. >
  363. <stop stop-color="#2086B8" />
  364. <stop offset="1" stop-color="#46D3F6" />
  365. </linearGradient>
  366. <linearGradient
  367. id="paint1_linear_87_7796"
  368. x1="23.8302"
  369. y1="19.6668"
  370. x2="30.2108"
  371. y2="15.2082"
  372. gradientUnits="userSpaceOnUse"
  373. >
  374. <stop stop-color="#1694DB" />
  375. <stop offset="1" stop-color="#62C3FE" />
  376. </linearGradient>
  377. <linearGradient
  378. id="paint2_linear_87_7796"
  379. x1="8.51037"
  380. y1="7.33333"
  381. x2="23.3335"
  382. y2="15.9348"
  383. gradientUnits="userSpaceOnUse"
  384. >
  385. <stop stop-color="#0D3D78" />
  386. <stop offset="1" stop-color="#063B83" />
  387. </linearGradient>
  388. <linearGradient
  389. id="paint3_linear_87_7796"
  390. x1="-0.340429"
  391. y1="19.9998"
  392. x2="14.5634"
  393. y2="14.4649"
  394. gradientUnits="userSpaceOnUse"
  395. >
  396. <stop stop-color="#16589B" />
  397. <stop offset="1" stop-color="#1464B7" />
  398. </linearGradient>
  399. </defs>
  400. </svg>
  401. <div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
  402. </DropdownMenu.SubTrigger>
  403. <DropdownMenu.SubContent
  404. class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
  405. side={$mobile ? 'bottom' : 'right'}
  406. sideOffset={$mobile ? 5 : 0}
  407. alignOffset={$mobile ? 0 : -8}
  408. >
  409. {#if $config?.features?.enable_onedrive_personal}
  410. <DropdownMenu.Item
  411. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
  412. on:click={() => {
  413. uploadOneDriveHandler('personal');
  414. }}
  415. >
  416. <div class="flex flex-col">
  417. <div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
  418. </div>
  419. </DropdownMenu.Item>
  420. {/if}
  421. {#if $config?.features?.enable_onedrive_business}
  422. <DropdownMenu.Item
  423. class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
  424. on:click={() => {
  425. uploadOneDriveHandler('organizations');
  426. }}
  427. >
  428. <div class="flex flex-col">
  429. <div class="line-clamp-1">
  430. {$i18n.t('Microsoft OneDrive (work/school)')}
  431. </div>
  432. <div class="text-xs text-gray-500">{$i18n.t('Includes SharePoint')}</div>
  433. </div>
  434. </DropdownMenu.Item>
  435. {/if}
  436. </DropdownMenu.SubContent>
  437. </DropdownMenu.Sub>
  438. {/if}
  439. {/if}
  440. </div>
  441. {:else if tab === 'knowledge'}
  442. <div in:fly={{ x: 20, duration: 150 }}>
  443. <button
  444. class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
  445. on:click={() => {
  446. tab = '';
  447. }}
  448. >
  449. <ChevronLeft />
  450. <div class="flex items-center w-full justify-between">
  451. <div>
  452. {$i18n.t('Knowledge')}
  453. </div>
  454. </div>
  455. </button>
  456. <Knowledge {onSelect} />
  457. </div>
  458. {:else if tab === 'notes'}
  459. <div in:fly={{ x: 20, duration: 150 }}>
  460. <button
  461. class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
  462. on:click={() => {
  463. tab = '';
  464. }}
  465. >
  466. <ChevronLeft />
  467. <div class="flex items-center w-full justify-between">
  468. <div>
  469. {$i18n.t('Notes')}
  470. </div>
  471. </div>
  472. </button>
  473. <Notes {onSelect} />
  474. </div>
  475. {:else if tab === 'chats'}
  476. <div in:fly={{ x: 20, duration: 150 }}>
  477. <button
  478. class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
  479. on:click={() => {
  480. tab = '';
  481. }}
  482. >
  483. <ChevronLeft />
  484. <div class="flex items-center w-full justify-between">
  485. <div>
  486. {$i18n.t('Chats')}
  487. </div>
  488. </div>
  489. </button>
  490. <Chats {onSelect} />
  491. </div>
  492. {/if}
  493. </DropdownMenu.Content>
  494. </div>
  495. </Dropdown>