InputMenu.svelte 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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 { createPicker } from '$lib/utils/google-drive-picker';
  8. import Dropdown from '$lib/components/common/Dropdown.svelte';
  9. import Tooltip from '$lib/components/common/Tooltip.svelte';
  10. import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
  11. import Camera from '$lib/components/icons/Camera.svelte';
  12. import Note from '$lib/components/icons/Note.svelte';
  13. import Clip from '$lib/components/icons/Clip.svelte';
  14. import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
  15. import Refresh from '$lib/components/icons/Refresh.svelte';
  16. import Agile from '$lib/components/icons/Agile.svelte';
  17. import ClockRotateRight from '$lib/components/icons/ClockRotateRight.svelte';
  18. import Database from '$lib/components/icons/Database.svelte';
  19. import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
  20. import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
  21. import PageEdit from '$lib/components/icons/PageEdit.svelte';
  22. import Chats from './InputMenu/Chats.svelte';
  23. import Notes from './InputMenu/Notes.svelte';
  24. import Knowledge from './InputMenu/Knowledge.svelte';
  25. const i18n = getContext('i18n');
  26. export let files = [];
  27. export let selectedModels: string[] = [];
  28. export let fileUploadCapableModels: string[] = [];
  29. export let screenCaptureHandler: Function;
  30. export let uploadFilesHandler: Function;
  31. export let inputFilesHandler: Function;
  32. export let uploadGoogleDriveHandler: Function;
  33. export let uploadOneDriveHandler: Function;
  34. export let onClose: Function;
  35. let show = false;
  36. let tab = '';
  37. let fileUploadEnabled = true;
  38. $: fileUploadEnabled =
  39. fileUploadCapableModels.length === selectedModels.length &&
  40. ($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
  41. const detectMobile = () => {
  42. const userAgent = navigator.userAgent || navigator.vendor || window.opera;
  43. return /android|iphone|ipad|ipod|windows phone/i.test(userAgent);
  44. };
  45. const handleFileChange = (event) => {
  46. const inputFiles = Array.from(event.target?.files);
  47. if (inputFiles && inputFiles.length > 0) {
  48. console.log(inputFiles);
  49. inputFilesHandler(inputFiles);
  50. }
  51. };
  52. const onSelect = (item) => {
  53. if (files.find((f) => f.id === item.id)) {
  54. return;
  55. }
  56. files = [
  57. ...files,
  58. {
  59. ...item,
  60. status: 'processed'
  61. }
  62. ];
  63. show = false;
  64. };
  65. </script>
  66. <!-- Hidden file input used to open the camera on mobile -->
  67. <input
  68. id="camera-input"
  69. type="file"
  70. accept="image/*"
  71. capture="environment"
  72. on:change={handleFileChange}
  73. style="display: none;"
  74. />
  75. <Dropdown
  76. bind:show
  77. on:change={(e) => {
  78. if (e.detail === false) {
  79. onClose();
  80. }
  81. }}
  82. >
  83. <Tooltip content={$i18n.t('More')}>
  84. <slot />
  85. </Tooltip>
  86. <div slot="content">
  87. <DropdownMenu.Content
  88. 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"
  89. sideOffset={4}
  90. alignOffset={-6}
  91. side="bottom"
  92. align="start"
  93. transition={flyAndScale}
  94. >
  95. {#if tab === ''}
  96. <div in:fly={{ x: -20, duration: 150 }}>
  97. <Tooltip
  98. content={fileUploadCapableModels.length !== selectedModels.length
  99. ? $i18n.t('Model(s) do not support file upload')
  100. : !fileUploadEnabled
  101. ? $i18n.t('You do not have permission to upload files.')
  102. : ''}
  103. className="w-full"
  104. >
  105. <DropdownMenu.Item
  106. 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 {!fileUploadEnabled
  107. ? 'opacity-50'
  108. : ''}"
  109. on:click={() => {
  110. if (fileUploadEnabled) {
  111. uploadFilesHandler();
  112. }
  113. }}
  114. >
  115. <Clip />
  116. <div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
  117. </DropdownMenu.Item>
  118. </Tooltip>
  119. <Tooltip
  120. content={fileUploadCapableModels.length !== selectedModels.length
  121. ? $i18n.t('Model(s) do not support file upload')
  122. : !fileUploadEnabled
  123. ? $i18n.t('You do not have permission to upload files.')
  124. : ''}
  125. className="w-full"
  126. >
  127. <DropdownMenu.Item
  128. 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 {!fileUploadEnabled
  129. ? 'opacity-50'
  130. : ''}"
  131. on:click={() => {
  132. if (fileUploadEnabled) {
  133. if (!detectMobile()) {
  134. screenCaptureHandler();
  135. } else {
  136. const cameraInputElement = document.getElementById('camera-input');
  137. if (cameraInputElement) {
  138. cameraInputElement.click();
  139. }
  140. }
  141. }
  142. }}
  143. >
  144. <Camera />
  145. <div class=" line-clamp-1">{$i18n.t('Capture')}</div>
  146. </DropdownMenu.Item>
  147. </Tooltip>
  148. {#if $config?.features?.enable_notes ?? false}
  149. <Tooltip
  150. content={fileUploadCapableModels.length !== selectedModels.length
  151. ? $i18n.t('Model(s) do not support file upload')
  152. : !fileUploadEnabled
  153. ? $i18n.t('You do not have permission to upload files.')
  154. : ''}
  155. className="w-full"
  156. >
  157. <button
  158. 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 rounded-xl {!fileUploadEnabled
  159. ? 'opacity-50'
  160. : ''}"
  161. on:click={() => {
  162. tab = 'notes';
  163. }}
  164. >
  165. <PageEdit />
  166. <div class="flex items-center w-full justify-between">
  167. <div class=" line-clamp-1">
  168. {$i18n.t('Attach Notes')}
  169. </div>
  170. <div class="text-gray-500">
  171. <ChevronRight />
  172. </div>
  173. </div>
  174. </button>
  175. </Tooltip>
  176. {/if}
  177. <Tooltip
  178. content={fileUploadCapableModels.length !== selectedModels.length
  179. ? $i18n.t('Model(s) do not support file upload')
  180. : !fileUploadEnabled
  181. ? $i18n.t('You do not have permission to upload files.')
  182. : ''}
  183. className="w-full"
  184. >
  185. <button
  186. 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 rounded-xl {!fileUploadEnabled
  187. ? 'opacity-50'
  188. : ''}"
  189. on:click={() => {
  190. tab = 'knowledge';
  191. }}
  192. >
  193. <Database />
  194. <div class="flex items-center w-full justify-between">
  195. <div class=" line-clamp-1">
  196. {$i18n.t('Attach Knowledge')}
  197. </div>
  198. <div class="text-gray-500">
  199. <ChevronRight />
  200. </div>
  201. </div>
  202. </button>
  203. </Tooltip>
  204. {#if ($chats ?? []).length > 0}
  205. <Tooltip
  206. content={fileUploadCapableModels.length !== selectedModels.length
  207. ? $i18n.t('Model(s) do not support file upload')
  208. : !fileUploadEnabled
  209. ? $i18n.t('You do not have permission to upload files.')
  210. : ''}
  211. className="w-full"
  212. >
  213. <button
  214. 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 rounded-xl {!fileUploadEnabled
  215. ? 'opacity-50'
  216. : ''}"
  217. on:click={() => {
  218. tab = 'chats';
  219. }}
  220. >
  221. <ClockRotateRight />
  222. <div class="flex items-center w-full justify-between">
  223. <div class=" line-clamp-1">
  224. {$i18n.t('Reference Chats')}
  225. </div>
  226. <div class="text-gray-500">
  227. <ChevronRight />
  228. </div>
  229. </div>
  230. </button>
  231. </Tooltip>
  232. {/if}
  233. {#if fileUploadEnabled}
  234. {#if $config?.features?.enable_google_drive_integration}
  235. <DropdownMenu.Item
  236. 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"
  237. on:click={() => {
  238. uploadGoogleDriveHandler();
  239. }}
  240. >
  241. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
  242. <path
  243. 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"
  244. fill="#0066da"
  245. />
  246. <path
  247. 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"
  248. fill="#00ac47"
  249. />
  250. <path
  251. 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"
  252. fill="#ea4335"
  253. />
  254. <path
  255. 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"
  256. fill="#00832d"
  257. />
  258. <path
  259. 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"
  260. fill="#2684fc"
  261. />
  262. <path
  263. 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"
  264. fill="#ffba00"
  265. />
  266. </svg>
  267. <div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
  268. </DropdownMenu.Item>
  269. {/if}
  270. {#if $config?.features?.enable_onedrive_integration && ($config?.features?.enable_onedrive_personal || $config?.features?.enable_onedrive_business)}
  271. <DropdownMenu.Sub>
  272. <DropdownMenu.SubTrigger
  273. 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 w-full"
  274. >
  275. <svg
  276. xmlns="http://www.w3.org/2000/svg"
  277. viewBox="0 0 32 32"
  278. class="w-5 h-5"
  279. fill="none"
  280. >
  281. <mask
  282. id="mask0_87_7796"
  283. style="mask-type:alpha"
  284. maskUnits="userSpaceOnUse"
  285. x="0"
  286. y="6"
  287. width="32"
  288. height="20"
  289. >
  290. <path
  291. 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"
  292. fill="#C4C4C4"
  293. />
  294. </mask>
  295. <g mask="url(#mask0_87_7796)">
  296. <path
  297. 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"
  298. fill="url(#paint0_linear_87_7796)"
  299. />
  300. <path
  301. 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"
  302. fill="url(#paint1_linear_87_7796)"
  303. />
  304. <path
  305. 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"
  306. fill="url(#paint2_linear_87_7796)"
  307. />
  308. <path
  309. 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"
  310. fill="url(#paint3_linear_87_7796)"
  311. />
  312. </g>
  313. <defs>
  314. <linearGradient
  315. id="paint0_linear_87_7796"
  316. x1="4.42591"
  317. y1="24.6668"
  318. x2="27.2309"
  319. y2="23.2764"
  320. gradientUnits="userSpaceOnUse"
  321. >
  322. <stop stop-color="#2086B8" />
  323. <stop offset="1" stop-color="#46D3F6" />
  324. </linearGradient>
  325. <linearGradient
  326. id="paint1_linear_87_7796"
  327. x1="23.8302"
  328. y1="19.6668"
  329. x2="30.2108"
  330. y2="15.2082"
  331. gradientUnits="userSpaceOnUse"
  332. >
  333. <stop stop-color="#1694DB" />
  334. <stop offset="1" stop-color="#62C3FE" />
  335. </linearGradient>
  336. <linearGradient
  337. id="paint2_linear_87_7796"
  338. x1="8.51037"
  339. y1="7.33333"
  340. x2="23.3335"
  341. y2="15.9348"
  342. gradientUnits="userSpaceOnUse"
  343. >
  344. <stop stop-color="#0D3D78" />
  345. <stop offset="1" stop-color="#063B83" />
  346. </linearGradient>
  347. <linearGradient
  348. id="paint3_linear_87_7796"
  349. x1="-0.340429"
  350. y1="19.9998"
  351. x2="14.5634"
  352. y2="14.4649"
  353. gradientUnits="userSpaceOnUse"
  354. >
  355. <stop stop-color="#16589B" />
  356. <stop offset="1" stop-color="#1464B7" />
  357. </linearGradient>
  358. </defs>
  359. </svg>
  360. <div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
  361. </DropdownMenu.SubTrigger>
  362. <DropdownMenu.SubContent
  363. 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"
  364. side={$mobile ? 'bottom' : 'right'}
  365. sideOffset={$mobile ? 5 : 0}
  366. alignOffset={$mobile ? 0 : -8}
  367. >
  368. {#if $config?.features?.enable_onedrive_personal}
  369. <DropdownMenu.Item
  370. 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"
  371. on:click={() => {
  372. uploadOneDriveHandler('personal');
  373. }}
  374. >
  375. <div class="flex flex-col">
  376. <div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
  377. <div class="text-xs text-gray-500">
  378. {$i18n.t('Includes OneDrive Consumer')}
  379. </div>
  380. </div>
  381. </DropdownMenu.Item>
  382. {/if}
  383. {#if $config?.features?.enable_onedrive_business}
  384. <DropdownMenu.Item
  385. 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"
  386. on:click={() => {
  387. uploadOneDriveHandler('organizations');
  388. }}
  389. >
  390. <div class="flex flex-col">
  391. <div class="line-clamp-1">
  392. {$i18n.t('Microsoft OneDrive (work/school)')}
  393. </div>
  394. <div class="text-xs text-gray-500">{$i18n.t('Includes SharePoint')}</div>
  395. </div>
  396. </DropdownMenu.Item>
  397. {/if}
  398. </DropdownMenu.SubContent>
  399. </DropdownMenu.Sub>
  400. {/if}
  401. {/if}
  402. </div>
  403. {:else if tab === 'knowledge'}
  404. <div in:fly={{ x: 20, duration: 150 }}>
  405. <button
  406. 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"
  407. on:click={() => {
  408. tab = '';
  409. }}
  410. >
  411. <ChevronLeft />
  412. <div class="flex items-center w-full justify-between">
  413. <div>
  414. {$i18n.t('Knowledge')}
  415. </div>
  416. </div>
  417. </button>
  418. <Knowledge {onSelect} />
  419. </div>
  420. {:else if tab === 'notes'}
  421. <div in:fly={{ x: 20, duration: 150 }}>
  422. <button
  423. 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"
  424. on:click={() => {
  425. tab = '';
  426. }}
  427. >
  428. <ChevronLeft />
  429. <div class="flex items-center w-full justify-between">
  430. <div>
  431. {$i18n.t('Notes')}
  432. </div>
  433. </div>
  434. </button>
  435. <Notes {onSelect} />
  436. </div>
  437. {:else if tab === 'chats'}
  438. <div in:fly={{ x: 20, duration: 150 }}>
  439. <button
  440. 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"
  441. on:click={() => {
  442. tab = '';
  443. }}
  444. >
  445. <ChevronLeft />
  446. <div class="flex items-center w-full justify-between">
  447. <div>
  448. {$i18n.t('Chats')}
  449. </div>
  450. </div>
  451. </button>
  452. <Chats {onSelect} />
  453. </div>
  454. {/if}
  455. </DropdownMenu.Content>
  456. </div>
  457. </Dropdown>