Thomas G. Lopes commited on
Commit
7c08d14
·
1 Parent(s): 96cfab0

add markdown parsing

Browse files
package.json CHANGED
@@ -74,8 +74,10 @@
74
  },
75
  "type": "module",
76
  "dependencies": {
 
77
  "dequal": "^2.0.3",
78
  "eslint-plugin-svelte": "^3.11.0",
 
79
  "remult": "^3.0.2",
80
  "typia": "^8.0.0"
81
  },
 
74
  },
75
  "type": "module",
76
  "dependencies": {
77
+ "@tailwindcss/typography": "^0.5.16",
78
  "dequal": "^2.0.3",
79
  "eslint-plugin-svelte": "^3.11.0",
80
+ "marked": "^16.1.2",
81
  "remult": "^3.0.2",
82
  "typia": "^8.0.0"
83
  },
pnpm-lock.yaml CHANGED
@@ -8,12 +8,18 @@ importers:
8
 
9
  .:
10
  dependencies:
 
 
 
11
  dequal:
12
  specifier: ^2.0.3
13
  version: 2.0.3
14
  eslint-plugin-svelte:
15
  specifier: ^3.11.0
16
 
 
 
17
  remult:
18
  specifier: ^3.0.2
19
  version: 3.0.2
@@ -1128,6 +1134,11 @@ packages:
1128
  '@tailwindcss/[email protected]':
1129
  resolution: {integrity: sha512-BT/E+pdMqulavEAVM5NCpxmGEwHiLDPpkmg/c/X25ZBW+izTe+aZ+v1gf/HXTrihRoCxrUp5U4YyHsBTzspQKQ==}
1130
 
 
 
 
 
 
1131
  '@testing-library/[email protected]':
1132
  resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
1133
  engines: {node: '>=18'}
@@ -2207,6 +2218,12 @@ packages:
2207
  resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
2208
  engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
2209
 
 
 
 
 
 
 
2210
2211
  resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
2212
 
@@ -2233,6 +2250,11 @@ packages:
2233
2234
  resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
2235
 
 
 
 
 
 
2236
2237
  resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
2238
  engines: {node: '>=10'}
@@ -2518,6 +2540,10 @@ packages:
2518
  peerDependencies:
2519
  postcss: ^8.4.29
2520
 
 
 
 
 
2521
2522
  resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
2523
  engines: {node: '>=4'}
@@ -4045,6 +4071,14 @@ snapshots:
4045
  postcss: 8.5.3
4046
  tailwindcss: 4.0.9
4047
 
 
 
 
 
 
 
 
 
4048
  '@testing-library/[email protected]':
4049
  dependencies:
4050
  '@babel/code-frame': 7.27.1
@@ -5168,6 +5202,10 @@ snapshots:
5168
  dependencies:
5169
  p-locate: 6.0.0
5170
 
 
 
 
 
5171
5172
 
5173
@@ -5189,6 +5227,8 @@ snapshots:
5189
  dependencies:
5190
  '@jridgewell/sourcemap-codec': 1.5.0
5191
 
 
 
5192
5193
  dependencies:
5194
  escape-string-regexp: 4.0.0
@@ -5456,6 +5496,11 @@ snapshots:
5456
  dependencies:
5457
  postcss: 8.5.3
5458
 
 
 
 
 
 
5459
5460
  dependencies:
5461
  cssesc: 3.0.0
 
8
 
9
  .:
10
  dependencies:
11
+ '@tailwindcss/typography':
12
+ specifier: ^0.5.16
13
+ version: 0.5.16([email protected])
14
  dequal:
15
  specifier: ^2.0.3
16
  version: 2.0.3
17
  eslint-plugin-svelte:
18
  specifier: ^3.11.0
19
20
+ marked:
21
+ specifier: ^16.1.2
22
+ version: 16.1.2
23
  remult:
24
  specifier: ^3.0.2
25
  version: 3.0.2
 
