MessageInput.svelte 33 KB

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