MessageInput.svelte 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
  5. import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
  6. import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
  7. const dispatch = createEventDispatcher();
  8. import {
  9. type Model,
  10. mobile,
  11. settings,
  12. showSidebar,
  13. models,
  14. config,
  15. showCallOverlay,
  16. tools,
  17. user as _user,
  18. showControls,
  19. TTSWorker
  20. } from '$lib/stores';
  21. import {
  22. blobToFile,
  23. compressImage,
  24. createMessagesList,
  25. extractCurlyBraceWords
  26. } from '$lib/utils';
  27. import { transcribeAudio } from '$lib/apis/audio';
  28. import { uploadFile } from '$lib/apis/files';
  29. import { generateAutoCompletion } from '$lib/apis';
  30. import { deleteFileById } from '$lib/apis/files';
  31. import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
  32. import InputMenu from './MessageInput/InputMenu.svelte';
  33. import VoiceRecording from './MessageInput/VoiceRecording.svelte';
  34. import FilesOverlay from './MessageInput/FilesOverlay.svelte';
  35. import Commands from './MessageInput/Commands.svelte';
  36. import ToolServersModal from './ToolServersModal.svelte';
  37. import RichTextInput from '../common/RichTextInput.svelte';
  38. import Tooltip from '../common/Tooltip.svelte';
  39. import FileItem from '../common/FileItem.svelte';
  40. import Image from '../common/Image.svelte';
  41. import XMark from '../icons/XMark.svelte';
  42. import Headphone from '../icons/Headphone.svelte';
  43. import GlobeAlt from '../icons/GlobeAlt.svelte';
  44. import Photo from '../icons/Photo.svelte';
  45. import Wrench from '../icons/Wrench.svelte';
  46. import CommandLine from '../icons/CommandLine.svelte';
  47. import Sparkles from '../icons/Sparkles.svelte';
  48. import { KokoroWorker } from '$lib/workers/KokoroWorker';
  49. const i18n = getContext('i18n');
  50. export let transparentBackground = false;
  51. export let onChange: Function = () => {};
  52. export let createMessagePair: Function;
  53. export let stopResponse: Function;
  54. export let autoScroll = false;
  55. export let atSelectedModel: Model | undefined = undefined;
  56. export let selectedModels: [''];
  57. let selectedModelIds = [];
  58. $: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
  59. export let history;
  60. export let taskIds = null;
  61. export let prompt = '';
  62. export let files = [];
  63. export let toolServers = [];
  64. export let selectedToolIds = [];
  65. export let selectedFilterIds = [];
  66. export let imageGenerationEnabled = false;
  67. export let webSearchEnabled = false;
  68. export let codeInterpreterEnabled = false;
  69. $: onChange({
  70. prompt,
  71. files: files.filter((file) => file.type !== 'image'),
  72. selectedToolIds,
  73. selectedFilterIds,
  74. imageGenerationEnabled,
  75. webSearchEnabled,
  76. codeInterpreterEnabled
  77. });
  78. let showTools = false;
  79. let loaded = false;
  80. let recording = false;
  81. let isComposing = false;
  82. let chatInputContainerElement;
  83. let chatInputElement;
  84. let filesInputElement;
  85. let commandsElement;
  86. let inputFiles;
  87. let dragged = false;
  88. let user = null;
  89. export let placeholder = '';
  90. let visionCapableModels = [];
  91. $: visionCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
  92. (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
  93. );
  94. let fileUploadCapableModels = [];
  95. $: fileUploadCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
  96. (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.file_upload ?? true
  97. );
  98. let webSearchCapableModels = [];
  99. $: webSearchCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
  100. (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.web_search ?? true
  101. );
  102. let imageGenerationCapableModels = [];
  103. $: imageGenerationCapableModels = (
  104. atSelectedModel?.id ? [atSelectedModel.id] : selectedModels
  105. ).filter(
  106. (model) =>
  107. $models.find((m) => m.id === model)?.info?.meta?.capabilities?.image_generation ?? true
  108. );
  109. let codeInterpreterCapableModels = [];
  110. $: codeInterpreterCapableModels = (
  111. atSelectedModel?.id ? [atSelectedModel.id] : selectedModels
  112. ).filter(
  113. (model) =>
  114. $models.find((m) => m.id === model)?.info?.meta?.capabilities?.code_interpreter ?? true
  115. );
  116. let toggleFilters = [];
  117. $: toggleFilters = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels)
  118. .map((id) => ($models.find((model) => model.id === id) || {})?.filters ?? [])
  119. .reduce((acc, filters) => acc.filter((f1) => filters.some((f2) => f2.id === f1.id)));
  120. const scrollToBottom = () => {
  121. const element = document.getElementById('messages-container');
  122. element.scrollTo({
  123. top: element.scrollHeight,
  124. behavior: 'smooth'
  125. });
  126. };
  127. const screenCaptureHandler = async () => {
  128. try {
  129. // Request screen media
  130. const mediaStream = await navigator.mediaDevices.getDisplayMedia({
  131. video: { cursor: 'never' },
  132. audio: false
  133. });
  134. // Once the user selects a screen, temporarily create a video element
  135. const video = document.createElement('video');
  136. video.srcObject = mediaStream;
  137. // Ensure the video loads without affecting user experience or tab switching
  138. await video.play();
  139. // Set up the canvas to match the video dimensions
  140. const canvas = document.createElement('canvas');
  141. canvas.width = video.videoWidth;
  142. canvas.height = video.videoHeight;
  143. // Grab a single frame from the video stream using the canvas
  144. const context = canvas.getContext('2d');
  145. context.drawImage(video, 0, 0, canvas.width, canvas.height);
  146. // Stop all video tracks (stop screen sharing) after capturing the image
  147. mediaStream.getTracks().forEach((track) => track.stop());
  148. // bring back focus to this current tab, so that the user can see the screen capture
  149. window.focus();
  150. // Convert the canvas to a Base64 image URL
  151. const imageUrl = canvas.toDataURL('image/png');
  152. // Add the captured image to the files array to render it
  153. files = [...files, { type: 'image', url: imageUrl }];
  154. // Clean memory: Clear video srcObject
  155. video.srcObject = null;
  156. } catch (error) {
  157. // Handle any errors (e.g., user cancels screen sharing)
  158. console.error('Error capturing screen:', error);
  159. }
  160. };
  161. const uploadFileHandler = async (file, fullContext: boolean = false) => {
  162. if ($_user?.role !== 'admin' && !($_user?.permissions?.chat?.file_upload ?? true)) {
  163. toast.error($i18n.t('You do not have permission to upload files.'));
  164. return null;
  165. }
  166. const tempItemId = uuidv4();
  167. const fileItem = {
  168. type: 'file',
  169. file: '',
  170. id: null,
  171. url: '',
  172. name: file.name,
  173. collection_name: '',
  174. status: 'uploading',
  175. size: file.size,
  176. error: '',
  177. itemId: tempItemId,
  178. ...(fullContext ? { context: 'full' } : {})
  179. };
  180. if (fileItem.size == 0) {
  181. toast.error($i18n.t('You cannot upload an empty file.'));
  182. return null;
  183. }
  184. files = [...files, fileItem];
  185. try {
  186. // During the file upload, file content is automatically extracted.
  187. const uploadedFile = await uploadFile(localStorage.token, file);
  188. if (uploadedFile) {
  189. console.log('File upload completed:', {
  190. id: uploadedFile.id,
  191. name: fileItem.name,
  192. collection: uploadedFile?.meta?.collection_name
  193. });
  194. if (uploadedFile.error) {
  195. console.warn('File upload warning:', uploadedFile.error);
  196. toast.warning(uploadedFile.error);
  197. }
  198. fileItem.status = 'uploaded';
  199. fileItem.file = uploadedFile;
  200. fileItem.id = uploadedFile.id;
  201. fileItem.collection_name =
  202. uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
  203. fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
  204. files = files;
  205. } else {
  206. files = files.filter((item) => item?.itemId !== tempItemId);
  207. }
  208. } catch (e) {
  209. toast.error(`${e}`);
  210. files = files.filter((item) => item?.itemId !== tempItemId);
  211. }
  212. };
  213. const inputFilesHandler = async (inputFiles) => {
  214. console.log('Input files handler called with:', inputFiles);
  215. inputFiles.forEach((file) => {
  216. console.log('Processing file:', {
  217. name: file.name,
  218. type: file.type,
  219. size: file.size,
  220. extension: file.name.split('.').at(-1)
  221. });
  222. if (
  223. ($config?.file?.max_size ?? null) !== null &&
  224. file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
  225. ) {
  226. console.log('File exceeds max size limit:', {
  227. fileSize: file.size,
  228. maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
  229. });
  230. toast.error(
  231. $i18n.t(`File size should not exceed {{maxSize}} MB.`, {
  232. maxSize: $config?.file?.max_size
  233. })
  234. );
  235. return;
  236. }
  237. if (
  238. ['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
  239. ) {
  240. if (visionCapableModels.length === 0) {
  241. toast.error($i18n.t('Selected model(s) do not support image inputs'));
  242. return;
  243. }
  244. let reader = new FileReader();
  245. reader.onload = async (event) => {
  246. let imageUrl = event.target.result;
  247. if ($settings?.imageCompression ?? false) {
  248. const width = $settings?.imageCompressionSize?.width ?? null;
  249. const height = $settings?.imageCompressionSize?.height ?? null;
  250. if (width || height) {
  251. imageUrl = await compressImage(imageUrl, width, height);
  252. }
  253. }
  254. files = [
  255. ...files,
  256. {
  257. type: 'image',
  258. url: `${imageUrl}`
  259. }
  260. ];
  261. };
  262. reader.readAsDataURL(file);
  263. } else {
  264. uploadFileHandler(file);
  265. }
  266. });
  267. };
  268. const handleKeyDown = (event: KeyboardEvent) => {
  269. if (event.key === 'Escape') {
  270. console.log('Escape');
  271. dragged = false;
  272. }
  273. };
  274. const onDragOver = (e) => {
  275. e.preventDefault();
  276. // Check if a file is being dragged.
  277. if (e.dataTransfer?.types?.includes('Files')) {
  278. dragged = true;
  279. } else {
  280. dragged = false;
  281. }
  282. };
  283. const onDragLeave = () => {
  284. dragged = false;
  285. };
  286. const onDrop = async (e) => {
  287. e.preventDefault();
  288. console.log(e);
  289. if (e.dataTransfer?.files) {
  290. const inputFiles = Array.from(e.dataTransfer?.files);
  291. if (inputFiles && inputFiles.length > 0) {
  292. console.log(inputFiles);
  293. inputFilesHandler(inputFiles);
  294. }
  295. }
  296. dragged = false;
  297. };
  298. onMount(async () => {
  299. loaded = true;
  300. window.setTimeout(() => {
  301. const chatInput = document.getElementById('chat-input');
  302. chatInput?.focus();
  303. }, 0);
  304. window.addEventListener('keydown', handleKeyDown);
  305. await tick();
  306. const dropzoneElement = document.getElementById('chat-container');
  307. dropzoneElement?.addEventListener('dragover', onDragOver);
  308. dropzoneElement?.addEventListener('drop', onDrop);
  309. dropzoneElement?.addEventListener('dragleave', onDragLeave);
  310. });
  311. onDestroy(() => {
  312. console.log('destroy');
  313. window.removeEventListener('keydown', handleKeyDown);
  314. const dropzoneElement = document.getElementById('chat-container');
  315. if (dropzoneElement) {
  316. dropzoneElement?.removeEventListener('dragover', onDragOver);
  317. dropzoneElement?.removeEventListener('drop', onDrop);
  318. dropzoneElement?.removeEventListener('dragleave', onDragLeave);
  319. }
  320. });
  321. </script>
  322. <FilesOverlay show={dragged} />
  323. <ToolServersModal bind:show={showTools} {selectedToolIds} />
  324. {#if loaded}
  325. <div class="w-full font-primary">
  326. <div class=" mx-auto inset-x-0 bg-transparent flex justify-center">
  327. <div
  328. class="flex flex-col px-3 {($settings?.widescreenMode ?? null)
  329. ? 'max-w-full'
  330. : 'max-w-6xl'} w-full"
  331. >
  332. <div class="relative">
  333. {#if autoScroll === false && history?.currentId}
  334. <div
  335. class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
  336. >
  337. <button
  338. class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
  339. on:click={() => {
  340. autoScroll = true;
  341. scrollToBottom();
  342. }}
  343. >
  344. <svg
  345. xmlns="http://www.w3.org/2000/svg"
  346. viewBox="0 0 20 20"
  347. fill="currentColor"
  348. class="w-5 h-5"
  349. >
  350. <path
  351. fill-rule="evenodd"
  352. 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"
  353. clip-rule="evenodd"
  354. />
  355. </svg>
  356. </button>
  357. </div>
  358. {/if}
  359. </div>
  360. <div class="w-full relative">
  361. {#if atSelectedModel !== undefined}
  362. <div
  363. class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-linear-to-t from-white dark:from-gray-900 z-10"
  364. >
  365. <div class="flex items-center justify-between w-full">
  366. <div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500">
  367. <img
  368. crossorigin="anonymous"
  369. alt="model profile"
  370. class="size-3.5 max-w-[28px] object-cover rounded-full"
  371. src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
  372. ?.profile_image_url ??
  373. ($i18n.language === 'dg-DG'
  374. ? `/doge.png`
  375. : `${WEBUI_BASE_URL}/static/favicon.png`)}
  376. />
  377. <div class="translate-y-[0.5px]">
  378. Talking to <span class=" font-medium">{atSelectedModel.name}</span>
  379. </div>
  380. </div>
  381. <div>
  382. <button
  383. class="flex items-center dark:text-gray-500"
  384. on:click={() => {
  385. atSelectedModel = undefined;
  386. }}
  387. >
  388. <XMark />
  389. </button>
  390. </div>
  391. </div>
  392. </div>
  393. {/if}
  394. <Commands
  395. bind:this={commandsElement}
  396. bind:prompt
  397. bind:files
  398. on:upload={(e) => {
  399. dispatch('upload', e.detail);
  400. }}
  401. on:select={(e) => {
  402. const data = e.detail;
  403. if (data?.type === 'model') {
  404. atSelectedModel = data.data;
  405. }
  406. const chatInputElement = document.getElementById('chat-input');
  407. chatInputElement?.focus();
  408. }}
  409. />
  410. </div>
  411. </div>
  412. </div>
  413. <div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
  414. <div
  415. class="{($settings?.widescreenMode ?? null)
  416. ? 'max-w-full'
  417. : 'max-w-6xl'} px-2.5 mx-auto inset-x-0"
  418. >
  419. <div class="">
  420. <input
  421. bind:this={filesInputElement}
  422. bind:files={inputFiles}
  423. type="file"
  424. hidden
  425. multiple
  426. on:change={async () => {
  427. if (inputFiles && inputFiles.length > 0) {
  428. const _inputFiles = Array.from(inputFiles);
  429. inputFilesHandler(_inputFiles);
  430. } else {
  431. toast.error($i18n.t(`File not found.`));
  432. }
  433. filesInputElement.value = '';
  434. }}
  435. />
  436. {#if recording}
  437. <VoiceRecording
  438. bind:recording
  439. onCancel={async () => {
  440. recording = false;
  441. await tick();
  442. document.getElementById('chat-input')?.focus();
  443. }}
  444. onConfirm={async (data) => {
  445. const { text, filename } = data;
  446. prompt = `${prompt}${text} `;
  447. recording = false;
  448. await tick();
  449. document.getElementById('chat-input')?.focus();
  450. if ($settings?.speechAutoSend ?? false) {
  451. dispatch('submit', prompt);
  452. }
  453. }}
  454. />
  455. {:else}
  456. <form
  457. class="w-full flex gap-1.5"
  458. on:submit|preventDefault={() => {
  459. // check if selectedModels support image input
  460. dispatch('submit', prompt);
  461. }}
  462. >
  463. <div
  464. class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
  465. dir={$settings?.chatDirection ?? 'auto'}
  466. >
  467. {#if files.length > 0}
  468. <div class="mx-2 mt-2.5 -mb-1 flex items-center flex-wrap gap-2">
  469. {#each files as file, fileIdx}
  470. {#if file.type === 'image'}
  471. <div class=" relative group">
  472. <div class="relative flex items-center">
  473. <Image
  474. src={file.url}
  475. alt="input"
  476. imageClassName=" size-14 rounded-xl object-cover"
  477. />
  478. {#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
  479. <Tooltip
  480. className=" absolute top-1 left-1"
  481. content={$i18n.t('{{ models }}', {
  482. models: [
  483. ...(atSelectedModel ? [atSelectedModel] : selectedModels)
  484. ]
  485. .filter((id) => !visionCapableModels.includes(id))
  486. .join(', ')
  487. })}
  488. >
  489. <svg
  490. xmlns="http://www.w3.org/2000/svg"
  491. viewBox="0 0 24 24"
  492. fill="currentColor"
  493. class="size-4 fill-yellow-300"
  494. >
  495. <path
  496. fill-rule="evenodd"
  497. 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"
  498. clip-rule="evenodd"
  499. />
  500. </svg>
  501. </Tooltip>
  502. {/if}
  503. </div>
  504. <div class=" absolute -top-1 -right-1">
  505. <button
  506. class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
  507. type="button"
  508. on:click={() => {
  509. files.splice(fileIdx, 1);
  510. files = files;
  511. }}
  512. >
  513. <svg
  514. xmlns="http://www.w3.org/2000/svg"
  515. viewBox="0 0 20 20"
  516. fill="currentColor"
  517. class="size-4"
  518. >
  519. <path
  520. 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"
  521. />
  522. </svg>
  523. </button>
  524. </div>
  525. </div>
  526. {:else}
  527. <FileItem
  528. item={file}
  529. name={file.name}
  530. type={file.type}
  531. size={file?.size}
  532. loading={file.status === 'uploading'}
  533. dismissible={true}
  534. edit={true}
  535. on:dismiss={async () => {
  536. try {
  537. if (file.type !== 'collection' && !file?.collection) {
  538. if (file.id) {
  539. // This will handle both file deletion and Chroma cleanup
  540. await deleteFileById(localStorage.token, file.id);
  541. }
  542. }
  543. } catch (error) {
  544. console.error('Error deleting file:', error);
  545. }
  546. // Remove from UI state
  547. files.splice(fileIdx, 1);
  548. files = files;
  549. }}
  550. on:click={() => {
  551. console.log(file);
  552. }}
  553. />
  554. {/if}
  555. {/each}
  556. </div>
  557. {/if}
  558. <div class="px-2.5">
  559. {#if $settings?.richTextInput ?? true}
  560. <div
  561. class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
  562. id="chat-input-container"
  563. >
  564. <RichTextInput
  565. bind:this={chatInputElement}
  566. bind:value={prompt}
  567. id="chat-input"
  568. messageInput={true}
  569. shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
  570. (!$mobile ||
  571. !(
  572. 'ontouchstart' in window ||
  573. navigator.maxTouchPoints > 0 ||
  574. navigator.msMaxTouchPoints > 0
  575. ))}
  576. placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
  577. largeTextAsFile={$settings?.largeTextAsFile ?? false}
  578. autocomplete={$config?.features?.enable_autocomplete_generation &&
  579. ($settings?.promptAutocomplete ?? false)}
  580. generateAutoCompletion={async (text) => {
  581. if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
  582. toast.error($i18n.t('Please select a model first.'));
  583. }
  584. const res = await generateAutoCompletion(
  585. localStorage.token,
  586. selectedModelIds.at(0),
  587. text,
  588. history?.currentId
  589. ? createMessagesList(history, history.currentId)
  590. : null
  591. ).catch((error) => {
  592. console.log(error);
  593. return null;
  594. });
  595. console.log(res);
  596. return res;
  597. }}
  598. oncompositionstart={() => (isComposing = true)}
  599. oncompositionend={() => (isComposing = false)}
  600. on:keydown={async (e) => {
  601. e = e.detail.event;
  602. const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
  603. const commandsContainerElement =
  604. document.getElementById('commands-container');
  605. if (e.key === 'Escape') {
  606. stopResponse();
  607. }
  608. // Command/Ctrl + Shift + Enter to submit a message pair
  609. if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
  610. e.preventDefault();
  611. createMessagePair(prompt);
  612. }
  613. // Check if Ctrl + R is pressed
  614. if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
  615. e.preventDefault();
  616. console.log('regenerate');
  617. const regenerateButton = [
  618. ...document.getElementsByClassName('regenerate-response-button')
  619. ]?.at(-1);
  620. regenerateButton?.click();
  621. }
  622. if (prompt === '' && e.key == 'ArrowUp') {
  623. e.preventDefault();
  624. const userMessageElement = [
  625. ...document.getElementsByClassName('user-message')
  626. ]?.at(-1);
  627. if (userMessageElement) {
  628. userMessageElement.scrollIntoView({ block: 'center' });
  629. const editButton = [
  630. ...document.getElementsByClassName('edit-user-message-button')
  631. ]?.at(-1);
  632. editButton?.click();
  633. }
  634. }
  635. if (commandsContainerElement) {
  636. if (commandsContainerElement && e.key === 'ArrowUp') {
  637. e.preventDefault();
  638. commandsElement.selectUp();
  639. const commandOptionButton = [
  640. ...document.getElementsByClassName('selected-command-option-button')
  641. ]?.at(-1);
  642. commandOptionButton.scrollIntoView({ block: 'center' });
  643. }
  644. if (commandsContainerElement && e.key === 'ArrowDown') {
  645. e.preventDefault();
  646. commandsElement.selectDown();
  647. const commandOptionButton = [
  648. ...document.getElementsByClassName('selected-command-option-button')
  649. ]?.at(-1);
  650. commandOptionButton.scrollIntoView({ block: 'center' });
  651. }
  652. if (commandsContainerElement && e.key === 'Tab') {
  653. e.preventDefault();
  654. const commandOptionButton = [
  655. ...document.getElementsByClassName('selected-command-option-button')
  656. ]?.at(-1);
  657. commandOptionButton?.click();
  658. }
  659. if (commandsContainerElement && e.key === 'Enter') {
  660. e.preventDefault();
  661. const commandOptionButton = [
  662. ...document.getElementsByClassName('selected-command-option-button')
  663. ]?.at(-1);
  664. if (commandOptionButton) {
  665. commandOptionButton?.click();
  666. } else {
  667. document.getElementById('send-message-button')?.click();
  668. }
  669. }
  670. } else {
  671. if (
  672. !$mobile ||
  673. !(
  674. 'ontouchstart' in window ||
  675. navigator.maxTouchPoints > 0 ||
  676. navigator.msMaxTouchPoints > 0
  677. )
  678. ) {
  679. if (isComposing) {
  680. return;
  681. }
  682. // Uses keyCode '13' for Enter key for chinese/japanese keyboards.
  683. //
  684. // Depending on the user's settings, it will send the message
  685. // either when Enter is pressed or when Ctrl+Enter is pressed.
  686. const enterPressed =
  687. ($settings?.ctrlEnterToSend ?? false)
  688. ? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
  689. : (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
  690. if (enterPressed) {
  691. e.preventDefault();
  692. if (prompt !== '' || files.length > 0) {
  693. dispatch('submit', prompt);
  694. }
  695. }
  696. }
  697. }
  698. if (e.key === 'Escape') {
  699. console.log('Escape');
  700. atSelectedModel = undefined;
  701. selectedToolIds = [];
  702. selectedFilterIds = [];
  703. webSearchEnabled = false;
  704. imageGenerationEnabled = false;
  705. codeInterpreterEnabled = false;
  706. }
  707. }}
  708. on:paste={async (e) => {
  709. e = e.detail.event;
  710. console.log(e);
  711. const clipboardData = e.clipboardData || window.clipboardData;
  712. if (clipboardData && clipboardData.items) {
  713. for (const item of clipboardData.items) {
  714. if (item.type.indexOf('image') !== -1) {
  715. const blob = item.getAsFile();
  716. const reader = new FileReader();
  717. reader.onload = function (e) {
  718. files = [
  719. ...files,
  720. {
  721. type: 'image',
  722. url: `${e.target.result}`
  723. }
  724. ];
  725. };
  726. reader.readAsDataURL(blob);
  727. } else if (item.type === 'text/plain') {
  728. if ($settings?.largeTextAsFile ?? false) {
  729. const text = clipboardData.getData('text/plain');
  730. if (text.length > PASTED_TEXT_CHARACTER_LIMIT) {
  731. e.preventDefault();
  732. const blob = new Blob([text], { type: 'text/plain' });
  733. const file = new File([blob], `Pasted_Text_${Date.now()}.txt`, {
  734. type: 'text/plain'
  735. });
  736. await uploadFileHandler(file, true);
  737. }
  738. }
  739. }
  740. }
  741. }
  742. }}
  743. />
  744. </div>
  745. {:else}
  746. <textarea
  747. id="chat-input"
  748. dir="auto"
  749. bind:this={chatInputElement}
  750. class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
  751. placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
  752. bind:value={prompt}
  753. on:compositionstart={() => (isComposing = true)}
  754. on:compositionend={() => (isComposing = false)}
  755. on:keydown={async (e) => {
  756. const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
  757. const commandsContainerElement =
  758. document.getElementById('commands-container');
  759. if (e.key === 'Escape') {
  760. stopResponse();
  761. }
  762. // Command/Ctrl + Shift + Enter to submit a message pair
  763. if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
  764. e.preventDefault();
  765. createMessagePair(prompt);
  766. }
  767. // Check if Ctrl + R is pressed
  768. if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
  769. e.preventDefault();
  770. console.log('regenerate');
  771. const regenerateButton = [
  772. ...document.getElementsByClassName('regenerate-response-button')
  773. ]?.at(-1);
  774. regenerateButton?.click();
  775. }
  776. if (prompt === '' && e.key == 'ArrowUp') {
  777. e.preventDefault();
  778. const userMessageElement = [
  779. ...document.getElementsByClassName('user-message')
  780. ]?.at(-1);
  781. const editButton = [
  782. ...document.getElementsByClassName('edit-user-message-button')
  783. ]?.at(-1);
  784. console.log(userMessageElement);
  785. userMessageElement?.scrollIntoView({ block: 'center' });
  786. editButton?.click();
  787. }
  788. if (commandsContainerElement) {
  789. if (commandsContainerElement && e.key === 'ArrowUp') {
  790. e.preventDefault();
  791. commandsElement.selectUp();
  792. const container = document.getElementById('command-options-container');
  793. const commandOptionButton = [
  794. ...document.getElementsByClassName('selected-command-option-button')
  795. ]?.at(-1);
  796. if (commandOptionButton && container) {
  797. const elTop = commandOptionButton.offsetTop;
  798. const elHeight = commandOptionButton.offsetHeight;
  799. const containerHeight = container.clientHeight;
  800. // Center the selected button in the container
  801. container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
  802. }
  803. }
  804. if (commandsContainerElement && e.key === 'ArrowDown') {
  805. e.preventDefault();
  806. commandsElement.selectDown();
  807. const container = document.getElementById('command-options-container');
  808. const commandOptionButton = [
  809. ...document.getElementsByClassName('selected-command-option-button')
  810. ]?.at(-1);
  811. if (commandOptionButton && container) {
  812. const elTop = commandOptionButton.offsetTop;
  813. const elHeight = commandOptionButton.offsetHeight;
  814. const containerHeight = container.clientHeight;
  815. // Center the selected button in the container
  816. container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
  817. }
  818. }
  819. if (commandsContainerElement && e.key === 'Enter') {
  820. e.preventDefault();
  821. const commandOptionButton = [
  822. ...document.getElementsByClassName('selected-command-option-button')
  823. ]?.at(-1);
  824. if (e.shiftKey) {
  825. prompt = `${prompt}\n`;
  826. } else if (commandOptionButton) {
  827. commandOptionButton?.click();
  828. } else {
  829. document.getElementById('send-message-button')?.click();
  830. }
  831. }
  832. if (commandsContainerElement && e.key === 'Tab') {
  833. e.preventDefault();
  834. const commandOptionButton = [
  835. ...document.getElementsByClassName('selected-command-option-button')
  836. ]?.at(-1);
  837. commandOptionButton?.click();
  838. }
  839. } else {
  840. if (
  841. !$mobile ||
  842. !(
  843. 'ontouchstart' in window ||
  844. navigator.maxTouchPoints > 0 ||
  845. navigator.msMaxTouchPoints > 0
  846. )
  847. ) {
  848. if (isComposing) {
  849. return;
  850. }
  851. // Prevent Enter key from creating a new line
  852. const isCtrlPressed = e.ctrlKey || e.metaKey;
  853. const enterPressed =
  854. ($settings?.ctrlEnterToSend ?? false)
  855. ? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
  856. : (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
  857. if (enterPressed) {
  858. e.preventDefault();
  859. }
  860. // Submit the prompt when Enter key is pressed
  861. if ((prompt !== '' || files.length > 0) && enterPressed) {
  862. dispatch('submit', prompt);
  863. }
  864. }
  865. }
  866. if (e.key === 'Tab') {
  867. const words = extractCurlyBraceWords(prompt);
  868. if (words.length > 0) {
  869. const word = words.at(0);
  870. const fullPrompt = prompt;
  871. prompt = prompt.substring(0, word?.endIndex + 1);
  872. await tick();
  873. e.target.scrollTop = e.target.scrollHeight;
  874. prompt = fullPrompt;
  875. await tick();
  876. e.preventDefault();
  877. e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
  878. }
  879. e.target.style.height = '';
  880. e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
  881. }
  882. if (e.key === 'Escape') {
  883. console.log('Escape');
  884. atSelectedModel = undefined;
  885. selectedToolIds = [];
  886. selectedFilterIds = [];
  887. webSearchEnabled = false;
  888. imageGenerationEnabled = false;
  889. codeInterpreterEnabled = false;
  890. }
  891. }}
  892. rows="1"
  893. on:input={async (e) => {
  894. e.target.style.height = '';
  895. e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
  896. }}
  897. on:focus={async (e) => {
  898. e.target.style.height = '';
  899. e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
  900. }}
  901. on:paste={async (e) => {
  902. const clipboardData = e.clipboardData || window.clipboardData;
  903. if (clipboardData && clipboardData.items) {
  904. for (const item of clipboardData.items) {
  905. if (item.type.indexOf('image') !== -1) {
  906. const blob = item.getAsFile();
  907. const reader = new FileReader();
  908. reader.onload = function (e) {
  909. files = [
  910. ...files,
  911. {
  912. type: 'image',
  913. url: `${e.target.result}`
  914. }
  915. ];
  916. };
  917. reader.readAsDataURL(blob);
  918. } else if (item.type === 'text/plain') {
  919. if ($settings?.largeTextAsFile ?? false) {
  920. const text = clipboardData.getData('text/plain');
  921. if (text.length > PASTED_TEXT_CHARACTER_LIMIT) {
  922. e.preventDefault();
  923. const blob = new Blob([text], { type: 'text/plain' });
  924. const file = new File([blob], `Pasted_Text_${Date.now()}.txt`, {
  925. type: 'text/plain'
  926. });
  927. await uploadFileHandler(file, true);
  928. }
  929. }
  930. }
  931. }
  932. }
  933. }}
  934. />
  935. {/if}
  936. </div>
  937. <div class=" flex justify-between mt-1 mb-2.5 mx-0.5 max-w-full" dir="ltr">
  938. <div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5">
  939. <InputMenu
  940. bind:selectedToolIds
  941. selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
  942. {fileUploadCapableModels}
  943. {screenCaptureHandler}
  944. {inputFilesHandler}
  945. uploadFilesHandler={() => {
  946. filesInputElement.click();
  947. }}
  948. uploadGoogleDriveHandler={async () => {
  949. try {
  950. const fileData = await createPicker();
  951. if (fileData) {
  952. const file = new File([fileData.blob], fileData.name, {
  953. type: fileData.blob.type
  954. });
  955. await uploadFileHandler(file);
  956. } else {
  957. console.log('No file was selected from Google Drive');
  958. }
  959. } catch (error) {
  960. console.error('Google Drive Error:', error);
  961. toast.error(
  962. $i18n.t('Error accessing Google Drive: {{error}}', {
  963. error: error.message
  964. })
  965. );
  966. }
  967. }}
  968. uploadOneDriveHandler={async (authorityType) => {
  969. try {
  970. const fileData = await pickAndDownloadFile(authorityType);
  971. if (fileData) {
  972. const file = new File([fileData.blob], fileData.name, {
  973. type: fileData.blob.type || 'application/octet-stream'
  974. });
  975. await uploadFileHandler(file);
  976. } else {
  977. console.log('No file was selected from OneDrive');
  978. }
  979. } catch (error) {
  980. console.error('OneDrive Error:', error);
  981. }
  982. }}
  983. onClose={async () => {
  984. await tick();
  985. const chatInput = document.getElementById('chat-input');
  986. chatInput?.focus();
  987. }}
  988. >
  989. <button
  990. class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
  991. type="button"
  992. aria-label="More"
  993. >
  994. <svg
  995. xmlns="http://www.w3.org/2000/svg"
  996. viewBox="0 0 20 20"
  997. fill="currentColor"
  998. class="size-5"
  999. >
  1000. <path
  1001. d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
  1002. />
  1003. </svg>
  1004. </button>
  1005. </InputMenu>
  1006. <div class="flex gap-1 items-center overflow-x-auto scrollbar-none flex-1">
  1007. {#if toolServers.length + selectedToolIds.length > 0}
  1008. <Tooltip
  1009. content={$i18n.t('{{COUNT}} Available Tools', {
  1010. COUNT: toolServers.length + selectedToolIds.length
  1011. })}
  1012. >
  1013. <button
  1014. class="translate-y-[0.5px] flex gap-1 items-center text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 rounded-lg p-1 self-center transition"
  1015. aria-label="Available Tools"
  1016. type="button"
  1017. on:click={() => {
  1018. showTools = !showTools;
  1019. }}
  1020. >
  1021. <Wrench className="size-4" strokeWidth="1.75" />
  1022. <span class="text-sm font-medium text-gray-600 dark:text-gray-300">
  1023. {toolServers.length + selectedToolIds.length}
  1024. </span>
  1025. </button>
  1026. </Tooltip>
  1027. {/if}
  1028. {#if $_user}
  1029. {#each toggleFilters as filter, filterIdx (filter.id)}
  1030. <Tooltip content={filter?.description} placement="top">
  1031. <button
  1032. on:click|preventDefault={() => {
  1033. if (selectedFilterIds.includes(filter.id)) {
  1034. selectedFilterIds = selectedFilterIds.filter(
  1035. (id) => id !== filter.id
  1036. );
  1037. } else {
  1038. selectedFilterIds = [...selectedFilterIds, filter.id];
  1039. }
  1040. }}
  1041. type="button"
  1042. class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {selectedFilterIds.includes(
  1043. filter.id
  1044. )
  1045. ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
  1046. : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
  1047. >
  1048. {#if filter?.icon}
  1049. <div class="size-5 items-center flex justify-center">
  1050. <img
  1051. src={filter.icon}
  1052. class="size-4.5 {filter.icon.includes('svg')
  1053. ? 'dark:invert-[80%]'
  1054. : ''}"
  1055. style="fill: currentColor;"
  1056. alt={filter.name}
  1057. />
  1058. </div>
  1059. {:else}
  1060. <Sparkles className="size-5" strokeWidth="1.75" />
  1061. {/if}
  1062. <span
  1063. class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
  1064. >{filter?.name}</span
  1065. >
  1066. </button>
  1067. </Tooltip>
  1068. {/each}
  1069. {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === webSearchCapableModels.length && $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
  1070. <Tooltip content={$i18n.t('Search the internet')} placement="top">
  1071. <button
  1072. on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
  1073. type="button"
  1074. class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {webSearchEnabled ||
  1075. ($settings?.webSearch ?? false) === 'always'
  1076. ? 'bg-blue-100 dark:bg-blue-500/20 border-blue-400/20 text-blue-500 dark:text-blue-400'
  1077. : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800'}"
  1078. >
  1079. <GlobeAlt className="size-5" strokeWidth="1.75" />
  1080. <span
  1081. class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
  1082. >{$i18n.t('Web Search')}</span
  1083. >
  1084. </button>
  1085. </Tooltip>
  1086. {/if}
  1087. {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === imageGenerationCapableModels.length && $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)}
  1088. <Tooltip content={$i18n.t('Generate an image')} placement="top">
  1089. <button
  1090. on:click|preventDefault={() =>
  1091. (imageGenerationEnabled = !imageGenerationEnabled)}
  1092. type="button"
  1093. class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {imageGenerationEnabled
  1094. ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
  1095. : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
  1096. >
  1097. <Photo className="size-5" strokeWidth="1.75" />
  1098. <span
  1099. class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
  1100. >{$i18n.t('Image')}</span
  1101. >
  1102. </button>
  1103. </Tooltip>
  1104. {/if}
  1105. {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === codeInterpreterCapableModels.length && $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
  1106. <Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
  1107. <button
  1108. on:click|preventDefault={() =>
  1109. (codeInterpreterEnabled = !codeInterpreterEnabled)}
  1110. type="button"
  1111. class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {codeInterpreterEnabled
  1112. ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400 '
  1113. : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
  1114. >
  1115. <CommandLine className="size-5" strokeWidth="1.75" />
  1116. <span
  1117. class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
  1118. >{$i18n.t('Code Interpreter')}</span
  1119. >
  1120. </button>
  1121. </Tooltip>
  1122. {/if}
  1123. {/if}
  1124. </div>
  1125. </div>
  1126. <div class="self-end flex space-x-1 mr-1 shrink-0">
  1127. {#if (!history?.currentId || history.messages[history.currentId]?.done == true) && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.stt ?? true))}
  1128. <Tooltip content={$i18n.t('Record voice')}>
  1129. <button
  1130. id="voice-input-button"
  1131. class=" text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200 transition rounded-full p-1.5 mr-0.5 self-center"
  1132. type="button"
  1133. on:click={async () => {
  1134. try {
  1135. let stream = await navigator.mediaDevices
  1136. .getUserMedia({ audio: true })
  1137. .catch(function (err) {
  1138. toast.error(
  1139. $i18n.t(
  1140. `Permission denied when accessing microphone: {{error}}`,
  1141. {
  1142. error: err
  1143. }
  1144. )
  1145. );
  1146. return null;
  1147. });
  1148. if (stream) {
  1149. recording = true;
  1150. const tracks = stream.getTracks();
  1151. tracks.forEach((track) => track.stop());
  1152. }
  1153. stream = null;
  1154. } catch {
  1155. toast.error($i18n.t('Permission denied when accessing microphone'));
  1156. }
  1157. }}
  1158. aria-label="Voice Input"
  1159. >
  1160. <svg
  1161. xmlns="http://www.w3.org/2000/svg"
  1162. viewBox="0 0 20 20"
  1163. fill="currentColor"
  1164. class="w-5 h-5 translate-y-[0.5px]"
  1165. >
  1166. <path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
  1167. <path
  1168. 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"
  1169. />
  1170. </svg>
  1171. </button>
  1172. </Tooltip>
  1173. {/if}
  1174. {#if (taskIds && taskIds.length > 0) || (history.currentId && history.messages[history.currentId]?.done != true)}
  1175. <div class=" flex items-center">
  1176. <Tooltip content={$i18n.t('Stop')}>
  1177. <button
  1178. 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"
  1179. on:click={() => {
  1180. stopResponse();
  1181. }}
  1182. >
  1183. <svg
  1184. xmlns="http://www.w3.org/2000/svg"
  1185. viewBox="0 0 24 24"
  1186. fill="currentColor"
  1187. class="size-5"
  1188. >
  1189. <path
  1190. fill-rule="evenodd"
  1191. 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"
  1192. clip-rule="evenodd"
  1193. />
  1194. </svg>
  1195. </button>
  1196. </Tooltip>
  1197. </div>
  1198. {:else if prompt === '' && files.length === 0 && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.call ?? true))}
  1199. <div class=" flex items-center">
  1200. <Tooltip content={$i18n.t('Call')}>
  1201. <button
  1202. class=" bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full p-1.5 self-center"
  1203. type="button"
  1204. on:click={async () => {
  1205. if (selectedModels.length > 1) {
  1206. toast.error($i18n.t('Select only one model to call'));
  1207. return;
  1208. }
  1209. if ($config.audio.stt.engine === 'web') {
  1210. toast.error(
  1211. $i18n.t('Call feature is not supported when using Web STT engine')
  1212. );
  1213. return;
  1214. }
  1215. // check if user has access to getUserMedia
  1216. try {
  1217. let stream = await navigator.mediaDevices.getUserMedia({
  1218. audio: true
  1219. });
  1220. // If the user grants the permission, proceed to show the call overlay
  1221. if (stream) {
  1222. const tracks = stream.getTracks();
  1223. tracks.forEach((track) => track.stop());
  1224. }
  1225. stream = null;
  1226. if ($settings.audio?.tts?.engine === 'browser-kokoro') {
  1227. // If the user has not initialized the TTS worker, initialize it
  1228. if (!$TTSWorker) {
  1229. await TTSWorker.set(
  1230. new KokoroWorker({
  1231. dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
  1232. })
  1233. );
  1234. await $TTSWorker.init();
  1235. }
  1236. }
  1237. showCallOverlay.set(true);
  1238. showControls.set(true);
  1239. } catch (err) {
  1240. // If the user denies the permission or an error occurs, show an error message
  1241. toast.error(
  1242. $i18n.t('Permission denied when accessing media devices')
  1243. );
  1244. }
  1245. }}
  1246. aria-label="Call"
  1247. >
  1248. <Headphone className="size-5" />
  1249. </button>
  1250. </Tooltip>
  1251. </div>
  1252. {:else}
  1253. <div class=" flex items-center">
  1254. <Tooltip content={$i18n.t('Send message')}>
  1255. <button
  1256. id="send-message-button"
  1257. class="{!(prompt === '' && files.length === 0)
  1258. ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
  1259. : 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 self-center"
  1260. type="submit"
  1261. disabled={prompt === '' && files.length === 0}
  1262. >
  1263. <svg
  1264. xmlns="http://www.w3.org/2000/svg"
  1265. viewBox="0 0 16 16"
  1266. fill="currentColor"
  1267. class="size-5"
  1268. >
  1269. <path
  1270. fill-rule="evenodd"
  1271. 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"
  1272. clip-rule="evenodd"
  1273. />
  1274. </svg>
  1275. </button>
  1276. </Tooltip>
  1277. </div>
  1278. {/if}
  1279. </div>
  1280. </div>
  1281. </div>
  1282. </form>
  1283. {/if}
  1284. </div>
  1285. </div>
  1286. </div>
  1287. </div>
  1288. {/if}