Playground.svelte 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <script lang="ts">
  2. import { goto } from '$app/navigation';
  3. import { onMount, tick, getContext } from 'svelte';
  4. import { toast } from 'svelte-sonner';
  5. import {
  6. LITELLM_API_BASE_URL,
  7. OLLAMA_API_BASE_URL,
  8. OPENAI_API_BASE_URL,
  9. WEBUI_API_BASE_URL
  10. } from '$lib/constants';
  11. import { WEBUI_NAME, config, user, models, settings } from '$lib/stores';
  12. import { cancelOllamaRequest, generateChatCompletion } from '$lib/apis/ollama';
  13. import { generateOpenAIChatCompletion } from '$lib/apis/openai';
  14. import { splitStream } from '$lib/utils';
  15. import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte';
  16. import Selector from '$lib/components/chat/ModelSelector/Selector.svelte';
  17. const i18n = getContext('i18n');
  18. let mode = 'chat';
  19. let loaded = false;
  20. let text = '';
  21. let selectedModelId = '';
  22. let loading = false;
  23. let currentRequestId = null;
  24. let stopResponseFlag = false;
  25. let messagesContainerElement: HTMLDivElement;
  26. let textCompletionAreaElement: HTMLTextAreaElement;
  27. let system = '';
  28. let messages = [
  29. {
  30. role: 'user',
  31. content: ''
  32. }
  33. ];
  34. const scrollToBottom = () => {
  35. const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement;
  36. if (element) {
  37. element.scrollTop = element?.scrollHeight;
  38. }
  39. };
  40. // const cancelHandler = async () => {
  41. // if (currentRequestId) {
  42. // const res = await cancelOllamaRequest(localStorage.token, currentRequestId);
  43. // currentRequestId = null;
  44. // loading = false;
  45. // }
  46. // };
  47. const stopResponse = () => {
  48. stopResponseFlag = true;
  49. console.log('stopResponse');
  50. };
  51. const textCompletionHandler = async () => {
  52. const model = $models.find((model) => model.id === selectedModelId);
  53. const [res, controller] = await generateOpenAIChatCompletion(
  54. localStorage.token,
  55. {
  56. model: model.id,
  57. stream: true,
  58. messages: [
  59. {
  60. role: 'assistant',
  61. content: text
  62. }
  63. ]
  64. },
  65. model.external
  66. ? model.source === 'litellm'
  67. ? `${LITELLM_API_BASE_URL}/v1`
  68. : `${OPENAI_API_BASE_URL}`
  69. : `${OLLAMA_API_BASE_URL}/v1`
  70. );
  71. if (res && res.ok) {
  72. const reader = res.body
  73. .pipeThrough(new TextDecoderStream())
  74. .pipeThrough(splitStream('\n'))
  75. .getReader();
  76. while (true) {
  77. const { value, done } = await reader.read();
  78. if (done || stopResponseFlag) {
  79. if (stopResponseFlag) {
  80. controller.abort('User: Stop Response');
  81. }
  82. currentRequestId = null;
  83. break;
  84. }
  85. try {
  86. let lines = value.split('\n');
  87. for (const line of lines) {
  88. if (line !== '') {
  89. if (line === 'data: [DONE]') {
  90. // responseMessage.done = true;
  91. console.log('done');
  92. } else {
  93. let data = JSON.parse(line.replace(/^data: /, ''));
  94. console.log(data);
  95. if ('request_id' in data) {
  96. currentRequestId = data.request_id;
  97. } else {
  98. text += data.choices[0].delta.content ?? '';
  99. }
  100. }
  101. }
  102. }
  103. } catch (error) {
  104. console.log(error);
  105. }
  106. scrollToBottom();
  107. }
  108. }
  109. };
  110. const chatCompletionHandler = async () => {
  111. const model = $models.find((model) => model.id === selectedModelId);
  112. const [res, controller] = await generateOpenAIChatCompletion(
  113. localStorage.token,
  114. {
  115. model: model.id,
  116. stream: true,
  117. messages: [
  118. system
  119. ? {
  120. role: 'system',
  121. content: system
  122. }
  123. : undefined,
  124. ...messages
  125. ].filter((message) => message)
  126. },
  127. model.external
  128. ? model.source === 'litellm'
  129. ? `${LITELLM_API_BASE_URL}/v1`
  130. : `${OPENAI_API_BASE_URL}`
  131. : `${OLLAMA_API_BASE_URL}/v1`
  132. );
  133. let responseMessage;
  134. if (messages.at(-1)?.role === 'assistant') {
  135. responseMessage = messages.at(-1);
  136. } else {
  137. responseMessage = {
  138. role: 'assistant',
  139. content: ''
  140. };
  141. messages.push(responseMessage);
  142. messages = messages;
  143. }
  144. await tick();
  145. const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`);
  146. if (res && res.ok) {
  147. const reader = res.body
  148. .pipeThrough(new TextDecoderStream())
  149. .pipeThrough(splitStream('\n'))
  150. .getReader();
  151. while (true) {
  152. const { value, done } = await reader.read();
  153. if (done || stopResponseFlag) {
  154. if (stopResponseFlag) {
  155. controller.abort('User: Stop Response');
  156. }
  157. currentRequestId = null;
  158. break;
  159. }
  160. try {
  161. let lines = value.split('\n');
  162. for (const line of lines) {
  163. if (line !== '') {
  164. console.log(line);
  165. if (line === 'data: [DONE]') {
  166. // responseMessage.done = true;
  167. messages = messages;
  168. } else {
  169. let data = JSON.parse(line.replace(/^data: /, ''));
  170. console.log(data);
  171. if ('request_id' in data) {
  172. currentRequestId = data.request_id;
  173. } else {
  174. if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
  175. continue;
  176. } else {
  177. textareaElement.style.height = textareaElement.scrollHeight + 'px';
  178. responseMessage.content += data.choices[0].delta.content ?? '';
  179. messages = messages;
  180. textareaElement.style.height = textareaElement.scrollHeight + 'px';
  181. await tick();
  182. }
  183. }
  184. }
  185. }
  186. }
  187. } catch (error) {
  188. console.log(error);
  189. }
  190. scrollToBottom();
  191. }
  192. }
  193. };
  194. const submitHandler = async () => {
  195. if (selectedModelId) {
  196. loading = true;
  197. if (mode === 'complete') {
  198. await textCompletionHandler();
  199. } else if (mode === 'chat') {
  200. await chatCompletionHandler();
  201. }
  202. loading = false;
  203. stopResponseFlag = false;
  204. currentRequestId = null;
  205. }
  206. };
  207. onMount(async () => {
  208. if ($user?.role !== 'admin') {
  209. await goto('/');
  210. }
  211. if ($settings?.models) {
  212. selectedModelId = $settings?.models[0];
  213. } else if ($config?.default_models) {
  214. selectedModelId = $config?.default_models.split(',')[0];
  215. } else {
  216. selectedModelId = '';
  217. }
  218. loaded = true;
  219. });
  220. </script>
  221. <svelte:head>
  222. <title>
  223. {$i18n.t('Playground')} | {$WEBUI_NAME}
  224. </title>
  225. </svelte:head>
  226. <div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
  227. <div class="mx-auto w-full md:px-0 h-full">
  228. <div class=" flex flex-col h-full">
  229. <div class="flex flex-col justify-between mb-2.5 gap-1">
  230. <div class="flex justify-between items-center gap-2">
  231. <div class=" text-lg font-semibold self-center flex">
  232. {$i18n.t('Playground')}
  233. <span class=" text-xs text-gray-500 self-center ml-1">{$i18n.t('(Beta)')}</span>
  234. </div>
  235. <div>
  236. <button
  237. class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode === 'chat' &&
  238. 'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' &&
  239. 'text-green-600 dark:text-green-200 bg-green-200/30'} "
  240. on:click={() => {
  241. if (mode === 'complete') {
  242. mode = 'chat';
  243. } else {
  244. mode = 'complete';
  245. }
  246. }}
  247. >
  248. {#if mode === 'complete'}
  249. {$i18n.t('Text Completion')}
  250. {:else if mode === 'chat'}
  251. {$i18n.t('Chat')}
  252. {/if}
  253. <div>
  254. <svg
  255. xmlns="http://www.w3.org/2000/svg"
  256. viewBox="0 0 16 16"
  257. fill="currentColor"
  258. class="w-3 h-3"
  259. >
  260. <path
  261. fill-rule="evenodd"
  262. d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z"
  263. clip-rule="evenodd"
  264. />
  265. </svg>
  266. </div>
  267. </button>
  268. </div>
  269. </div>
  270. <div class="flex flex-col gap-1 w-full">
  271. <div class="flex w-full">
  272. <div class="overflow-hidden w-full">
  273. <div class="max-w-full">
  274. <Selector
  275. placeholder={$i18n.t('Select a model')}
  276. items={$models
  277. .filter((model) => model.name !== 'hr')
  278. .map((model) => ({
  279. value: model.id,
  280. label: model.name,
  281. info: model
  282. }))}
  283. bind:value={selectedModelId}
  284. />
  285. </div>
  286. </div>
  287. </div>
  288. <!-- <button
  289. class=" self-center dark:hover:text-gray-300"
  290. id="open-settings-button"
  291. on:click={async () => {}}
  292. >
  293. <svg
  294. xmlns="http://www.w3.org/2000/svg"
  295. fill="none"
  296. viewBox="0 0 24 24"
  297. stroke-width="1.5"
  298. stroke="currentColor"
  299. class="w-4 h-4"
  300. >
  301. <path
  302. stroke-linecap="round"
  303. stroke-linejoin="round"
  304. d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
  305. />
  306. <path
  307. stroke-linecap="round"
  308. stroke-linejoin="round"
  309. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  310. />
  311. </svg>
  312. </button> -->
  313. </div>
  314. </div>
  315. {#if mode === 'chat'}
  316. <div class="p-1">
  317. <div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg">
  318. <div class=" text-sm font-medium">{$i18n.t('System')}</div>
  319. <textarea
  320. id="system-textarea"
  321. class="w-full h-full bg-transparent resize-none outline-none text-sm"
  322. bind:value={system}
  323. placeholder={$i18n.t("You're a helpful assistant.")}
  324. rows="4"
  325. />
  326. </div>
  327. </div>
  328. {/if}
  329. <div
  330. class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
  331. id="messages-container"
  332. bind:this={messagesContainerElement}
  333. >
  334. <div class=" h-full w-full flex flex-col">
  335. <div class="flex-1 p-1">
  336. {#if mode === 'complete'}
  337. <textarea
  338. id="text-completion-textarea"
  339. bind:this={textCompletionAreaElement}
  340. class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
  341. bind:value={text}
  342. placeholder={$i18n.t("You're a helpful assistant.")}
  343. />
  344. {:else if mode === 'chat'}
  345. <ChatCompletion bind:messages />
  346. {/if}
  347. </div>
  348. </div>
  349. </div>
  350. <div class="pb-3">
  351. {#if !loading}
  352. <button
  353. class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg"
  354. on:click={() => {
  355. submitHandler();
  356. }}
  357. >
  358. {$i18n.t('Submit')}
  359. </button>
  360. {:else}
  361. <button
  362. class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg"
  363. on:click={() => {
  364. stopResponse();
  365. }}
  366. >
  367. {$i18n.t('Cancel')}
  368. </button>
  369. {/if}
  370. </div>
  371. </div>
  372. </div>
  373. </div>
  374. <style>
  375. .scrollbar-hidden::-webkit-scrollbar {
  376. display: none; /* for Chrome, Safari and Opera */
  377. }
  378. .scrollbar-hidden {
  379. -ms-overflow-style: none; /* IE and Edge */
  380. scrollbar-width: none; /* Firefox */
  381. }
  382. </style>