Placeholder.svelte 6.2 KB

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