Skip to content

Commit 9dcd673

Browse files
committed
feat(chat): 代码块语法高亮 + GFM 表格渲染,提取共享 codeTheme 模块
- CollapsibleContent 集成 react-syntax-highlighter + remark-gfm - 提取 warmAcademicTheme 到 src/lib/codeTheme.ts,DocumentReader 与 Chat 共用 - 微调若干 store atom / GlobalHeader / ProjectList 细节
1 parent f74ea13 commit 9dcd673

9 files changed

Lines changed: 111 additions & 61 deletions

File tree

.changeset/v0-27-0.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"lovcode": minor
3+
---
4+
5+
Chat experience upgrades:
6+
7+
- 数据源细分为 cli / app-code / app-web / app-cowork,会话详情支持双层 tab 切换
8+
- 会话详情底部支持直接输入消息继续对话
9+
- 会话详情合并同角色连续消息,菜单分组重构
10+
- Chat markdown 渲染升级:支持 GFM 表格、代码块语法高亮(Warm Academic 主题)
11+
- 提取 `codeTheme.ts` 共享模块,DocumentReader 与 Chat 复用同一套代码主题

src/components/DocumentReader.tsx

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,7 @@ import { docReaderCollapsedGroupsAtom } from "../store";
44
import Markdown from "react-markdown";
55
import remarkGfm from "remark-gfm";
66
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
7-
import type { CSSProperties } from "react";
8-
9-
// Warm Academic code theme - matches design system
10-
const warmAcademicTheme: { [key: string]: CSSProperties } = {
11-
'code[class*="language-"]': {
12-
color: "#181818",
13-
background: "none",
14-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
15-
fontSize: "0.875rem",
16-
textAlign: "left",
17-
whiteSpace: "pre",
18-
wordSpacing: "normal",
19-
wordBreak: "normal",
20-
wordWrap: "normal",
21-
lineHeight: "1.6",
22-
},
23-
'pre[class*="language-"]': {
24-
color: "#181818",
25-
background: "#F0EEE6",
26-
padding: "1rem",
27-
margin: "0",
28-
overflow: "auto",
29-
},
30-
comment: { color: "#87867F", fontStyle: "italic" },
31-
prolog: { color: "#87867F" },
32-
doctype: { color: "#87867F" },
33-
cdata: { color: "#87867F" },
34-
punctuation: { color: "#5C5C5C" },
35-
property: { color: "#CC785C" },
36-
tag: { color: "#CC785C" },
37-
boolean: { color: "#CC785C" },
38-
number: { color: "#CC785C" },
39-
constant: { color: "#CC785C" },
40-
symbol: { color: "#CC785C" },
41-
deleted: { color: "#CC785C" },
42-
selector: { color: "#6B7F59" },
43-
"attr-name": { color: "#6B7F59" },
44-
string: { color: "#6B7F59" },
45-
char: { color: "#6B7F59" },
46-
builtin: { color: "#6B7F59" },
47-
inserted: { color: "#6B7F59" },
48-
operator: { color: "#87867F" },
49-
entity: { color: "#87867F", cursor: "help" },
50-
url: { color: "#87867F" },
51-
".language-css .token.string": { color: "#87867F" },
52-
".style .token.string": { color: "#87867F" },
53-
atrule: { color: "#7D6B99" },
54-
"attr-value": { color: "#7D6B99" },
55-
keyword: { color: "#7D6B99" },
56-
function: { color: "#4A6785" },
57-
"class-name": { color: "#4A6785" },
58-
regex: { color: "#CC785C" },
59-
important: { color: "#CC785C", fontWeight: "bold" },
60-
variable: { color: "#CC785C" },
61-
};
7+
import { warmAcademicTheme } from "../lib/codeTheme";
628
// Lucide icons (no Radix equivalent)
639
import { PanelLeftClose, PanelLeft, PanelRightClose, PanelRight, Maximize2, Minimize2 } from "lucide-react";
6410
// Radix icons

