MessageInput.svelte 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { onMount, tick, getContext } from 'svelte';
  4. import { type Model, mobile, modelfiles, settings, showSidebar, models } from '$lib/stores';
  5. import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
  6. import {
  7. uploadDocToVectorDB,
  8. uploadWebToVectorDB,
  9. uploadYoutubeTranscriptionToVectorDB
  10. } from '$lib/apis/rag';
  11. import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
  12. import { transcribeAudio } from '$lib/apis/audio';
  13. import Prompts from './MessageInput/PromptCommands.svelte';
  14. import Suggestions from './MessageInput/Suggestions.svelte';
  15. import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
  16. import Documents from './MessageInput/Documents.svelte';
  17. import Models from './MessageInput/Models.svelte';
  18. import Tooltip from '../common/Tooltip.svelte';
  19. import XMark from '$lib/components/icons/XMark.svelte';
  20. const i18n = getContext('i18n');
  21. export let submitPrompt: Function;
  22. export let stopResponse: Function;
  23. export let autoScroll = true;
  24. export let atSelectedModel: Model | undefined;
  25. export let selectedModels: [''];
  26. let chatTextAreaElement: HTMLTextAreaElement;
  27. let filesInputElement;
  28. let promptsElement;
  29. let documentsElement;
  30. let modelsElement;
  31. let inputFiles;
  32. let dragged = false;
  33. let user = null;
  34. let chatInputPlaceholder = '';
  35. export let files = [];
  36. export let fileUploadEnabled = true;
  37. export let speechRecognitionEnabled = true;
  38. export let prompt = '';
  39. export let messages = [];
  40. let speechRecognition;
  41. let visionCapableState = 'all';
  42. $: if (prompt) {
  43. if (chatTextAreaElement) {
  44. chatTextAreaElement.style.height = '';
  45. chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
  46. }
  47. }
  48. // $: {
  49. // if (atSelectedModel || selectedModels) {
  50. // visionCapableState = checkModelsAreVisionCapable();
  51. // if (visionCapableState === 'none') {
  52. // // Remove all image files
  53. // const fileCount = files.length;
  54. // files = files.filter((file) => file.type != 'image');
  55. // if (files.length < fileCount) {
  56. // toast.warning($i18n.t('All selected models do not support image input, removed images'));
  57. // }
  58. // }
  59. // }
  60. // }
  61. const checkModelsAreVisionCapable = () => {
  62. let modelsToCheck = [];
  63. if (atSelectedModel !== undefined) {
  64. modelsToCheck = [atSelectedModel.id];
  65. } else {
  66. modelsToCheck = selectedModels;
  67. }
  68. if (modelsToCheck.length == 0 || modelsToCheck[0] == '') {
  69. return 'all';
  70. }
  71. let visionCapableCount = 0;
  72. for (const modelName of modelsToCheck) {
  73. const model = $models.find((m) => m.id === modelName);
  74. if (!model) {
  75. continue;
  76. }
  77. if (model.custom_info?.meta.vision_capable ?? true) {
  78. visionCapableCount++;
  79. }
  80. }
  81. if (visionCapableCount == modelsToCheck.length) {
  82. return 'all';
  83. } else if (visionCapableCount == 0) {
  84. return 'none';
  85. } else {
  86. return 'some';
  87. }
  88. };
  89. let mediaRecorder;
  90. let audioChunks = [];
  91. let isRecording = false;
  92. const MIN_DECIBELS = -45;
  93. const scrollToBottom = () => {
  94. const element = document.getElementById('messages-container');
  95. element.scrollTop = element.scrollHeight;
  96. };
  97. const startRecording = async () => {
  98. const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  99. mediaRecorder = new MediaRecorder(stream);
  100. mediaRecorder.onstart = () => {
  101. isRecording = true;
  102. console.log('Recording started');
  103. };
  104. mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
  105. mediaRecorder.onstop = async () => {
  106. isRecording = false;
  107. console.log('Recording stopped');
  108. // Create a blob from the audio chunks
  109. const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
  110. const file = blobToFile(audioBlob, 'recording.wav');
  111. const res = await transcribeAudio(localStorage.token, file).catch((error) => {
  112. toast.error(error);
  113. return null;
  114. });
  115. if (res) {
  116. prompt = res.text;
  117. await tick();
  118. chatTextAreaElement?.focus();
  119. if (prompt !== '' && $settings?.speechAutoSend === true) {
  120. submitPrompt(prompt, user);
  121. }
  122. }
  123. // saveRecording(audioBlob);
  124. audioChunks = [];
  125. };
  126. // Start recording
  127. mediaRecorder.start();
  128. // Monitor silence
  129. monitorSilence(stream);
  130. };
  131. const monitorSilence = (stream) => {
  132. const audioContext = new AudioContext();
  133. const audioStreamSource = audioContext.createMediaStreamSource(stream);
  134. const analyser = audioContext.createAnalyser();
  135. analyser.minDecibels = MIN_DECIBELS;
  136. audioStreamSource.connect(analyser);
  137. const bufferLength = analyser.frequencyBinCount;
  138. const domainData = new Uint8Array(bufferLength);
  139. let lastSoundTime = Date.now();
  140. const detectSound = () => {
  141. analyser.getByteFrequencyData(domainData);
  142. if (domainData.some((value) => value > 0)) {
  143. lastSoundTime = Date.now();
  144. }
  145. if (isRecording && Date.now() - lastSoundTime > 3000) {
  146. mediaRecorder.stop();
  147. audioContext.close();
  148. return;
  149. }
  150. window.requestAnimationFrame(detectSound);
  151. };
  152. window.requestAnimationFrame(detectSound);
  153. };
  154. const saveRecording = (blob) => {
  155. const url = URL.createObjectURL(blob);
  156. const a = document.createElement('a');
  157. document.body.appendChild(a);
  158. a.style = 'display: none';
  159. a.href = url;
  160. a.download = 'recording.wav';
  161. a.click();
  162. window.URL.revokeObjectURL(url);
  163. };
  164. const speechRecognitionHandler = () => {
  165. // Check if SpeechRecognition is supported
  166. if (isRecording) {
  167. if (speechRecognition) {
  168. speechRecognition.stop();
  169. }
  170. if (mediaRecorder) {
  171. mediaRecorder.stop();
  172. }
  173. } else {
  174. isRecording = true;
  175. if ($settings?.audio?.STTEngine ?? '' !== '') {
  176. startRecording();
  177. } else {
  178. if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
  179. // Create a SpeechRecognition object
  180. speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
  181. // Set continuous to true for continuous recognition
  182. speechRecognition.continuous = true;
  183. // Set the timeout for turning off the recognition after inactivity (in milliseconds)
  184. const inactivityTimeout = 3000; // 3 seconds
  185. let timeoutId;
  186. // Start recognition
  187. speechRecognition.start();
  188. // Event triggered when speech is recognized
  189. speechRecognition.onresult = async (event) => {
  190. // Clear the inactivity timeout
  191. clearTimeout(timeoutId);
  192. // Handle recognized speech
  193. console.log(event);
  194. const transcript = event.results[Object.keys(event.results).length - 1][0].transcript;
  195. prompt = `${prompt}${transcript}`;
  196. await tick();
  197. chatTextAreaElement?.focus();
  198. // Restart the inactivity timeout
  199. timeoutId = setTimeout(() => {
  200. console.log('Speech recognition turned off due to inactivity.');
  201. speechRecognition.stop();
  202. }, inactivityTimeout);
  203. };
  204. // Event triggered when recognition is ended
  205. speechRecognition.onend = function () {
  206. // Restart recognition after it ends
  207. console.log('recognition ended');
  208. isRecording = false;
  209. if (prompt !== '' && $settings?.speechAutoSend === true) {
  210. submitPrompt(prompt, user);
  211. }
  212. };
  213. // Event triggered when an error occurs
  214. speechRecognition.onerror = function (event) {
  215. console.log(event);
  216. toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
  217. isRecording = false;
  218. };
  219. } else {
  220. toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
  221. }
  222. }
  223. }
  224. };
  225. const uploadDoc = async (file) => {
  226. console.log(file);
  227. const doc = {
  228. type: 'doc',
  229. name: file.name,
  230. collection_name: '',
  231. upload_status: false,
  232. error: ''
  233. };
  234. try {
  235. files = [...files, doc];
  236. if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
  237. const res = await transcribeAudio(localStorage.token, file).catch((error) => {
  238. toast.error(error);
  239. return null;
  240. });
  241. if (res) {
  242. console.log(res);
  243. const blob = new Blob([res.text], { type: 'text/plain' });
  244. file = blobToFile(blob, `${file.name}.txt`);
  245. }
  246. }
  247. const res = await uploadDocToVectorDB(localStorage.token, '', file);
  248. if (res) {
  249. doc.upload_status = true;
  250. doc.collection_name = res.collection_name;
  251. files = files;
  252. }
  253. } catch (e) {
  254. // Remove the failed doc from the files array
  255. files = files.filter((f) => f.name !== file.name);
  256. toast.error(e);
  257. }
  258. };
  259. const uploadWeb = async (url) => {
  260. console.log(url);
  261. const doc = {
  262. type: 'doc',
  263. name: url,
  264. collection_name: '',
  265. upload_status: false,
  266. url: url,
  267. error: ''
  268. };
  269. try {
  270. files = [...files, doc];
  271. const res = await uploadWebToVectorDB(localStorage.token, '', url);
  272. if (res) {
  273. doc.upload_status = true;
  274. doc.collection_name = res.collection_name;
  275. files = files;
  276. }
  277. } catch (e) {
  278. // Remove the failed doc from the files array
  279. files = files.filter((f) => f.name !== url);
  280. toast.error(e);
  281. }
  282. };
  283. const uploadYoutubeTranscription = async (url) => {
  284. console.log(url);
  285. const doc = {
  286. type: 'doc',
  287. name: url,
  288. collection_name: '',
  289. upload_status: false,
  290. url: url,
  291. error: ''
  292. };
  293. try {
  294. files = [...files, doc];
  295. const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
  296. if (res) {
  297. doc.upload_status = true;
  298. doc.collection_name = res.collection_name;
  299. files = files;
  300. }
  301. } catch (e) {
  302. // Remove the failed doc from the files array
  303. files = files.filter((f) => f.name !== url);
  304. toast.error(e);
  305. }
  306. };
  307. onMount(() => {
  308. window.setTimeout(() => chatTextAreaElement?.focus(), 0);
  309. const dropZone = document.querySelector('body');
  310. const handleKeyDown = (event: KeyboardEvent) => {
  311. if (event.key === 'Escape') {
  312. console.log('Escape');
  313. dragged = false;
  314. }
  315. };
  316. const onDragOver = (e) => {
  317. e.preventDefault();
  318. dragged = true;
  319. };
  320. const onDragLeave = () => {
  321. dragged = false;
  322. };
  323. const onDrop = async (e) => {
  324. e.preventDefault();
  325. console.log(e);
  326. if (e.dataTransfer?.files) {
  327. const inputFiles = Array.from(e.dataTransfer?.files);
  328. if (inputFiles && inputFiles.length > 0) {
  329. inputFiles.forEach((file) => {
  330. console.log(file, file.name.split('.').at(-1));
  331. if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
  332. if (visionCapableState == 'none') {
  333. toast.error($i18n.t('Selected models do not support image inputs'));
  334. return;
  335. }
  336. let reader = new FileReader();
  337. reader.onload = (event) => {
  338. files = [
  339. ...files,
  340. {
  341. type: 'image',
  342. url: `${event.target.result}`
  343. }
  344. ];
  345. };
  346. reader.readAsDataURL(file);
  347. } else if (
  348. SUPPORTED_FILE_TYPE.includes(file['type']) ||
  349. SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
  350. ) {
  351. uploadDoc(file);
  352. } else {
  353. toast.error(
  354. $i18n.t(
  355. `Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
  356. { file_type: file['type'] }
  357. )
  358. );
  359. uploadDoc(file);
  360. }
  361. });
  362. } else {
  363. toast.error($i18n.t(`File not found.`));
  364. }
  365. }
  366. dragged = false;
  367. };
  368. window.addEventListener('keydown', handleKeyDown);
  369. dropZone?.addEventListener('dragover', onDragOver);
  370. dropZone?.addEventListener('drop', onDrop);
  371. dropZone?.addEventListener('dragleave', onDragLeave);
  372. return () => {
  373. window.removeEventListener('keydown', handleKeyDown);
  374. dropZone?.removeEventListener('dragover', onDragOver);
  375. dropZone?.removeEventListener('drop', onDrop);
  376. dropZone?.removeEventListener('dragleave', onDragLeave);
  377. };
  378. });
  379. </script>
  380. {#if dragged}
  381. <div
  382. class="fixed {$showSidebar
  383. ? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
  384. : 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
  385. id="dropzone"
  386. role="region"
  387. aria-label="Drag and Drop Container"
  388. >
  389. <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
  390. <div class="m-auto pt-64 flex flex-col justify-center">
  391. <div class="max-w-md">
  392. <AddFilesPlaceholder />
  393. </div>
  394. </div>
  395. </div>
  396. </div>
  397. {/if}
  398. <div class="fixed bottom-0 {$showSidebar ? 'left-0 md:left-[260px]' : 'left-0'} right-0">
  399. <div class="w-full">
  400. <div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
  401. <div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
  402. <div class="relative">
  403. {#if autoScroll === false && messages.length > 0}
  404. <div class=" absolute -top-12 left-0 right-0 flex justify-center z-30">
  405. <button
  406. class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
  407. on:click={() => {
  408. autoScroll = true;
  409. scrollToBottom();
  410. }}
  411. >
  412. <svg
  413. xmlns="http://www.w3.org/2000/svg"
  414. viewBox="0 0 20 20"
  415. fill="currentColor"
  416. class="w-5 h-5"
  417. >
  418. <path
  419. fill-rule="evenodd"
  420. d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
  421. clip-rule="evenodd"
  422. />
  423. </svg>
  424. </button>
  425. </div>
  426. {/if}
  427. </div>
  428. <div class="w-full relative">
  429. {#if prompt.charAt(0) === '/'}
  430. <Prompts bind:this={promptsElement} bind:prompt />
  431. {:else if prompt.charAt(0) === '#'}
  432. <Documents
  433. bind:this={documentsElement}
  434. bind:prompt
  435. on:youtube={(e) => {
  436. console.log(e);
  437. uploadYoutubeTranscription(e.detail);
  438. }}
  439. on:url={(e) => {
  440. console.log(e);
  441. uploadWeb(e.detail);
  442. }}
  443. on:select={(e) => {
  444. console.log(e);
  445. files = [
  446. ...files,
  447. {
  448. type: e?.detail?.type ?? 'doc',
  449. ...e.detail,
  450. upload_status: true
  451. }
  452. ];
  453. }}
  454. />
  455. {/if}
  456. <Models
  457. bind:this={modelsElement}
  458. bind:prompt
  459. bind:user
  460. bind:chatInputPlaceholder
  461. {messages}
  462. on:select={(e) => {
  463. atSelectedModel = e.detail;
  464. chatTextAreaElement?.focus();
  465. }}
  466. />
  467. {#if atSelectedModel !== undefined}
  468. <div
  469. class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
  470. >
  471. <div class="flex items-center gap-2 text-sm dark:text-gray-500">
  472. <img
  473. crossorigin="anonymous"
  474. alt="model profile"
  475. class="size-5 max-w-[28px] object-cover rounded-full"
  476. src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
  477. ?.profile_image_url ??
  478. ($i18n.language === 'dg-DG'
  479. ? `/doge.png`
  480. : `${WEBUI_BASE_URL}/static/favicon.png`)}
  481. />
  482. <div>
  483. Talking to <span class=" font-medium">{atSelectedModel.name}</span>
  484. </div>
  485. </div>
  486. <div>
  487. <button
  488. class="flex items-center"
  489. on:click={() => {
  490. atSelectedModel = undefined;
  491. }}
  492. >
  493. <XMark />
  494. </button>
  495. </div>
  496. </div>
  497. {/if}
  498. </div>
  499. </div>
  500. </div>
  501. <div class="bg-white dark:bg-gray-900">
  502. <div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
  503. <div class=" pb-2">
  504. <input
  505. bind:this={filesInputElement}
  506. bind:files={inputFiles}
  507. type="file"
  508. hidden
  509. multiple
  510. on:change={async () => {
  511. if (inputFiles && inputFiles.length > 0) {
  512. const _inputFiles = Array.from(inputFiles);
  513. _inputFiles.forEach((file) => {
  514. if (
  515. ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])
  516. ) {
  517. if (visionCapableState === 'none') {
  518. toast.error($i18n.t('Selected models do not support image inputs'));
  519. inputFiles = null;
  520. filesInputElement.value = '';
  521. return;
  522. }
  523. let reader = new FileReader();
  524. reader.onload = (event) => {
  525. files = [
  526. ...files,
  527. {
  528. type: 'image',
  529. url: `${event.target.result}`
  530. }
  531. ];
  532. inputFiles = null;
  533. filesInputElement.value = '';
  534. };
  535. reader.readAsDataURL(file);
  536. } else if (
  537. SUPPORTED_FILE_TYPE.includes(file['type']) ||
  538. SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
  539. ) {
  540. uploadDoc(file);
  541. filesInputElement.value = '';
  542. } else {
  543. toast.error(
  544. $i18n.t(
  545. `Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
  546. { file_type: file['type'] }
  547. )
  548. );
  549. uploadDoc(file);
  550. filesInputElement.value = '';
  551. }
  552. });
  553. } else {
  554. toast.error($i18n.t(`File not found.`));
  555. }
  556. }}
  557. />
  558. <form
  559. dir={$settings?.chatDirection ?? 'LTR'}
  560. class=" flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100"
  561. on:submit|preventDefault={() => {
  562. submitPrompt(prompt, user);
  563. }}
  564. >
  565. {#if files.length > 0}
  566. <div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
  567. {#each files as file, fileIdx}
  568. <div class=" relative group">
  569. {#if file.type === 'image'}
  570. <div class="relative">
  571. <img
  572. src={file.url}
  573. alt="input"
  574. class=" h-16 w-16 rounded-xl object-cover"
  575. />
  576. {#if visionCapableState === 'some'}
  577. <Tooltip
  578. className=" absolute top-0 left-0"
  579. content={$i18n.t('A selected model does not support image input')}
  580. >
  581. <svg
  582. xmlns="http://www.w3.org/2000/svg"
  583. viewBox="0 0 24 24"
  584. fill="currentColor"
  585. class="w-6 h-6 fill-yellow-300"
  586. >
  587. <path
  588. fill-rule="evenodd"
  589. d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
  590. clip-rule="evenodd"
  591. />
  592. </svg>
  593. </Tooltip>
  594. {/if}
  595. </div>
  596. {:else if file.type === 'doc'}
  597. <div
  598. class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
  599. >
  600. <div class="p-2.5 bg-red-400 text-white rounded-lg">
  601. {#if file.upload_status}
  602. <svg
  603. xmlns="http://www.w3.org/2000/svg"
  604. viewBox="0 0 24 24"
  605. fill="currentColor"
  606. class="w-6 h-6"
  607. >
  608. <path
  609. fill-rule="evenodd"
  610. d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
  611. clip-rule="evenodd"
  612. />
  613. <path
  614. d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
  615. />
  616. </svg>
  617. {:else}
  618. <svg
  619. class=" w-6 h-6 translate-y-[0.5px]"
  620. fill="currentColor"
  621. viewBox="0 0 24 24"
  622. xmlns="http://www.w3.org/2000/svg"
  623. ><style>
  624. .spinner_qM83 {
  625. animation: spinner_8HQG 1.05s infinite;
  626. }
  627. .spinner_oXPr {
  628. animation-delay: 0.1s;
  629. }
  630. .spinner_ZTLf {
  631. animation-delay: 0.2s;
  632. }
  633. @keyframes spinner_8HQG {
  634. 0%,
  635. 57.14% {
  636. animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
  637. transform: translate(0);
  638. }
  639. 28.57% {
  640. animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
  641. transform: translateY(-6px);
  642. }
  643. 100% {
  644. transform: translate(0);
  645. }
  646. }
  647. </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
  648. class="spinner_qM83 spinner_oXPr"
  649. cx="12"
  650. cy="12"
  651. r="2.5"
  652. /><circle
  653. class="spinner_qM83 spinner_ZTLf"
  654. cx="20"
  655. cy="12"
  656. r="2.5"
  657. /></svg
  658. >
  659. {/if}
  660. </div>
  661. <div class="flex flex-col justify-center -space-y-0.5">
  662. <div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
  663. {file.name}
  664. </div>
  665. <div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
  666. </div>
  667. </div>
  668. {:else if file.type === 'collection'}
  669. <div
  670. class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
  671. >
  672. <div class="p-2.5 bg-red-400 text-white rounded-lg">
  673. <svg
  674. xmlns="http://www.w3.org/2000/svg"
  675. viewBox="0 0 24 24"
  676. fill="currentColor"
  677. class="w-6 h-6"
  678. >
  679. <path
  680. d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
  681. />
  682. <path
  683. d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
  684. />
  685. </svg>
  686. </div>
  687. <div class="flex flex-col justify-center -space-y-0.5">
  688. <div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
  689. {file?.title ?? `#${file.name}`}
  690. </div>
  691. <div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
  692. </div>
  693. </div>
  694. {/if}
  695. <div class=" absolute -top-1 -right-1">
  696. <button
  697. class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
  698. type="button"
  699. on:click={() => {
  700. files.splice(fileIdx, 1);
  701. files = files;
  702. }}
  703. >
  704. <svg
  705. xmlns="http://www.w3.org/2000/svg"
  706. viewBox="0 0 20 20"
  707. fill="currentColor"
  708. class="w-4 h-4"
  709. >
  710. <path
  711. 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"
  712. />
  713. </svg>
  714. </button>
  715. </div>
  716. </div>
  717. {/each}
  718. </div>
  719. {/if}
  720. <div class=" flex">
  721. {#if fileUploadEnabled}
  722. <div class=" self-end mb-2 ml-1">
  723. <Tooltip content={$i18n.t('Upload files')}>
  724. <button
  725. class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
  726. type="button"
  727. on:click={() => {
  728. filesInputElement.click();
  729. }}
  730. >
  731. <svg
  732. xmlns="http://www.w3.org/2000/svg"
  733. viewBox="0 0 16 16"
  734. fill="currentColor"
  735. class="w-[1.2rem] h-[1.2rem]"
  736. >
  737. <path
  738. d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
  739. />
  740. </svg>
  741. </button>
  742. </Tooltip>
  743. </div>
  744. {/if}
  745. <textarea
  746. id="chat-textarea"
  747. bind:this={chatTextAreaElement}
  748. class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
  749. ? ''
  750. : ' pl-4'} rounded-xl resize-none h-[48px]"
  751. placeholder={chatInputPlaceholder !== ''
  752. ? chatInputPlaceholder
  753. : isRecording
  754. ? $i18n.t('Listening...')
  755. : $i18n.t('Send a Message')}
  756. bind:value={prompt}
  757. on:keypress={(e) => {
  758. if (
  759. !$mobile ||
  760. !(
  761. 'ontouchstart' in window ||
  762. navigator.maxTouchPoints > 0 ||
  763. navigator.msMaxTouchPoints > 0
  764. )
  765. ) {
  766. if (e.keyCode == 13 && !e.shiftKey) {
  767. e.preventDefault();
  768. }
  769. if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
  770. submitPrompt(prompt, user);
  771. }
  772. }
  773. }}
  774. on:keydown={async (e) => {
  775. const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
  776. // Check if Ctrl + R is pressed
  777. if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
  778. e.preventDefault();
  779. console.log('regenerate');
  780. const regenerateButton = [
  781. ...document.getElementsByClassName('regenerate-response-button')
  782. ]?.at(-1);
  783. regenerateButton?.click();
  784. }
  785. if (prompt === '' && e.key == 'ArrowUp') {
  786. e.preventDefault();
  787. const userMessageElement = [
  788. ...document.getElementsByClassName('user-message')
  789. ]?.at(-1);
  790. const editButton = [
  791. ...document.getElementsByClassName('edit-user-message-button')
  792. ]?.at(-1);
  793. console.log(userMessageElement);
  794. userMessageElement.scrollIntoView({ block: 'center' });
  795. editButton?.click();
  796. }
  797. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
  798. e.preventDefault();
  799. (promptsElement || documentsElement || modelsElement).selectUp();
  800. const commandOptionButton = [
  801. ...document.getElementsByClassName('selected-command-option-button')
  802. ]?.at(-1);
  803. commandOptionButton.scrollIntoView({ block: 'center' });
  804. }
  805. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
  806. e.preventDefault();
  807. (promptsElement || documentsElement || modelsElement).selectDown();
  808. const commandOptionButton = [
  809. ...document.getElementsByClassName('selected-command-option-button')
  810. ]?.at(-1);
  811. commandOptionButton.scrollIntoView({ block: 'center' });
  812. }
  813. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
  814. e.preventDefault();
  815. const commandOptionButton = [
  816. ...document.getElementsByClassName('selected-command-option-button')
  817. ]?.at(-1);
  818. if (commandOptionButton) {
  819. commandOptionButton?.click();
  820. } else {
  821. document.getElementById('send-message-button')?.click();
  822. }
  823. }
  824. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
  825. e.preventDefault();
  826. const commandOptionButton = [
  827. ...document.getElementsByClassName('selected-command-option-button')
  828. ]?.at(-1);
  829. commandOptionButton?.click();
  830. } else if (e.key === 'Tab') {
  831. const words = findWordIndices(prompt);
  832. if (words.length > 0) {
  833. const word = words.at(0);
  834. const fullPrompt = prompt;
  835. prompt = prompt.substring(0, word?.endIndex + 1);
  836. await tick();
  837. e.target.scrollTop = e.target.scrollHeight;
  838. prompt = fullPrompt;
  839. await tick();
  840. e.preventDefault();
  841. e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
  842. }
  843. e.target.style.height = '';
  844. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  845. }
  846. if (e.key === 'Escape') {
  847. console.log('Escape');
  848. atSelectedModel = undefined;
  849. }
  850. }}
  851. rows="1"
  852. on:input={(e) => {
  853. e.target.style.height = '';
  854. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  855. user = null;
  856. }}
  857. on:focus={(e) => {
  858. e.target.style.height = '';
  859. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  860. }}
  861. on:paste={(e) => {
  862. const clipboardData = e.clipboardData || window.clipboardData;
  863. if (clipboardData && clipboardData.items) {
  864. for (const item of clipboardData.items) {
  865. if (item.type.indexOf('image') !== -1) {
  866. const blob = item.getAsFile();
  867. const reader = new FileReader();
  868. reader.onload = function (e) {
  869. files = [
  870. ...files,
  871. {
  872. type: 'image',
  873. url: `${e.target.result}`
  874. }
  875. ];
  876. };
  877. reader.readAsDataURL(blob);
  878. }
  879. }
  880. }
  881. }}
  882. />
  883. <div class="self-end mb-2 flex space-x-1 mr-1">
  884. {#if messages.length == 0 || messages.at(-1).done == true}
  885. <Tooltip content={$i18n.t('Record voice')}>
  886. {#if speechRecognitionEnabled}
  887. <button
  888. id="voice-input-button"
  889. class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
  890. type="button"
  891. on:click={() => {
  892. speechRecognitionHandler();
  893. }}
  894. >
  895. {#if isRecording}
  896. <svg
  897. class=" w-5 h-5 translate-y-[0.5px]"
  898. fill="currentColor"
  899. viewBox="0 0 24 24"
  900. xmlns="http://www.w3.org/2000/svg"
  901. ><style>
  902. .spinner_qM83 {
  903. animation: spinner_8HQG 1.05s infinite;
  904. }
  905. .spinner_oXPr {
  906. animation-delay: 0.1s;
  907. }
  908. .spinner_ZTLf {
  909. animation-delay: 0.2s;
  910. }
  911. @keyframes spinner_8HQG {
  912. 0%,
  913. 57.14% {
  914. animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
  915. transform: translate(0);
  916. }
  917. 28.57% {
  918. animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
  919. transform: translateY(-6px);
  920. }
  921. 100% {
  922. transform: translate(0);
  923. }
  924. }
  925. </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
  926. class="spinner_qM83 spinner_oXPr"
  927. cx="12"
  928. cy="12"
  929. r="2.5"
  930. /><circle
  931. class="spinner_qM83 spinner_ZTLf"
  932. cx="20"
  933. cy="12"
  934. r="2.5"
  935. /></svg
  936. >
  937. {:else}
  938. <svg
  939. xmlns="http://www.w3.org/2000/svg"
  940. viewBox="0 0 20 20"
  941. fill="currentColor"
  942. class="w-5 h-5 translate-y-[0.5px]"
  943. >
  944. <path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
  945. <path
  946. d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
  947. />
  948. </svg>
  949. {/if}
  950. </button>
  951. {/if}
  952. </Tooltip>
  953. <Tooltip content={$i18n.t('Send message')}>
  954. <button
  955. id="send-message-button"
  956. class="{prompt !== ''
  957. ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
  958. : 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
  959. type="submit"
  960. disabled={prompt === ''}
  961. >
  962. <svg
  963. xmlns="http://www.w3.org/2000/svg"
  964. viewBox="0 0 16 16"
  965. fill="currentColor"
  966. class="w-5 h-5"
  967. >
  968. <path
  969. fill-rule="evenodd"
  970. d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
  971. clip-rule="evenodd"
  972. />
  973. </svg>
  974. </button>
  975. </Tooltip>
  976. {:else}
  977. <button
  978. class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
  979. on:click={stopResponse}
  980. >
  981. <svg
  982. xmlns="http://www.w3.org/2000/svg"
  983. viewBox="0 0 24 24"
  984. fill="currentColor"
  985. class="w-5 h-5"
  986. >
  987. <path
  988. fill-rule="evenodd"
  989. d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
  990. clip-rule="evenodd"
  991. />
  992. </svg>
  993. </button>
  994. {/if}
  995. </div>
  996. </div>
  997. </form>
  998. <div class="mt-1.5 text-xs text-gray-500 text-center">
  999. {$i18n.t('LLMs can make mistakes. Verify important information.')}
  1000. </div>
  1001. </div>
  1002. </div>
  1003. </div>
  1004. </div>
  1005. </div>
  1006. <style>
  1007. .scrollbar-hidden:active::-webkit-scrollbar-thumb,
  1008. .scrollbar-hidden:focus::-webkit-scrollbar-thumb,
  1009. .scrollbar-hidden:hover::-webkit-scrollbar-thumb {
  1010. visibility: visible;
  1011. }
  1012. .scrollbar-hidden::-webkit-scrollbar-thumb {
  1013. visibility: hidden;
  1014. }
  1015. </style>