Playground.svelte 11 KB

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