+page.svelte 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  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 auto_render from 'katex/dist/contrib/auto-render.mjs';
  10. import 'katex/dist/katex.min.css';
  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 Promise.all([await createNewChat(true), await setDBandLoadChats()]);
  36. });
  37. //////////////////////////
  38. // Helper functions
  39. //////////////////////////
  40. const splitStream = (splitOn) => {
  41. let buffer = '';
  42. return new TransformStream({
  43. transform(chunk, controller) {
  44. buffer += chunk;
  45. const parts = buffer.split(splitOn);
  46. parts.slice(0, -1).forEach((part) => controller.enqueue(part));
  47. buffer = parts[parts.length - 1];
  48. },
  49. flush(controller) {
  50. if (buffer) controller.enqueue(buffer);
  51. }
  52. });
  53. };
  54. const copyToClipboard = (text) => {
  55. if (!navigator.clipboard) {
  56. var textArea = document.createElement('textarea');
  57. textArea.value = text;
  58. // Avoid scrolling to bottom
  59. textArea.style.top = '0';
  60. textArea.style.left = '0';
  61. textArea.style.position = 'fixed';
  62. document.body.appendChild(textArea);
  63. textArea.focus();
  64. textArea.select();
  65. try {
  66. var successful = document.execCommand('copy');
  67. var msg = successful ? 'successful' : 'unsuccessful';
  68. console.log('Fallback: Copying text command was ' + msg);
  69. } catch (err) {
  70. console.error('Fallback: Oops, unable to copy', err);
  71. }
  72. document.body.removeChild(textArea);
  73. return;
  74. }
  75. navigator.clipboard.writeText(text).then(
  76. function () {
  77. console.log('Async: Copying to clipboard was successful!');
  78. toast.success('Copying to clipboard was successful!');
  79. },
  80. function (err) {
  81. console.error('Async: Could not copy text: ', err);
  82. }
  83. );
  84. };
  85. const createCopyCodeBlockButton = () => {
  86. // use a class selector if available
  87. let blocks = document.querySelectorAll('pre');
  88. console.log(blocks);
  89. blocks.forEach((block) => {
  90. // only add button if browser supports Clipboard API
  91. if (navigator.clipboard && block.childNodes.length < 2) {
  92. let code = block.querySelector('code');
  93. code.style.borderTopRightRadius = 0;
  94. code.style.borderTopLeftRadius = 0;
  95. let topBarDiv = document.createElement('div');
  96. topBarDiv.style.backgroundColor = '#202123';
  97. topBarDiv.style.overflowX = 'auto';
  98. topBarDiv.style.display = 'flex';
  99. topBarDiv.style.justifyContent = 'space-between';
  100. topBarDiv.style.padding = '0 1rem';
  101. topBarDiv.style.paddingTop = '4px';
  102. topBarDiv.style.borderTopRightRadius = '8px';
  103. topBarDiv.style.borderTopLeftRadius = '8px';
  104. let langDiv = document.createElement('div');
  105. let codeClassNames = code?.className.split(' ');
  106. langDiv.textContent =
  107. codeClassNames[0] === 'hljs' ? codeClassNames[1].slice(9) : codeClassNames[0].slice(9);
  108. langDiv.style.color = 'white';
  109. langDiv.style.margin = '4px';
  110. langDiv.style.fontSize = '0.75rem';
  111. let button = document.createElement('button');
  112. button.textContent = 'Copy Code';
  113. button.style.background = 'none';
  114. button.style.fontSize = '0.75rem';
  115. button.style.border = 'none';
  116. button.style.margin = '4px';
  117. button.style.cursor = 'pointer';
  118. button.style.color = '#ddd';
  119. button.addEventListener('click', () => copyCode(block, button));
  120. topBarDiv.appendChild(langDiv);
  121. topBarDiv.appendChild(button);
  122. block.prepend(topBarDiv);
  123. // button.addEventListener('click', async () => {
  124. // await copyCode(block, button);
  125. // });
  126. }
  127. });
  128. async function copyCode(block, button) {
  129. let code = block.querySelector('code');
  130. let text = code.innerText;
  131. await navigator.clipboard.writeText(text);
  132. // visual feedback that task is completed
  133. button.innerText = 'Copied!';
  134. setTimeout(() => {
  135. button.innerText = 'Copy Code';
  136. }, 1000);
  137. }
  138. };
  139. const renderLatex = () => {
  140. let chatMessageElements = document.getElementsByClassName('chat-assistant');
  141. // let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
  142. for (const element of chatMessageElements) {
  143. auto_render(element, {
  144. // customised options
  145. // • auto-render specific keys, e.g.:
  146. delimiters: [
  147. { left: '$$', right: '$$', display: true },
  148. { left: '$', right: '$', display: true },
  149. { left: '\\(', right: '\\)', display: true },
  150. { left: '\\[', right: '\\]', display: true }
  151. ],
  152. // • rendering keys, e.g.:
  153. throwOnError: false
  154. });
  155. }
  156. };
  157. //////////////////////////
  158. // Web functions
  159. //////////////////////////
  160. const createNewChat = async (init = false) => {
  161. if (init || messages.length > 0) {
  162. chatId = uuidv4();
  163. messages = [];
  164. title = '';
  165. settings = JSON.parse(localStorage.getItem('settings') ?? JSON.stringify(settings));
  166. API_BASE_URL = settings?.API_BASE_URL ?? BUILD_TIME_API_BASE_URL;
  167. console.log(API_BASE_URL);
  168. if (models.length === 0) {
  169. await getModelTags();
  170. }
  171. selectedModel =
  172. settings.model && models.map((model) => model.name).includes(settings.model)
  173. ? settings.model
  174. : '';
  175. console.log(chatId);
  176. }
  177. };
  178. const setDBandLoadChats = async () => {
  179. db = await openDB('Chats', 1, {
  180. upgrade(db) {
  181. const store = db.createObjectStore('chats', {
  182. keyPath: 'id',
  183. autoIncrement: true
  184. });
  185. store.createIndex('timestamp', 'timestamp');
  186. }
  187. });
  188. chats = await db.getAllFromIndex('chats', 'timestamp');
  189. };
  190. const saveDefaultModel = () => {
  191. settings.model = selectedModel;
  192. localStorage.setItem('settings', JSON.stringify(settings));
  193. toast.success('Default model updated');
  194. };
  195. const saveSettings = async (updated) => {
  196. console.log(updated);
  197. settings = { ...settings, ...updated };
  198. localStorage.setItem('settings', JSON.stringify(settings));
  199. API_BASE_URL = updated?.API_BASE_URL ?? API_BASE_URL;
  200. await getModelTags();
  201. };
  202. const loadChat = async (id) => {
  203. const chat = await db.get('chats', id);
  204. if (chatId !== chat.id) {
  205. if (chat.messages.length > 0) {
  206. chat.messages.at(-1).done = true;
  207. }
  208. messages = chat.messages;
  209. title = chat.title;
  210. chatId = chat.id;
  211. selectedModel = chat.model ?? selectedModel;
  212. settings.system = chat.system ?? settings.system;
  213. settings.temperature = chat.temperature ?? settings.temperature;
  214. await tick();
  215. renderLatex();
  216. hljs.highlightAll();
  217. createCopyCodeBlockButton();
  218. }
  219. };
  220. const editChatTitle = async (id, _title) => {
  221. const chat = await db.get('chats', id);
  222. console.log(chat);
  223. await db.put('chats', {
  224. ...chat,
  225. title: _title
  226. });
  227. title = _title;
  228. chats = await db.getAllFromIndex('chats', 'timestamp');
  229. };
  230. const deleteChat = async (id) => {
  231. createNewChat();
  232. const chat = await db.delete('chats', id);
  233. console.log(chat);
  234. chats = await db.getAllFromIndex('chats', 'timestamp');
  235. };
  236. const deleteChatHistory = async () => {
  237. const tx = db.transaction('chats', 'readwrite');
  238. await Promise.all([tx.store.clear(), tx.done]);
  239. chats = await db.getAllFromIndex('chats', 'timestamp');
  240. };
  241. const importChatHistory = async (chatHistory) => {
  242. for (const chat of chatHistory) {
  243. console.log(chat);
  244. await db.put('chats', {
  245. id: chat.id,
  246. model: chat.model,
  247. system: chat.system,
  248. options: chat.options,
  249. title: chat.title,
  250. timestamp: chat.timestamp,
  251. messages: chat.messages
  252. });
  253. }
  254. chats = await db.getAllFromIndex('chats', 'timestamp');
  255. console.log(chats);
  256. };
  257. const exportChatHistory = async () => {
  258. chats = await db.getAllFromIndex('chats', 'timestamp');
  259. let blob = new Blob([JSON.stringify(chats)], { type: 'application/json' });
  260. saveAs(blob, `chat-export-${Date.now()}.json`);
  261. };
  262. const openSettings = async () => {
  263. showSettings = true;
  264. };
  265. const editMessage = async (messageIdx) => {
  266. messages = messages.map((message, idx) => {
  267. if (messageIdx === idx) {
  268. message.edit = true;
  269. message.editedContent = message.content;
  270. }
  271. return message;
  272. });
  273. };
  274. const confirmEditMessage = async (messageIdx) => {
  275. let userPrompt = messages.at(messageIdx).editedContent;
  276. messages.splice(messageIdx, messages.length - messageIdx);
  277. messages = messages;
  278. await submitPrompt(userPrompt);
  279. };
  280. const cancelEditMessage = (messageIdx) => {
  281. messages = messages.map((message, idx) => {
  282. if (messageIdx === idx) {
  283. message.edit = undefined;
  284. message.editedContent = undefined;
  285. }
  286. return message;
  287. });
  288. console.log(messages);
  289. };
  290. const rateMessage = async (messageIdx, rating) => {
  291. messages = messages.map((message, idx) => {
  292. if (messageIdx === idx) {
  293. message.rating = rating;
  294. }
  295. return message;
  296. });
  297. await db.put('chats', {
  298. id: chatId,
  299. title: title === '' ? 'New Chat' : title,
  300. model: selectedModel,
  301. system: settings.system,
  302. options: {
  303. temperature: settings.temperature
  304. },
  305. timestamp: Date.now(),
  306. messages: messages
  307. });
  308. console.log(messages);
  309. };
  310. //////////////////////////
  311. // Ollama functions
  312. //////////////////////////
  313. const getModelTags = async (url = null, type = 'all') => {
  314. const res = await fetch(`${url === null ? API_BASE_URL : url}/tags`, {
  315. method: 'GET',
  316. headers: {
  317. Accept: 'application/json',
  318. 'Content-Type': 'application/json'
  319. }
  320. })
  321. .then(async (res) => {
  322. if (!res.ok) throw await res.json();
  323. return res.json();
  324. })
  325. .catch((error) => {
  326. console.log(error);
  327. toast.error('Server connection failed');
  328. return null;
  329. });
  330. console.log(res);
  331. if (type === 'all') {
  332. if (settings.OPENAI_API_KEY) {
  333. // Validate OPENAI_API_KEY
  334. const openaiModelRes = await fetch(`https://api.openai.com/v1/models`, {
  335. method: 'GET',
  336. headers: {
  337. 'Content-Type': 'application/json',
  338. Authorization: `Bearer ${settings.OPENAI_API_KEY}`
  339. }
  340. })
  341. .then(async (res) => {
  342. if (!res.ok) throw await res.json();
  343. return res.json();
  344. })
  345. .catch((error) => {
  346. console.log(error);
  347. toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
  348. return null;
  349. });
  350. const openaiModels = openaiModelRes?.data ?? null;
  351. if (openaiModels) {
  352. models = [
  353. ...(res?.models ?? []),
  354. { name: 'hr' },
  355. ...openaiModels
  356. .map((model) => ({ name: model.id, label: 'OpenAI' }))
  357. .filter((model) => model.name.includes('gpt'))
  358. ];
  359. } else {
  360. models = res?.models ?? [];
  361. }
  362. } else {
  363. models = res?.models ?? [];
  364. }
  365. return models;
  366. } else {
  367. return res?.models ?? null;
  368. }
  369. };
  370. const sendPrompt = async (userPrompt) => {
  371. if (selectedModel.includes('gpt-')) {
  372. await sendPromptOpenAI(userPrompt);
  373. } else {
  374. await sendPromptOllama(userPrompt);
  375. }
  376. };
  377. const sendPromptOllama = async (userPrompt) => {
  378. let responseMessage = {
  379. role: 'assistant',
  380. content: ''
  381. };
  382. messages = [...messages, responseMessage];
  383. window.scrollTo({ top: document.body.scrollHeight });
  384. const res = await fetch(`${API_BASE_URL}/generate`, {
  385. method: 'POST',
  386. headers: {
  387. 'Content-Type': 'text/event-stream'
  388. },
  389. body: JSON.stringify({
  390. model: selectedModel,
  391. prompt: userPrompt,
  392. system: settings.system ?? undefined,
  393. options: {
  394. seed: settings.seed ?? undefined,
  395. temperature: settings.temperature ?? undefined,
  396. repeat_penalty: settings.repeat_penalty ?? undefined,
  397. top_k: settings.top_k ?? undefined,
  398. top_p: settings.top_p ?? undefined
  399. },
  400. format: settings.requestFormat ?? undefined,
  401. context:
  402. messages.length > 3 && messages.at(-3).context != undefined
  403. ? messages.at(-3).context
  404. : undefined
  405. })
  406. });
  407. const reader = res.body
  408. .pipeThrough(new TextDecoderStream())
  409. .pipeThrough(splitStream('\n'))
  410. .getReader();
  411. while (true) {
  412. const { value, done } = await reader.read();
  413. if (done || stopResponseFlag) {
  414. if (stopResponseFlag) {
  415. responseMessage.done = true;
  416. messages = messages;
  417. hljs.highlightAll();
  418. createCopyCodeBlockButton();
  419. renderLatex();
  420. }
  421. break;
  422. }
  423. try {
  424. let lines = value.split('\n');
  425. for (const line of lines) {
  426. if (line !== '') {
  427. console.log(line);
  428. let data = JSON.parse(line);
  429. if (data.done == false) {
  430. if (responseMessage.content == '' && data.response == '\n') {
  431. continue;
  432. } else {
  433. responseMessage.content += data.response;
  434. messages = messages;
  435. }
  436. } else {
  437. responseMessage.done = true;
  438. responseMessage.context = data.context;
  439. messages = messages;
  440. hljs.highlightAll();
  441. createCopyCodeBlockButton();
  442. renderLatex();
  443. }
  444. }
  445. }
  446. } catch (error) {
  447. console.log(error);
  448. }
  449. if (autoScroll) {
  450. window.scrollTo({ top: document.body.scrollHeight });
  451. }
  452. await db.put('chats', {
  453. id: chatId,
  454. title: title === '' ? 'New Chat' : title,
  455. model: selectedModel,
  456. system: settings.system,
  457. options: {
  458. temperature: settings.temperature
  459. },
  460. timestamp: Date.now(),
  461. messages: messages
  462. });
  463. }
  464. stopResponseFlag = false;
  465. await tick();
  466. if (autoScroll) {
  467. window.scrollTo({ top: document.body.scrollHeight });
  468. }
  469. if (messages.length == 2) {
  470. await generateChatTitle(chatId, userPrompt);
  471. }
  472. };
  473. const sendPromptOpenAI = async (userPrompt) => {
  474. if (settings.OPENAI_API_KEY) {
  475. if (models) {
  476. let responseMessage = {
  477. role: 'assistant',
  478. content: ''
  479. };
  480. messages = [...messages, responseMessage];
  481. window.scrollTo({ top: document.body.scrollHeight });
  482. const res = await fetch(`https://api.openai.com/v1/chat/completions`, {
  483. method: 'POST',
  484. headers: {
  485. 'Content-Type': 'application/json',
  486. Authorization: `Bearer ${settings.OPENAI_API_KEY}`
  487. },
  488. body: JSON.stringify({
  489. model: selectedModel,
  490. stream: true,
  491. messages: [
  492. settings.system
  493. ? {
  494. role: 'system',
  495. content: settings.system
  496. }
  497. : undefined,
  498. ...messages
  499. ]
  500. .filter((message) => message)
  501. .map((message) => ({ ...message, done: undefined })),
  502. temperature: settings.temperature ?? undefined,
  503. top_p: settings.top_p ?? undefined,
  504. frequency_penalty: settings.repeat_penalty ?? undefined
  505. })
  506. });
  507. const reader = res.body
  508. .pipeThrough(new TextDecoderStream())
  509. .pipeThrough(splitStream('\n'))
  510. .getReader();
  511. while (true) {
  512. const { value, done } = await reader.read();
  513. if (done || stopResponseFlag) {
  514. if (stopResponseFlag) {
  515. responseMessage.done = true;
  516. messages = messages;
  517. }
  518. break;
  519. }
  520. try {
  521. let lines = value.split('\n');
  522. for (const line of lines) {
  523. if (line !== '') {
  524. console.log(line);
  525. if (line === 'data: [DONE]') {
  526. responseMessage.done = true;
  527. messages = messages;
  528. } else {
  529. let data = JSON.parse(line.replace(/^data: /, ''));
  530. console.log(data);
  531. if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
  532. continue;
  533. } else {
  534. responseMessage.content += data.choices[0].delta.content ?? '';
  535. messages = messages;
  536. }
  537. }
  538. }
  539. }
  540. } catch (error) {
  541. console.log(error);
  542. }
  543. if (autoScroll) {
  544. window.scrollTo({ top: document.body.scrollHeight });
  545. }
  546. await db.put('chats', {
  547. id: chatId,
  548. title: title === '' ? 'New Chat' : title,
  549. model: selectedModel,
  550. system: settings.system,
  551. options: {
  552. temperature: settings.temperature
  553. },
  554. timestamp: Date.now(),
  555. messages: messages
  556. });
  557. }
  558. stopResponseFlag = false;
  559. hljs.highlightAll();
  560. createCopyCodeBlockButton();
  561. renderLatex();
  562. await tick();
  563. if (autoScroll) {
  564. window.scrollTo({ top: document.body.scrollHeight });
  565. }
  566. if (messages.length == 2) {
  567. await setChatTitle(chatId, userPrompt);
  568. }
  569. }
  570. }
  571. };
  572. const submitPrompt = async (userPrompt) => {
  573. console.log('submitPrompt');
  574. if (selectedModel === '') {
  575. toast.error('Model not selected');
  576. } else if (messages.length != 0 && messages.at(-1).done != true) {
  577. console.log('wait');
  578. } else {
  579. document.getElementById('chat-textarea').style.height = '';
  580. messages = [
  581. ...messages,
  582. {
  583. role: 'user',
  584. content: userPrompt
  585. }
  586. ];
  587. prompt = '';
  588. if (messages.length == 0) {
  589. await db.put('chats', {
  590. id: chatId,
  591. model: selectedModel,
  592. system: settings.system,
  593. options: {
  594. temperature: settings.temperature
  595. },
  596. title: 'New Chat',
  597. timestamp: Date.now(),
  598. messages: messages
  599. });
  600. chats = await db.getAllFromIndex('chats', 'timestamp');
  601. }
  602. setTimeout(() => {
  603. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  604. }, 50);
  605. await sendPrompt(userPrompt);
  606. chats = await db.getAllFromIndex('chats', 'timestamp');
  607. }
  608. };
  609. const regenerateResponse = async () => {
  610. console.log('regenerateResponse');
  611. if (messages.length != 0 && messages.at(-1).done == true) {
  612. messages.splice(messages.length - 1, 1);
  613. messages = messages;
  614. let userMessage = messages.at(-1);
  615. let userPrompt = userMessage.content;
  616. await sendPrompt(userPrompt);
  617. chats = await db.getAllFromIndex('chats', 'timestamp');
  618. }
  619. };
  620. const stopResponse = () => {
  621. stopResponseFlag = true;
  622. console.log('stopResponse');
  623. };
  624. const generateChatTitle = async (_chatId, userPrompt) => {
  625. console.log('generateChatTitle');
  626. const res = await fetch(`${API_BASE_URL}/generate`, {
  627. method: 'POST',
  628. headers: {
  629. 'Content-Type': 'text/event-stream'
  630. },
  631. body: JSON.stringify({
  632. model: selectedModel,
  633. prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
  634. stream: false
  635. })
  636. })
  637. .then(async (res) => {
  638. if (!res.ok) throw await res.json();
  639. return res.json();
  640. })
  641. .catch((error) => {
  642. console.log(error);
  643. return null;
  644. });
  645. if (res) {
  646. await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
  647. }
  648. };
  649. const setChatTitle = async (_chatId, _title) => {
  650. const chat = await db.get('chats', _chatId);
  651. await db.put('chats', { ...chat, title: _title });
  652. if (chat.id === chatId) {
  653. title = _title;
  654. }
  655. };
  656. </script>
  657. <svelte:window
  658. on:scroll={(e) => {
  659. autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
  660. }}
  661. />
  662. <div class="app">
  663. <div
  664. class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row"
  665. >
  666. <Navbar
  667. selectedChatId={chatId}
  668. {chats}
  669. {title}
  670. {loadChat}
  671. {editChatTitle}
  672. {deleteChat}
  673. {createNewChat}
  674. {importChatHistory}
  675. {exportChatHistory}
  676. {deleteChatHistory}
  677. {openSettings}
  678. />
  679. <SettingsModal bind:show={showSettings} {saveSettings} {getModelTags} />
  680. <div class="min-h-screen w-full flex justify-center">
  681. <div class=" py-2.5 flex flex-col justify-between w-full">
  682. <div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-14">
  683. <div class="flex justify-between my-2 text-sm">
  684. <select
  685. id="models"
  686. class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-800"
  687. bind:value={selectedModel}
  688. disabled={messages.length != 0}
  689. >
  690. <option class=" text-gray-700" value="" selected>Select a model</option>
  691. {#each models as model}
  692. {#if model.name === 'hr'}
  693. <hr />
  694. {:else}
  695. <option value={model.name} class="text-gray-700 text-lg">{model.name}</option>
  696. {/if}
  697. {/each}
  698. </select>
  699. <button
  700. class=" self-center dark:hover:text-gray-300"
  701. on:click={() => {
  702. openSettings();
  703. }}
  704. >
  705. <svg
  706. xmlns="http://www.w3.org/2000/svg"
  707. fill="none"
  708. viewBox="0 0 24 24"
  709. stroke-width="1.5"
  710. stroke="currentColor"
  711. class="w-4 h-4"
  712. >
  713. <path
  714. stroke-linecap="round"
  715. stroke-linejoin="round"
  716. 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"
  717. />
  718. <path
  719. stroke-linecap="round"
  720. stroke-linejoin="round"
  721. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  722. />
  723. </svg>
  724. </button>
  725. </div>
  726. <!-- <div class="flex flex-col">
  727. {#each selectedModels as selectedModel, selectedModelIdx}
  728. <div class="flex">
  729. <select
  730. id="models"
  731. class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400"
  732. bind:value={selectedModel}
  733. disabled={messages.length != 0}
  734. >
  735. <option value="" selected>Select a model</option>
  736. {#each models as model}
  737. {#if model.name === 'hr'}
  738. <hr />
  739. {:else}
  740. <option value={model.name} class=" text-lg">{model.name}</option>
  741. {/if}
  742. {/each}
  743. </select>
  744. {#if selectedModelIdx === selectedModels.length - 1}
  745. <button
  746. class=" self-center {selectedModelIdx === 0
  747. ? 'mr-3'
  748. : 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600"
  749. disabled={selectedModels.length === 3}
  750. on:click={() => {
  751. if (selectedModels.length < 3) {
  752. selectedModels = ['', ...selectedModels];
  753. }
  754. }}
  755. >
  756. <svg
  757. xmlns="http://www.w3.org/2000/svg"
  758. fill="none"
  759. viewBox="0 0 24 24"
  760. stroke-width="1.5"
  761. stroke="currentColor"
  762. class="w-4 h-4"
  763. >
  764. <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
  765. </svg>
  766. </button>
  767. {:else}
  768. <button
  769. class=" self-center dark:hover:text-gray-300 {selectedModelIdx === 0
  770. ? 'mr-3'
  771. : 'mr-7'}"
  772. on:click={() => {
  773. selectedModels.splice(selectedModelIdx, 1);
  774. selectedModels = selectedModels;
  775. }}
  776. >
  777. <svg
  778. xmlns="http://www.w3.org/2000/svg"
  779. fill="none"
  780. viewBox="0 0 24 24"
  781. stroke-width="1.5"
  782. stroke="currentColor"
  783. class="w-4 h-4"
  784. >
  785. <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
  786. </svg>
  787. </button>
  788. {/if}
  789. {#if selectedModelIdx === 0}
  790. <button
  791. class=" self-center dark:hover:text-gray-300"
  792. on:click={() => {
  793. openSettings();
  794. }}
  795. >
  796. <svg
  797. xmlns="http://www.w3.org/2000/svg"
  798. fill="none"
  799. viewBox="0 0 24 24"
  800. stroke-width="1.5"
  801. stroke="currentColor"
  802. class="w-4 h-4"
  803. >
  804. <path
  805. stroke-linecap="round"
  806. stroke-linejoin="round"
  807. 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"
  808. />
  809. <path
  810. stroke-linecap="round"
  811. stroke-linejoin="round"
  812. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  813. />
  814. </svg>
  815. </button>
  816. {/if}
  817. </div>
  818. {/each}
  819. </div> -->
  820. <div class="text-left mt-1.5 text-xs text-gray-500">
  821. <button on:click={saveDefaultModel}> Set as default</button>
  822. </div>
  823. </div>
  824. <div class=" h-full mt-10 mb-32 w-full flex flex-col">
  825. {#if messages.length == 0}
  826. <div class="m-auto text-center max-w-md pb-56 px-2">
  827. <div class="flex justify-center mt-8">
  828. <img src="/ollama.png" class=" w-16 invert-[10%] dark:invert-[100%] rounded-full" />
  829. </div>
  830. <div class=" mt-1 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
  831. How can I help you today?
  832. </div>
  833. </div>
  834. {:else}
  835. {#each messages as message, messageIdx}
  836. <div class=" w-full">
  837. <div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
  838. <div class=" flex w-full">
  839. <div class=" mr-4">
  840. <img
  841. src="{message.role == 'user'
  842. ? settings.gravatarUrl
  843. ? settings.gravatarUrl
  844. : '/user'
  845. : '/favicon'}.png"
  846. class=" max-w-[28px] object-cover rounded-full"
  847. />
  848. </div>
  849. <div class="w-full">
  850. <div class=" self-center font-bold mb-0.5">
  851. {message.role === 'user' ? 'You' : 'Ollama'}
  852. </div>
  853. {#if message.role !== 'user' && message.content === ''}
  854. <div class="w-full mt-3">
  855. <div class="animate-pulse flex w-full">
  856. <div class="space-y-2 w-full">
  857. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
  858. <div class="grid grid-cols-3 gap-4">
  859. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
  860. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
  861. </div>
  862. <div class="grid grid-cols-4 gap-4">
  863. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
  864. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
  865. <div
  866. class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4"
  867. />
  868. </div>
  869. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded" />
  870. </div>
  871. </div>
  872. </div>
  873. {:else}
  874. <div
  875. class="prose chat-{message.role} w-full max-w-full dark: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-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
  876. >
  877. {#if message.role == 'user'}
  878. {#if message?.edit === true}
  879. <div class=" w-full">
  880. <textarea
  881. class=" bg-transparent outline-none w-full resize-none"
  882. bind:value={message.editedContent}
  883. on:input={(e) => {
  884. e.target.style.height = '';
  885. e.target.style.height = `${e.target.scrollHeight}px`;
  886. }}
  887. on:focus={(e) => {
  888. e.target.style.height = '';
  889. e.target.style.height = `${e.target.scrollHeight}px`;
  890. }}
  891. />
  892. <div class=" flex justify-end space-x-2 text-sm font-medium">
  893. <button
  894. class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
  895. on:click={() => {
  896. confirmEditMessage(messageIdx);
  897. }}
  898. >
  899. Save & Submit
  900. </button>
  901. <button
  902. class=" px-4 py-2.5 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
  903. on:click={() => {
  904. cancelEditMessage(messageIdx);
  905. }}
  906. >
  907. Cancel
  908. </button>
  909. </div>
  910. </div>
  911. {:else}
  912. <div class="w-full">
  913. {message.content}
  914. <div class=" flex justify-start space-x-1">
  915. <button
  916. class="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
  917. on:click={() => {
  918. editMessage(messageIdx);
  919. }}
  920. >
  921. <svg
  922. xmlns="http://www.w3.org/2000/svg"
  923. fill="none"
  924. viewBox="0 0 24 24"
  925. stroke-width="1.5"
  926. stroke="currentColor"
  927. class="w-4 h-4"
  928. >
  929. <path
  930. stroke-linecap="round"
  931. stroke-linejoin="round"
  932. d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
  933. />
  934. </svg>
  935. </button>
  936. </div>
  937. </div>
  938. {/if}
  939. {:else}
  940. <div class="w-full">
  941. {@html marked(message.content.replace('\\\\', '\\\\\\'))}
  942. {#if message.done}
  943. <div class=" flex justify-start space-x-1 -mt-2">
  944. <button
  945. class="{messageIdx + 1 === messages.length
  946. ? 'visible'
  947. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  948. on:click={() => {
  949. copyToClipboard(message.content);
  950. }}
  951. >
  952. <svg
  953. xmlns="http://www.w3.org/2000/svg"
  954. fill="none"
  955. viewBox="0 0 24 24"
  956. stroke-width="1.5"
  957. stroke="currentColor"
  958. class="w-4 h-4"
  959. >
  960. <path
  961. stroke-linecap="round"
  962. stroke-linejoin="round"
  963. 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"
  964. />
  965. </svg>
  966. </button>
  967. <button
  968. class="{messageIdx + 1 === messages.length
  969. ? 'visible'
  970. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  971. on:click={() => {
  972. rateMessage(messageIdx, 1);
  973. }}
  974. >
  975. <svg
  976. stroke="currentColor"
  977. fill="none"
  978. stroke-width="2"
  979. viewBox="0 0 24 24"
  980. stroke-linecap="round"
  981. stroke-linejoin="round"
  982. class="w-4 h-4"
  983. xmlns="http://www.w3.org/2000/svg"
  984. ><path
  985. 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"
  986. /></svg
  987. >
  988. </button>
  989. <button
  990. class="{messageIdx + 1 === messages.length
  991. ? 'visible'
  992. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  993. on:click={() => {
  994. rateMessage(messageIdx, -1);
  995. }}
  996. >
  997. <svg
  998. stroke="currentColor"
  999. fill="none"
  1000. stroke-width="2"
  1001. viewBox="0 0 24 24"
  1002. stroke-linecap="round"
  1003. stroke-linejoin="round"
  1004. class="w-4 h-4"
  1005. xmlns="http://www.w3.org/2000/svg"
  1006. ><path
  1007. 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"
  1008. /></svg
  1009. >
  1010. </button>
  1011. {#if messageIdx + 1 === messages.length}
  1012. <button
  1013. class="{messageIdx + 1 === messages.length
  1014. ? 'visible'
  1015. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  1016. on:click={regenerateResponse}
  1017. >
  1018. <svg
  1019. xmlns="http://www.w3.org/2000/svg"
  1020. fill="none"
  1021. viewBox="0 0 24 24"
  1022. stroke-width="1.5"
  1023. stroke="currentColor"
  1024. class="w-4 h-4"
  1025. >
  1026. <path
  1027. stroke-linecap="round"
  1028. stroke-linejoin="round"
  1029. d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
  1030. />
  1031. </svg>
  1032. </button>
  1033. {/if}
  1034. </div>
  1035. {/if}
  1036. </div>
  1037. {/if}
  1038. </div>
  1039. {/if}
  1040. </div>
  1041. <!-- {} -->
  1042. </div>
  1043. </div>
  1044. </div>
  1045. {/each}
  1046. {/if}
  1047. </div>
  1048. </div>
  1049. <div class="fixed bottom-0 w-full">
  1050. <div class=" bg-gradient-to-t from-white/90 dark:from-gray-900 pt-5">
  1051. <div class="max-w-3xl p-2.5 -mb-0.5 mx-auto inset-x-0">
  1052. {#if messages.length == 0 && suggestions !== 'false'}
  1053. <Suggestions {submitPrompt} />
  1054. {/if}
  1055. <form
  1056. class=" flex relative w-full"
  1057. on:submit|preventDefault={() => {
  1058. submitPrompt(prompt);
  1059. }}
  1060. >
  1061. <textarea
  1062. id="chat-textarea"
  1063. class="rounded-xl dark:bg-gray-700 dark:text-gray-100 outline-none border dark:border-gray-700 w-full py-3 px-5 pr-12 resize-none"
  1064. placeholder="Send a message"
  1065. bind:value={prompt}
  1066. on:keypress={(e) => {
  1067. if (e.keyCode == 13 && !e.shiftKey) {
  1068. e.preventDefault();
  1069. }
  1070. if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
  1071. submitPrompt(prompt);
  1072. }
  1073. }}
  1074. rows="1"
  1075. on:input={(e) => {
  1076. e.target.style.height = '';
  1077. e.target.style.height = Math.min(e.target.scrollHeight, 200) + 2 + 'px';
  1078. }}
  1079. />
  1080. <div class=" absolute right-0 bottom-0">
  1081. <div class="pr-3 pb-[9px]">
  1082. {#if messages.length == 0 || messages.at(-1).done == true}
  1083. <button
  1084. class="{prompt !== ''
  1085. ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
  1086. : 'text-white bg-gray-100 dark:text-gray-800 dark:bg-gray-600 disabled'} transition rounded-lg p-1.5"
  1087. type="submit"
  1088. disabled={prompt === ''}
  1089. >
  1090. <svg
  1091. xmlns="http://www.w3.org/2000/svg"
  1092. viewBox="0 0 20 20"
  1093. fill="currentColor"
  1094. class="w-5 h-5"
  1095. >
  1096. <path
  1097. fill-rule="evenodd"
  1098. d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z"
  1099. clip-rule="evenodd"
  1100. />
  1101. </svg>
  1102. </button>
  1103. {:else}
  1104. <button
  1105. class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-lg p-1.5"
  1106. on:click={stopResponse}
  1107. >
  1108. <svg
  1109. xmlns="http://www.w3.org/2000/svg"
  1110. viewBox="0 0 24 24"
  1111. fill="currentColor"
  1112. class="w-5 h-5"
  1113. >
  1114. <path
  1115. fill-rule="evenodd"
  1116. d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
  1117. clip-rule="evenodd"
  1118. />
  1119. </svg>
  1120. </button>
  1121. {/if}
  1122. </div>
  1123. </div>
  1124. </form>
  1125. <div class="mt-2.5 text-xs text-gray-500 text-center">
  1126. LLMs can make mistakes. Verify important information.
  1127. </div>
  1128. </div>
  1129. </div>
  1130. </div>
  1131. </div>
  1132. </div>
  1133. </div>
  1134. <style>
  1135. .loading {
  1136. display: inline-block;
  1137. clip-path: inset(0 1ch 0 0);
  1138. animation: l 1s steps(3) infinite;
  1139. letter-spacing: -0.5px;
  1140. }
  1141. @keyframes l {
  1142. to {
  1143. clip-path: inset(0 -1ch 0 0);
  1144. }
  1145. }
  1146. pre[class*='language-'] {
  1147. position: relative;
  1148. overflow: auto;
  1149. /* make space */
  1150. margin: 5px 0;
  1151. padding: 1.75rem 0 1.75rem 1rem;
  1152. border-radius: 10px;
  1153. }
  1154. pre[class*='language-'] button {
  1155. position: absolute;
  1156. top: 5px;
  1157. right: 5px;
  1158. font-size: 0.9rem;
  1159. padding: 0.15rem;
  1160. background-color: #828282;
  1161. border: ridge 1px #7b7b7c;
  1162. border-radius: 5px;
  1163. text-shadow: #c4c4c4 0 0 2px;
  1164. }
  1165. pre[class*='language-'] button:hover {
  1166. cursor: pointer;
  1167. background-color: #bcbabb;
  1168. }
  1169. </style>