fix(chat): 修复暗黑模式下 Markdown 和滚动条样式 — 响应式 useIsDark hook + 动态主题切换
This commit is contained in:
@@ -5,19 +5,19 @@
|
|||||||
"source": "vercel/ai",
|
"source": "vercel/ai",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/use-ai-sdk/SKILL.md",
|
"skillPath": "skills/use-ai-sdk/SKILL.md",
|
||||||
"computedHash": "c99d2a95b3a5f8fad218f288503f9e724ba0f12bf4e8aaf2c792a9f2bc318ab6"
|
"computedHash": "2249889eb47ef0f61c4dba4cf2afe01c1c8dd793bb4c24230347c1ab909bb7dd"
|
||||||
},
|
},
|
||||||
"ant-design": {
|
"ant-design": {
|
||||||
"source": "ant-design/antd-skill",
|
"source": "ant-design/antd-skill",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/ant-design/SKILL.md",
|
"skillPath": "skills/ant-design/SKILL.md",
|
||||||
"computedHash": "096d4ac9513e43030f960aab49b50168a3d5eb35be86926ac6e96e5998ea9466"
|
"computedHash": "4d0447d48fced080b2825ecc0fb4d7ca836c8015882899c643acca0b864d5179"
|
||||||
},
|
},
|
||||||
"antd": {
|
"antd": {
|
||||||
"source": "ant-design/antd-skill",
|
"source": "ant-design/antd-skill",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/antd/SKILL.md",
|
"skillPath": "skills/antd/SKILL.md",
|
||||||
"computedHash": "5e26c8042060bb811118927b5daf637af7929a00fa973dd8f5f804f3ba6e2bf2"
|
"computedHash": "4295010f09f85855cab9e9de9ec7f96c14541474b4f3f9d6ef89006430931b94"
|
||||||
},
|
},
|
||||||
"x-components": {
|
"x-components": {
|
||||||
"source": "ant-design/x",
|
"source": "ant-design/x",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "overlayscrollbars/styles/overlayscrollbars.css";
|
|||||||
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useIsDark } from "../../../../hooks/use-is-dark";
|
||||||
import { useChatScroll } from "./use-chat-scroll";
|
import { useChatScroll } from "./use-chat-scroll";
|
||||||
|
|
||||||
interface ChatScrollAreaProps {
|
interface ChatScrollAreaProps {
|
||||||
@@ -18,6 +19,7 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||||
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const isDark = useIsDark();
|
||||||
|
|
||||||
const handleOsInitialized = useCallback(() => {
|
const handleOsInitialized = useCallback(() => {
|
||||||
const os = osRef.current;
|
const os = osRef.current;
|
||||||
@@ -38,7 +40,10 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro
|
|||||||
events={{ initialized: handleOsInitialized }}
|
events={{ initialized: handleOsInitialized }}
|
||||||
options={{
|
options={{
|
||||||
overflow: { x: "hidden", y: "scroll" },
|
overflow: { x: "hidden", y: "scroll" },
|
||||||
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
|
scrollbars: {
|
||||||
|
autoHide: "move",
|
||||||
|
theme: isDark ? "os-theme-custom-dark" : "os-theme-custom",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
ref={osRef}
|
ref={osRef}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,27 @@ import { CopyOutlined } from "@ant-design/icons";
|
|||||||
import { CodeHighlighter } from "@ant-design/x";
|
import { CodeHighlighter } from "@ant-design/x";
|
||||||
import { App, Button, Flex, Typography } from "antd";
|
import { App, Button, Flex, Typography } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
|
|
||||||
|
import { useIsDark } from "../../../../../hooks/use-is-dark";
|
||||||
|
|
||||||
|
type SyntaxTheme = Record<string, Record<string, null | number | string>>;
|
||||||
|
|
||||||
|
const customOneDark: SyntaxTheme = {
|
||||||
|
...(oneDark as SyntaxTheme),
|
||||||
|
'pre[class*="language-"]': {
|
||||||
|
...(oneDark as SyntaxTheme)['pre[class*="language-"]'],
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const customOneLight: SyntaxTheme = {
|
||||||
|
...(oneLight as SyntaxTheme),
|
||||||
|
'pre[class*="language-"]': {
|
||||||
|
...(oneLight as SyntaxTheme)['pre[class*="language-"]'],
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface CodeBlockWithCopyProps {
|
interface CodeBlockWithCopyProps {
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
@@ -13,14 +34,14 @@ interface CodeBlockWithCopyProps {
|
|||||||
|
|
||||||
export function CodeBlockWithCopy({ block, children, className, lang }: CodeBlockWithCopyProps) {
|
export function CodeBlockWithCopy({ block, children, className, lang }: CodeBlockWithCopyProps) {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
const isDark = useIsDark();
|
||||||
|
|
||||||
if (!block) {
|
if (!block) {
|
||||||
return <code className={className}>{children}</code>;
|
return <code className={className}>{children}</code>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeText = extractText(children);
|
const codeText = extractText(children);
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
const displayLang = lang ?? "plaintext";
|
||||||
const displayLang = lang || "plaintext";
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
void navigator.clipboard.writeText(codeText).then(() => {
|
void navigator.clipboard.writeText(codeText).then(() => {
|
||||||
@@ -44,7 +65,11 @@ export function CodeBlockWithCopy({ block, children, className, lang }: CodeBloc
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeHighlighter header={header} lang={displayLang}>
|
<CodeHighlighter
|
||||||
|
header={header}
|
||||||
|
highlightProps={{ style: (isDark ? customOneDark : customOneLight) as React.CSSProperties }}
|
||||||
|
lang={displayLang}
|
||||||
|
>
|
||||||
{codeText}
|
{codeText}
|
||||||
</CodeHighlighter>
|
</CodeHighlighter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { XMarkdown } from "@ant-design/x-markdown";
|
import { XMarkdown } from "@ant-design/x-markdown";
|
||||||
|
import "@ant-design/x-markdown/themes/dark.css";
|
||||||
import "@ant-design/x-markdown/themes/light.css";
|
import "@ant-design/x-markdown/themes/light.css";
|
||||||
import { Typography } from "antd";
|
import { Typography } from "antd";
|
||||||
|
|
||||||
import type { PartProps } from "./types";
|
import type { PartProps } from "./types";
|
||||||
|
|
||||||
|
import { useIsDark } from "../../../../../hooks/use-is-dark";
|
||||||
import { CodeBlockWithCopy } from "./CodeBlockWithCopy";
|
import { CodeBlockWithCopy } from "./CodeBlockWithCopy";
|
||||||
|
|
||||||
interface TextPartProps extends PartProps {
|
interface TextPartProps extends PartProps {
|
||||||
@@ -18,6 +20,7 @@ const xmarkdownComponents = {
|
|||||||
|
|
||||||
export function TextPart({ isStreaming, part, role }: TextPartProps) {
|
export function TextPart({ isStreaming, part, role }: TextPartProps) {
|
||||||
const text = typeof part["text"] === "string" ? part["text"] : "";
|
const text = typeof part["text"] === "string" ? part["text"] : "";
|
||||||
|
const isDark = useIsDark();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="part-body">
|
<div className="part-body">
|
||||||
@@ -25,7 +28,7 @@ export function TextPart({ isStreaming, part, role }: TextPartProps) {
|
|||||||
<Typography.Paragraph className="message-body-text">{text}</Typography.Paragraph>
|
<Typography.Paragraph className="message-body-text">{text}</Typography.Paragraph>
|
||||||
) : (
|
) : (
|
||||||
<XMarkdown
|
<XMarkdown
|
||||||
className="x-markdown-light"
|
className={isDark ? "x-markdown-dark" : "x-markdown-light"}
|
||||||
components={xmarkdownComponents}
|
components={xmarkdownComponents}
|
||||||
content={text}
|
content={text}
|
||||||
streaming={{ hasNextChunk: isStreaming }}
|
streaming={{ hasNextChunk: isStreaming }}
|
||||||
|
|||||||
3
src/web/css.d.ts
vendored
3
src/web/css.d.ts
vendored
@@ -1 +1,4 @@
|
|||||||
declare module "*.css";
|
declare module "*.css";
|
||||||
|
declare module "react-syntax-highlighter/dist/esm/styles/prism" {
|
||||||
|
export { oneDark, oneLight } from "react-syntax-highlighter";
|
||||||
|
}
|
||||||
|
|||||||
6
src/web/hooks/use-is-dark.ts
Normal file
6
src/web/hooks/use-is-dark.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { theme } from "antd";
|
||||||
|
|
||||||
|
export function useIsDark(): boolean {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
return token.colorBgBase === "#000";
|
||||||
|
}
|
||||||
@@ -225,23 +225,42 @@ body {
|
|||||||
--os-handle-interactive-area-offset: 4px;
|
--os-handle-interactive-area-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.x-markdown-light table {
|
.os-theme-custom-dark {
|
||||||
|
--os-size: 8px;
|
||||||
|
--os-padding-perpendicular: 2px;
|
||||||
|
--os-padding-axis: 2px;
|
||||||
|
--os-track-border-radius: 10px;
|
||||||
|
--os-handle-border-radius: 10px;
|
||||||
|
--os-handle-bg: rgba(255, 255, 255, 0.15);
|
||||||
|
--os-handle-bg-hover: rgba(255, 255, 255, 0.25);
|
||||||
|
--os-handle-bg-active: rgba(255, 255, 255, 0.35);
|
||||||
|
--os-handle-min-size: 33px;
|
||||||
|
--os-handle-max-size: none;
|
||||||
|
--os-handle-interactive-area-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-markdown-light table,
|
||||||
|
.x-markdown-dark table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.x-markdown-light th,
|
.x-markdown-light th,
|
||||||
.x-markdown-light td {
|
.x-markdown-light td,
|
||||||
|
.x-markdown-dark th,
|
||||||
|
.x-markdown-dark td {
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.x-markdown-light th {
|
.x-markdown-light th,
|
||||||
|
.x-markdown-dark th {
|
||||||
background: var(--ant-color-fill-quaternary);
|
background: var(--ant-color-fill-quaternary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.x-markdown-light .x-md-table-wrap {
|
.x-markdown-light .x-md-table-wrap,
|
||||||
|
.x-markdown-dark .x-md-table-wrap {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user