Unverified Commit 13b0e7d6 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #4434 from open-webui/dev

0.3.13
parents 8d257ed5 c8badfe2
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
url: OpenAIUrl, url: OpenAIUrl,
batch_size: OpenAIBatchSize batch_size: OpenAIBatchSize
} }
} }
: {}) : {})
}).catch(async (error) => { }).catch(async (error) => {
toast.error(error); toast.error(error);
......
...@@ -579,8 +579,8 @@ ...@@ -579,8 +579,8 @@
let selectedModelIds = modelId let selectedModelIds = modelId
? [modelId] ? [modelId]
: atSelectedModel !== undefined : atSelectedModel !== undefined
? [atSelectedModel.id] ? [atSelectedModel.id]
: selectedModels; : selectedModels;
// Create response messages for each selected model // Create response messages for each selected model
const responseMessageIds = {}; const responseMessageIds = {};
...@@ -739,11 +739,11 @@ ...@@ -739,11 +739,11 @@
? await getAndUpdateUserLocation(localStorage.token) ? await getAndUpdateUserLocation(localStorage.token)
: undefined : undefined
)}${ )}${
responseMessage?.userContext ?? null (responseMessage?.userContext ?? null)
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}` ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: '' : ''
}` }`
} }
: undefined, : undefined,
...messages ...messages
] ]
...@@ -811,10 +811,10 @@ ...@@ -811,10 +811,10 @@
options: { options: {
...(params ?? $settings.params ?? {}), ...(params ?? $settings.params ?? {}),
stop: stop:
params?.stop ?? $settings?.params?.stop ?? undefined (params?.stop ?? $settings?.params?.stop ?? undefined)
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map( ? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) (str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
) )
: undefined, : undefined,
num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
repeat_penalty: repeat_penalty:
...@@ -877,6 +877,10 @@ ...@@ -877,6 +877,10 @@
} else { } else {
responseMessage.content += data.message.content; responseMessage.content += data.message.content;
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
navigator.vibrate(5);
}
const sentences = extractSentencesForAudio(responseMessage.content); const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop(); sentences.pop();
...@@ -1056,10 +1060,10 @@ ...@@ -1056,10 +1060,10 @@
stream: true, stream: true,
model: model.id, model: model.id,
stream_options: stream_options:
model.info?.meta?.capabilities?.usage ?? false (model.info?.meta?.capabilities?.usage ?? false)
? { ? {
include_usage: true include_usage: true
} }
: undefined, : undefined,
messages: [ messages: [
params?.system || $settings.system || (responseMessage?.userContext ?? null) params?.system || $settings.system || (responseMessage?.userContext ?? null)
...@@ -1072,11 +1076,11 @@ ...@@ -1072,11 +1076,11 @@
? await getAndUpdateUserLocation(localStorage.token) ? await getAndUpdateUserLocation(localStorage.token)
: undefined : undefined
)}${ )}${
responseMessage?.userContext ?? null (responseMessage?.userContext ?? null)
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}` ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: '' : ''
}` }`
} }
: undefined, : undefined,
...messages ...messages
] ]
...@@ -1092,7 +1096,7 @@ ...@@ -1092,7 +1096,7 @@
text: text:
arr.length - 1 !== idx arr.length - 1 !== idx
? message.content ? message.content
: message?.raContent ?? message.content : (message?.raContent ?? message.content)
}, },
...message.files ...message.files
.filter((file) => file.type === 'image') .filter((file) => file.type === 'image')
...@@ -1103,20 +1107,20 @@ ...@@ -1103,20 +1107,20 @@
} }
})) }))
] ]
} }
: { : {
content: content:
arr.length - 1 !== idx arr.length - 1 !== idx
? message.content ? message.content
: message?.raContent ?? message.content : (message?.raContent ?? message.content)
}) })
})), })),
seed: params?.seed ?? $settings?.params?.seed ?? undefined, seed: params?.seed ?? $settings?.params?.seed ?? undefined,
stop: stop:
params?.stop ?? $settings?.params?.stop ?? undefined (params?.stop ?? $settings?.params?.stop ?? undefined)
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map( ? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) (str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
) )
: undefined, : undefined,
temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined, temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined, top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
...@@ -1177,6 +1181,10 @@ ...@@ -1177,6 +1181,10 @@
} else { } else {
responseMessage.content += value; responseMessage.content += value;
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
navigator.vibrate(5);
}
const sentences = extractSentencesForAudio(responseMessage.content); const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop(); sentences.pop();
......
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
import FileItem from '$lib/components/common/FileItem.svelte'; import FileItem from '$lib/components/common/FileItem.svelte';
import Collapsible from '$lib/components/common/Collapsible.svelte'; import Collapsible from '$lib/components/common/Collapsible.svelte';
import { user } from '$lib/stores';
export let models = []; export let models = [];
export let chatFiles = []; export let chatFiles = [];
...@@ -78,7 +80,7 @@ ...@@ -78,7 +80,7 @@
<Collapsible title={$i18n.t('Advanced Params')} open={true}> <Collapsible title={$i18n.t('Advanced Params')} open={true}>
<div class="text-sm mt-1.5" slot="content"> <div class="text-sm mt-1.5" slot="content">
<div> <div>
<AdvancedParams bind:params /> <AdvancedParams admin={$user?.role === 'admin'} bind:params />
</div> </div>
</div> </div>
</Collapsible> </Collapsible>
......
...@@ -609,10 +609,10 @@ ...@@ -609,10 +609,10 @@
style="font-size:{rmsLevel * 100 > 4 style="font-size:{rmsLevel * 100 > 4
? '4.5' ? '4.5'
: rmsLevel * 100 > 2 : rmsLevel * 100 > 2
? '4.25' ? '4.25'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? '3.75' ? '3.75'
: '3.5'}rem;width: 100%; text-align:center;" : '3.5'}rem;width: 100%; text-align:center;"
> >
{emoji} {emoji}
</div> </div>
...@@ -658,10 +658,10 @@ ...@@ -658,10 +658,10 @@
class=" {rmsLevel * 100 > 4 class=" {rmsLevel * 100 > 4
? ' size-[4.5rem]' ? ' size-[4.5rem]'
: rmsLevel * 100 > 2 : rmsLevel * 100 > 2
? ' size-16' ? ' size-16'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-14' ? 'size-14'
: 'size-12'} transition-all rounded-full {(model?.info?.meta : 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png' ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat' ? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white" : 'bg-black dark:bg-white'} bg-black dark:bg-white"
...@@ -691,10 +691,10 @@ ...@@ -691,10 +691,10 @@
style="font-size:{rmsLevel * 100 > 4 style="font-size:{rmsLevel * 100 > 4
? '13' ? '13'
: rmsLevel * 100 > 2 : rmsLevel * 100 > 2
? '12' ? '12'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? '11.5' ? '11.5'
: '11'}rem;width:100%;text-align:center;" : '11'}rem;width:100%;text-align:center;"
> >
{emoji} {emoji}
</div> </div>
...@@ -740,10 +740,10 @@ ...@@ -740,10 +740,10 @@
class=" {rmsLevel * 100 > 4 class=" {rmsLevel * 100 > 4
? ' size-52' ? ' size-52'
: rmsLevel * 100 > 2 : rmsLevel * 100 > 2
? 'size-48' ? 'size-48'
: rmsLevel * 100 > 1 : rmsLevel * 100 > 1
? 'size-[11.5rem]' ? 'size-[11.5rem]'
: 'size-44'} transition-all rounded-full {(model?.info?.meta : 'size-44'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png' ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat' ? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} " : 'bg-black dark:bg-white'} "
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
title: $i18n.t('All Documents'), title: $i18n.t('All Documents'),
collection_names: $documents.map((doc) => doc.collection_name) collection_names: $documents.map((doc) => doc.collection_name)
} }
] ]
: []), : []),
...$documents ...$documents
.reduce((a, e, i, arr) => { .reduce((a, e, i, arr) => {
......
...@@ -305,7 +305,7 @@ ...@@ -305,7 +305,7 @@
{#each messages as message, messageIdx} {#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}"> <div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
<div <div
class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null class="flex flex-col justify-between px-5 mb-3 {($settings?.widescreenMode ?? null)
? 'max-w-full' ? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group" : 'max-w-5xl'} mx-auto rounded-lg group"
> >
...@@ -317,10 +317,10 @@ ...@@ -317,10 +317,10 @@
{message} {message}
isFirstMessage={messageIdx === 0} isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? [] ? (history.messages[message.parentId]?.childrenIds ?? [])
: Object.values(history.messages) : (Object.values(history.messages)
.filter((message) => message.parentId === null) .filter((message) => message.parentId === null)
.map((message) => message.id) ?? []} .map((message) => message.id) ?? [])}
{confirmEditMessage} {confirmEditMessage}
{showPreviousMessage} {showPreviousMessage}
{showNextMessage} {showNextMessage}
......
...@@ -60,8 +60,8 @@ ...@@ -60,8 +60,8 @@
href={document?.metadata?.file_id href={document?.metadata?.file_id
? `/api/v1/files/${document?.metadata?.file_id}/content` ? `/api/v1/files/${document?.metadata?.file_id}/content`
: document.source.name.includes('http') : document.source.name.includes('http')
? document.source.name ? document.source.name
: `#`} : `#`}
target="_blank" target="_blank"
> >
{document?.metadata?.name ?? document.source.name} {document?.metadata?.name ?? document.source.name}
......
<script lang="ts"> <script lang="ts">
import Spinner from '$lib/components/common/Spinner.svelte';
import { copyToClipboard } from '$lib/utils';
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.min.css';
import { loadPyodide } from 'pyodide'; import { loadPyodide } from 'pyodide';
import { onMount, tick } from 'svelte'; import mermaid from 'mermaid';
import { getContext, getAllContexts, onMount } from 'svelte';
import { copyToClipboard } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
import PyodideWorker from '$lib/workers/pyodide.worker?worker'; import PyodideWorker from '$lib/workers/pyodide.worker?worker';
const i18n = getContext('i18n');
export let id = ''; export let id = '';
export let token;
export let lang = ''; export let lang = '';
export let code = ''; export let code = '';
let mermaidHtml = null;
let highlightedCode = null; let highlightedCode = null;
let executing = false; let executing = false;
...@@ -204,70 +212,102 @@ __builtins__.input = input`); ...@@ -204,70 +212,102 @@ __builtins__.input = input`);
}; };
let debounceTimeout; let debounceTimeout;
$: if (code) { $: if (code) {
// Function to perform the code highlighting if (lang === 'mermaid' && (token?.raw ?? '').endsWith('```')) {
const highlightCode = () => { (async () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code; try {
}; const { svg } = await mermaid.render(`mermaid-${id}`, code);
mermaidHtml = svg;
} catch (error) {
console.error('Error:', error);
}
})();
} else {
// Function to perform the code highlighting
const highlightCode = () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
};
// Clear the previous timeout if it exists
clearTimeout(debounceTimeout);
// Set a new timeout to debounce the code highlighting
debounceTimeout = setTimeout(highlightCode, 10);
}
}
// Clear the previous timeout if it exists onMount(async () => {
clearTimeout(debounceTimeout); await mermaid.initialize({ startOnLoad: true });
// Set a new timeout to debounce the code highlighting if (lang === 'mermaid' && (token?.raw ?? '').endsWith('```')) {
debounceTimeout = setTimeout(highlightCode, 10); try {
} const { svg } = await mermaid.render(`mermaid-${id}`, code);
mermaidHtml = svg;
} catch (error) {
console.error('Error:', error);
}
}
});
</script> </script>
<div class="my-2" dir="ltr"> <div class="my-2" dir="ltr">
<div {#if lang === 'mermaid'}
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto" {#if mermaidHtml}
> {@html mermaidHtml}
<div class="p-1">{@html lang}</div> {:else}
<pre class=" mermaid-{id}">{code}</pre>
<div class="flex items-center"> {/if}
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))} {:else}
{#if executing} <div
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div> class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
{:else} >
<button <div class="p-1">{@html lang}</div>
class="copy-code-button bg-none border-none p-1"
on:click={() => { <div class="flex items-center">
executePython(code); {#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
}}>Run</button {#if executing}
> <div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
{:else}
<button
class="copy-code-button bg-none border-none p-1"
on:click={() => {
executePython(code);
}}>{$i18n.t('Run')}</button
>
{/if}
{/if} {/if}
{/if} <button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode} >{copied ? $i18n.t('Copied') : $i18n.t('Copy Code')}</button
>{copied ? 'Copied' : 'Copy Code'}</button >
> </div>
</div>
</div>
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre"
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
></pre>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div> </div>
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre"
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
></pre>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{/if}
{/if} {/if}
</div> </div>
<script lang="ts">
import katex from 'katex';
import 'katex/contrib/mhchem';
export let content: string;
export let displayMode: boolean = false;
</script>
{@html katex.renderToString(content, { displayMode, throwOnError: false })}
<script lang="ts"> <script lang="ts">
import type { Token } from 'marked'; import type { Token } from 'marked';
import { unescapeHtml } from '$lib/utils'; import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
import { onMount } from 'svelte';
import Image from '$lib/components/common/Image.svelte'; import Image from '$lib/components/common/Image.svelte';
import KatexRenderer from './KatexRenderer.svelte';
export let id: string; export let id: string;
export let tokens: Token[]; export let tokens: Token[];
</script> </script>
...@@ -25,14 +28,18 @@ ...@@ -25,14 +28,18 @@
<svelte:self id={`${id}-em`} tokens={token.tokens} /> <svelte:self id={`${id}-em`} tokens={token.tokens} />
</em> </em>
{:else if token.type === 'codespan'} {:else if token.type === 'codespan'}
<code class="codespan">{unescapeHtml(token.text.replaceAll('&amp;', '&'))}</code> <code class="codespan">{revertSanitizedResponseContent(token.raw)}</code>
{:else if token.type === 'br'} {:else if token.type === 'br'}
<br /> <br />
{:else if token.type === 'del'} {:else if token.type === 'del'}
<del> <del>
<svelte:self id={`${id}-del`} tokens={token.tokens} /> <svelte:self id={`${id}-del`} tokens={token.tokens} />
</del> </del>
{:else if token.type === 'inlineKatex'}
{#if token.text}
<KatexRenderer content={revertSanitizedResponseContent(token.text)} displayMode={false} />
{/if}
{:else if token.type === 'text'} {:else if token.type === 'text'}
{unescapeHtml(token.text)} {token.raw}
{/if} {/if}
{/each} {/each}
<script lang="ts"> <script lang="ts">
import { marked } from 'marked'; import { onMount } from 'svelte';
import type { Token } from 'marked'; import type { Token } from 'marked';
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils'; import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
import { onMount } from 'svelte';
import Image from '$lib/components/common/Image.svelte';
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte'; import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte'; import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
import KatexRenderer from './KatexRenderer.svelte';
export let id: string; export let id: string;
export let tokens: Token[]; export let tokens: Token[];
export let top = true; export let top = true;
let containerElement;
const headerComponent = (depth: number) => { const headerComponent = (depth: number) => {
return 'h' + depth; return 'h' + depth;
}; };
const renderer = new marked.Renderer();
// For code blocks with simple backticks
renderer.codespan = (code) => {
return `<code class="codespan">${code.replaceAll('&amp;', '&')}</code>`;
};
let codes = [];
renderer.code = (code, lang) => {
codes.push({
code: code,
lang: lang
});
codes = codes;
const codeId = `${id}-${codes.length}`;
const interval = setInterval(() => {
const codeElement = document.getElementById(`code-${codeId}`);
if (codeElement) {
clearInterval(interval);
// If the code is already loaded, don't load it again
if (codeElement.innerHTML) {
return;
}
new CodeBlock({
target: codeElement,
props: {
id: `${id}-${codes.length}`,
lang: lang,
code: revertSanitizedResponseContent(code)
},
hydrate: true,
$$inline: true
});
}
}, 10);
return `<div id="code-${id}-${codes.length}"></div>`;
};
let images = [];
renderer.image = (href, title, text) => {
images.push({
href: href,
title: title,
text: text
});
images = images;
const imageId = `${id}-${images.length}`;
const interval = setInterval(() => {
const imageElement = document.getElementById(`image-${imageId}`);
if (imageElement) {
clearInterval(interval);
// If the image is already loaded, don't load it again
if (imageElement.innerHTML) {
return;
}
console.log('image', href, text);
new Image({
target: imageElement,
props: {
src: href,
alt: text
},
$$inline: true
});
}
}, 10);
return `<div id="image-${id}-${images.length}"></div>`;
};
// Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
const origLinkRenderer = renderer.link;
renderer.link = (href, title, text) => {
const html = origLinkRenderer.call(renderer, href, title, text);
return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
};
const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extensions: any;
};
$: if (tokens) {
images = [];
codes = [];
}
</script> </script>
<div bind:this={containerElement} class="flex flex-col"> <!-- {JSON.stringify(tokens)} -->
{#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)} {#each tokens as token, tokenIdx}
{#if token.type === 'code'} {#if token.type === 'hr'}
{#if token.lang === 'mermaid'} <hr />
<pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre> {:else if token.type === 'heading'}
{:else} <svelte:element this={headerComponent(token.depth)}>
<CodeBlock <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
id={`${id}-${tokenIdx}`} </svelte:element>
lang={token?.lang ?? ''} {:else if token.type === 'code'}
code={revertSanitizedResponseContent(token?.text ?? '')} <CodeBlock
/> id={`${id}-${tokenIdx}`}
{/if} {token}
lang={token?.lang ?? ''}
code={revertSanitizedResponseContent(token?.text ?? '')}
/>
{:else if token.type === 'table'}
<table>
<thead>
<tr>
{#each token.header as header, headerIdx}
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
/>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each token.rows as row, rowIdx}
<tr>
{#each row ?? [] as cell, cellIdx}
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
{:else if token.type === 'blockquote'}
<blockquote>
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
</blockquote>
{:else if token.type === 'list'}
{#if token.ordered}
<ol start={token.start || 1}>
{#each token.items as item, itemIdx}
<li>
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
/>
</li>
{/each}
</ol>
{:else}
<ul>
{#each token.items as item, itemIdx}
<li>
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
/>
</li>
{/each}
</ul>
{/if}
{:else if token.type === 'html'}
{@html token.text}
{:else if token.type === 'paragraph'}
<p>
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
</p>
{:else if token.type === 'text'}
{#if top}
<p>
{#if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
{:else}
{unescapeHtml(token.text)}
{/if}
</p>
{:else if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
{:else} {:else}
{@html marked.parse(token.raw, { {unescapeHtml(token.text)}
...defaults, {/if}
gfm: true, {:else if token.type === 'inlineKatex'}
breaks: true, {#if token.text}
renderer <KatexRenderer
})} content={revertSanitizedResponseContent(token.text)}
displayMode={token?.displayMode ?? false}
/>
{/if}
{:else if token.type === 'blockKatex'}
{#if token.text}
<KatexRenderer
content={revertSanitizedResponseContent(token.text)}
displayMode={token?.displayMode ?? false}
/>
{/if} {/if}
{/each} {:else if token.type === 'space'}
</div> {''}
{:else}
{console.log('Unknown token', token)}
{/if}
{/each}
...@@ -2,10 +2,6 @@ ...@@ -2,10 +2,6 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { marked } from 'marked'; import { marked } from 'marked';
import tippy from 'tippy.js';
import auto_render from 'katex/dist/contrib/auto-render.mjs';
import 'katex/dist/katex.min.css';
import mermaid from 'mermaid';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
...@@ -79,104 +75,24 @@ ...@@ -79,104 +75,24 @@
let tokens; let tokens;
import 'katex/dist/katex.min.css';
import markedKatex from '$lib/utils/katex-extension';
const options = {
throwOnError: false
};
marked.use(markedKatex(options));
$: (async () => { $: (async () => {
if (message?.content) { if (message?.content) {
tokens = marked.lexer( tokens = marked.lexer(
replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name) replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name)
); );
// console.log(message?.content, tokens);
} }
})(); })();
$: if (message) {
renderStyling();
}
const renderStyling = async () => {
await tick();
if (tooltipInstance) {
tooltipInstance[0]?.destroy();
}
renderLatex();
if (message.info) {
let tooltipContent = '';
if (message.info.openai) {
tooltipContent = `prompt_tokens: ${message.info.prompt_tokens ?? 'N/A'}<br/>
completion_tokens: ${message.info.completion_tokens ?? 'N/A'}<br/>
total_tokens: ${message.info.total_tokens ?? 'N/A'}`;
} else {
tooltipContent = `response_token/s: ${
`${
Math.round(
((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
) / 100
} tokens` ?? 'N/A'
}<br/>
prompt_token/s: ${
Math.round(
((message.info.prompt_eval_count ?? 0) /
(message.info.prompt_eval_duration / 1000000000)) *
100
) / 100 ?? 'N/A'
} tokens<br/>
total_duration: ${
Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
'N/A'
}ms<br/>
load_duration: ${
Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
}ms<br/>
prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
prompt_eval_duration: ${
Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) /
100 ?? 'N/A'
}ms<br/>
eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
eval_duration: ${
Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
}ms<br/>
approximate_total: ${approximateToHumanReadable(message.info.total_duration)}`;
}
tooltipInstance = tippy(`#info-${message.id}`, {
content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
allowHTML: true,
theme: 'dark',
arrow: false,
offset: [0, 4]
});
}
};
const renderLatex = () => {
let chatMessageElements = document
.getElementById(`message-${message.id}`)
?.getElementsByClassName('chat-assistant');
if (chatMessageElements) {
for (const element of chatMessageElements) {
auto_render(element, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{ left: '$$', right: '$$', display: false },
{ left: '$ ', right: ' $', display: false },
{ left: '\\pu{', right: '}', display: false },
{ left: '\\ce{', right: '}', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '( ', right: ' )', display: false },
{ left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
],
// • rendering keys, e.g.:
throwOnError: false
});
}
}
};
const playAudio = (idx) => { const playAudio = (idx) => {
return new Promise((res) => { return new Promise((res) => {
speakingIdx = idx; speakingIdx = idx;
...@@ -242,7 +158,7 @@ ...@@ -242,7 +158,7 @@
const res = await synthesizeOpenAISpeech( const res = await synthesizeOpenAISpeech(
localStorage.token, localStorage.token,
$settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
? $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice ? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
: $config?.audio?.tts?.voice, : $config?.audio?.tts?.voice,
sentence sentence
).catch((error) => { ).catch((error) => {
...@@ -330,14 +246,12 @@ ...@@ -330,14 +246,12 @@
editedContent = ''; editedContent = '';
await tick(); await tick();
renderStyling();
}; };
const cancelEditMessage = async () => { const cancelEditMessage = async () => {
edit = false; edit = false;
editedContent = ''; editedContent = '';
await tick(); await tick();
renderStyling();
}; };
const generateImage = async (message) => { const generateImage = async (message) => {
...@@ -362,21 +276,11 @@ ...@@ -362,21 +276,11 @@
$: if (!edit) { $: if (!edit) {
(async () => { (async () => {
await tick(); await tick();
renderStyling();
await mermaid.run({
querySelector: '.mermaid'
});
})(); })();
} }
onMount(async () => { onMount(async () => {
await tick(); await tick();
renderStyling();
await mermaid.run({
querySelector: '.mermaid'
});
}); });
</script> </script>
...@@ -420,7 +324,7 @@ ...@@ -420,7 +324,7 @@
{/if} {/if}
<div <div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-2 prose-ol:-my-2 prose-li:-my-3 whitespace-pre-line" class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line"
> >
<div> <div>
{#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0} {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
...@@ -841,31 +745,71 @@ ...@@ -841,31 +745,71 @@
{/if} {/if}
{#if message.info} {#if message.info}
<Tooltip content={$i18n.t('Generation Info')} placement="bottom"> <Tooltip
<button content={message.info.openai
class=" {isLastMessage ? `prompt_tokens: ${message.info.prompt_tokens ?? 'N/A'}<br/>
? 'visible' completion_tokens: ${message.info.completion_tokens ?? 'N/A'}<br/>
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap" total_tokens: ${message.info.total_tokens ?? 'N/A'}`
on:click={() => { : `response_token/s: ${
console.log(message); `${
}} Math.round(
id="info-{message.id}" ((message.info.eval_count ?? 0) /
> (message.info.eval_duration / 1000000000)) *
<svg 100
xmlns="http://www.w3.org/2000/svg" ) / 100
fill="none" } tokens` ?? 'N/A'
viewBox="0 0 24 24" }<br/>
stroke-width="2.3" prompt_token/s: ${
stroke="currentColor" Math.round(
class="w-4 h-4" ((message.info.prompt_eval_count ?? 0) /
(message.info.prompt_eval_duration / 1000000000)) *
100
) / 100 ?? 'N/A'
} tokens<br/>
total_duration: ${
Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
}ms<br/>
load_duration: ${
Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
}ms<br/>
prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
prompt_eval_duration: ${
Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) / 100 ??
'N/A'
}ms<br/>
eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
eval_duration: ${
Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
}ms<br/>
approximate_total: ${approximateToHumanReadable(message.info.total_duration)}`}
placement="top"
>
<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
<button
class=" {isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
on:click={() => {
console.log(message);
}}
id="info-{message.id}"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" viewBox="0 0 24 24"
/> stroke-width="2.3"
</svg> stroke="currentColor"
</button> class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
</button>
</Tooltip>
</Tooltip> </Tooltip>
{/if} {/if}
......
...@@ -62,8 +62,8 @@ ...@@ -62,8 +62,8 @@
{#if !($settings?.chatBubble ?? true)} {#if !($settings?.chatBubble ?? true)}
<ProfileImage <ProfileImage
src={message.user src={message.user
? $models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png' ? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png')
: user?.profile_image_url ?? '/user.png'} : (user?.profile_image_url ?? '/user.png')}
/> />
{/if} {/if}
<div class="w-full overflow-hidden pl-1"> <div class="w-full overflow-hidden pl-1">
...@@ -96,7 +96,7 @@ ...@@ -96,7 +96,7 @@
{#if message.files} {#if message.files}
<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap"> <div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
{#each message.files as file} {#each message.files as file}
<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}> <div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
{#if file.type === 'image'} {#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" /> <img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else} {:else}
...@@ -162,12 +162,12 @@ ...@@ -162,12 +162,12 @@
</div> </div>
{:else} {:else}
<div class="w-full"> <div class="w-full">
<div class="flex {$settings?.chatBubble ?? true ? 'justify-end' : ''} mb-2"> <div class="flex {($settings?.chatBubble ?? true) ? 'justify-end' : ''} mb-2">
<div <div
class="rounded-3xl {$settings?.chatBubble ?? true class="rounded-3xl {($settings?.chatBubble ?? true)
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${ ? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
message.files ? 'rounded-tr-lg' : '' message.files ? 'rounded-tr-lg' : ''
}` }`
: ''} " : ''} "
> >
<pre id="user-message">{message.content}</pre> <pre id="user-message">{message.content}</pre>
...@@ -175,7 +175,7 @@ ...@@ -175,7 +175,7 @@
</div> </div>
<div <div
class=" flex {$settings?.chatBubble ?? true class=" flex {($settings?.chatBubble ?? true)
? 'justify-end' ? 'justify-end'
: ''} text-gray-600 dark:text-gray-500" : ''} text-gray-600 dark:text-gray-500"
> >
......
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
$: filteredItems = searchValue $: filteredItems = searchValue
? fuse.search(searchValue).map((e) => { ? fuse.search(searchValue).map((e) => {
return e.item; return e.item;
}) })
: items.filter((item) => !item.model?.info?.meta?.hidden); : items.filter((item) => !item.model?.info?.meta?.hidden);
const pullModelHandler = async () => { const pullModelHandler = async () => {
......
...@@ -65,8 +65,8 @@ ...@@ -65,8 +65,8 @@
{updateAvailable === null {updateAvailable === null
? $i18n.t('Checking for updates...') ? $i18n.t('Checking for updates...')
: updateAvailable : updateAvailable
? `(v${version.latest} ${$i18n.t('available!')})` ? `(v${version.latest} ${$i18n.t('available!')})`
: $i18n.t('(latest)')} : $i18n.t('(latest)')}
</a> </a>
</div> </div>
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
use_mmap: null, use_mmap: null,
use_mlock: null, use_mlock: null,
num_thread: null, num_thread: null,
num_gpu: null,
template: null template: null
}; };
...@@ -864,6 +865,52 @@ ...@@ -864,6 +865,52 @@
{/if} {/if}
</div> </div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('num_gpu (Ollama)')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button"
on:click={() => {
params.num_gpu = (params?.num_gpu ?? null) === null ? 0 : null;
}}
>
{#if (params?.num_gpu ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if (params?.num_gpu ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="256"
step="1"
bind:value={params.num_gpu}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={params.num_gpu}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="256"
step="1"
/>
</div>
</div>
{/if}
</div>
<!-- <div class=" py-0.5 w-full justify-between"> <!-- <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
let showEmojiInCall = false; let showEmojiInCall = false;
let voiceInterruption = false; let voiceInterruption = false;
let hapticFeedback = false;
const toggleSplitLargeChunks = async () => { const toggleSplitLargeChunks = async () => {
splitLargeChunks = !splitLargeChunks; splitLargeChunks = !splitLargeChunks;
...@@ -70,6 +71,11 @@ ...@@ -70,6 +71,11 @@
saveSettings({ voiceInterruption: voiceInterruption }); saveSettings({ voiceInterruption: voiceInterruption });
}; };
const toggleHapticFeedback = async () => {
hapticFeedback = !hapticFeedback;
saveSettings({ hapticFeedback: hapticFeedback });
};
const toggleUserLocation = async () => { const toggleUserLocation = async () => {
userLocation = !userLocation; userLocation = !userLocation;
...@@ -151,6 +157,8 @@ ...@@ -151,6 +157,8 @@
chatDirection = $settings.chatDirection ?? 'LTR'; chatDirection = $settings.chatDirection ?? 'LTR';
userLocation = $settings.userLocation ?? false; userLocation = $settings.userLocation ?? false;
hapticFeedback = $settings.hapticFeedback ?? false;
defaultModelId = $settings?.models?.at(0) ?? ''; defaultModelId = $settings?.models?.at(0) ?? '';
if ($config?.default_models) { if ($config?.default_models) {
defaultModelId = $config.default_models.split(',')[0]; defaultModelId = $config.default_models.split(',')[0];
...@@ -438,6 +446,26 @@ ...@@ -438,6 +446,26 @@
</div> </div>
</div> </div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleHapticFeedback();
}}
type="button"
>
{#if hapticFeedback === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div> <div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
<div> <div>
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
on:click={() => { on:click={() => {
valves[property] = valves[property] =
(valves[property] ?? null) === null (valves[property] ?? null) === null
? valvesSpec.properties[property]?.default ?? '' ? (valvesSpec.properties[property]?.default ?? '')
: null; : null;
dispatch('change'); dispatch('change');
......
...@@ -83,8 +83,8 @@ ...@@ -83,8 +83,8 @@
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
? 'bg-gray-200 dark:bg-gray-900' ? 'bg-gray-200 dark:bg-gray-900'
: selected : selected
? 'bg-gray-100 dark:bg-gray-950' ? 'bg-gray-100 dark:bg-gray-950'
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis" : 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
> >
<input <input
use:focusEdit use:focusEdit
...@@ -97,8 +97,8 @@ ...@@ -97,8 +97,8 @@
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
? 'bg-gray-200 dark:bg-gray-900' ? 'bg-gray-200 dark:bg-gray-900'
: selected : selected
? 'bg-gray-100 dark:bg-gray-950' ? 'bg-gray-100 dark:bg-gray-950'
: ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis" : ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
href="/c/{chat.id}" href="/c/{chat.id}"
on:click={() => { on:click={() => {
dispatch('select'); dispatch('select');
...@@ -134,8 +134,8 @@ ...@@ -134,8 +134,8 @@
{chat.id === $chatId || confirmEdit {chat.id === $chatId || confirmEdit
? 'from-gray-200 dark:from-gray-900' ? 'from-gray-200 dark:from-gray-900'
: selected : selected
? 'from-gray-100 dark:from-gray-950' ? 'from-gray-100 dark:from-gray-950'
: 'invisible group-hover:visible from-gray-100 dark:from-gray-950'} : 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80% absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80%
to-transparent" to-transparent"
......
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
? { ? {
role: 'system', role: 'system',
content: system content: system
} }
: undefined, : undefined,
...messages ...messages
].filter((message) => message) ].filter((message) => message)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment