William Zebrowski commited on
Commit
40c9478
·
1 Parent(s): e639365
src/lib/components/chat/Messages/Citations.svelte ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CitationsModal from './CitationsModal.svelte';
3
+
4
+ export let citations = [];
5
+
6
+ let showCitationModal = false;
7
+ let selectedCitation = null;
8
+ </script>
9
+
10
+ <CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
11
+
12
+ <div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
13
+ {#each citations.reduce((acc, citation) => {
14
+ citation.document.forEach((document, index) => {
15
+ const metadata = citation.metadata?.[index];
16
+ const id = metadata?.source ?? 'N/A';
17
+ let source = citation?.source;
18
+
19
+ if (metadata?.name) {
20
+ source = { ...source, name: metadata.name };
21
+ }
22
+
23
+ // Check if ID looks like a URL
24
+ if (id.startsWith('http://') || id.startsWith('https://')) {
25
+ source = { name: id };
26
+ }
27
+
28
+ const existingSource = acc.find((item) => item.id === id);
29
+
30
+ if (existingSource) {
31
+ existingSource.document.push(document);
32
+ existingSource.metadata.push(metadata);
33
+ } else {
34
+ acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
35
+ }
36
+ });
37
+ return acc;
38
+ }, []) as citation, idx}
39
+ <div class="flex gap-1 text-xs font-semibold">
40
+ <button
41
+ class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
42
+ on:click={() => {
43
+ showCitationModal = true;
44
+ selectedCitation = citation;
45
+ }}
46
+ >
47
+ <div class="bg-white dark:bg-gray-700 rounded-full size-4">
48
+ {idx + 1}
49
+ </div>
50
+ <div class="flex-1 mx-2 line-clamp-1">
51
+ {citation.source.name}
52
+ </div>
53
+ </button>
54
+ </div>
55
+ {/each}
56
+ </div>
src/lib/components/chat/Messages/Error.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let content = '';
3
+ </script>
4
+
5
+ <div
6
+ class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
7
+ >
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ fill="none"
11
+ viewBox="0 0 24 24"
12
+ stroke-width="1.5"
13
+ stroke="currentColor"
14
+ class="w-5 h-5 self-center"
15
+ >
16
+ <path
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
20
+ />
21
+ </svg>
22
+
23
+ <div class=" self-center">
24
+ {content}
25
+ </div>
26
+ </div>
src/lib/components/chat/Messages/Markdown.svelte ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ import { marked } from 'marked';
3
+ import markedKatex from '$lib/utils/marked/katex-extension';
4
+ import { replaceTokens, processResponseContent } from '$lib/utils';
5
+ import { user } from '$lib/stores';
6
+
7
+ import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
8
+
9
+ export let id;
10
+ export let content;
11
+ export let model = null;
12
+
13
+ let tokens = [];
14
+
15
+ const options = {
16
+ throwOnError: false
17
+ };
18
+
19
+ marked.use(markedKatex(options));
20
+
21
+ $: (async () => {
22
+ if (content) {
23
+ tokens = marked.lexer(
24
+ replaceTokens(processResponseContent(content), model?.name, $user?.name)
25
+ );
26
+ }
27
+ })();
28
+ </script>
29
+
30
+ {#key id}
31
+ <MarkdownTokens {tokens} {id} />
32
+ {/key}
src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import katex from 'katex';
3
+ import 'katex/contrib/mhchem';
4
+
5
+ export let content: string;
6
+ export let displayMode: boolean = false;
7
+ </script>
8
+
9
+ {@html katex.renderToString(content, { displayMode, throwOnError: false })}
src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import DOMPurify from 'dompurify';
3
+ import type { Token } from 'marked';
4
+ import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
5
+ import { onMount } from 'svelte';
6
+ import Image from '$lib/components/common/Image.svelte';
7
+
8
+ import KatexRenderer from './KatexRenderer.svelte';
9
+ import { WEBUI_BASE_URL } from '$lib/constants';
10
+
11
+ export let id: string;
12
+ export let tokens: Token[];
13
+ </script>
14
+
15
+ {#each tokens as token}
16
+ {#if token.type === 'escape'}
17
+ {unescapeHtml(token.text)}
18
+ {:else if token.type === 'html'}
19
+ {@const html = DOMPurify.sanitize(token.text)}
20
+ {#if html && html.includes('<video')}
21
+ {@html html}
22
+ {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
23
+ {@html `${token.text}`}
24
+ {:else}
25
+ {token.text}
26
+ {/if}
27
+ {:else if token.type === 'link'}
28
+ <a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
29
+ {:else if token.type === 'image'}
30
+ <Image src={token.href} alt={token.text} />
31
+ {:else if token.type === 'strong'}
32
+ <strong>
33
+ <svelte:self id={`${id}-strong`} tokens={token.tokens} />
34
+ </strong>
35
+ {:else if token.type === 'em'}
36
+ <em>
37
+ <svelte:self id={`${id}-em`} tokens={token.tokens} />
38
+ </em>
39
+ {:else if token.type === 'codespan'}
40
+ <code class="codespan">{unescapeHtml(token.text)}</code>
41
+ {:else if token.type === 'br'}
42
+ <br />
43
+ {:else if token.type === 'del'}
44
+ <del>
45
+ <svelte:self id={`${id}-del`} tokens={token.tokens} />
46
+ </del>
47
+ {:else if token.type === 'inlineKatex'}
48
+ {#if token.text}
49
+ <KatexRenderer content={revertSanitizedResponseContent(token.text)} displayMode={false} />
50
+ {/if}
51
+ {:else if token.type === 'iframe'}
52
+ <iframe
53
+ src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
54
+ title={token.fileId}
55
+ width="100%"
56
+ frameborder="0"
57
+ onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
58
+ ></iframe>
59
+ {:else if token.type === 'text'}
60
+ {token.raw}
61
+ {/if}
62
+ {/each}
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import DOMPurify from 'dompurify';
3
+ import { onMount } from 'svelte';
4
+ import { marked, type Token } from 'marked';
5
+ import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
6
+
7
+ import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
8
+ import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
9
+ import KatexRenderer from './KatexRenderer.svelte';
10
+ import { WEBUI_BASE_URL } from '$lib/constants';
11
+
12
+ export let id: string;
13
+ export let tokens: Token[];
14
+ export let top = true;
15
+
16
+ const headerComponent = (depth: number) => {
17
+ return 'h' + depth;
18
+ };
19
+ </script>
20
+
21
+ <!-- {JSON.stringify(tokens)} -->
22
+ {#each tokens as token, tokenIdx}
23
+ {#if token.type === 'hr'}
24
+ <hr />
25
+ {:else if token.type === 'heading'}
26
+ <svelte:element this={headerComponent(token.depth)}>
27
+ <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
28
+ </svelte:element>
29
+ {:else if token.type === 'code'}
30
+ <CodeBlock
31
+ id={`${id}-${tokenIdx}`}
32
+ {token}
33
+ lang={token?.lang ?? ''}
34
+ code={revertSanitizedResponseContent(token?.text ?? '')}
35
+ />
36
+ {:else if token.type === 'table'}
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ {#each token.header as header, headerIdx}
41
+ <th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
42
+ <MarkdownInlineTokens
43
+ id={`${id}-${tokenIdx}-header-${headerIdx}`}
44
+ tokens={header.tokens}
45
+ />
46
+ </th>
47
+ {/each}
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ {#each token.rows as row, rowIdx}
52
+ <tr>
53
+ {#each row ?? [] as cell, cellIdx}
54
+ <td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
55
+ <MarkdownInlineTokens
56
+ id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
57
+ tokens={cell.tokens}
58
+ />
59
+ </td>
60
+ {/each}
61
+ </tr>
62
+ {/each}
63
+ </tbody>
64
+ </table>
65
+ {:else if token.type === 'blockquote'}
66
+ <blockquote>
67
+ <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
68
+ </blockquote>
69
+ {:else if token.type === 'list'}
70
+ {#if token.ordered}
71
+ <ol start={token.start || 1}>
72
+ {#each token.items as item, itemIdx}
73
+ <li>
74
+ <svelte:self
75
+ id={`${id}-${tokenIdx}-${itemIdx}`}
76
+ tokens={item.tokens}
77
+ top={token.loose}
78
+ />
79
+ </li>
80
+ {/each}
81
+ </ol>
82
+ {:else}
83
+ <ul>
84
+ {#each token.items as item, itemIdx}
85
+ <li>
86
+ <svelte:self
87
+ id={`${id}-${tokenIdx}-${itemIdx}`}
88
+ tokens={item.tokens}
89
+ top={token.loose}
90
+ />
91
+ </li>
92
+ {/each}
93
+ </ul>
94
+ {/if}
95
+ {:else if token.type === 'html'}
96
+ {@const html = DOMPurify.sanitize(token.text)}
97
+ {#if html && html.includes('<video')}
98
+ {@html html}
99
+ {:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
100
+ {@html `${token.text}`}
101
+ {:else}
102
+ {token.text}
103
+ {/if}
104
+ {:else if token.type === 'iframe'}
105
+ <iframe
106
+ src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
107
+ title={token.fileId}
108
+ width="100%"
109
+ frameborder="0"
110
+ onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
111
+ ></iframe>
112
+ {:else if token.type === 'paragraph'}
113
+ <p>
114
+ <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
115
+ </p>
116
+ {:else if token.type === 'text'}
117
+ {#if top}
118
+ <p>
119
+ {#if token.tokens}
120
+ <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
121
+ {:else}
122
+ {unescapeHtml(token.text)}
123
+ {/if}
124
+ </p>
125
+ {:else if token.tokens}
126
+ <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
127
+ {:else}
128
+ {unescapeHtml(token.text)}
129
+ {/if}
130
+ {:else if token.type === 'inlineKatex'}
131
+ {#if token.text}
132
+ <KatexRenderer
133
+ content={revertSanitizedResponseContent(token.text)}
134
+ displayMode={token?.displayMode ?? false}
135
+ />
136
+ {/if}
137
+ {:else if token.type === 'blockKatex'}
138
+ {#if token.text}
139
+ <KatexRenderer
140
+ content={revertSanitizedResponseContent(token.text)}
141
+ displayMode={token?.displayMode ?? false}
142
+ />
143
+ {/if}
144
+ {:else if token.type === 'space'}
145
+ {''}
146
+ {:else}
147
+ {console.log('Unknown token', token)}
148
+ {/if}
149
+ {/each}
src/lib/components/chat/Messages/MultiResponseMessages.svelte ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import dayjs from 'dayjs';
3
+ import { onMount, tick, getContext } from 'svelte';
4
+ import { createEventDispatcher } from 'svelte';
5
+
6
+ import { mobile, settings } from '$lib/stores';
7
+
8
+ import { generateMoACompletion } from '$lib/apis';
9
+ import { updateChatById } from '$lib/apis/chats';
10
+ import { createOpenAITextStream } from '$lib/apis/streaming';
11
+
12
+ import ResponseMessage from './ResponseMessage.svelte';
13
+ import Tooltip from '$lib/components/common/Tooltip.svelte';
14
+ import Merge from '$lib/components/icons/Merge.svelte';
15
+
16
+ import Markdown from './Markdown.svelte';
17
+ import Name from './Name.svelte';
18
+ import Skeleton from './Skeleton.svelte';
19
+
20
+ const i18n = getContext('i18n');
21
+
22
+ export let chatId;
23
+
24
+ export let history;
25
+ export let messages = [];
26
+ export let messageIdx;
27
+
28
+ export let parentMessage;
29
+ export let isLastMessage;
30
+
31
+ export let readOnly = false;
32
+
33
+ export let updateChatMessages: Function;
34
+ export let confirmEditResponseMessage: Function;
35
+ export let rateMessage: Function;
36
+
37
+ export let copyToClipboard: Function;
38
+ export let continueGeneration: Function;
39
+ export let mergeResponses: Function;
40
+ export let regenerateResponse: Function;
41
+
42
+ const dispatch = createEventDispatcher();
43
+
44
+ let currentMessageId;
45
+ let groupedMessages = {};
46
+ let groupedMessagesIdx = {};
47
+
48
+ $: if (parentMessage) {
49
+ initHandler();
50
+ }
51
+
52
+ const showPreviousMessage = (modelIdx) => {
53
+ groupedMessagesIdx[modelIdx] = Math.max(0, groupedMessagesIdx[modelIdx] - 1);
54
+ let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
55
+
56
+ console.log(messageId);
57
+ let messageChildrenIds = history.messages[messageId].childrenIds;
58
+
59
+ while (messageChildrenIds.length !== 0) {
60
+ messageId = messageChildrenIds.at(-1);
61
+ messageChildrenIds = history.messages[messageId].childrenIds;
62
+ }
63
+
64
+ history.currentId = messageId;
65
+ dispatch('change');
66
+ };
67
+
68
+ const showNextMessage = (modelIdx) => {
69
+ groupedMessagesIdx[modelIdx] = Math.min(
70
+ groupedMessages[modelIdx].messages.length - 1,
71
+ groupedMessagesIdx[modelIdx] + 1
72
+ );
73
+
74
+ let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
75
+ console.log(messageId);
76
+
77
+ let messageChildrenIds = history.messages[messageId].childrenIds;
78
+
79
+ while (messageChildrenIds.length !== 0) {
80
+ messageId = messageChildrenIds.at(-1);
81
+ messageChildrenIds = history.messages[messageId].childrenIds;
82
+ }
83
+
84
+ history.currentId = messageId;
85
+ dispatch('change');
86
+ };
87
+
88
+ const initHandler = async () => {
89
+ await tick();
90
+ currentMessageId = messages[messageIdx].id;
91
+
92
+ groupedMessages = parentMessage?.models.reduce((a, model, modelIdx) => {
93
+ // Find all messages that are children of the parent message and have the same model
94
+ const modelMessages = parentMessage?.childrenIds
95
+ .map((id) => history.messages[id])
96
+ .filter((m) => m.modelIdx === modelIdx);
97
+
98
+ return {
99
+ ...a,
100
+ [modelIdx]: { messages: modelMessages }
101
+ };
102
+ }, {});
103
+
104
+ groupedMessagesIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
105
+ const idx = groupedMessages[modelIdx].messages.findIndex((m) => m.id === currentMessageId);
106
+ if (idx !== -1) {
107
+ return {
108
+ ...a,
109
+ [modelIdx]: idx
110
+ };
111
+ } else {
112
+ return {
113
+ ...a,
114
+ [modelIdx]: 0
115
+ };
116
+ }
117
+ }, {});
118
+ };
119
+
120
+ const mergeResponsesHandler = async () => {
121
+ const responses = Object.keys(groupedMessages).map((modelIdx) => {
122
+ const { messages } = groupedMessages[modelIdx];
123
+ return messages[groupedMessagesIdx[modelIdx]].content;
124
+ });
125
+ mergeResponses(currentMessageId, responses, chatId);
126
+ };
127
+
128
+ onMount(async () => {
129
+ initHandler();
130
+ });
131
+ </script>
132
+
133
+ <div>
134
+ <div
135
+ class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
136
+ id="responses-container-{chatId}-{parentMessage.id}"
137
+ >
138
+ {#key currentMessageId}
139
+ {#each Object.keys(groupedMessages) as modelIdx}
140
+ {#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0}
141
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
142
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
143
+ {@const message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]}
144
+
145
+ <div
146
+ class=" snap-center w-full max-w-full m-1 border {history.messages[currentMessageId]
147
+ ?.modelIdx == modelIdx
148
+ ? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
149
+ $mobile ? 'min-w-full' : 'min-w-[32rem]'
150
+ }`
151
+ : `border-gray-50 dark:border-gray-850 border-dashed ${
152
+ $mobile ? 'min-w-full' : 'min-w-80'
153
+ }`} transition-all p-5 rounded-2xl"
154
+ on:click={() => {
155
+ if (currentMessageId != message.id) {
156
+ currentMessageId = message.id;
157
+ let messageId = message.id;
158
+ console.log(messageId);
159
+ //
160
+ let messageChildrenIds = history.messages[messageId].childrenIds;
161
+ while (messageChildrenIds.length !== 0) {
162
+ messageId = messageChildrenIds.at(-1);
163
+ messageChildrenIds = history.messages[messageId].childrenIds;
164
+ }
165
+ history.currentId = messageId;
166
+ dispatch('change');
167
+ }
168
+ }}
169
+ >
170
+ {#key history.currentId}
171
+ {#if message}
172
+ <ResponseMessage
173
+ {message}
174
+ siblings={groupedMessages[modelIdx].messages.map((m) => m.id)}
175
+ isLastMessage={true}
176
+ {updateChatMessages}
177
+ {confirmEditResponseMessage}
178
+ showPreviousMessage={() => showPreviousMessage(modelIdx)}
179
+ showNextMessage={() => showNextMessage(modelIdx)}
180
+ {readOnly}
181
+ {rateMessage}
182
+ {copyToClipboard}
183
+ {continueGeneration}
184
+ regenerateResponse={async (message) => {
185
+ regenerateResponse(message);
186
+ await tick();
187
+ groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1;
188
+ }}
189
+ on:save={async (e) => {
190
+ console.log('save', e);
191
+
192
+ const message = e.detail;
193
+ history.messages[message.id] = message;
194
+ await updateChatById(localStorage.token, chatId, {
195
+ messages: messages,
196
+ history: history
197
+ });
198
+ }}
199
+ />
200
+ {/if}
201
+ {/key}
202
+ </div>
203
+ {/if}
204
+ {/each}
205
+ {/key}
206
+ </div>
207
+
208
+ {#if !readOnly && isLastMessage}
209
+ {#if !Object.keys(groupedMessages).find((modelIdx) => {
210
+ const { messages } = groupedMessages[modelIdx];
211
+ return !messages[groupedMessagesIdx[modelIdx]].done;
212
+ })}
213
+ <div class="flex justify-end">
214
+ <div class="w-full">
215
+ {#if history.messages[currentMessageId]?.merged?.status}
216
+ {@const message = history.messages[currentMessageId]?.merged}
217
+
218
+ <div class="w-full rounded-xl pl-5 pr-2 py-2">
219
+ <Name>
220
+ Merged Response
221
+
222
+ {#if message.timestamp}
223
+ <span
224
+ class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
225
+ >
226
+ {dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
227
+ </span>
228
+ {/if}
229
+ </Name>
230
+
231
+ <div class="mt-1 markdown-prose w-full min-w-full">
232
+ {#if (message?.content ?? '') === ''}
233
+ <Skeleton />
234
+ {:else}
235
+ <Markdown id={`merged`} content={message.content ?? ''} />
236
+ {/if}
237
+ </div>
238
+ </div>
239
+ {/if}
240
+ </div>
241
+
242
+ <div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
243
+ <Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
244
+ <button
245
+ type="button"
246
+ id="merge-response-button"
247
+ class="{true
248
+ ? 'visible'
249
+ : 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
250
+ on:click={() => {
251
+ mergeResponsesHandler();
252
+ }}
253
+ >
254
+ <Merge className=" size-5 " />
255
+ </button>
256
+ </Tooltip>
257
+ </div>
258
+ </div>
259
+ {/if}
260
+ {/if}
261
+ </div>
src/lib/components/icons/ChatBubbleOval.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let className = 'size-4';
3
+ export let strokeWidth = '1.5';
4
+ </script>
5
+
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ stroke-width={strokeWidth}
11
+ stroke="currentColor"
12
+ class={className}
13
+ >
14
+ <path
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z"
18
+ />
19
+ </svg>
src/lib/components/icons/EyeSlash.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let className = 'w-4 h-4';
3
+ export let strokeWidth = '1.5';
4
+ </script>
5
+
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ stroke-width={strokeWidth}
11
+ stroke="currentColor"
12
+ class={className}
13
+ >
14
+ <path
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
18
+ />
19
+ </svg>
src/lib/components/icons/Merge.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let className = 'w-4 h-4';
3
+ export let strokeWidth = '1.5';
4
+ </script>
5
+
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ stroke-width={strokeWidth}
11
+ stroke="currentColor"
12
+ class={className}
13
+ >
14
+ <path
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ d="M8 8v8m0-8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm0 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm6-2a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm0 0h-1a5 5 0 0 1-5-5v-.5"
18
+ />
19
+ </svg>
src/lib/utils/marked/katex-extension.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import katex from 'katex';
2
+
3
+ const DELIMITER_LIST = [
4
+ { left: '$$', right: '$$', display: false },
5
+ { left: '$', right: '$', display: false },
6
+ { left: '\\pu{', right: '}', display: false },
7
+ { left: '\\ce{', right: '}', display: false },
8
+ { left: '\\(', right: '\\)', display: false },
9
+ { left: '( ', right: ' )', display: false },
10
+ { left: '\\[', right: '\\]', display: true },
11
+ { left: '[ ', right: ' ]', display: true }
12
+ ];
13
+
14
+ // const DELIMITER_LIST = [
15
+ // { left: '$$', right: '$$', display: false },
16
+ // { left: '$', right: '$', display: false },
17
+ // ];
18
+
19
+ // const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/;
20
+ // const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
21
+
22
+ let inlinePatterns = [];
23
+ let blockPatterns = [];
24
+
25
+ function escapeRegex(string) {
26
+ return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
27
+ }
28
+
29
+ function generateRegexRules(delimiters) {
30
+ delimiters.forEach((delimiter) => {
31
+ const { left, right } = delimiter;
32
+ // Ensure regex-safe delimiters
33
+ const escapedLeft = escapeRegex(left);
34
+ const escapedRight = escapeRegex(right);
35
+
36
+ // Inline pattern - Capture group $1, token content, followed by end delimiter and normal punctuation marks.
37
+ // Example: $text$
38
+ inlinePatterns.push(
39
+ `${escapedLeft}((?:\\\\.|[^\\\\\\n])*?(?:\\\\.|[^\\\\\\n${escapedRight}]))${escapedRight}`
40
+ );
41
+
42
+ // Block pattern - Starts and ends with the delimiter on new lines. Example:
43
+ // $$\ncontent here\n$$
44
+ blockPatterns.push(`${escapedLeft}\n((?:\\\\[^]|[^\\\\])+?)\n${escapedRight}`);
45
+ });
46
+
47
+ const inlineRule = new RegExp(`^(${inlinePatterns.join('|')})(?=[\\s?!.,:?!。,:]|$)`, 'u');
48
+ const blockRule = new RegExp(`^(${blockPatterns.join('|')})(?:\n|$)`, 'u');
49
+
50
+ return { inlineRule, blockRule };
51
+ }
52
+
53
+ const { inlineRule, blockRule } = generateRegexRules(DELIMITER_LIST);
54
+
55
+ export default function (options = {}) {
56
+ return {
57
+ extensions: [
58
+ inlineKatex(options, createRenderer(options, false)),
59
+ blockKatex(options, createRenderer(options, true))
60
+ ]
61
+ };
62
+ }
63
+
64
+ function createRenderer(options, newlineAfter) {
65
+ return (token) =>
66
+ katex.renderToString(token.text, { ...options, displayMode: token.displayMode }) +
67
+ (newlineAfter ? '\n' : '');
68
+ }
69
+
70
+ function inlineKatex(options, renderer) {
71
+ const ruleReg = inlineRule;
72
+ return {
73
+ name: 'inlineKatex',
74
+ level: 'inline',
75
+ start(src) {
76
+ let index;
77
+ let indexSrc = src;
78
+
79
+ while (indexSrc) {
80
+ index = indexSrc.indexOf('$');
81
+ if (index === -1) {
82
+ return;
83
+ }
84
+ const f = index === 0 || indexSrc.charAt(index - 1) === ' ';
85
+ if (f) {
86
+ const possibleKatex = indexSrc.substring(index);
87
+
88
+ if (possibleKatex.match(ruleReg)) {
89
+ return index;
90
+ }
91
+ }
92
+
93
+ indexSrc = indexSrc.substring(index + 1).replace(/^\$+/, '');
94
+ }
95
+ },
96
+ tokenizer(src, tokens) {
97
+ const match = src.match(ruleReg);
98
+
99
+ if (match) {
100
+ const text = match
101
+ .slice(2)
102
+ .filter((item) => item)
103
+ .find((item) => item.trim());
104
+
105
+ return {
106
+ type: 'inlineKatex',
107
+ raw: match[0],
108
+ text: text
109
+ };
110
+ }
111
+ },
112
+ renderer
113
+ };
114
+ }
115
+
116
+ function blockKatex(options, renderer) {
117
+ return {
118
+ name: 'blockKatex',
119
+ level: 'block',
120
+ tokenizer(src, tokens) {
121
+ const match = src.match(blockRule);
122
+
123
+ if (match) {
124
+ const text = match
125
+ .slice(2)
126
+ .filter((item) => item)
127
+ .find((item) => item.trim());
128
+
129
+ return {
130
+ type: 'blockKatex',
131
+ raw: match[0],
132
+ text: text
133
+ };
134
+ }
135
+ },
136
+ renderer
137
+ };
138
+ }