+page.svelte 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176
  1. <script lang="ts">
  2. import { openDB, deleteDB } from 'idb';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import { marked } from 'marked';
  5. import fileSaver from 'file-saver';
  6. const { saveAs } = fileSaver;
  7. import hljs from 'highlight.js';
  8. import 'highlight.js/styles/github-dark.min.css';
  9. import katex from 'katex';
  10. import auto_render from 'katex/dist/contrib/auto-render.mjs';
  11. import toast from 'svelte-french-toast';
  12. import { API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants';
  13. import { onMount, tick } from 'svelte';
  14. import Navbar from '$lib/components/layout/Navbar.svelte';
  15. import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
  16. import Suggestions from '$lib/components/chat/Suggestions.svelte';
  17. let API_BASE_URL = BUILD_TIME_API_BASE_URL;
  18. let db;
  19. let selectedModel = '';
  20. let settings = {
  21. system: null,
  22. temperature: null
  23. };
  24. let models = [];
  25. let chats = [];
  26. let chatId = uuidv4();
  27. let title = '';
  28. let prompt = '';
  29. let messages = [];
  30. let showSettings = false;
  31. let stopResponseFlag = false;
  32. let autoScroll = true;
  33. let suggestions = ''; // $page.url.searchParams.get('suggestions');
  34. onMount(async () => {
  35. await createNewChat(true);
  36. await setDBandLoadChats();
  37. });
  38. //////////////////////////
  39. // Helper functions
  40. //////////////////////////
  41. const splitStream = (splitOn) => {
  42. let buffer = '';
  43. return new TransformStream({
  44. transform(chunk, controller) {
  45. buffer += chunk;
  46. const parts = buffer.split(splitOn);
  47. parts.slice(0, -1).forEach((part) => controller.enqueue(part));
  48. buffer = parts[parts.length - 1];
  49. },
  50. flush(controller) {
  51. if (buffer) controller.enqueue(buffer);
  52. }
  53. });
  54. };
  55. const copyToClipboard = (text) => {
  56. if (!navigator.clipboard) {
  57. var textArea = document.createElement('textarea');
  58. textArea.value = text;
  59. // Avoid scrolling to bottom
  60. textArea.style.top = '0';
  61. textArea.style.left = '0';
  62. textArea.style.position = 'fixed';
  63. document.body.appendChild(textArea);
  64. textArea.focus();
  65. textArea.select();
  66. try {
  67. var successful = document.execCommand('copy');
  68. var msg = successful ? 'successful' : 'unsuccessful';
  69. console.log('Fallback: Copying text command was ' + msg);
  70. } catch (err) {
  71. console.error('Fallback: Oops, unable to copy', err);
  72. }
  73. document.body.removeChild(textArea);
  74. return;
  75. }
  76. navigator.clipboard.writeText(text).then(
  77. function () {
  78. console.log('Async: Copying to clipboard was successful!');
  79. toast.success('Copying to clipboard was successful!');
  80. },
  81. function (err) {
  82. console.error('Async: Could not copy text: ', err);
  83. }
  84. );
  85. };
  86. const createCopyCodeBlockButton = () => {
  87. // use a class selector if available
  88. let blocks = document.querySelectorAll('pre');
  89. console.log(blocks);
  90. blocks.forEach((block) => {
  91. // only add button if browser supports Clipboard API
  92. if (navigator.clipboard && block.childNodes.length < 2) {
  93. let button = document.createElement('button');
  94. button.innerText = 'Copy Code';
  95. block.appendChild(button);
  96. button.addEventListener('click', async () => {
  97. await copyCode(block, button);
  98. });
  99. }
  100. });
  101. async function copyCode(block, button) {
  102. let code = block.querySelector('code');
  103. let text = code.innerText;
  104. await navigator.clipboard.writeText(text);
  105. // visual feedback that task is completed
  106. button.innerText = 'Code Copied';
  107. setTimeout(() => {
  108. button.innerText = 'Copy Code';
  109. }, 700);
  110. }
  111. };
  112. const renderLatex = () => {
  113. let chatMessageElements = document.getElementsByClassName('chat-assistant');
  114. // let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
  115. for (const element of chatMessageElements) {
  116. auto_render(element, {
  117. // customised options
  118. // • auto-render specific keys, e.g.:
  119. delimiters: [
  120. { left: '$$', right: '$$', display: true },
  121. { left: '$', right: '$', display: false },
  122. { left: '\\(', right: '\\)', display: false },
  123. { left: '\\[', right: '\\]', display: true }
  124. ],
  125. // • rendering keys, e.g.:
  126. throwOnError: false,
  127. output: 'mathml'
  128. });
  129. }
  130. };
  131. //////////////////////////
  132. // Web functions
  133. //////////////////////////
  134. const createNewChat = async (init = false) => {
  135. if (init || messages.length > 0) {
  136. chatId = uuidv4();
  137. messages = [];
  138. title = '';
  139. settings = JSON.parse(localStorage.getItem('settings') ?? JSON.stringify(settings));
  140. API_BASE_URL = settings?.API_BASE_URL ?? BUILD_TIME_API_BASE_URL;
  141. console.log(API_BASE_URL);
  142. if (models.length === 0) {
  143. await getModelTags();
  144. }
  145. selectedModel =
  146. settings.model && models.map((model) => model.name).includes(settings.model)
  147. ? settings.model
  148. : '';
  149. console.log(chatId);
  150. }
  151. };
  152. const setDBandLoadChats = async () => {
  153. db = await openDB('Chats', 1, {
  154. upgrade(db) {
  155. const store = db.createObjectStore('chats', {
  156. keyPath: 'id',
  157. autoIncrement: true
  158. });
  159. store.createIndex('timestamp', 'timestamp');
  160. }
  161. });
  162. chats = await db.getAllFromIndex('chats', 'timestamp');
  163. };
  164. const saveDefaultModel = () => {
  165. settings.model = selectedModel;
  166. localStorage.setItem('settings', JSON.stringify(settings));
  167. toast.success('Default model updated');
  168. };
  169. const saveSettings = async (updated) => {
  170. settings = { ...settings, ...updated };
  171. localStorage.setItem('settings', JSON.stringify(settings));
  172. await getModelTags();
  173. };
  174. const loadChat = async (id) => {
  175. const chat = await db.get('chats', id);
  176. if (chatId !== chat.id) {
  177. if (chat.messages.length > 0) {
  178. chat.messages.at(-1).done = true;
  179. }
  180. messages = chat.messages;
  181. title = chat.title;
  182. chatId = chat.id;
  183. selectedModel = chat.model ?? selectedModel;
  184. settings.system = chat.system ?? settings.system;
  185. settings.temperature = chat.temperature ?? settings.temperature;
  186. await tick();
  187. hljs.highlightAll();
  188. createCopyCodeBlockButton();
  189. renderLatex();
  190. }
  191. };
  192. const editChatTitle = async (id, _title) => {
  193. const chat = await db.get('chats', id);
  194. console.log(chat);
  195. await db.put('chats', {
  196. ...chat,
  197. title: _title
  198. });
  199. title = _title;
  200. chats = await db.getAllFromIndex('chats', 'timestamp');
  201. };
  202. const deleteChat = async (id) => {
  203. createNewChat();
  204. const chat = await db.delete('chats', id);
  205. console.log(chat);
  206. chats = await db.getAllFromIndex('chats', 'timestamp');
  207. };
  208. const deleteChatHistory = async () => {
  209. const tx = db.transaction('chats', 'readwrite');
  210. await Promise.all([tx.store.clear(), tx.done]);
  211. chats = await db.getAllFromIndex('chats', 'timestamp');
  212. };
  213. const importChatHistory = async (chatHistory) => {
  214. for (const chat of chatHistory) {
  215. console.log(chat);
  216. await db.put('chats', {
  217. id: chat.id,
  218. model: chat.model,
  219. system: chat.system,
  220. options: chat.options,
  221. title: chat.title,
  222. timestamp: chat.timestamp,
  223. messages: chat.messages
  224. });
  225. }
  226. chats = await db.getAllFromIndex('chats', 'timestamp');
  227. console.log(chats);
  228. };
  229. const exportChatHistory = async () => {
  230. chats = await db.getAllFromIndex('chats', 'timestamp');
  231. let blob = new Blob([JSON.stringify(chats)], { type: 'application/json' });
  232. saveAs(blob, `chat-export-${Date.now()}.json`);
  233. };
  234. const openSettings = async () => {
  235. showSettings = true;
  236. };
  237. const editMessage = async (messageIdx) => {
  238. messages = messages.map((message, idx) => {
  239. if (messageIdx === idx) {
  240. message.edit = true;
  241. message.editedContent = message.content;
  242. }
  243. return message;
  244. });
  245. };
  246. const confirmEditMessage = async (messageIdx) => {
  247. let userPrompt = messages.at(messageIdx).editedContent;
  248. messages.splice(messageIdx, messages.length - messageIdx);
  249. messages = messages;
  250. await submitPrompt(userPrompt);
  251. };
  252. const cancelEditMessage = (messageIdx) => {
  253. messages = messages.map((message, idx) => {
  254. if (messageIdx === idx) {
  255. message.edit = undefined;
  256. message.editedContent = undefined;
  257. }
  258. return message;
  259. });
  260. console.log(messages);
  261. };
  262. const rateMessage = async (messageIdx, rating) => {
  263. messages = messages.map((message, idx) => {
  264. if (messageIdx === idx) {
  265. message.rating = rating;
  266. }
  267. return message;
  268. });
  269. await db.put('chats', {
  270. id: chatId,
  271. title: title === '' ? 'New Chat' : title,
  272. model: selectedModel,
  273. system: settings.system,
  274. options: {
  275. temperature: settings.temperature
  276. },
  277. timestamp: Date.now(),
  278. messages: messages
  279. });
  280. console.log(messages);
  281. };
  282. //////////////////////////
  283. // Ollama functions
  284. //////////////////////////
  285. const getModelTags = async (url = null) => {
  286. const res = await fetch(`${url === null ? API_BASE_URL : url}/tags`, {
  287. method: 'GET',
  288. headers: {
  289. Accept: 'application/json',
  290. 'Content-Type': 'application/json'
  291. }
  292. })
  293. .then(async (res) => {
  294. if (!res.ok) throw await res.json();
  295. return res.json();
  296. })
  297. .catch((error) => {
  298. console.log(error);
  299. toast.error('Server connection failed');
  300. return null;
  301. });
  302. console.log(res);
  303. if (settings.OPENAI_API_KEY) {
  304. // Validate OPENAI_API_KEY
  305. const openaiModels = await fetch(`https://api.openai.com/v1/models`, {
  306. method: 'GET',
  307. headers: {
  308. 'Content-Type': 'application/json',
  309. Authorization: `Bearer ${settings.OPENAI_API_KEY}`
  310. }
  311. })
  312. .then(async (res) => {
  313. if (!res.ok) throw await res.json();
  314. return res.json();
  315. })
  316. .catch((error) => {
  317. console.log(error);
  318. toast.error(`OpenAI: ${error.error.message}`);
  319. return null;
  320. });
  321. console.log(openaiModels);
  322. if (openaiModels) {
  323. models = [
  324. ...(res?.models ?? []),
  325. { name: 'hr' },
  326. { name: 'gpt-3.5-turbo', label: '(OpenAI)' }
  327. ];
  328. } else {
  329. models = res?.models ?? [];
  330. }
  331. } else {
  332. models = res?.models ?? [];
  333. }
  334. return models;
  335. };
  336. const sendPrompt = async (userPrompt) => {
  337. if (selectedModel === 'gpt-3.5-turbo') {
  338. await sendPromptOpenAI(userPrompt);
  339. } else {
  340. await sendPromptOllama(userPrompt);
  341. }
  342. };
  343. const sendPromptOllama = async (userPrompt) => {
  344. let responseMessage = {
  345. role: 'assistant',
  346. content: ''
  347. };
  348. messages = [...messages, responseMessage];
  349. window.scrollTo({ top: document.body.scrollHeight });
  350. const res = await fetch(`${API_BASE_URL}/generate`, {
  351. method: 'POST',
  352. headers: {
  353. 'Content-Type': 'text/event-stream'
  354. },
  355. body: JSON.stringify({
  356. model: selectedModel,
  357. prompt: userPrompt,
  358. system: settings.system ?? undefined,
  359. options:
  360. settings.temperature != null
  361. ? {
  362. temperature: settings.temperature
  363. }
  364. : undefined,
  365. context:
  366. messages.length > 3 && messages.at(-3).context != undefined
  367. ? messages.at(-3).context
  368. : undefined
  369. })
  370. });
  371. const reader = res.body
  372. .pipeThrough(new TextDecoderStream())
  373. .pipeThrough(splitStream('\n'))
  374. .getReader();
  375. while (true) {
  376. const { value, done } = await reader.read();
  377. if (done || stopResponseFlag) {
  378. if (stopResponseFlag) {
  379. responseMessage.done = true;
  380. messages = messages;
  381. hljs.highlightAll();
  382. createCopyCodeBlockButton();
  383. renderLatex();
  384. }
  385. break;
  386. }
  387. try {
  388. let lines = value.split('\n');
  389. for (const line of lines) {
  390. if (line !== '') {
  391. console.log(line);
  392. let data = JSON.parse(line);
  393. if (data.done == false) {
  394. if (responseMessage.content == '' && data.response == '\n') {
  395. continue;
  396. } else {
  397. responseMessage.content += data.response;
  398. messages = messages;
  399. }
  400. } else {
  401. responseMessage.done = true;
  402. responseMessage.context = data.context;
  403. messages = messages;
  404. hljs.highlightAll();
  405. createCopyCodeBlockButton();
  406. renderLatex();
  407. }
  408. }
  409. }
  410. } catch (error) {
  411. console.log(error);
  412. }
  413. if (autoScroll) {
  414. window.scrollTo({ top: document.body.scrollHeight });
  415. }
  416. await db.put('chats', {
  417. id: chatId,
  418. title: title === '' ? 'New Chat' : title,
  419. model: selectedModel,
  420. system: settings.system,
  421. options: {
  422. temperature: settings.temperature
  423. },
  424. timestamp: Date.now(),
  425. messages: messages
  426. });
  427. }
  428. stopResponseFlag = false;
  429. await tick();
  430. if (autoScroll) {
  431. window.scrollTo({ top: document.body.scrollHeight });
  432. }
  433. if (messages.length == 2) {
  434. await generateChatTitle(chatId, userPrompt);
  435. }
  436. };
  437. const sendPromptOpenAI = async (userPrompt) => {
  438. if (settings.OPENAI_API_KEY) {
  439. if (models) {
  440. let responseMessage = {
  441. role: 'assistant',
  442. content: ''
  443. };
  444. messages = [...messages, responseMessage];
  445. window.scrollTo({ top: document.body.scrollHeight });
  446. const res = await fetch(`https://api.openai.com/v1/chat/completions`, {
  447. method: 'POST',
  448. headers: {
  449. 'Content-Type': 'application/json',
  450. Authorization: `Bearer ${settings.OPENAI_API_KEY}`
  451. },
  452. body: JSON.stringify({
  453. model: 'gpt-3.5-turbo',
  454. stream: true,
  455. messages: messages.map((message) => ({ ...message, done: undefined }))
  456. })
  457. });
  458. const reader = res.body
  459. .pipeThrough(new TextDecoderStream())
  460. .pipeThrough(splitStream('\n'))
  461. .getReader();
  462. while (true) {
  463. const { value, done } = await reader.read();
  464. if (done || stopResponseFlag) {
  465. if (stopResponseFlag) {
  466. responseMessage.done = true;
  467. messages = messages;
  468. }
  469. break;
  470. }
  471. try {
  472. let lines = value.split('\n');
  473. for (const line of lines) {
  474. if (line !== '') {
  475. console.log(line);
  476. if (line === 'data: [DONE]') {
  477. responseMessage.done = true;
  478. messages = messages;
  479. } else {
  480. let data = JSON.parse(line.replace(/^data: /, ''));
  481. console.log(data);
  482. if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
  483. continue;
  484. } else {
  485. responseMessage.content += data.choices[0].delta.content ?? '';
  486. messages = messages;
  487. }
  488. }
  489. }
  490. }
  491. } catch (error) {
  492. console.log(error);
  493. }
  494. if (autoScroll) {
  495. window.scrollTo({ top: document.body.scrollHeight });
  496. }
  497. await db.put('chats', {
  498. id: chatId,
  499. title: title === '' ? 'New Chat' : title,
  500. model: selectedModel,
  501. system: settings.system,
  502. options: {
  503. temperature: settings.temperature
  504. },
  505. timestamp: Date.now(),
  506. messages: messages
  507. });
  508. }
  509. stopResponseFlag = false;
  510. hljs.highlightAll();
  511. createCopyCodeBlockButton();
  512. renderLatex();
  513. await tick();
  514. if (autoScroll) {
  515. window.scrollTo({ top: document.body.scrollHeight });
  516. }
  517. if (messages.length == 2) {
  518. await setChatTitle(chatId, userPrompt);
  519. }
  520. }
  521. }
  522. };
  523. const submitPrompt = async (userPrompt) => {
  524. console.log('submitPrompt');
  525. if (selectedModel === '') {
  526. toast.error('Model not selected');
  527. } else if (messages.length != 0 && messages.at(-1).done != true) {
  528. console.log('wait');
  529. } else {
  530. if (messages.length == 0) {
  531. await db.put('chats', {
  532. id: chatId,
  533. model: selectedModel,
  534. system: settings.system,
  535. options: {
  536. temperature: settings.temperature
  537. },
  538. title: 'New Chat',
  539. timestamp: Date.now(),
  540. messages: messages
  541. });
  542. chats = await db.getAllFromIndex('chats', 'timestamp');
  543. }
  544. messages = [
  545. ...messages,
  546. {
  547. role: 'user',
  548. content: userPrompt
  549. }
  550. ];
  551. prompt = '';
  552. setTimeout(() => {
  553. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  554. }, 50);
  555. await sendPrompt(userPrompt);
  556. chats = await db.getAllFromIndex('chats', 'timestamp');
  557. }
  558. };
  559. const regenerateResponse = async () => {
  560. console.log('regenerateResponse');
  561. if (messages.length != 0 && messages.at(-1).done == true) {
  562. messages.splice(messages.length - 1, 1);
  563. messages = messages;
  564. let userMessage = messages.at(-1);
  565. let userPrompt = userMessage.content;
  566. await sendPrompt(userPrompt);
  567. chats = await db.getAllFromIndex('chats', 'timestamp');
  568. }
  569. };
  570. const stopResponse = () => {
  571. stopResponseFlag = true;
  572. console.log('stopResponse');
  573. };
  574. const generateChatTitle = async (_chatId, userPrompt) => {
  575. console.log('generateChatTitle');
  576. const res = await fetch(`${API_BASE_URL}/generate`, {
  577. method: 'POST',
  578. headers: {
  579. 'Content-Type': 'text/event-stream'
  580. },
  581. body: JSON.stringify({
  582. model: selectedModel,
  583. prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
  584. stream: false
  585. })
  586. })
  587. .then(async (res) => {
  588. if (!res.ok) throw await res.json();
  589. return res.json();
  590. })
  591. .catch((error) => {
  592. console.log(error);
  593. return null;
  594. });
  595. if (res) {
  596. await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
  597. }
  598. };
  599. const setChatTitle = async (_chatId, _title) => {
  600. const chat = await db.get('chats', _chatId);
  601. await db.put('chats', { ...chat, title: _title });
  602. if (chat.id === chatId) {
  603. title = _title;
  604. }
  605. };
  606. </script>
  607. <svelte:window
  608. on:scroll={(e) => {
  609. autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 30;
  610. }}
  611. />
  612. <div class="app text-gray-100">
  613. <div class=" bg-gray-800 min-h-screen overflow-auto flex flex-row">
  614. <Navbar
  615. selectedChatId={chatId}
  616. {chats}
  617. {title}
  618. {loadChat}
  619. {editChatTitle}
  620. {deleteChat}
  621. {createNewChat}
  622. {importChatHistory}
  623. {exportChatHistory}
  624. {deleteChatHistory}
  625. {openSettings}
  626. />
  627. <SettingsModal bind:show={showSettings} {saveSettings} {getModelTags} />
  628. <div class="min-h-screen w-full flex justify-center">
  629. <div class=" py-2.5 flex flex-col justify-between w-full">
  630. <div class="max-w-2xl mx-auto w-full px-2.5 mt-14">
  631. <div class="p-3 rounded-lg bg-gray-900">
  632. <div>
  633. <label
  634. for="models"
  635. class="block mb-2 text-sm font-medium text-gray-200 flex justify-between"
  636. >
  637. <div class="self-center">Model</div>
  638. <button
  639. class=" self-center hover:text-gray-300"
  640. on:click={() => {
  641. openSettings();
  642. }}
  643. >
  644. <svg
  645. xmlns="http://www.w3.org/2000/svg"
  646. fill="none"
  647. viewBox="0 0 24 24"
  648. stroke-width="1.5"
  649. stroke="currentColor"
  650. class="w-4 h-4"
  651. >
  652. <path
  653. stroke-linecap="round"
  654. stroke-linejoin="round"
  655. 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"
  656. />
  657. <path
  658. stroke-linecap="round"
  659. stroke-linejoin="round"
  660. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  661. />
  662. </svg>
  663. </button>
  664. </label>
  665. <div>
  666. <select
  667. id="models"
  668. class="outline-none border border-gray-600 bg-gray-700 text-gray-200 text-sm rounded-lg block w-full p-2.5 placeholder-gray-400"
  669. bind:value={selectedModel}
  670. disabled={messages.length != 0}
  671. >
  672. <option value="" selected>Select a model</option>
  673. {#each models as model}
  674. {#if model.name === 'hr'}
  675. <hr />
  676. {:else}
  677. <option value={model.name}>{model.name}</option>
  678. {/if}
  679. {/each}
  680. </select>
  681. <div class="text-right mt-1.5 text-xs text-gray-500">
  682. <button on:click={saveDefaultModel}> Set as default</button>
  683. </div>
  684. </div>
  685. </div>
  686. </div>
  687. </div>
  688. <div class=" h-full mb-48 w-full flex flex-col">
  689. {#if messages.length == 0}
  690. <div class="m-auto text-center max-w-md pb-16">
  691. <div class="flex justify-center mt-8">
  692. <img src="/ollama.png" class="w-16 invert-[80%]" />
  693. </div>
  694. <div class="mt-6 text-3xl text-gray-500 font-semibold">
  695. Get up and running with large language models, locally.
  696. </div>
  697. <div class=" my-4 text-gray-600">
  698. Run Llama 2, Code Llama, and other models. <br /> Customize and create your own.
  699. </div>
  700. </div>
  701. {:else}
  702. {#each messages as message, messageIdx}
  703. <div class=" w-full {message.role == 'user' ? '' : ' bg-gray-700'}">
  704. <div class="flex justify-between p-5 py-10 max-w-3xl mx-auto rounded-lg group">
  705. <div class="space-x-7 flex w-full">
  706. <div class="">
  707. <img
  708. src="/{message.role == 'user' ? 'user' : 'favicon'}.png"
  709. class=" max-w-[32px] object-cover rounded"
  710. />
  711. </div>
  712. {#if message.role != 'user' && message.content == ''}
  713. <div class="w-full pr-28">
  714. <div class="animate-pulse flex w-full">
  715. <div class="space-y-2 w-full">
  716. <div class="h-2 bg-gray-600 rounded mr-14" />
  717. <div class="grid grid-cols-3 gap-4">
  718. <div class="h-2 bg-gray-600 rounded col-span-2" />
  719. <div class="h-2 bg-gray-600 rounded col-span-1" />
  720. </div>
  721. <div class="grid grid-cols-4 gap-4">
  722. <div class="h-2 bg-gray-600 rounded col-span-1" />
  723. <div class="h-2 bg-gray-600 rounded col-span-2" />
  724. <div class="h-2 bg-gray-600 rounded col-span-1 mr-4" />
  725. </div>
  726. <div class="h-2 bg-gray-600 rounded" />
  727. </div>
  728. </div>
  729. </div>
  730. {:else}
  731. <div
  732. class="prose chat-{message.role} w-full max-w-full prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-8 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
  733. >
  734. {#if message.role == 'user'}
  735. {#if message?.edit === true}
  736. <div>
  737. <textarea
  738. class=" bg-transparent outline-none w-full resize-none"
  739. bind:value={message.editedContent}
  740. on:input={(e) => {
  741. e.target.style.height = '';
  742. e.target.style.height = `${e.target.scrollHeight}px`;
  743. }}
  744. on:focus={(e) => {
  745. e.target.style.height = '';
  746. e.target.style.height = `${e.target.scrollHeight}px`;
  747. }}
  748. />
  749. <div class=" flex justify-end space-x-2 text-sm text-gray-100">
  750. <button
  751. class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 transition rounded-lg"
  752. on:click={() => {
  753. confirmEditMessage(messageIdx);
  754. }}
  755. >
  756. Save & Submit
  757. </button>
  758. <button
  759. class=" px-4 py-2.5 bg-gray-800 hover:bg-gray-700 transition outline outline-1 outline-gray-600 rounded-lg"
  760. on:click={() => {
  761. cancelEditMessage(messageIdx);
  762. }}
  763. >
  764. Cancel
  765. </button>
  766. </div>
  767. </div>
  768. {:else}
  769. {message.content}
  770. {/if}
  771. {:else}
  772. {@html marked.parse(message.content)}
  773. {#if message.done}
  774. <div class=" flex justify-end space-x-1 text-gray-400">
  775. <button
  776. class="p-1 rounded hover:bg-gray-800 {message.rating === 1
  777. ? 'bg-gray-800'
  778. : ''} transition"
  779. on:click={() => {
  780. rateMessage(messageIdx, 1);
  781. }}
  782. >
  783. <svg
  784. stroke="currentColor"
  785. fill="none"
  786. stroke-width="2"
  787. viewBox="0 0 24 24"
  788. stroke-linecap="round"
  789. stroke-linejoin="round"
  790. class="w-4 h-4"
  791. xmlns="http://www.w3.org/2000/svg"
  792. ><path
  793. d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
  794. /></svg
  795. >
  796. </button>
  797. <button
  798. class="p-1 rounded hover:bg-gray-800 {message.rating === -1
  799. ? 'bg-gray-800'
  800. : ''} transition"
  801. on:click={() => {
  802. rateMessage(messageIdx, -1);
  803. }}
  804. >
  805. <svg
  806. stroke="currentColor"
  807. fill="none"
  808. stroke-width="2"
  809. viewBox="0 0 24 24"
  810. stroke-linecap="round"
  811. stroke-linejoin="round"
  812. class="w-4 h-4"
  813. xmlns="http://www.w3.org/2000/svg"
  814. ><path
  815. d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
  816. /></svg
  817. >
  818. </button>
  819. </div>
  820. {/if}
  821. {/if}
  822. </div>
  823. {/if}
  824. <!-- {} -->
  825. </div>
  826. <div>
  827. {#if message.role == 'user'}
  828. {#if message?.edit !== true}
  829. <button
  830. class="invisible group-hover:visible p-1 rounded hover:bg-gray-700 transition"
  831. on:click={() => {
  832. editMessage(messageIdx);
  833. }}
  834. >
  835. <svg
  836. xmlns="http://www.w3.org/2000/svg"
  837. viewBox="0 0 20 20"
  838. fill="currentColor"
  839. class="w-4 h-4"
  840. >
  841. <path
  842. d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
  843. />
  844. <path
  845. d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
  846. />
  847. </svg>
  848. </button>
  849. {/if}
  850. {:else if message.done}
  851. <button
  852. class="p-1 rounded hover:bg-gray-700 transition"
  853. on:click={() => {
  854. copyToClipboard(message.content);
  855. }}
  856. >
  857. <svg
  858. xmlns="http://www.w3.org/2000/svg"
  859. fill="none"
  860. viewBox="0 0 24 24"
  861. stroke-width="1.5"
  862. stroke="currentColor"
  863. class="w-4 h-4"
  864. >
  865. <path
  866. stroke-linecap="round"
  867. stroke-linejoin="round"
  868. d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
  869. />
  870. </svg>
  871. </button>
  872. {/if}
  873. </div>
  874. </div>
  875. </div>
  876. {/each}
  877. {/if}
  878. </div>
  879. </div>
  880. <div class="fixed bottom-0 w-full">
  881. <!-- <hr class=" mb-3 border-gray-600" /> -->
  882. <div class=" bg-gradient-to-t from-gray-900 pt-5">
  883. <div class="max-w-3xl p-2.5 -mb-0.5 mx-auto inset-x-0">
  884. {#if messages.length == 0 && suggestions !== 'false'}
  885. <Suggestions {submitPrompt} />
  886. {/if}
  887. {#if messages.length != 0 && messages.at(-1).role == 'assistant'}
  888. {#if messages.at(-1).done == true}
  889. <div class=" flex justify-end mb-2.5">
  890. <button
  891. class=" flex px-4 py-2.5 bg-gray-800 hover:bg-gray-700 outline outline-1 outline-gray-600 rounded-lg"
  892. on:click={regenerateResponse}
  893. >
  894. <div class=" self-center mr-1">
  895. <svg
  896. xmlns="http://www.w3.org/2000/svg"
  897. viewBox="0 0 20 20"
  898. fill="currentColor"
  899. class="w-4 h-4"
  900. >
  901. <path
  902. fill-rule="evenodd"
  903. d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
  904. clip-rule="evenodd"
  905. />
  906. </svg>
  907. </div>
  908. <div class=" self-center text-sm">Regenerate</div>
  909. </button>
  910. </div>
  911. {:else}
  912. <div class=" flex justify-end mb-2.5">
  913. <button
  914. class=" flex px-4 py-2.5 bg-gray-800 hover:bg-gray-700 outline outline-1 outline-gray-600 rounded-lg"
  915. on:click={stopResponse}
  916. >
  917. <div class=" self-center mr-1">
  918. <svg
  919. xmlns="http://www.w3.org/2000/svg"
  920. viewBox="0 0 20 20"
  921. fill="currentColor"
  922. class="w-4 h-4"
  923. >
  924. <path
  925. fill-rule="evenodd"
  926. d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h4.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-4.5a.75.75 0 01-.75-.75v-4.5z"
  927. clip-rule="evenodd"
  928. />
  929. </svg>
  930. </div>
  931. <div class=" self-center text-sm">Stop generating</div>
  932. </button>
  933. </div>
  934. {/if}
  935. {/if}
  936. <form
  937. class=" flex shadow-sm relative w-full"
  938. on:submit|preventDefault={() => {
  939. submitPrompt(prompt);
  940. }}
  941. >
  942. <textarea
  943. class="rounded-xl bg-gray-700 outline-none w-full py-3 px-5 pr-12 resize-none"
  944. placeholder="Send a message"
  945. bind:value={prompt}
  946. on:keypress={(e) => {
  947. if (e.keyCode == 13 && !e.shiftKey) {
  948. e.preventDefault();
  949. }
  950. if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
  951. submitPrompt(prompt);
  952. e.target.style.height = '';
  953. }
  954. }}
  955. rows="1"
  956. on:input={(e) => {
  957. e.target.style.height = '';
  958. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
  959. }}
  960. />
  961. <div class=" absolute right-0 bottom-0">
  962. <div class="pr-3 pb-2">
  963. {#if messages.length == 0 || messages.at(-1).done == true}
  964. <button
  965. class="{prompt !== ''
  966. ? 'bg-emerald-600 text-gray-100 hover:bg-emerald-700 '
  967. : 'text-gray-600 disabled'} transition rounded p-2"
  968. type="submit"
  969. disabled={prompt === ''}
  970. >
  971. <svg
  972. xmlns="http://www.w3.org/2000/svg"
  973. viewBox="0 0 16 16"
  974. fill="none"
  975. class="w-4 h-4"
  976. ><path
  977. d="M.5 1.163A1 1 0 0 1 1.97.28l12.868 6.837a1 1 0 0 1 0 1.766L1.969 15.72A1 1 0 0 1 .5 14.836V10.33a1 1 0 0 1 .816-.983L8.5 8 1.316 6.653A1 1 0 0 1 .5 5.67V1.163Z"
  978. fill="currentColor"
  979. /></svg
  980. >
  981. </button>
  982. {:else}
  983. <div class="loading mb-1.5 mr-1 font-semibold text-lg">...</div>
  984. {/if}
  985. </div>
  986. </div>
  987. </form>
  988. <div class="mt-2.5 text-xs text-gray-500 text-center">
  989. LLMs may produce inaccurate information about people, places, or facts.
  990. </div>
  991. </div>
  992. </div>
  993. </div>
  994. </div>
  995. <div class=" hidden katex" />
  996. <!-- <main class="w-full flex justify-center">
  997. <div class="max-w-lg w-screen p-5" />
  998. </main> -->
  999. </div>
  1000. </div>
  1001. <style>
  1002. .loading {
  1003. display: inline-block;
  1004. clip-path: inset(0 1ch 0 0);
  1005. animation: l 1s steps(3) infinite;
  1006. letter-spacing: -0.5px;
  1007. }
  1008. @keyframes l {
  1009. to {
  1010. clip-path: inset(0 -1ch 0 0);
  1011. }
  1012. }
  1013. pre[class*='language-'] {
  1014. position: relative;
  1015. overflow: auto;
  1016. /* make space */
  1017. margin: 5px 0;
  1018. padding: 1.75rem 0 1.75rem 1rem;
  1019. border-radius: 10px;
  1020. }
  1021. pre[class*='language-'] button {
  1022. position: absolute;
  1023. top: 5px;
  1024. right: 5px;
  1025. font-size: 0.9rem;
  1026. padding: 0.15rem;
  1027. background-color: #828282;
  1028. border: ridge 1px #7b7b7c;
  1029. border-radius: 5px;
  1030. text-shadow: #c4c4c4 0 0 2px;
  1031. }
  1032. pre[class*='language-'] button:hover {
  1033. cursor: pointer;
  1034. background-color: #bcbabb;
  1035. }
  1036. </style>