1134
  '@tailwindcss/[email protected]':
1135
  resolution: {integrity: sha512-BT/E+pdMqulavEAVM5NCpxmGEwHiLDPpkmg/c/X25ZBW+izTe+aZ+v1gf/HXTrihRoCxrUp5U4YyHsBTzspQKQ==}
1136
 
1137
+ '@tailwindcss/[email protected]':
1138
+ resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==}
1139
+ peerDependencies:
1140
+ tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
1141
+
1142
  '@testing-library/[email protected]':
1143
  resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
1144
  engines: {node: '>=18'}
 
2218
  resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
2219
  engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
2220
 
2221
2222
+ resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
2223
+
2224
2225
+ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
2226
+
2227
2228
  resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
2229
 
 
2250
2251
  resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
2252
 
2253
2254
+ resolution: {integrity: sha512-rNQt5EvRinalby7zJZu/mB+BvaAY2oz3wCuCjt1RDrWNpS1Pdf9xqMOeC9Hm5adBdcV/3XZPJpG58eT+WBc0XQ==}
2255
+ engines: {node: '>= 20'}
2256
+ hasBin: true
2257
+
2258
2259
  resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
2260
  engines: {node: '>=10'}
 
2540
  peerDependencies:
2541
  postcss: ^8.4.29
2542
 
2543
2544
+ resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
2545
+ engines: {node: '>=4'}
2546
+
2547
2548
  resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==}
2549
  engines: {node: '>=4'}
 
4071
  postcss: 8.5.3
4072
  tailwindcss: 4.0.9
4073
 
4074
4075
+ dependencies:
4076
+ lodash.castarray: 4.4.0
4077
+ lodash.isplainobject: 4.0.6
4078
+ lodash.merge: 4.6.2
4079
+ postcss-selector-parser: 6.0.10
4080
+ tailwindcss: 4.0.9
4081
+
4082
  '@testing-library/[email protected]':
4083
  dependencies:
4084
  '@babel/code-frame': 7.27.1
 
5202
  dependencies:
5203
  p-locate: 6.0.0
5204
 
5205
5206
+
5207
5208
+
5209
5210
 
5211
 
5227
  dependencies:
5228
  '@jridgewell/sourcemap-codec': 1.5.0
5229
 
5230
5231
+
5232
5233
  dependencies:
5234
  escape-string-regexp: 4.0.0
 
5496
  dependencies:
5497
  postcss: 8.5.3
5498
 
5499
5500
+ dependencies:
5501
+ cssesc: 3.0.0
5502
+ util-deprecate: 1.0.2
5503
+
5504
5505
  dependencies:
5506
  cssesc: 3.0.0
src/app.css CHANGED
@@ -2,6 +2,7 @@
2
  @import "tailwindcss";
3
 
4
  @plugin '@tailwindcss/container-queries';
 
5
 
6
  @custom-variant dark (&:where(.dark, .dark *));
7
 
 
2
  @import "tailwindcss";
3
 
4
  @plugin '@tailwindcss/container-queries';
5
+ @plugin '@tailwindcss/typography';
6
 
7
  @custom-variant dark (&:where(.dark, .dark *));
8
 
src/lib/components/inference-playground/generation-config.svelte CHANGED
@@ -137,6 +137,18 @@
137
  </span>
138
  <button class="btn-mini ml-auto" type="button" onclick={openExtraParamsModal}>edit</button>
139
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
140
  </div>
141
 
142
  <StructuredOutputModal {conversation} />
 
137
  </span>
138
  <button class="btn-mini ml-auto" type="button" onclick={openExtraParamsModal}>edit</button>
139
  </div>
140
+
141
+ <label class="mt-2 flex cursor-pointer items-center justify-between">
142
+ <input
143
+ type="checkbox"
144
+ bind:checked={() => conversation.data.parseMarkdown, v => conversation.update({ parseMarkdown: v })}
145
+ class="peer sr-only"
146
+ />
147
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-300">Parse Markdown</span>
148
+ <div
149
+ class="peer relative h-5 w-9 rounded-full bg-gray-200 peer-checked:bg-black peer-focus:outline-hidden after:absolute after:start-[2px] after:top-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white dark:border-gray-600 dark:bg-gray-700 dark:peer-checked:bg-blue-600"
150
+ ></div>
151
+ </label>
152
  </div>
153
 
154
  <StructuredOutputModal {conversation} />
src/lib/components/inference-playground/message-textarea.svelte CHANGED
@@ -5,14 +5,14 @@
5
  import { images } from "$lib/state/images.svelte";
6
  import type { ConversationMessage } from "$lib/types.js";
7
  import { fileToDataURL } from "$lib/utils/file.js";
 
8
  import { cmdOrCtrl } from "$lib/utils/platform.js";
9
  import { FileUpload } from "melt/builders";
 
10
  import IconImage from "~icons/carbon/image-reference";
11
  import IconMaximize from "~icons/carbon/maximize";
12
  import Tooltip from "../tooltip.svelte";
13
  import { previewImage } from "./img-preview.svelte";
14
- import { omit } from "$lib/utils/object.svelte";
15
- import { fade } from "svelte/transition";
16
 
17
  const multiple = $derived(conversations.active.length > 1);
18
  const loading = $derived(conversations.generating);
 
5
  import { images } from "$lib/state/images.svelte";
6
  import type { ConversationMessage } from "$lib/types.js";
7
  import { fileToDataURL } from "$lib/utils/file.js";
8
+ import { omit } from "$lib/utils/object.svelte";
9
  import { cmdOrCtrl } from "$lib/utils/platform.js";
10
  import { FileUpload } from "melt/builders";
11
+ import { fade } from "svelte/transition";
12
  import IconImage from "~icons/carbon/image-reference";
13
  import IconMaximize from "~icons/carbon/maximize";
14
  import Tooltip from "../tooltip.svelte";
15
  import { previewImage } from "./img-preview.svelte";
 
 
16
 
17
  const multiple = $derived(conversations.active.length > 1);
18
  const loading = $derived(conversations.generating);
src/lib/components/inference-playground/message.svelte CHANGED
@@ -16,6 +16,7 @@
16
  import IconCustom from "../icon-custom.svelte";
17
  import LocalToasts from "../local-toasts.svelte";
18
  import { previewImage } from "./img-preview.svelte";
 
19
 
20
  type Props = {
21
  conversation: ConversationClass;
@@ -59,6 +60,13 @@
59
  if (message?.role === "assistant") return "Regenerate";
60
  return isLast ? "Generate from here" : "Regenerate from here";
61
  });
 
 
 
 
 
 
 
62
  </script>
63
 
64
  <div
@@ -88,28 +96,38 @@
88
  {message?.role}
89
  </div>
90
  <div class="flex w-full gap-4">
91
- <textarea
92
- value={message?.content}
93
- onchange={e => {
94
- const el = e.target as HTMLTextAreaElement;
95
- const content = el?.value;
96
- if (!message || !content) return;
97
- conversation.updateMessage({ index, message: { ...message, content } });
98
- }}
99
- onkeydown={e => {
100
- if ((e.ctrlKey || e.metaKey) && e.key === "g") {
101
- e.preventDefault();
102
- e.stopPropagation();
103
- onRegen?.();
104
- }
105
- }}
106
- placeholder="Enter {message?.role} message"
107
- class="grow resize-none overflow-hidden rounded-lg bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:bg-white focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
108
- rows="1"
109
- data-message
110
- data-test-id={TEST_IDS.message}
111
- {@attach autosized.attachment}
112
- ></textarea>
 
 
 
 
 
 
 
 
 
 
113
 
114
  <!-- Sticky wrapper for action buttons -->
115
  <div class={["top-8 z-10 self-start", shouldStick && "sticky"]}>
 
