Saurabh Kumar Bajpai commited on
Commit
7bef330
·
1 Parent(s): 9f58b4d

feat: render chat markdown tables and code

Browse files
frontend/e2e/auth-and-chat.spec.ts CHANGED
@@ -81,6 +81,17 @@ test("creates an account from the signup form", async ({ page }) => {
81
 
82
  test("uploads a document and chats with it", async ({ page }) => {
83
  const documents: typeof uploadedDocument[] = [];
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  await page.addInitScript(() => {
86
  localStorage.setItem("token", "access-token");
@@ -103,10 +114,9 @@ test("uploads a document and chats with it", async ({ page }) => {
103
  status: 200,
104
  headers: { "content-type": "text/event-stream" },
105
  body: [
106
- 'data: {"type":"token","data":"A short"}\n\n',
107
- 'data: {"type":"token","data":" summary."}\n\n',
108
- 'data: {"type":"sources","data":[]}\n\n',
109
- 'data: {"type":"done"}\n\n',
110
  ].join(""),
111
  });
112
  });
@@ -127,4 +137,7 @@ test("uploads a document and chats with it", async ({ page }) => {
127
 
128
  await expect(page.getByText("Summarize this document")).toBeVisible();
129
  await expect(page.getByText("A short summary.")).toBeVisible();
 
 
 
130
  });
 
81
 
82
  test("uploads a document and chats with it", async ({ page }) => {
83
  const documents: typeof uploadedDocument[] = [];
84
+ const markdownAnswer = [
85
+ "A short summary.",
86
+ "",
87
+ "| Field | Value |",
88
+ "| --- | --- |",
89
+ "| Pages | 1 |",
90
+ "",
91
+ "```ts",
92
+ "const answer = 42;",
93
+ "```",
94
+ ].join("\n");
95
 
96
  await page.addInitScript(() => {
97
  localStorage.setItem("token", "access-token");
 
114
  status: 200,
115
  headers: { "content-type": "text/event-stream" },
116
  body: [
117
+ `data: ${JSON.stringify({ type: "token", data: markdownAnswer })}\n\n`,
118
+ `data: ${JSON.stringify({ type: "sources", data: [] })}\n\n`,
119
+ `data: ${JSON.stringify({ type: "done" })}\n\n`,
 
120
  ].join(""),
121
  });
122
  });
 
137
 
138
  await expect(page.getByText("Summarize this document")).toBeVisible();
139
  await expect(page.getByText("A short summary.")).toBeVisible();
140
+ await expect(page.getByRole("columnheader", { name: "Field" })).toBeVisible();
141
+ await expect(page.getByRole("cell", { name: "Pages" })).toBeVisible();
142
+ await expect(page.getByText("const answer = 42;")).toBeVisible();
143
  });
frontend/package-lock.json CHANGED
@@ -19,6 +19,7 @@
19
  "react-dropzone": "^15.0.0",
20
  "react-markdown": "^10.1.0",
21
  "react-pdf": "^10.4.1",
 
22
  "remark-gfm": "^4.0.1",
23
  "shadcn": "^4.3.1",
24
  "tailwind-merge": "^3.5.0",
@@ -6021,6 +6022,19 @@
6021
  "node": ">= 0.4"
6022
  }
6023
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
6024
  "node_modules/hast-util-to-jsx-runtime": {
6025
  "version": "2.3.6",
6026
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -6048,6 +6062,22 @@
6048
  "url": "https://opencollective.com/unified"
6049
  }
6050
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6051
  "node_modules/hast-util-whitespace": {
6052
  "version": "3.0.0",
6053
  "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@@ -6088,6 +6118,15 @@
6088
  "hermes-estree": "0.25.1"
6089
  }
6090
  },
 
 
 
 
 
 
 
 
 
6091
  "node_modules/hono": {
6092
  "version": "4.12.14",
6093
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
@@ -7418,6 +7457,21 @@
7418
  "loose-envify": "cli.js"
7419
  }
7420
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7421
  "node_modules/lru-cache": {
7422
  "version": "5.1.1",
7423
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -9629,6 +9683,23 @@
9629
  "url": "https://github.com/sponsors/ljharb"
9630
  }
9631
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9632
  "node_modules/remark-gfm": {
9633
  "version": "4.0.1",
9634
  "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -11107,6 +11178,20 @@
11107
  "url": "https://opencollective.com/unified"
11108
  }
11109
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11110
  "node_modules/unist-util-is": {
11111
  "version": "6.0.1",
11112
  "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
 
19
  "react-dropzone": "^15.0.0",
20
  "react-markdown": "^10.1.0",
21
  "react-pdf": "^10.4.1",
22
+ "rehype-highlight": "^7.0.2",
23
  "remark-gfm": "^4.0.1",
24
  "shadcn": "^4.3.1",
25
  "tailwind-merge": "^3.5.0",
 
6022
  "node": ">= 0.4"
6023
  }
6024
  },
6025
+ "node_modules/hast-util-is-element": {
6026
+ "version": "3.0.0",
6027
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
6028
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
6029
+ "license": "MIT",
6030
+ "dependencies": {
6031
+ "@types/hast": "^3.0.0"
6032
+ },
6033
+ "funding": {
6034
+ "type": "opencollective",
6035
+ "url": "https://opencollective.com/unified"
6036
+ }
6037
+ },
6038
  "node_modules/hast-util-to-jsx-runtime": {
6039
  "version": "2.3.6",
6040
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
 
6062
  "url": "https://opencollective.com/unified"
6063
  }
6064
  },
6065
+ "node_modules/hast-util-to-text": {
6066
+ "version": "4.0.2",
6067
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
6068
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
6069
+ "license": "MIT",
6070
+ "dependencies": {
6071
+ "@types/hast": "^3.0.0",
6072
+ "@types/unist": "^3.0.0",
6073
+ "hast-util-is-element": "^3.0.0",
6074
+ "unist-util-find-after": "^5.0.0"
6075
+ },
6076
+ "funding": {
6077
+ "type": "opencollective",
6078
+ "url": "https://opencollective.com/unified"
6079
+ }
6080
+ },
6081
  "node_modules/hast-util-whitespace": {
6082
  "version": "3.0.0",
6083
  "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
 
6118
  "hermes-estree": "0.25.1"
6119
  }
6120
  },
6121
+ "node_modules/highlight.js": {
6122
+ "version": "11.11.1",
6123
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
6124
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
6125
+ "license": "BSD-3-Clause",
6126
+ "engines": {
6127
+ "node": ">=12.0.0"
6128
+ }
6129
+ },
6130
  "node_modules/hono": {
6131
  "version": "4.12.14",
6132
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
 
7457
  "loose-envify": "cli.js"
7458
  }
7459
  },
7460
+ "node_modules/lowlight": {
7461
+ "version": "3.3.0",
7462
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
7463
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
7464
+ "license": "MIT",
7465
+ "dependencies": {
7466
+ "@types/hast": "^3.0.0",
7467
+ "devlop": "^1.0.0",
7468
+ "highlight.js": "~11.11.0"
7469
+ },
7470
+ "funding": {
7471
+ "type": "github",
7472
+ "url": "https://github.com/sponsors/wooorm"
7473
+ }
7474
+ },
7475
  "node_modules/lru-cache": {
7476
  "version": "5.1.1",
7477
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
 
9683
  "url": "https://github.com/sponsors/ljharb"
9684
  }
9685
  },
9686
+ "node_modules/rehype-highlight": {
9687
+ "version": "7.0.2",
9688
+ "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
9689
+ "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
9690
+ "license": "MIT",
9691
+ "dependencies": {
9692
+ "@types/hast": "^3.0.0",
9693
+ "hast-util-to-text": "^4.0.0",
9694
+ "lowlight": "^3.0.0",
9695
+ "unist-util-visit": "^5.0.0",
9696
+ "vfile": "^6.0.0"
9697
+ },
9698
+ "funding": {
9699
+ "type": "opencollective",
9700
+ "url": "https://opencollective.com/unified"
9701
+ }
9702
+ },
9703
  "node_modules/remark-gfm": {
9704
  "version": "4.0.1",
9705
  "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
 
11178
  "url": "https://opencollective.com/unified"
11179
  }
11180
  },
11181
+ "node_modules/unist-util-find-after": {
11182
+ "version": "5.0.0",
11183
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
11184
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
11185
+ "license": "MIT",
11186
+ "dependencies": {
11187
+ "@types/unist": "^3.0.0",
11188
+ "unist-util-is": "^6.0.0"
11189
+ },
11190
+ "funding": {
11191
+ "type": "opencollective",
11192
+ "url": "https://opencollective.com/unified"
11193
+ }
11194
+ },
11195
  "node_modules/unist-util-is": {
11196
  "version": "6.0.1",
11197
  "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
frontend/package.json CHANGED
@@ -22,6 +22,7 @@
22
  "react-dropzone": "^15.0.0",
23
  "react-markdown": "^10.1.0",
24
  "react-pdf": "^10.4.1",
 
25
  "remark-gfm": "^4.0.1",
26
  "shadcn": "^4.3.1",
27
  "tailwind-merge": "^3.5.0",
 
22
  "react-dropzone": "^15.0.0",
23
  "react-markdown": "^10.1.0",
24
  "react-pdf": "^10.4.1",
25
+ "rehype-highlight": "^7.0.2",
26
  "remark-gfm": "^4.0.1",
27
  "shadcn": "^4.3.1",
28
  "tailwind-merge": "^3.5.0",
frontend/src/app/globals.css CHANGED
@@ -182,6 +182,16 @@
182
  .prose-chat p { margin-bottom: 0.75em; line-height: 1.7; }
183
  .prose-chat ul, .prose-chat ol { padding-left: 1.5em; margin-bottom: 0.75em; }
184
  .prose-chat li { margin-bottom: 0.25em; }
 
 
 
 
 
 
 
 
 
 
185
  .prose-chat code {
186
  background: oklch(1 0 0 / 8%);
187
  padding: 0.15em 0.4em;
@@ -198,6 +208,35 @@
198
  .prose-chat pre code {
199
  background: none;
200
  padding: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
202
  .prose-chat blockquote {
203
  border-left: 3px solid oklch(0.65 0.2 265);
@@ -211,5 +250,8 @@
211
 
212
  .light .prose-chat code { background: oklch(0 0 0 / 6%); }
213
  .light .prose-chat pre { background: oklch(0.96 0 0); }
 
 
 
214
  .light .prose-chat strong { color: oklch(0.2 0 0); }
215
- .light .prose-chat blockquote { color: oklch(0.4 0 0); }
 
182
  .prose-chat p { margin-bottom: 0.75em; line-height: 1.7; }
183
  .prose-chat ul, .prose-chat ol { padding-left: 1.5em; margin-bottom: 0.75em; }
184
  .prose-chat li { margin-bottom: 0.25em; }
185
+ .prose-chat table {
186
+ width: 100%;
187
+ }
188
+ .prose-chat th,
189
+ .prose-chat td {
190
+ white-space: nowrap;
191
+ }
192
+ .prose-chat tbody tr:last-child td {
193
+ border-bottom: 0;
194
+ }
195
  .prose-chat code {
196
  background: oklch(1 0 0 / 8%);
197
  padding: 0.15em 0.4em;
 
208
  .prose-chat pre code {
209
  background: none;
210
  padding: 0;
211
+ color: inherit;
212
+ font-size: 0.92em;
213
+ }
214
+ .prose-chat pre code[data-language]::before {
215
+ content: attr(data-language);
216
+ display: block;
217
+ margin-bottom: 0.75rem;
218
+ color: oklch(0.72 0 0);
219
+ font-family: var(--font-geist-sans), system-ui, sans-serif;
220
+ font-size: 0.72rem;
221
+ font-weight: 600;
222
+ letter-spacing: 0;
223
+ text-transform: uppercase;
224
+ }
225
+ .prose-chat .hljs-keyword,
226
+ .prose-chat .hljs-selector-tag,
227
+ .prose-chat .hljs-title.function_ {
228
+ color: oklch(0.78 0.16 305);
229
+ }
230
+ .prose-chat .hljs-string,
231
+ .prose-chat .hljs-attr {
232
+ color: oklch(0.82 0.15 150);
233
+ }
234
+ .prose-chat .hljs-number,
235
+ .prose-chat .hljs-literal {
236
+ color: oklch(0.82 0.13 80);
237
+ }
238
+ .prose-chat .hljs-comment {
239
+ color: oklch(0.62 0 0);
240
  }
241
  .prose-chat blockquote {
242
  border-left: 3px solid oklch(0.65 0.2 265);
 
250
 
251
  .light .prose-chat code { background: oklch(0 0 0 / 6%); }
252
  .light .prose-chat pre { background: oklch(0.96 0 0); }
253
+ .light .prose-chat pre code { color: oklch(0.2 0 0); }
254
+ .light .prose-chat pre code[data-language]::before { color: oklch(0.45 0 0); }
255
+ .light .prose-chat .hljs-comment { color: oklch(0.5 0 0); }
256
  .light .prose-chat strong { color: oklch(0.2 0 0); }
257
+ .light .prose-chat blockquote { color: oklch(0.4 0 0); }
frontend/src/components/chat/MessageBubble.tsx CHANGED
@@ -1,7 +1,8 @@
1
  "use client";
2
 
3
  import { useState, useRef } from "react";
4
- import ReactMarkdown from "react-markdown";
 
5
  import remarkGfm from "remark-gfm";
6
  import type { ChatMsg } from "@/store/chat-store";
7
  import { Brain, User, Copy, Check } from "lucide-react";
@@ -11,6 +12,43 @@ interface Props {
11
  message: ChatMsg;
12
  }
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  export default function MessageBubble({ message }: Props) {
15
  const isUser = message.role === "user";
16
  const [copied, setCopied] = useState(false);
@@ -71,7 +109,11 @@ export default function MessageBubble({ message }: Props) {
71
  )}
72
  <div className={`prose-chat text-sm ${message.content ? "pr-7" : ""}`}>
73
  {message.content ? (
74
- <ReactMarkdown remarkPlugins={[remarkGfm]}>
 
 
 
 
75
  {message.content}
76
  </ReactMarkdown>
77
  ) : message.isStreaming ? (
 
1
  "use client";
2
 
3
  import { useState, useRef } from "react";
4
+ import ReactMarkdown, { type Components } from "react-markdown";
5
+ import rehypeHighlight from "rehype-highlight";
6
  import remarkGfm from "remark-gfm";
7
  import type { ChatMsg } from "@/store/chat-store";
8
  import { Brain, User, Copy, Check } from "lucide-react";
 
12
  message: ChatMsg;
13
  }
14
 
15
+ const markdownComponents: Components = {
16
+ table: ({ children }) => (
17
+ <div className="my-3 overflow-x-auto rounded-lg border border-border/70">
18
+ <table className="min-w-full border-collapse text-left text-sm">
19
+ {children}
20
+ </table>
21
+ </div>
22
+ ),
23
+ thead: ({ children }) => (
24
+ <thead className="bg-muted/60 text-foreground">{children}</thead>
25
+ ),
26
+ th: ({ children }) => (
27
+ <th className="border-b border-border/70 px-3 py-2 font-semibold">
28
+ {children}
29
+ </th>
30
+ ),
31
+ td: ({ children }) => (
32
+ <td className="border-b border-border/50 px-3 py-2 align-top">
33
+ {children}
34
+ </td>
35
+ ),
36
+ pre: ({ children }) => (
37
+ <pre className="not-prose my-3 overflow-x-auto rounded-lg border border-border/70 bg-zinc-950 p-3 text-sm text-zinc-100">
38
+ {children}
39
+ </pre>
40
+ ),
41
+ code: ({ className, children, ...props }) => {
42
+ const language = /language-(\w+)/.exec(className ?? "")?.[1];
43
+
44
+ return (
45
+ <code className={className} data-language={language} {...props}>
46
+ {children}
47
+ </code>
48
+ );
49
+ },
50
+ };
51
+
52
  export default function MessageBubble({ message }: Props) {
53
  const isUser = message.role === "user";
54
  const [copied, setCopied] = useState(false);
 
109
  )}
110
  <div className={`prose-chat text-sm ${message.content ? "pr-7" : ""}`}>
111
  {message.content ? (
112
+ <ReactMarkdown
113
+ remarkPlugins={[remarkGfm]}
114
+ rehypePlugins={[rehypeHighlight]}
115
+ components={markdownComponents}
116
+ >
117
  {message.content}
118
  </ReactMarkdown>
119
  ) : message.isStreaming ? (