Placeholder.svelte 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { marked } from 'marked';
  4. import { onMount, getContext, tick, createEventDispatcher } from 'svelte';
  5. import { blur, fade } from 'svelte/transition';
  6. const dispatch = createEventDispatcher();
  7. import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
  8. import { sanitizeResponseContent, findWordIndices } from '$lib/utils';
  9. import { WEBUI_BASE_URL } from '$lib/constants';
  10. import Suggestions from './Suggestions.svelte';
  11. import Tooltip from '$lib/components/common/Tooltip.svelte';
  12. import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
  13. import MessageInput from './MessageInput.svelte';
  14. const i18n = getContext('i18n');
  15. export let transparentBackground = false;
  16. export let createMessagePair: Function;
  17. export let stopResponse: Function;
  18. export let autoScroll = false;
  19. export let atSelectedModel: Model | undefined;
  20. export let selectedModels: [''];
  21. export let history;
  22. export let prompt = '';
  23. export let files = [];
  24. export let selectedToolIds = [];
  25. export let imageGenerationEnabled = false;
  26. export let codeInterpreterEnabled = false;
  27. export let webSearchEnabled = false;
  28. let models = [];
  29. const selectSuggestionPrompt = async (p) => {
  30. let text = p;
  31. if (p.includes('{{CLIPBOARD}}')) {
  32. const clipboardText = await navigator.clipboard.readText().catch((err) => {
  33. toast.error($i18n.t('Failed to read clipboard contents'));
  34. return '{{CLIPBOARD}}';
  35. });
  36. text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
  37. console.log('Clipboard text:', clipboardText, text);
  38. }
  39. prompt = text;
  40. console.log(prompt);
  41. await tick();
  42. const chatInputContainerElement = document.getElementById('chat-input-container');
  43. const chatInputElement = document.getElementById('chat-input');
  44. if (chatInputContainerElement) {
  45. chatInputContainerElement.style.height = '';
  46. chatInputContainerElement.style.height =
  47. Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
  48. }
  49. await tick();
  50. if (chatInputElement) {
  51. chatInputElement.focus();
  52. chatInputElement.dispatchEvent(new Event('input'));
  53. }
  54. await tick();
  55. };
  56. let selectedModelIdx = 0;
  57. $: if (selectedModels.length > 0) {
  58. selectedModelIdx = models.length - 1;
  59. }
  60. $: models = selectedModels.map((id) => $_models.find((m) => m.id === id));
  61. onMount(() => {});
  62. </script>
  63. <div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
  64. {#if $temporaryChatEnabled}
  65. <Tooltip
  66. content="This chat won't appear in history and your messages will not be saved."
  67. className="w-full flex justify-center mb-0.5"
  68. placement="top"
  69. >
  70. <div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
  71. <EyeSlash strokeWidth="2.5" className="size-5" /> Temporary Chat
  72. </div>
  73. </Tooltip>
  74. {/if}
  75. <div
  76. class="w-full text-3xl text-gray-800 dark:text-gray-100 font-medium text-center flex items-center gap-4 font-primary"
  77. >
  78. <div class="w-full flex flex-col justify-center items-center">
  79. <div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5">
  80. <div class="flex flex-shrink-0 justify-center">
  81. <div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
  82. {#each models as model, modelIdx}
  83. <Tooltip
  84. content={(models[modelIdx]?.info?.meta?.tags ?? [])
  85. .map((tag) => tag.name.toUpperCase())
  86. .join(', ')}
  87. placement="top"
  88. >
  89. <button
  90. on:click={() => {
  91. selectedModelIdx = modelIdx;
  92. }}
  93. >
  94. <img
  95. crossorigin="anonymous"
  96. src={model?.info?.meta?.profile_image_url ??
  97. ($i18n.language === 'dg-DG'
  98. ? `/doge.png`
  99. : `${WEBUI_BASE_URL}/static/favicon.png`)}
  100. class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-200 dark:border-none"
  101. alt="logo"
  102. draggable="false"
  103. />
  104. </button>
  105. </Tooltip>
  106. {/each}
  107. </div>
  108. </div>
  109. <div class=" text-3xl @sm:text-4xl line-clamp-1" in:fade={{ duration: 100 }}>
  110. {#if models[selectedModelIdx]?.name}
  111. {models[selectedModelIdx]?.name}
  112. {:else}
  113. {$i18n.t('Hello, {{name}}', { name: $user.name })}
  114. {/if}
  115. </div>
  116. </div>
  117. <div class="flex mt-1 mb-2">
  118. <div in:fade={{ duration: 100, delay: 50 }}>
  119. {#if models[selectedModelIdx]?.info?.meta?.description ?? null}
  120. <Tooltip
  121. className=" w-fit"
  122. content={marked.parse(
  123. sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
  124. )}
  125. placement="top"
  126. >
  127. <div
  128. class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
  129. >
  130. {@html marked.parse(
  131. sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
  132. )}
  133. </div>
  134. </Tooltip>
  135. {#if models[selectedModelIdx]?.info?.meta?.user}
  136. <div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
  137. By
  138. {#if models[selectedModelIdx]?.info?.meta?.user.community}
  139. <a
  140. href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user
  141. .username}"
  142. >{models[selectedModelIdx]?.info?.meta?.user.name
  143. ? models[selectedModelIdx]?.info?.meta?.user.name
  144. : `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a
  145. >
  146. {:else}
  147. {models[selectedModelIdx]?.info?.meta?.user.name}
  148. {/if}
  149. </div>
  150. {/if}
  151. {/if}
  152. </div>
  153. </div>
  154. <div class="text-base font-normal @md:max-w-3xl w-full py-3 {atSelectedModel ? 'mt-2' : ''}">
  155. <MessageInput
  156. {history}
  157. {selectedModels}
  158. bind:files
  159. bind:prompt
  160. bind:autoScroll
  161. bind:selectedToolIds
  162. bind:imageGenerationEnabled
  163. bind:codeInterpreterEnabled
  164. bind:webSearchEnabled
  165. bind:atSelectedModel
  166. {transparentBackground}
  167. {stopResponse}
  168. {createMessagePair}
  169. placeholder={$i18n.t('How can I help you today?')}
  170. on:upload={(e) => {
  171. dispatch('upload', e.detail);
  172. }}
  173. on:submit={(e) => {
  174. dispatch('submit', e.detail);
  175. }}
  176. />
  177. </div>
  178. </div>
  179. </div>
  180. <div class="mx-auto max-w-2xl font-primary" in:fade={{ duration: 200, delay: 200 }}>
  181. <div class="mx-5">
  182. <Suggestions
  183. suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
  184. $config?.default_prompt_suggestions ??
  185. []}
  186. inputValue={prompt}
  187. on:select={(e) => {
  188. selectSuggestionPrompt(e.detail);
  189. }}
  190. />
  191. </div>
  192. </div>
  193. </div>