MessageInput.svelte 34 KB

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