Spaces:
Running
Running
fix: render markdown tables and persist message sources after refresh
Browse files- Add preprocessMarkdown to fix concatenated table rows from AI responses
- Map sources field in loadRoomMessages so Sources footer survives page refresh
- Move ChatSource interface before RoomMessage and add sources field to RoomMessage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/app/components/Main.tsx +28 -1
- src/services/api.ts +7 -6
src/app/components/Main.tsx
CHANGED
|
@@ -52,6 +52,32 @@ interface ChatRoom {
|
|
| 52 |
messagesLoaded: boolean;
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
// Markdown component overrides for clean rendering inside chat bubbles
|
| 56 |
const markdownComponents: Components = {
|
| 57 |
p: ({ children }) => (
|
|
@@ -254,6 +280,7 @@ export default function Main() {
|
|
| 254 |
role: m.role,
|
| 255 |
content: m.content,
|
| 256 |
timestamp: new Date(m.created_at).getTime(),
|
|
|
|
| 257 |
}));
|
| 258 |
setChats((prev) =>
|
| 259 |
prev.map((chat) =>
|
|
@@ -654,7 +681,7 @@ export default function Main() {
|
|
| 654 |
rehypePlugins={[rehypeKatex]}
|
| 655 |
components={markdownComponents}
|
| 656 |
>
|
| 657 |
-
{message.content}
|
| 658 |
</ReactMarkdown>
|
| 659 |
</div>
|
| 660 |
)}
|
|
|
|
| 52 |
messagesLoaded: boolean;
|
| 53 |
}
|
| 54 |
|
| 55 |
+
// Preprocess markdown to ensure tables are properly separated from surrounding text
|
| 56 |
+
function preprocessMarkdown(content: string): string {
|
| 57 |
+
// Step 1: Split concatenated table rows β "| val ||" means end of row + start of next
|
| 58 |
+
let result = content.replace(/\|\|/g, "|\n|");
|
| 59 |
+
|
| 60 |
+
// Step 2: Per-line β if a line has non-pipe text before a pipe table row, split them
|
| 61 |
+
const lines = result.split("\n");
|
| 62 |
+
const processed = lines.map((line) => {
|
| 63 |
+
const tableStart = line.indexOf("|");
|
| 64 |
+
if (tableStart > 0) {
|
| 65 |
+
const tableContent = line.slice(tableStart);
|
| 66 |
+
// Confirm it looks like a table row (at least 2 pipes)
|
| 67 |
+
if ((tableContent.match(/\|/g) ?? []).length >= 2) {
|
| 68 |
+
return line.slice(0, tableStart).trimEnd() + "\n\n" + tableContent;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
return line;
|
| 72 |
+
});
|
| 73 |
+
result = processed.join("\n");
|
| 74 |
+
|
| 75 |
+
// Step 3: Ensure blank line before table rows preceded by non-table text (not another row ending with |)
|
| 76 |
+
result = result.replace(/([^|\n])\n(\|)/g, "$1\n\n$2");
|
| 77 |
+
|
| 78 |
+
return result;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
// Markdown component overrides for clean rendering inside chat bubbles
|
| 82 |
const markdownComponents: Components = {
|
| 83 |
p: ({ children }) => (
|
|
|
|
| 280 |
role: m.role,
|
| 281 |
content: m.content,
|
| 282 |
timestamp: new Date(m.created_at).getTime(),
|
| 283 |
+
sources: m.sources ?? [],
|
| 284 |
}));
|
| 285 |
setChats((prev) =>
|
| 286 |
prev.map((chat) =>
|
|
|
|
| 681 |
rehypePlugins={[rehypeKatex]}
|
| 682 |
components={markdownComponents}
|
| 683 |
>
|
| 684 |
+
{preprocessMarkdown(message.content)}
|
| 685 |
</ReactMarkdown>
|
| 686 |
</div>
|
| 687 |
)}
|
src/services/api.ts
CHANGED
|
@@ -30,11 +30,18 @@ export interface CreateRoomResponse {
|
|
| 30 |
data: Room;
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
export interface RoomMessage {
|
| 34 |
id: string;
|
| 35 |
role: "user" | "assistant";
|
| 36 |
content: string;
|
| 37 |
created_at: string;
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
export interface RoomDetail extends Room {
|
|
@@ -58,12 +65,6 @@ export interface UploadDocumentResponse {
|
|
| 58 |
data: { id: string; filename: string; status: DocumentStatus };
|
| 59 |
}
|
| 60 |
|
| 61 |
-
export interface ChatSource {
|
| 62 |
-
document_id: string;
|
| 63 |
-
filename: string;
|
| 64 |
-
page_label: string | null;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
// βββ Base Client ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 68 |
|
| 69 |
const BASE_URL = ((import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL) ?? "";
|
|
|
|
| 30 |
data: Room;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
export interface ChatSource {
|
| 34 |
+
document_id: string;
|
| 35 |
+
filename: string;
|
| 36 |
+
page_label: string | null;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
export interface RoomMessage {
|
| 40 |
id: string;
|
| 41 |
role: "user" | "assistant";
|
| 42 |
content: string;
|
| 43 |
created_at: string;
|
| 44 |
+
sources?: ChatSource[];
|
| 45 |
}
|
| 46 |
|
| 47 |
export interface RoomDetail extends Room {
|
|
|
|
| 65 |
data: { id: string; filename: string; status: DocumentStatus };
|
| 66 |
}
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
// βββ Base Client ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
|
| 70 |
const BASE_URL = ((import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL) ?? "";
|