|
1 | 1 | <script lang="ts">
|
| 2 | + import { Remarkable } from 'remarkable'; |
2 | 3 | import Template from './template.svelte';
|
3 | 4 |
|
| 5 | + const markdownInstance = new Remarkable(); |
| 6 | +
|
4 | 7 | import { Alert, AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
|
5 | 8 | import { user } from '$lib/stores/user';
|
6 | 9 | import { useCompletion } from 'ai/svelte';
|
|
19 | 22 | credentials: 'include'
|
20 | 23 | });
|
21 | 24 |
|
22 |
| - let question = $input; |
23 |
| -
|
24 | 25 | const examples = [
|
25 | 26 | 'How to add platform in the console?',
|
26 | 27 | 'How can I manage users, permissions, and access control in Appwrite?',
|
|
87 | 88 |
|
88 | 89 | $: answer = parseCompletion($completion);
|
89 | 90 |
|
| 91 | + function renderMarkdown(answer: string): string { |
| 92 | + const trimmedAnswer = answer |
| 93 | + .trim() |
| 94 | + .replace(/[ \t]+/g, ' ') |
| 95 | + .replace(/\n[ \t]+/g, '\n') |
| 96 | + .replace(/\n+/g, '\n'); |
| 97 | +
|
| 98 | + // targeting links in plain text. |
| 99 | + const processedAnswer = trimmedAnswer |
| 100 | + .replace(/(\[(.*?)]\((.*?)\))|https?:\/\/\S+/g, (match, fullMarkdownLink, _, __) => |
| 101 | + fullMarkdownLink ? match : `[${match}](${match})` |
| 102 | + ) |
| 103 | + .replace(/https?:\/\/\S+##/g, (url) => url.replace(/##/, '#')); |
| 104 | +
|
| 105 | + const formattedAnswer = processedAnswer.replace( |
| 106 | + /(^|\n)Sources:/g, |
| 107 | + (_, prefix) => `${prefix}\nSources:` |
| 108 | + ); |
| 109 | +
|
| 110 | + let renderedHTML = markdownInstance.render(formattedAnswer); |
| 111 | +
|
| 112 | + // add target blank to open links in a new tab. |
| 113 | + renderedHTML = renderedHTML.replace(/<a\s+href="([^"]+)"/g, '<a href="$1" target="_blank"'); |
| 114 | +
|
| 115 | + return renderedHTML; |
| 116 | + } |
| 117 | +
|
90 | 118 | function getInitials(name: string) {
|
91 | 119 | const [first, last] = name.split(' ');
|
92 | 120 | return `${first?.[0] ?? ''}${last?.[0] ?? ''}`;
|
93 | 121 | }
|
| 122 | +
|
| 123 | + let previousQuestion = ''; |
| 124 | + $: if ($input) { |
| 125 | + previousQuestion = $input; |
| 126 | + } |
| 127 | +
|
| 128 | + $: if (!$isLoading && answer) { |
| 129 | + // reset input if answer received. |
| 130 | + $input = ''; |
| 131 | + } |
94 | 132 | </script>
|
95 | 133 |
|
96 | 134 | <Template
|
|
142 | 180 | <div class="content">
|
143 | 181 | <div class="u-flex u-gap-8 u-cross-center">
|
144 | 182 | <div class="avatar is-size-x-small">{getInitials($user.name)}</div>
|
145 |
| - <p class="u-opacity-75">{question}</p> |
| 183 | + <p class="u-opacity-75">{previousQuestion}</p> |
146 | 184 | </div>
|
147 | 185 | <div class="u-flex u-gap-8 u-margin-block-start-24">
|
148 | 186 | <div class="logo">
|
|
154 | 192 | {:else}
|
155 | 193 | {#each answer as part}
|
156 | 194 | {#if part.type === 'text'}
|
157 |
| - <p>{part.value.trimStart()}</p> |
| 195 | + <p>{@html renderMarkdown(part.value.trim())}</p> |
158 | 196 | {:else if part.type === 'code'}
|
159 | 197 | {#key part.value}
|
160 | 198 | <div
|
|
196 | 234 | class="input-text-wrapper u-width-full-line"
|
197 | 235 | style="--amount-of-buttons: 1;"
|
198 | 236 | on:submit|preventDefault={(e) => {
|
199 |
| - question = $input; |
200 | 237 | handleSubmit(e);
|
201 | 238 | }}>
|
202 | 239 | <!-- svelte-ignore a11y-autofocus -->
|
|
269 | 306 | .answer {
|
270 | 307 | overflow: hidden;
|
271 | 308 |
|
272 |
| - p { |
| 309 | + p:first-of-type { |
273 | 310 | white-space: pre-wrap;
|
274 | 311 | }
|
275 | 312 | }
|
| 313 | +
|
| 314 | + :global(.answer ul), |
| 315 | + :global(.answer ol) { |
| 316 | + gap: 1rem; |
| 317 | + display: grid; |
| 318 | + } |
| 319 | +
|
| 320 | + :global(.answer a) { |
| 321 | + text-decoration: underline; |
| 322 | + } |
276 | 323 | }
|
277 | 324 |
|
278 | 325 | .footer {
|
|
0 commit comments