16
  import IconCustom from "../icon-custom.svelte";
17
  import LocalToasts from "../local-toasts.svelte";
18
  import { previewImage } from "./img-preview.svelte";
19
+ import { marked } from "marked";
20
 
21
  type Props = {
22
  conversation: ConversationClass;
 
60
  if (message?.role === "assistant") return "Regenerate";
61
  return isLast ? "Generate from here" : "Regenerate from here";
62
  });
63
+
64
+ const parsedContent = $derived.by(() => {
65
+ if (!conversation.data.parseMarkdown || !message?.content) {
66
+ return message?.content ?? "";
67
+ }
68
+ return marked(message.content);
69
+ });
70
  </script>
71
 
72
  <div
 
96
  {message?.role}
97
  </div>
98
  <div class="flex w-full gap-4">
99
+ {#if conversation.data.parseMarkdown && message?.role === "assistant"}
100
+ <div
101
+ class="prose prose-sm dark:prose-invert max-w-none grow rounded-lg bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:bg-white @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900"
102
+ data-message
103
+ data-test-id={TEST_IDS.message}
104
+ >
105
+ {@html parsedContent}
106
+ </div>
107
+ {:else}
108
+ <textarea
109
+ value={message?.content}
110
+ onchange={e => {
111
+ const el = e.target as HTMLTextAreaElement;
112
+ const content = el?.value;
113
+ if (!message || !content) return;
114
+ conversation.updateMessage({ index, message: { ...message, content } });
115
+ }}
116
+ onkeydown={e => {
117
+ if ((e.ctrlKey || e.metaKey) && e.key === "g") {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ onRegen?.();
121
+ }
122
+ }}
123
+ placeholder="Enter {message?.role} message"
124
+ class="grow resize-none overflow-hidden rounded-lg bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:bg-white focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
125
+ rows="1"
126
+ data-message
127
+ data-test-id={TEST_IDS.message}
128
+ {@attach autosized.attachment}
129
+ ></textarea>
130
+ {/if}
131
 
132
  <!-- Sticky wrapper for action buttons -->
133
  <div class={["top-8 z-10 self-start", shouldStick && "sticky"]}>
src/lib/spells/textarea-autosize.svelte.ts CHANGED
@@ -150,7 +150,7 @@ export class TextareaAutosize {
150
  }
151
 
152
  // Only update if height actually changed
153
- if (this.textareaHeight !== newHeight) {
154
  this.textareaHeight = newHeight;
155
  this.element.style[this.styleProp] = `${newHeight}px`;
156
  }
 
150
  }
151
 
152
  // Only update if height actually changed
153
+ if (this.textareaHeight !== newHeight || !this.element.style[this.styleProp]) {
154
  this.textareaHeight = newHeight;
155
  this.element.style[this.styleProp] = `${newHeight}px`;
156
  }
src/lib/state/conversations.svelte.ts CHANGED
@@ -38,6 +38,9 @@ export class ConversationEntity {
38
  schema?: string;
39
  };
40
 
 
 
 
41
  @Fields.json()
42
  messages?: ConversationMessage[];
43
 
@@ -82,6 +85,7 @@ function getDefaultConversation(projectId: string) {
82
  config: { ...defaultGenerationConfig },
83
  messages: [],
84
  streaming: true,
 
85
  createdAt: new Date(),
86
  } satisfies Partial<ConversationEntityMembers>;
87
  }
 
38
  schema?: string;
39
  };
40
 
41
+ @Fields.boolean()
42
+ parseMarkdown = false;
43
+
44
  @Fields.json()
45
  messages?: ConversationMessage[];
46
 
 
85
  config: { ...defaultGenerationConfig },
86
  messages: [],
87
  streaming: true,
88
+ parseMarkdown: false,
89
  createdAt: new Date(),
90
  } satisfies Partial<ConversationEntityMembers>;
91
  }