MessageInput.svelte 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { onMount, tick, getContext } from 'svelte';
  4. import { settings, showSidebar } from '$lib/stores';
  5. import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
  6. import Prompts from './MessageInput/PromptCommands.svelte';
  7. import Suggestions from './MessageInput/Suggestions.svelte';
  8. import {
  9. uploadDocToVectorDB,
  10. uploadWebToVectorDB,
  11. uploadYoutubeTranscriptionToVectorDB
  12. } from '$lib/apis/rag';
  13. import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
  14. import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
  15. import Documents from './MessageInput/Documents.svelte';
  16. import Models from './MessageInput/Models.svelte';
  17. import { transcribeAudio } from '$lib/apis/audio';
  18. import Tooltip from '../common/Tooltip.svelte';
  19. import Page from '../../../routes/(app)/+page.svelte';
  20. const i18n = getContext('i18n');
  21. export let submitPrompt: Function;
  22. export let stopResponse: Function;
  23. export let autoScroll = true;
  24. let chatTextAreaElement: HTMLTextAreaElement;
  25. let filesInputElement;
  26. let promptsElement;
  27. let documentsElement;
  28. let modelsElement;
  29. let inputFiles;
  30. let dragged = false;
  31. let user = null;
  32. let chatInputPlaceholder = '';
  33. export let files = [];
  34. export let fileUploadEnabled = true;
  35. export let speechRecognitionEnabled = true;
  36. export let prompt = '';
  37. export let messages = [];
  38. let speechRecognition;
  39. $: if (prompt) {
  40. if (chatTextAreaElement) {
  41. chatTextAreaElement.style.height = '';
  42. chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
  43. }
  44. }
  45. let mediaRecorder;
  46. let audioChunks = [];
  47. let isRecording = false;
  48. const MIN_DECIBELS = -45;
  49. const scrollToBottom = () => {
  50. const element = document.getElementById('messages-container');
  51. element.scrollTop = element.scrollHeight;
  52. };
  53. const startRecording = async () => {
  54. const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  55. mediaRecorder = new MediaRecorder(stream);
  56. mediaRecorder.onstart = () => {
  57. isRecording = true;
  58. console.log('Recording started');
  59. };
  60. mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
  61. mediaRecorder.onstop = async () => {
  62. isRecording = false;
  63. console.log('Recording stopped');
  64. // Create a blob from the audio chunks
  65. const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
  66. const file = blobToFile(audioBlob, 'recording.wav');
  67. const res = await transcribeAudio(localStorage.token, file).catch((error) => {
  68. toast.error(error);
  69. return null;
  70. });
  71. if (res) {
  72. prompt = res.text;
  73. await tick();
  74. chatTextAreaElement?.focus();
  75. if (prompt !== '' && $settings?.speechAutoSend === true) {
  76. submitPrompt(prompt, user);
  77. }
  78. }
  79. // saveRecording(audioBlob);
  80. audioChunks = [];
  81. };
  82. // Start recording
  83. mediaRecorder.start();
  84. // Monitor silence
  85. monitorSilence(stream);
  86. };
  87. const monitorSilence = (stream) => {
  88. const audioContext = new AudioContext();
  89. const audioStreamSource = audioContext.createMediaStreamSource(stream);
  90. const analyser = audioContext.createAnalyser();
  91. analyser.minDecibels = MIN_DECIBELS;
  92. audioStreamSource.connect(analyser);
  93. const bufferLength = analyser.frequencyBinCount;
  94. const domainData = new Uint8Array(bufferLength);
  95. let lastSoundTime = Date.now();
  96. const detectSound = () => {
  97. analyser.getByteFrequencyData(domainData);
  98. if (domainData.some((value) => value > 0)) {
  99. lastSoundTime = Date.now();
  100. }
  101. if (isRecording && Date.now() - lastSoundTime > 3000) {
  102. mediaRecorder.stop();
  103. audioContext.close();
  104. return;
  105. }
  106. window.requestAnimationFrame(detectSound);
  107. };
  108. window.requestAnimationFrame(detectSound);
  109. };
  110. const saveRecording = (blob) => {
  111. const url = URL.createObjectURL(blob);
  112. const a = document.createElement('a');
  113. document.body.appendChild(a);
  114. a.style = 'display: none';
  115. a.href = url;
  116. a.download = 'recording.wav';
  117. a.click();
  118. window.URL.revokeObjectURL(url);
  119. };
  120. const speechRecognitionHandler = () => {
  121. // Check if SpeechRecognition is supported
  122. if (isRecording) {
  123. if (speechRecognition) {
  124. speechRecognition.stop();
  125. }
  126. if (mediaRecorder) {
  127. mediaRecorder.stop();
  128. }
  129. } else {
  130. isRecording = true;
  131. if ($settings?.audio?.STTEngine ?? '' !== '') {
  132. startRecording();
  133. } else {
  134. if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
  135. // Create a SpeechRecognition object
  136. speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
  137. // Set continuous to true for continuous recognition
  138. speechRecognition.continuous = true;
  139. // Set the timeout for turning off the recognition after inactivity (in milliseconds)
  140. const inactivityTimeout = 3000; // 3 seconds
  141. let timeoutId;
  142. // Start recognition
  143. speechRecognition.start();
  144. // Event triggered when speech is recognized
  145. speechRecognition.onresult = async (event) => {
  146. // Clear the inactivity timeout
  147. clearTimeout(timeoutId);
  148. // Handle recognized speech
  149. console.log(event);
  150. const transcript = event.results[Object.keys(event.results).length - 1][0].transcript;
  151. prompt = `${prompt}${transcript}`;
  152. await tick();
  153. chatTextAreaElement?.focus();
  154. // Restart the inactivity timeout
  155. timeoutId = setTimeout(() => {
  156. console.log('Speech recognition turned off due to inactivity.');
  157. speechRecognition.stop();
  158. }, inactivityTimeout);
  159. };
  160. // Event triggered when recognition is ended
  161. speechRecognition.onend = function () {
  162. // Restart recognition after it ends
  163. console.log('recognition ended');
  164. isRecording = false;
  165. if (prompt !== '' && $settings?.speechAutoSend === true) {
  166. submitPrompt(prompt, user);
  167. }
  168. };
  169. // Event triggered when an error occurs
  170. speechRecognition.onerror = function (event) {
  171. console.log(event);
  172. toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
  173. isRecording = false;
  174. };
  175. } else {
  176. toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
  177. }
  178. }
  179. }
  180. };
  181. const uploadDoc = async (file) => {
  182. console.log(file);
  183. const doc = {
  184. type: 'doc',
  185. name: file.name,
  186. collection_name: '',
  187. upload_status: false,
  188. error: ''
  189. };
  190. try {
  191. files = [...files, doc];
  192. if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
  193. const res = await transcribeAudio(localStorage.token, file).catch((error) => {
  194. toast.error(error);
  195. return null;
  196. });
  197. if (res) {
  198. console.log(res);
  199. const blob = new Blob([res.text], { type: 'text/plain' });
  200. file = blobToFile(blob, `${file.name}.txt`);
  201. }
  202. }
  203. const res = await uploadDocToVectorDB(localStorage.token, '', file);
  204. if (res) {
  205. doc.upload_status = true;
  206. doc.collection_name = res.collection_name;
  207. files = files;
  208. }
  209. } catch (e) {
  210. // Remove the failed doc from the files array
  211. files = files.filter((f) => f.name !== file.name);
  212. toast.error(e);
  213. }
  214. };
  215. const uploadWeb = async (url) => {
  216. console.log(url);
  217. const doc = {
  218. type: 'doc',
  219. name: url,
  220. collection_name: '',
  221. upload_status: false,
  222. url: url,
  223. error: ''
  224. };
  225. try {
  226. files = [...files, doc];
  227. const res = await uploadWebToVectorDB(localStorage.token, '', url);
  228. if (res) {
  229. doc.upload_status = true;
  230. doc.collection_name = res.collection_name;
  231. files = files;
  232. }
  233. } catch (e) {
  234. // Remove the failed doc from the files array
  235. files = files.filter((f) => f.name !== url);
  236. toast.error(e);
  237. }
  238. };
  239. const uploadYoutubeTranscription = async (url) => {
  240. console.log(url);
  241. const doc = {
  242. type: 'doc',
  243. name: url,
  244. collection_name: '',
  245. upload_status: false,
  246. url: url,
  247. error: ''
  248. };
  249. try {
  250. files = [...files, doc];
  251. const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
  252. if (res) {
  253. doc.upload_status = true;
  254. doc.collection_name = res.collection_name;
  255. files = files;
  256. }
  257. } catch (e) {
  258. // Remove the failed doc from the files array
  259. files = files.filter((f) => f.name !== url);
  260. toast.error(e);
  261. }
  262. };
  263. onMount(() => {
  264. console.log(document.getElementById('sidebar'));
  265. window.setTimeout(() => chatTextAreaElement?.focus(), 0);
  266. const dropZone = document.querySelector('body');
  267. const handleKeyDown = (event: KeyboardEvent) => {
  268. if (event.key === 'Escape') {
  269. console.log('Escape');
  270. dragged = false;
  271. }
  272. };
  273. const onDragOver = (e) => {
  274. e.preventDefault();
  275. dragged = true;
  276. };
  277. const onDragLeave = () => {
  278. dragged = false;
  279. };
  280. const onDrop = async (e) => {
  281. e.preventDefault();
  282. console.log(e);
  283. if (e.dataTransfer?.files) {
  284. const inputFiles = Array.from(e.dataTransfer?.files);
  285. if (inputFiles && inputFiles.length > 0) {
  286. inputFiles.forEach((file) => {
  287. console.log(file, file.name.split('.').at(-1));
  288. if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
  289. let reader = new FileReader();
  290. reader.onload = (event) => {
  291. files = [
  292. ...files,
  293. {
  294. type: 'image',
  295. url: `${event.target.result}`
  296. }
  297. ];
  298. };
  299. reader.readAsDataURL(file);
  300. } else if (
  301. SUPPORTED_FILE_TYPE.includes(file['type']) ||
  302. SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
  303. ) {
  304. uploadDoc(file);
  305. } else {
  306. toast.error(
  307. $i18n.t(
  308. `Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
  309. { file_type: file['type'] }
  310. )
  311. );
  312. uploadDoc(file);
  313. }
  314. });
  315. } else {
  316. toast.error($i18n.t(`File not found.`));
  317. }
  318. }
  319. dragged = false;
  320. };
  321. window.addEventListener('keydown', handleKeyDown);
  322. dropZone?.addEventListener('dragover', onDragOver);
  323. dropZone?.addEventListener('drop', onDrop);
  324. dropZone?.addEventListener('dragleave', onDragLeave);
  325. return () => {
  326. window.removeEventListener('keydown', handleKeyDown);
  327. dropZone?.removeEventListener('dragover', onDragOver);
  328. dropZone?.removeEventListener('drop', onDrop);
  329. dropZone?.removeEventListener('dragleave', onDragLeave);
  330. };
  331. });
  332. </script>
  333. {#if dragged}
  334. <div
  335. class="fixed lg:w-[calc(100%-260px)] w-full h-full flex z-50 touch-none pointer-events-none"
  336. id="dropzone"
  337. role="region"
  338. aria-label="Drag and Drop Container"
  339. >
  340. <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
  341. <div class="m-auto pt-64 flex flex-col justify-center">
  342. <div class="max-w-md">
  343. <AddFilesPlaceholder />
  344. </div>
  345. </div>
  346. </div>
  347. </div>
  348. {/if}
  349. <div class="fixed bottom-0 {$showSidebar ? 'left-0 lg:left-[260px]' : 'left-0'} right-0">
  350. <div class="w-full">
  351. <div class=" px-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
  352. <div class="flex flex-col max-w-5xl w-full">
  353. <div class="relative">
  354. {#if autoScroll === false && messages.length > 0}
  355. <div class=" absolute -top-12 left-0 right-0 flex justify-center">
  356. <button
  357. class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
  358. on:click={() => {
  359. autoScroll = true;
  360. scrollToBottom();
  361. }}
  362. >
  363. <svg
  364. xmlns="http://www.w3.org/2000/svg"
  365. viewBox="0 0 20 20"
  366. fill="currentColor"
  367. class="w-5 h-5"
  368. >
  369. <path
  370. fill-rule="evenodd"
  371. 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"
  372. clip-rule="evenodd"
  373. />
  374. </svg>
  375. </button>
  376. </div>
  377. {/if}
  378. </div>
  379. <div class="w-full relative">
  380. {#if prompt.charAt(0) === '/'}
  381. <Prompts bind:this={promptsElement} bind:prompt />
  382. {:else if prompt.charAt(0) === '#'}
  383. <Documents
  384. bind:this={documentsElement}
  385. bind:prompt
  386. on:youtube={(e) => {
  387. console.log(e);
  388. uploadYoutubeTranscription(e.detail);
  389. }}
  390. on:url={(e) => {
  391. console.log(e);
  392. uploadWeb(e.detail);
  393. }}
  394. on:select={(e) => {
  395. console.log(e);
  396. files = [
  397. ...files,
  398. {
  399. type: e?.detail?.type ?? 'doc',
  400. ...e.detail,
  401. upload_status: true
  402. }
  403. ];
  404. }}
  405. />
  406. {/if}
  407. <Models
  408. bind:this={modelsElement}
  409. bind:prompt
  410. bind:user
  411. bind:chatInputPlaceholder
  412. {messages}
  413. />
  414. <!-- {#if messages.length == 0 && suggestionPrompts.length !== 0}
  415. <Suggestions {suggestionPrompts} {submitPrompt} />
  416. {/if} -->
  417. </div>
  418. </div>
  419. </div>
  420. <div class="bg-white dark:bg-gray-900">
  421. <div class="max-w-6xl px-2.5 lg:px-16 mx-auto inset-x-0">
  422. <div class=" pb-2">
  423. <input
  424. bind:this={filesInputElement}
  425. bind:files={inputFiles}
  426. type="file"
  427. hidden
  428. multiple
  429. on:change={async () => {
  430. if (inputFiles && inputFiles.length > 0) {
  431. const _inputFiles = Array.from(inputFiles);
  432. _inputFiles.forEach((file) => {
  433. if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
  434. let reader = new FileReader();
  435. reader.onload = (event) => {
  436. files = [
  437. ...files,
  438. {
  439. type: 'image',
  440. url: `${event.target.result}`
  441. }
  442. ];
  443. inputFiles = null;
  444. filesInputElement.value = '';
  445. };
  446. reader.readAsDataURL(file);
  447. } else if (
  448. SUPPORTED_FILE_TYPE.includes(file['type']) ||
  449. SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
  450. ) {
  451. uploadDoc(file);
  452. filesInputElement.value = '';
  453. } else {
  454. toast.error(
  455. $i18n.t(
  456. `Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
  457. { file_type: file['type'] }
  458. )
  459. );
  460. uploadDoc(file);
  461. filesInputElement.value = '';
  462. }
  463. });
  464. } else {
  465. toast.error($i18n.t(`File not found.`));
  466. }
  467. }}
  468. />
  469. <form
  470. class=" flex flex-col relative w-full rounded-3xl px-1.5 border border-gray-100 dark:border-gray-850 bg-white dark:bg-gray-900 dark:text-gray-100"
  471. on:submit|preventDefault={() => {
  472. submitPrompt(prompt, user);
  473. }}
  474. >
  475. {#if files.length > 0}
  476. <div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
  477. {#each files as file, fileIdx}
  478. <div class=" relative group">
  479. {#if file.type === 'image'}
  480. <img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" />
  481. {:else if file.type === 'doc'}
  482. <div
  483. 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"
  484. >
  485. <div class="p-2.5 bg-red-400 text-white rounded-lg">
  486. {#if file.upload_status}
  487. <svg
  488. xmlns="http://www.w3.org/2000/svg"
  489. viewBox="0 0 24 24"
  490. fill="currentColor"
  491. class="w-6 h-6"
  492. >
  493. <path
  494. fill-rule="evenodd"
  495. 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"
  496. clip-rule="evenodd"
  497. />
  498. <path
  499. 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"
  500. />
  501. </svg>
  502. {:else}
  503. <svg
  504. class=" w-6 h-6 translate-y-[0.5px]"
  505. fill="currentColor"
  506. viewBox="0 0 24 24"
  507. xmlns="http://www.w3.org/2000/svg"
  508. ><style>
  509. .spinner_qM83 {
  510. animation: spinner_8HQG 1.05s infinite;
  511. }
  512. .spinner_oXPr {
  513. animation-delay: 0.1s;
  514. }
  515. .spinner_ZTLf {
  516. animation-delay: 0.2s;
  517. }
  518. @keyframes spinner_8HQG {
  519. 0%,
  520. 57.14% {
  521. animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
  522. transform: translate(0);
  523. }
  524. 28.57% {
  525. animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
  526. transform: translateY(-6px);
  527. }
  528. 100% {
  529. transform: translate(0);
  530. }
  531. }
  532. </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
  533. class="spinner_qM83 spinner_oXPr"
  534. cx="12"
  535. cy="12"
  536. r="2.5"
  537. /><circle
  538. class="spinner_qM83 spinner_ZTLf"
  539. cx="20"
  540. cy="12"
  541. r="2.5"
  542. /></svg
  543. >
  544. {/if}
  545. </div>
  546. <div class="flex flex-col justify-center -space-y-0.5">
  547. <div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
  548. {file.name}
  549. </div>
  550. <div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
  551. </div>
  552. </div>
  553. {:else if file.type === 'collection'}
  554. <div
  555. 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"
  556. >
  557. <div class="p-2.5 bg-red-400 text-white rounded-lg">
  558. <svg
  559. xmlns="http://www.w3.org/2000/svg"
  560. viewBox="0 0 24 24"
  561. fill="currentColor"
  562. class="w-6 h-6"
  563. >
  564. <path
  565. 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"
  566. />
  567. <path
  568. 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"
  569. />
  570. </svg>
  571. </div>
  572. <div class="flex flex-col justify-center -space-y-0.5">
  573. <div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
  574. {file?.title ?? `#${file.name}`}
  575. </div>
  576. <div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
  577. </div>
  578. </div>
  579. {/if}
  580. <div class=" absolute -top-1 -right-1">
  581. <button
  582. class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
  583. type="button"
  584. on:click={() => {
  585. files.splice(fileIdx, 1);
  586. files = files;
  587. }}
  588. >
  589. <svg
  590. xmlns="http://www.w3.org/2000/svg"
  591. viewBox="0 0 20 20"
  592. fill="currentColor"
  593. class="w-4 h-4"
  594. >
  595. <path
  596. 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"
  597. />
  598. </svg>
  599. </button>
  600. </div>
  601. </div>
  602. {/each}
  603. </div>
  604. {/if}
  605. <div class=" flex">
  606. {#if fileUploadEnabled}
  607. <div class=" self-end mb-2 ml-1">
  608. <Tooltip content={$i18n.t('Upload files')}>
  609. <button
  610. 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"
  611. type="button"
  612. on:click={() => {
  613. filesInputElement.click();
  614. }}
  615. >
  616. <svg
  617. xmlns="http://www.w3.org/2000/svg"
  618. viewBox="0 0 16 16"
  619. fill="currentColor"
  620. class="w-[1.2rem] h-[1.2rem]"
  621. >
  622. <path
  623. 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"
  624. />
  625. </svg>
  626. </button>
  627. </Tooltip>
  628. </div>
  629. {/if}
  630. <textarea
  631. id="chat-textarea"
  632. bind:this={chatTextAreaElement}
  633. class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
  634. ? ''
  635. : ' pl-4'} rounded-xl resize-none h-[48px]"
  636. placeholder={chatInputPlaceholder !== ''
  637. ? chatInputPlaceholder
  638. : isRecording
  639. ? $i18n.t('Listening...')
  640. : $i18n.t('Send a Message')}
  641. bind:value={prompt}
  642. on:keypress={(e) => {
  643. if (window.innerWidth > 1024) {
  644. if (e.keyCode == 13 && !e.shiftKey) {
  645. e.preventDefault();
  646. }
  647. if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
  648. submitPrompt(prompt, user);
  649. }
  650. }
  651. }}
  652. on:keydown={async (e) => {
  653. const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
  654. // Check if Ctrl + R is pressed
  655. if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
  656. e.preventDefault();
  657. console.log('regenerate');
  658. const regenerateButton = [
  659. ...document.getElementsByClassName('regenerate-response-button')
  660. ]?.at(-1);
  661. regenerateButton?.click();
  662. }
  663. if (prompt === '' && e.key == 'ArrowUp') {
  664. e.preventDefault();
  665. const userMessageElement = [
  666. ...document.getElementsByClassName('user-message')
  667. ]?.at(-1);
  668. const editButton = [
  669. ...document.getElementsByClassName('edit-user-message-button')
  670. ]?.at(-1);
  671. console.log(userMessageElement);
  672. userMessageElement.scrollIntoView({ block: 'center' });
  673. editButton?.click();
  674. }
  675. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
  676. e.preventDefault();
  677. (promptsElement || documentsElement || modelsElement).selectUp();
  678. const commandOptionButton = [
  679. ...document.getElementsByClassName('selected-command-option-button')
  680. ]?.at(-1);
  681. commandOptionButton.scrollIntoView({ block: 'center' });
  682. }
  683. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
  684. e.preventDefault();
  685. (promptsElement || documentsElement || modelsElement).selectDown();
  686. const commandOptionButton = [
  687. ...document.getElementsByClassName('selected-command-option-button')
  688. ]?.at(-1);
  689. commandOptionButton.scrollIntoView({ block: 'center' });
  690. }
  691. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
  692. e.preventDefault();
  693. const commandOptionButton = [
  694. ...document.getElementsByClassName('selected-command-option-button')
  695. ]?.at(-1);
  696. if (commandOptionButton) {
  697. commandOptionButton?.click();
  698. } else {
  699. document.getElementById('send-message-button')?.click();
  700. }
  701. }
  702. if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
  703. e.preventDefault();
  704. const commandOptionButton = [
  705. ...document.getElementsByClassName('selected-command-option-button')
  706. ]?.at(-1);
  707. commandOptionButton?.click();
  708. } else if (e.key === 'Tab') {
  709. const words = findWordIndices(prompt);
  710. if (words.length > 0) {
  711. const word = words.at(0);
  712. const fullPrompt = prompt;
  713. prompt = prompt.substring(0, word?.endIndex + 1);
  714. await tick();
  715. e.target.scrollTop = e.target.scrollHeight;
  716. prompt = fullPrompt;
  717. await tick();
  718. e.preventDefault();
  719. e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
  720. }
  721. e.target.style.height = '';
  722. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  723. }
  724. }}
  725. rows="1"
  726. on:input={(e) => {
  727. e.target.style.height = '';
  728. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  729. user = null;
  730. }}
  731. on:focus={(e) => {
  732. e.target.style.height = '';
  733. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  734. }}
  735. on:paste={(e) => {
  736. const clipboardData = e.clipboardData || window.clipboardData;
  737. if (clipboardData && clipboardData.items) {
  738. for (const item of clipboardData.items) {
  739. if (item.type.indexOf('image') !== -1) {
  740. const blob = item.getAsFile();
  741. const reader = new FileReader();
  742. reader.onload = function (e) {
  743. files = [
  744. ...files,
  745. {
  746. type: 'image',
  747. url: `${e.target.result}`
  748. }
  749. ];
  750. };
  751. reader.readAsDataURL(blob);
  752. }
  753. }
  754. }
  755. }}
  756. />
  757. <div class="self-end mb-2 flex space-x-1 mr-1">
  758. {#if messages.length == 0 || messages.at(-1).done == true}
  759. <Tooltip content={$i18n.t('Record voice')}>
  760. {#if speechRecognitionEnabled}
  761. <button
  762. id="voice-input-button"
  763. 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"
  764. type="button"
  765. on:click={() => {
  766. speechRecognitionHandler();
  767. }}
  768. >
  769. {#if isRecording}
  770. <svg
  771. class=" w-5 h-5 translate-y-[0.5px]"
  772. fill="currentColor"
  773. viewBox="0 0 24 24"
  774. xmlns="http://www.w3.org/2000/svg"
  775. ><style>
  776. .spinner_qM83 {
  777. animation: spinner_8HQG 1.05s infinite;
  778. }
  779. .spinner_oXPr {
  780. animation-delay: 0.1s;
  781. }
  782. .spinner_ZTLf {
  783. animation-delay: 0.2s;
  784. }
  785. @keyframes spinner_8HQG {
  786. 0%,
  787. 57.14% {
  788. animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
  789. transform: translate(0);
  790. }
  791. 28.57% {
  792. animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
  793. transform: translateY(-6px);
  794. }
  795. 100% {
  796. transform: translate(0);
  797. }
  798. }
  799. </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
  800. class="spinner_qM83 spinner_oXPr"
  801. cx="12"
  802. cy="12"
  803. r="2.5"
  804. /><circle
  805. class="spinner_qM83 spinner_ZTLf"
  806. cx="20"
  807. cy="12"
  808. r="2.5"
  809. /></svg
  810. >
  811. {:else}
  812. <svg
  813. xmlns="http://www.w3.org/2000/svg"
  814. viewBox="0 0 20 20"
  815. fill="currentColor"
  816. class="w-5 h-5 translate-y-[0.5px]"
  817. >
  818. <path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
  819. <path
  820. 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"
  821. />
  822. </svg>
  823. {/if}
  824. </button>
  825. {/if}
  826. </Tooltip>
  827. <Tooltip content={$i18n.t('Send message')}>
  828. <button
  829. id="send-message-button"
  830. class="{prompt !== ''
  831. ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
  832. : 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
  833. type="submit"
  834. disabled={prompt === ''}
  835. >
  836. <svg
  837. xmlns="http://www.w3.org/2000/svg"
  838. viewBox="0 0 16 16"
  839. fill="currentColor"
  840. class="w-5 h-5"
  841. >
  842. <path
  843. fill-rule="evenodd"
  844. 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"
  845. clip-rule="evenodd"
  846. />
  847. </svg>
  848. </button>
  849. </Tooltip>
  850. {:else}
  851. <button
  852. 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"
  853. on:click={stopResponse}
  854. >
  855. <svg
  856. xmlns="http://www.w3.org/2000/svg"
  857. viewBox="0 0 24 24"
  858. fill="currentColor"
  859. class="w-5 h-5"
  860. >
  861. <path
  862. fill-rule="evenodd"
  863. 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"
  864. clip-rule="evenodd"
  865. />
  866. </svg>
  867. </button>
  868. {/if}
  869. </div>
  870. </div>
  871. </form>
  872. <div class="mt-1.5 text-xs text-gray-500 text-center">
  873. {$i18n.t('LLMs can make mistakes. Verify important information.')}
  874. </div>
  875. </div>
  876. </div>
  877. </div>
  878. </div>
  879. </div>