src/components/GlobalHeader/GlobalHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function GlobalHeader({
9090
isActive={primaryFeature === "chat"}
9191
onClick={() => handleMainNavClick("chat")}
9292
icon={<CounterClockwiseClockIcon className="w-4 h-4" />}
93-
label="Chat"
93+
label="History"
9494
/>
9595
<NavButton
9696
isActive={primaryFeature === "workspace"}

src/lib/codeTheme.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { CSSProperties } from "react";
2+
3+
// Warm Academic Prism theme — matches Lovstudio design system
4+
export const warmAcademicTheme: { [key: string]: CSSProperties } = {
5+
'code[class*="language-"]': {
6+
color: "#181818",
7+
background: "none",
8+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
9+
fontSize: "0.875rem",
10+
textAlign: "left",
11+
whiteSpace: "pre",
12+
wordSpacing: "normal",
13+
wordBreak: "normal",
14+
wordWrap: "normal",
15+
lineHeight: "1.6",
16+
},
17+
'pre[class*="language-"]': {
18+
color: "#181818",
19+
background: "#F0EEE6",
20+
padding: "1rem",
21+
margin: "0",
22+
overflow: "auto",
23+
},
24+
comment: { color: "#87867F", fontStyle: "italic" },
25+
prolog: { color: "#87867F" },
26+
doctype: { color: "#87867F" },
27+
cdata: { color: "#87867F" },
28+
punctuation: { color: "#5C5C5C" },
29+
property: { color: "#CC785C" },
30+
tag: { color: "#CC785C" },
31+
boolean: { color: "#CC785C" },
32+
number: { color: "#CC785C" },
33+
constant: { color: "#CC785C" },
34+
symbol: { color: "#CC785C" },
35+
deleted: { color: "#CC785C" },
36+
selector: { color: "#6B7F59" },
37+
"attr-name": { color: "#6B7F59" },
38+
string: { color: "#6B7F59" },
39+
char: { color: "#6B7F59" },
40+
builtin: { color: "#6B7F59" },
41+
inserted: { color: "#6B7F59" },
42+
operator: { color: "#87867F" },
43+
entity: { color: "#87867F", cursor: "help" },
44+
url: { color: "#87867F" },
45+
".language-css .token.string": { color: "#87867F" },
46+
".style .token.string": { color: "#87867F" },
47+
atrule: { color: "#7D6B99" },
48+
"attr-value": { color: "#7D6B99" },
49+
keyword: { color: "#7D6B99" },
50+
function: { color: "#4A6785" },
51+
"class-name": { color: "#4A6785" },
52+
regex: { color: "#CC785C" },
53+
important: { color: "#CC785C", fontWeight: "bold" },
54+
variable: { color: "#CC785C" },
55+
};

src/store/atoms/chat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { atomWithStorage } from "jotai/utils";
33
// MessageView
44
export const originalChatAtom = atomWithStorage("lovcode:originalChat", true);
55
export const markdownPreviewAtom = atomWithStorage("lovcode:markdownPreview", false);
6+
export const expandMessagesAtom = atomWithStorage("lovcode:expandMessages", true);
67

78
// SessionList
89
export const sessionContextTabAtom = atomWithStorage<"global" | "project">("lovcode:sessions:contextTab", "project");

src/store/atoms/ui.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function parseUrlToView(hash: string): View {
3838
const path = hash.startsWith("/") ? hash.slice(1) : hash;
3939
const segments = path.split("/").filter(Boolean);
4040

41-
if (segments.length === 0) return { type: "home" };
41+
if (segments.length === 0) return { type: "chat-projects" };
4242

4343
const [first, second] = segments;
4444

src/store/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export { expandedPathsAtom } from "./atoms/fileTree";
99

1010
// Chat atoms
1111
export {
12-
originalChatAtom, markdownPreviewAtom,
12+
originalChatAtom, markdownPreviewAtom, expandMessagesAtom,
1313
sessionContextTabAtom, sessionSelectModeAtom, hideEmptySessionsAtom, userPromptsOnlyAtom,
1414
chatViewModeAtom, allProjectsSortByAtom, hideEmptySessionsAllAtom,
1515
sidebarSessionSortByAtom, type SessionSortBy,

src/views/Chat/CollapsibleContent.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { useState, useLayoutEffect, useRef, useMemo } from "react";
22
import Markdown from "react-markdown";
3+
import remarkGfm from "remark-gfm";
4+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
5+
import { warmAcademicTheme } from "../../lib/codeTheme";
36
import { HighlightText } from "./HighlightText";
47

58
interface CollapsibleContentProps {
@@ -93,8 +96,42 @@ export function CollapsibleContent({ content, markdown, defaultCollapsed = false
9396
className={`text-ink text-sm leading-relaxed ${collapsed ? "overflow-hidden max-h-10" : ""}`}
9497
>
9598
{markdown ? (
96-
<div className="prose prose-sm max-w-none prose-p:my-1 prose-headings:my-2 prose-pre:my-2 prose-ul:my-1 prose-ol:my-1">
97-
<Markdown rehypePlugins={rehypePlugins as never}>{content}</Markdown>
99+
<div className="prose prose-sm max-w-none prose-p:my-1 prose-headings:my-2 prose-pre:my-0 prose-ul:my-1 prose-ol:my-1 prose-table:my-0 prose-th:px-2 prose-th:py-1 prose-td:px-2 prose-td:py-1 prose-th:border prose-td:border prose-th:border-border prose-td:border-border prose-table:border-collapse prose-code:before:hidden prose-code:after:hidden">
100+
<Markdown
101+
remarkPlugins={[remarkGfm]}
102+
rehypePlugins={rehypePlugins as never}
103+
components={{
104+
table: ({ node: _node, ...props }) => (
105+
<div className="my-2 overflow-x-auto">
106+
<table {...props} />
107+
</div>
108+
),
109+
code: ({ node: _node, inline, className, children, ...props }: { node?: unknown; inline?: boolean; className?: string; children?: React.ReactNode }) => {
110+
const match = /language-(\w+)/.exec(className || "");
111+
if (!inline && match) {
112+
return (
113+
<div className="my-2 rounded-md overflow-hidden border border-border">
114+
<SyntaxHighlighter
115+
style={warmAcademicTheme}
116+
language={match[1]}
117+
PreTag="div"
118+
customStyle={{ margin: 0, borderRadius: 0, padding: "0.75rem 1rem", background: "#F0EEE6" }}
119+
>
120+
{String(children).replace(/\n$/, "")}
121+
</SyntaxHighlighter>
122+
</div>
123+
);
124+
}
125+
return (
126+
<code className={`${className || ""} px-1 py-0.5 rounded bg-card-alt text-ink/90 font-mono text-[0.85em]`} {...props}>
127+
{children}
128+
</code>
129+
);
130+
},
131+
}}
132+
>
133+
{content}
134+
</Markdown>
98135
</div>
99136
) : (
100137
<p className="whitespace-pre-wrap break-words">

src/views/Chat/ProjectList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ export function ProjectList({ onSelectProject, onSelectSession }: ProjectListPro
458458
<div className="w-80 shrink-0 border-r border-border overflow-y-auto overscroll-contain">
459459
<div className="px-4 py-4">
460460
<div className="flex items-center justify-between mb-1">
461-
<h2 className="font-serif text-lg font-semibold text-ink">Chat History</h2>
461+
<h2 className="font-serif text-lg font-semibold text-ink">Lovcode History</h2>
462462
<button
463463
onClick={() => setSearchOpen(true)}
464464
className="flex items-center gap-1.5 p-1.5 rounded-md text-muted-foreground hover:text-ink hover:bg-card-alt transition-colors"

0 commit comments

Comments
 (0)