fix(chat): 修复暗黑模式下 Markdown 和滚动条样式 — 响应式 useIsDark hook + 动态主题切换
This commit is contained in:
@@ -5,19 +5,19 @@
|
||||
"source": "vercel/ai",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/use-ai-sdk/SKILL.md",
|
||||
"computedHash": "c99d2a95b3a5f8fad218f288503f9e724ba0f12bf4e8aaf2c792a9f2bc318ab6"
|
||||
"computedHash": "2249889eb47ef0f61c4dba4cf2afe01c1c8dd793bb4c24230347c1ab909bb7dd"
|
||||
},
|
||||
"ant-design": {
|
||||
"source": "ant-design/antd-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/ant-design/SKILL.md",
|
||||
"computedHash": "096d4ac9513e43030f960aab49b50168a3d5eb35be86926ac6e96e5998ea9466"
|
||||
"computedHash": "4d0447d48fced080b2825ecc0fb4d7ca836c8015882899c643acca0b864d5179"
|
||||
},
|
||||
"antd": {
|
||||
"source": "ant-design/antd-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/antd/SKILL.md",
|
||||
"computedHash": "5e26c8042060bb811118927b5daf637af7929a00fa973dd8f5f804f3ba6e2bf2"
|
||||
"computedHash": "4295010f09f85855cab9e9de9ec7f96c14541474b4f3f9d6ef89006430931b94"
|
||||
},
|
||||
"x-components": {
|
||||
"source": "ant-design/x",
|
||||
|
||||
@@ -6,6 +6,7 @@ import "overlayscrollbars/styles/overlayscrollbars.css";
|
||||
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { useIsDark } from "../../../../hooks/use-is-dark";
|
||||
import { useChatScroll } from "./use-chat-scroll";
|
||||
|
||||
interface ChatScrollAreaProps {
|
||||
@@ -18,6 +19,7 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
|
||||
const isDark = useIsDark();
|
||||
|
||||
const handleOsInitialized = useCallback(() => {
|
||||
const os = osRef.current;
|
||||
@@ -38,7 +40,10 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro
|
||||
events={{ initialized: handleOsInitialized }}
|
||||
options={{
|
||||
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}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,27 @@ import { CopyOutlined } from "@ant-design/icons";
|
||||
import { CodeHighlighter } from "@ant-design/x";
|
||||
import { App, Button, Flex, Typography } from "antd";
|
||||
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 {
|
||||
block?: boolean;
|
||||
@@ -13,14 +34,14 @@ interface CodeBlockWithCopyProps {
|
||||
|
||||
export function CodeBlockWithCopy({ block, children, className, lang }: CodeBlockWithCopyProps) {
|
||||
const { message } = App.useApp();
|
||||
const isDark = useIsDark();
|
||||
|
||||
if (!block) {
|
||||
return <code className={className}>{children}</code>;
|
||||
}
|
||||
|
||||
const codeText = extractText(children);
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const displayLang = lang || "plaintext";
|
||||
const displayLang = lang ?? "plaintext";
|
||||
|
||||
const handleCopy = () => {
|
||||
void navigator.clipboard.writeText(codeText).then(() => {
|
||||
@@ -44,7 +65,11 @@ export function CodeBlockWithCopy({ block, children, className, lang }: CodeBloc
|
||||
);
|
||||
|
||||
return (
|
||||
<CodeHighlighter header={header} lang={displayLang}>
|
||||
<CodeHighlighter
|
||||
header={header}
|
||||
highlightProps={{ style: (isDark ? customOneDark : customOneLight) as React.CSSProperties }}
|
||||
lang={displayLang}
|
||||
>
|
||||
{codeText}
|
||||
</CodeHighlighter>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { XMarkdown } from "@ant-design/x-markdown";
|
||||
import "@ant-design/x-markdown/themes/dark.css";
|
||||
import "@ant-design/x-markdown/themes/light.css";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import type { PartProps } from "./types";
|
||||
|
||||
import { useIsDark } from "../../../../../hooks/use-is-dark";
|
||||
import { CodeBlockWithCopy } from "./CodeBlockWithCopy";
|
||||
|
||||
interface TextPartProps extends PartProps {
|
||||
@@ -18,6 +20,7 @@ const xmarkdownComponents = {
|
||||
|
||||
export function TextPart({ isStreaming, part, role }: TextPartProps) {
|
||||
const text = typeof part["text"] === "string" ? part["text"] : "";
|
||||
const isDark = useIsDark();
|
||||
|
||||
return (
|
||||
<div className="part-body">
|
||||
@@ -25,7 +28,7 @@ export function TextPart({ isStreaming, part, role }: TextPartProps) {
|
||||
<Typography.Paragraph className="message-body-text">{text}</Typography.Paragraph>
|
||||
) : (
|
||||
<XMarkdown
|
||||
className="x-markdown-light"
|
||||
className={isDark ? "x-markdown-dark" : "x-markdown-light"}
|
||||
components={xmarkdownComponents}
|
||||
content={text}
|
||||
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 "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;
|
||||
}
|
||||
|
||||
.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;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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);
|
||||
padding: 6px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.x-markdown-light th {
|
||||
.x-markdown-light th,
|
||||
.x-markdown-dark th {
|
||||
background: var(--ant-color-fill-quaternary);
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user